Skip to content

Commit 81324df

Browse files
committed
Refine CCMetric
2 parents a0df296 + f7b2dee commit 81324df

9 files changed

Lines changed: 311 additions & 221 deletions

File tree

aibolit/config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from aibolit.metrics.NumberMethods.NumberMethods import NumberMethods as M8
2424
from aibolit.metrics.RFC.rfc import RFC as M9
2525
from aibolit.metrics.fanout.FanOut import FanOut as M10
26+
from aibolit.metrics.cc.main import CCMetric as M11
2627
from aibolit.patterns.array_as_argument.array_as_argument import ArrayAsArgument as P22
2728
from aibolit.patterns.assert_in_code.assert_in_code import AssertInCode as P1
2829
from aibolit.patterns.assign_null_finder.assign_null_finder import NullAssignment as P28
@@ -228,6 +229,7 @@ def get_patterns_config() -> PatternsConfig:
228229
{'name': 'Number of methods', 'code': 'M8', 'make': M8},
229230
{'name': 'Responce for class', 'code': 'M9', 'make': M9},
230231
{'name': 'Fan out', 'code': 'M10', 'make': M10},
232+
{'name': 'Cyclomatic Complexity', 'code': 'M11', 'make': M11},
231233
],
232234
'target': {
233235

aibolit/metrics/cc/README.md

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
1-
To run:
2-
1. Install java
3-
2. Install Maven `sudo apt install maven`
4-
3. Install Python 3.6 or higher
5-
4. Install dependencies: `pip3 install -r requirements.txt`
6-
5. Execute: `python3 run.py ../input/Complicated.java`, where:
7-
* first parameter is path to current folder run.py
8-
* second parameter is path to analyzed java file.
1+
# Cyclomatic Complexity metric for Java code analysis.
92

103
Usage:
11-
```
12-
from main import CCMetric
13-
input = `myfile.java`
14-
showlog = True
15-
metric = CCMetric(input)
16-
print(metric.value(showlog))
4+
5+
```python
6+
from aibolit.metrics.cc.main import CCMetric
7+
from aibolit.ast_framework import AST
8+
from aibolit.utils.ast_builder import build_ast
9+
10+
ast = AST.build_from_javalang(build_ast('MyClass.java'))
11+
metric = CCMetric()
12+
complexity = metric.value(ast)
13+
print(f"Total cyclomatic complexity: {complexity}")
1714
```

aibolit/metrics/cc/cyclical.xml

Lines changed: 0 additions & 14 deletions
This file was deleted.

aibolit/metrics/cc/main.py

Lines changed: 61 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,72 @@
11
# SPDX-FileCopyrightText: Copyright (c) 2019-2025 Aibolit
22
# SPDX-License-Identifier: MIT
33

4-
import os
5-
import shutil
6-
import subprocess
7-
import tempfile
8-
import uuid
4+
from aibolit.ast_framework import AST, ASTNode, ASTNodeType
95

10-
from bs4 import BeautifulSoup
116

7+
class CCMetric:
8+
"""
9+
Calculates cyclomatic complexity by counting
10+
decision points in all methods and constructors:
1211
13-
class CCMetric():
14-
"""Main Cyclical Complexity class."""
12+
- Base complexity: 1 per method/constructor
13+
- Decision points: if, while, for, do-while,
14+
switch cases, catch, throw,
15+
ternary, break, continue
16+
- Boolean operators (&&, ||) add additional complexity paths
17+
"""
1518

16-
input = ''
19+
def value(self, ast: AST) -> int:
20+
return sum(
21+
self._method_complexity(ast, node)
22+
for node in ast.get_proxy_nodes(
23+
ASTNodeType.METHOD_DECLARATION, ASTNodeType.CONSTRUCTOR_DECLARATION
24+
)
25+
)
1726

18-
def __init__(self, input):
19-
self.input = input
27+
def _method_complexity(self, ast: AST, method: ASTNode) -> int:
28+
method_ast = ast.get_subtree(method)
29+
return 1 + sum(self._node_complexity(ast, node) for node in method_ast.get_proxy_nodes())
2030

21-
def value(self, showoutput=False):
22-
"""Run Cyclical Complexity analaysis"""
31+
def _node_complexity(self, ast: AST, node: ASTNode) -> int:
32+
if node.node_type in _SIMPLE_NODES:
33+
return 1
34+
elif node.node_type in _CONDITION_NODES:
35+
return self._condition_complexity(ast, node)
36+
elif node.node_type == ASTNodeType.FOR_STATEMENT:
37+
return self._for_statement_complexity(ast, node)
38+
return 0
2339

24-
if len(self.input) == 0:
25-
raise ValueError('Empty file for analysis')
26-
try:
27-
root = os.path.join(tempfile.gettempdir(), uuid.uuid4().hex)
28-
dirName = os.path.join(root, 'src/main/java')
29-
os.makedirs(dirName)
30-
if os.path.isdir(self.input):
31-
shutil.copytree(self.input, os.path.join(dirName, self.input))
32-
elif os.path.isfile(self.input):
33-
pos1 = self.input.rfind('/')
34-
os.makedirs(dirName + '/' + self.input[0:pos1])
35-
shutil.copyfile(self.input, os.path.join(dirName, self.input))
36-
else:
37-
raise Exception('File ' + self.input + ' does not exist')
38-
tmppath = os.path.dirname(os.path.realpath(__file__))
39-
shutil.copyfile(os.path.join(tmppath, 'pom.xml'), root + '/pom.xml')
40-
shutil.copyfile(os.path.join(tmppath, 'cyclical.xml'), root + '/cyclical.xml')
41-
if showoutput:
42-
subprocess.run(['mvn', 'pmd:pmd'], cwd=root, check=False)
43-
else:
44-
subprocess.run(['mvn', 'pmd:pmd'], cwd=root,
45-
stdout=subprocess.DEVNULL,
46-
stderr=subprocess.DEVNULL, check=False)
47-
if os.path.isfile(root + '/target/pmd.xml'):
48-
res = self.__parseFile(root)
49-
return res
50-
raise Exception('File ' + self.input + ' analyze failed')
51-
finally:
52-
shutil.rmtree(root)
40+
def _condition_complexity(self, ast: AST, node: ASTNode) -> int:
41+
return 1 + self._expression_complexity(ast, node.condition)
5342

54-
def __parseFile(self, root):
55-
result = {'data': [], 'errors': []}
56-
content = []
57-
with open(root + '/target/pmd.xml', 'r', encoding='utf-8') as file:
58-
content = file.read()
59-
soup = BeautifulSoup(content, 'lxml')
60-
files = soup.find_all('file')
61-
for file in files:
62-
out = file.violation.string
63-
name = file['name']
64-
pos1 = name.find(root + '/src/main/java/')
65-
pos1 = pos1 + len(root + '/src/main/java/')
66-
name = name[pos1:]
67-
pos1 = out.find('has a total cyclomatic complexity')
68-
pos1 = out.find('of ', pos1)
69-
pos1 = pos1 + 3
70-
pos2 = out.find('(', pos1)
71-
complexity = int(out[pos1:pos2 - 1])
72-
result['data'].append({'file': name, 'complexity': complexity})
73-
errors = soup.find_all('error')
74-
for error in errors:
75-
name = error['filename']
76-
pos1 = name.find(root + '/src/main/java/')
77-
pos1 = pos1 + len(root + '/src/main/java/')
78-
name = name[pos1:]
79-
raise Exception(error['msg'])
80-
return result
43+
def _for_statement_complexity(self, ast: AST, node: ASTNode) -> int:
44+
if hasattr(node.control, 'condition'):
45+
return self._condition_complexity(ast, node.control)
46+
return 1
47+
48+
def _expression_complexity(self, ast: AST, expression: ASTNode | None) -> int:
49+
if expression is None:
50+
return 0
51+
expression_ast = ast.get_subtree(expression)
52+
return sum(
53+
1
54+
for node in expression_ast.get_proxy_nodes(ASTNodeType.BINARY_OPERATION)
55+
if node.operator in ('&&', '||')
56+
)
57+
58+
59+
_SIMPLE_NODES = (
60+
ASTNodeType.BREAK_STATEMENT,
61+
ASTNodeType.CATCH_CLAUSE,
62+
ASTNodeType.CONTINUE_STATEMENT,
63+
ASTNodeType.SWITCH_STATEMENT_CASE,
64+
ASTNodeType.THROW_STATEMENT,
65+
)
66+
67+
_CONDITION_NODES = (
68+
ASTNodeType.IF_STATEMENT,
69+
ASTNodeType.WHILE_STATEMENT,
70+
ASTNodeType.DO_STATEMENT,
71+
ASTNodeType.TERNARY_EXPRESSION,
72+
)

aibolit/metrics/cc/pom.xml

Lines changed: 0 additions & 67 deletions
This file was deleted.

aibolit/metrics/cc/requirements.txt

Lines changed: 0 additions & 2 deletions
This file was deleted.

test/metrics/cc/anton.java

Lines changed: 0 additions & 17 deletions
This file was deleted.

test/metrics/cc/ooo.java

Lines changed: 0 additions & 4 deletions
This file was deleted.

0 commit comments

Comments
 (0)