metripy 0.2.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of metripy might be problematic. Click here for more details.
- metripy-0.2.5.dist-info/METADATA +112 -0
- metripy-0.2.5.dist-info/RECORD +66 -0
- metripy-0.2.5.dist-info/WHEEL +5 -0
- metripy-0.2.5.dist-info/entry_points.txt +2 -0
- metripy-0.2.5.dist-info/licenses/LICENSE +21 -0
- metripy-0.2.5.dist-info/top_level.txt +1 -0
- src/Application/Analyzer.py +105 -0
- src/Application/Application.py +54 -0
- src/Application/Config/Config.py +13 -0
- src/Application/Config/File/ConfigFileReaderFactory.py +22 -0
- src/Application/Config/File/ConfigFileReaderInterface.py +14 -0
- src/Application/Config/File/JsonConfigFileReader.py +81 -0
- src/Application/Config/GitConfig.py +10 -0
- src/Application/Config/Parser.py +30 -0
- src/Application/Config/ProjectConfig.py +27 -0
- src/Application/Config/ReportConfig.py +10 -0
- src/Application/__init__.py +0 -0
- src/Component/Debug/Debugger.py +20 -0
- src/Component/File/Finder.py +37 -0
- src/Component/Output/CliOutput.py +49 -0
- src/Component/Output/ProgressBar.py +27 -0
- src/Dependency/Composer/Composer.py +30 -0
- src/Dependency/Composer/Packegist.py +55 -0
- src/Dependency/Dependency.py +30 -0
- src/Dependency/Npm/Npm.py +30 -0
- src/Dependency/Npm/NpmOrg.py +47 -0
- src/Dependency/Pip/Pip.py +69 -0
- src/Dependency/Pip/PyPi.py +49 -0
- src/Git/GitAnalyzer.py +86 -0
- src/LangAnalyzer/AbstractLangAnalyzer.py +65 -0
- src/LangAnalyzer/Generic/HalSteadAnalyzer.py +58 -0
- src/LangAnalyzer/Generic/__init__.py +0 -0
- src/LangAnalyzer/Php/PhpAnalyzer.py +193 -0
- src/LangAnalyzer/Php/PhpBasicAstParser.py +56 -0
- src/LangAnalyzer/Php/PhpBasicLocAnalyzer.py +174 -0
- src/LangAnalyzer/Php/PhpHalSteadAnalyzer.py +44 -0
- src/LangAnalyzer/Python/PythonAnalyzer.py +129 -0
- src/LangAnalyzer/Typescript/TypescriptAnalyzer.py +210 -0
- src/LangAnalyzer/Typescript/TypescriptAstParser.py +68 -0
- src/LangAnalyzer/Typescript/TypescriptBasicComplexityAnalyzer.py +114 -0
- src/LangAnalyzer/Typescript/TypescriptBasicLocAnalyzer.py +69 -0
- src/LangAnalyzer/Typescript/TypescriptHalSteadAnalyzer.py +55 -0
- src/LangAnalyzer/__init__.py +0 -0
- src/Metric/Code/AggregatedMetrics.py +42 -0
- src/Metric/Code/FileMetrics.py +33 -0
- src/Metric/Code/ModuleMetrics.py +32 -0
- src/Metric/Code/SegmentedMetrics.py +65 -0
- src/Metric/FileTree/FileTree.py +15 -0
- src/Metric/FileTree/FileTreeParser.py +42 -0
- src/Metric/Git/GitCodeHotspot.py +37 -0
- src/Metric/Git/GitContributor.py +37 -0
- src/Metric/Git/GitKnowledgeSilo.py +27 -0
- src/Metric/Git/GitMetrics.py +148 -0
- src/Metric/ProjectMetrics.py +55 -0
- src/Report/Csv/Reporter.py +12 -0
- src/Report/Html/Reporter.py +210 -0
- src/Report/Json/AbstractJsonReporter.py +10 -0
- src/Report/Json/GitJsonReporter.py +21 -0
- src/Report/Json/JsonReporter.py +12 -0
- src/Report/ReporterFactory.py +22 -0
- src/Report/ReporterInterface.py +17 -0
- src/Tree/ClassNode.py +32 -0
- src/Tree/FunctionNode.py +49 -0
- src/Tree/ModuleNode.py +42 -0
- src/__init__.py +0 -0
- src/codemetrics.py +15 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import math
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import lizard
|
|
5
|
+
|
|
6
|
+
from Component.Output.ProgressBar import ProgressBar
|
|
7
|
+
from LangAnalyzer.AbstractLangAnalyzer import AbstractLangAnalyzer
|
|
8
|
+
from LangAnalyzer.Php.PhpBasicAstParser import PhpBasicAstParser
|
|
9
|
+
from LangAnalyzer.Php.PhpBasicLocAnalyzer import PhpBasicLocAnalyzer
|
|
10
|
+
from LangAnalyzer.Php.PhpHalSteadAnalyzer import PhpHalSteadAnalyzer
|
|
11
|
+
from Metric.Code.FileMetrics import FileMetrics
|
|
12
|
+
from Tree.ClassNode import ClassNode
|
|
13
|
+
from Tree.FunctionNode import FunctionNode
|
|
14
|
+
from Tree.ModuleNode import ModuleNode
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PhpAnalyzer(AbstractLangAnalyzer):
|
|
18
|
+
def __init__(self):
|
|
19
|
+
super().__init__()
|
|
20
|
+
self.loc_analyzer = PhpBasicLocAnalyzer()
|
|
21
|
+
self.halstead_analyzer = PhpHalSteadAnalyzer()
|
|
22
|
+
|
|
23
|
+
def get_lang_name(self) -> str:
|
|
24
|
+
return "PHP"
|
|
25
|
+
|
|
26
|
+
def set_files(self, files: list[str]) -> None:
|
|
27
|
+
self.files = list(filter(lambda file: file.endswith(".php"), files))
|
|
28
|
+
|
|
29
|
+
def is_needed(self) -> bool:
|
|
30
|
+
return len(self.files) > 0
|
|
31
|
+
|
|
32
|
+
def run(self, progress_bar: ProgressBar) -> None:
|
|
33
|
+
for file in self.files:
|
|
34
|
+
with open(file, "r") as f:
|
|
35
|
+
code = f.read()
|
|
36
|
+
self.analyze(code, file)
|
|
37
|
+
progress_bar.advance()
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def full_name(
|
|
41
|
+
filename: str, item_name: str | None = None, class_name: str | None = None
|
|
42
|
+
) -> str:
|
|
43
|
+
if class_name is None:
|
|
44
|
+
if item_name is None:
|
|
45
|
+
return filename
|
|
46
|
+
return f"{filename}:{item_name}"
|
|
47
|
+
return f"{filename}:{class_name}:{item_name}"
|
|
48
|
+
|
|
49
|
+
def analyze(self, code: str, filename: str) -> None:
|
|
50
|
+
file_stem = Path(filename).stem
|
|
51
|
+
structure = PhpBasicAstParser.parse_php_structure(code)
|
|
52
|
+
|
|
53
|
+
lizard_result = lizard.analyze_file(filename)
|
|
54
|
+
complexity_data = {
|
|
55
|
+
func.name: {
|
|
56
|
+
"complexity": func.cyclomatic_complexity,
|
|
57
|
+
"start_line": func.start_line,
|
|
58
|
+
"end_line": func.end_line,
|
|
59
|
+
}
|
|
60
|
+
for func in lizard_result.function_list
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
classes: dict[str, ClassNode] = {}
|
|
64
|
+
functions: dict[str, FunctionNode] = {}
|
|
65
|
+
for obj in structure:
|
|
66
|
+
if obj["type"] == "class":
|
|
67
|
+
full_name = self.full_name(filename, obj["name"])
|
|
68
|
+
classes[full_name] = ClassNode(
|
|
69
|
+
full_name,
|
|
70
|
+
obj["name"],
|
|
71
|
+
obj["line"],
|
|
72
|
+
0,
|
|
73
|
+
0, # gets filled in later, based on methods
|
|
74
|
+
)
|
|
75
|
+
elif obj["type"] == "method" or obj["type"] == "function":
|
|
76
|
+
full_name = self.full_name(filename, obj["name"])
|
|
77
|
+
try:
|
|
78
|
+
function_node = FunctionNode(
|
|
79
|
+
full_name,
|
|
80
|
+
obj["name"],
|
|
81
|
+
obj["line"],
|
|
82
|
+
0,
|
|
83
|
+
complexity_data[f"{file_stem}::{obj['name']}"]["complexity"],
|
|
84
|
+
)
|
|
85
|
+
function_node.line_end = complexity_data[
|
|
86
|
+
f"{file_stem}::{obj['name']}"
|
|
87
|
+
]["end_line"]
|
|
88
|
+
except KeyError:
|
|
89
|
+
# no complexity data, function must be empty
|
|
90
|
+
function_node = FunctionNode(
|
|
91
|
+
full_name,
|
|
92
|
+
obj["name"],
|
|
93
|
+
obj["line"],
|
|
94
|
+
0,
|
|
95
|
+
0,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if obj["type"] == "method":
|
|
99
|
+
class_name = obj["class"]
|
|
100
|
+
full_class_name = self.full_name(filename, class_name)
|
|
101
|
+
class_node = classes.get(full_class_name)
|
|
102
|
+
if class_node is not None:
|
|
103
|
+
class_node.functions.append(function_node)
|
|
104
|
+
else:
|
|
105
|
+
raise ValueError(
|
|
106
|
+
f"Class node not found for function {full_class_name}"
|
|
107
|
+
)
|
|
108
|
+
functions[full_name] = function_node
|
|
109
|
+
|
|
110
|
+
# complexity of classes
|
|
111
|
+
for class_node in classes.values():
|
|
112
|
+
class_node.real_complexity = sum(
|
|
113
|
+
[func.complexity for func in class_node.functions]
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
loc_data = self.loc_analyzer.get_loc_metrics(code, filename)
|
|
117
|
+
|
|
118
|
+
full_name = self.full_name(filename)
|
|
119
|
+
module_node = ModuleNode(
|
|
120
|
+
full_name,
|
|
121
|
+
loc_data.get("lines", 0),
|
|
122
|
+
loc_data.get("linesOfCode", 0),
|
|
123
|
+
loc_data.get("logicalLinesOfCode", 0),
|
|
124
|
+
loc_data.get("commentLines", 0),
|
|
125
|
+
0, # multi-line comments - not directly available
|
|
126
|
+
loc_data.get("linesOfCode", 0) - loc_data.get("logicalLinesOfCode", 0),
|
|
127
|
+
loc_data.get("commentLines", 0),
|
|
128
|
+
)
|
|
129
|
+
module_node.classes.extend(classes.values())
|
|
130
|
+
module_node.functions.extend(functions.values())
|
|
131
|
+
|
|
132
|
+
code_lines = code.split("\n")
|
|
133
|
+
for func_name, function_node in functions.items():
|
|
134
|
+
lines = code_lines[function_node.lineno : function_node.line_end]
|
|
135
|
+
function_metrics = self.halstead_analyzer.calculate_halstead_metrics(
|
|
136
|
+
"\n".join(lines)
|
|
137
|
+
)
|
|
138
|
+
function_node.h1 = function_metrics["n1"]
|
|
139
|
+
function_node.h2 = function_metrics["n2"]
|
|
140
|
+
function_node.N1 = function_metrics["N1"]
|
|
141
|
+
function_node.N2 = function_metrics["N2"]
|
|
142
|
+
function_node.vocabulary = function_metrics["vocabulary"]
|
|
143
|
+
function_node.length = function_metrics["length"]
|
|
144
|
+
function_node.volume = function_metrics["volume"]
|
|
145
|
+
function_node.difficulty = function_metrics["difficulty"]
|
|
146
|
+
function_node.effort = function_metrics["effort"]
|
|
147
|
+
function_node.calculated_length = function_metrics["calculated_length"]
|
|
148
|
+
function_node.bugs = function_metrics["bugs"]
|
|
149
|
+
function_node.time = function_metrics["time"]
|
|
150
|
+
|
|
151
|
+
maintainability_index = self._calculate_maintainability_index(
|
|
152
|
+
functions.values(), module_node
|
|
153
|
+
)
|
|
154
|
+
module_node.maintainability_index = maintainability_index
|
|
155
|
+
self.modules[full_name] = module_node
|
|
156
|
+
|
|
157
|
+
def _calculate_maintainability_index(
|
|
158
|
+
self, functions: list[FunctionNode], module_node: ModuleNode
|
|
159
|
+
) -> float:
|
|
160
|
+
"""Calculate maintainability index for PHP"""
|
|
161
|
+
if not functions:
|
|
162
|
+
return 100.0
|
|
163
|
+
|
|
164
|
+
total_volume = sum(func.volume for func in functions)
|
|
165
|
+
total_complexity = sum(func.complexity for func in functions)
|
|
166
|
+
total_length = sum(func.length for func in functions)
|
|
167
|
+
|
|
168
|
+
if total_volume == 0 or total_length == 0:
|
|
169
|
+
return 100.0
|
|
170
|
+
|
|
171
|
+
# PHP maintainability index calculation
|
|
172
|
+
mi_base = max(
|
|
173
|
+
(
|
|
174
|
+
171
|
|
175
|
+
- 5.2 * math.log(total_volume)
|
|
176
|
+
- 0.23 * total_complexity
|
|
177
|
+
- 16.2 * math.log(total_length)
|
|
178
|
+
)
|
|
179
|
+
* 100
|
|
180
|
+
/ 171,
|
|
181
|
+
0,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# Comment weight
|
|
185
|
+
comment_weight = 0
|
|
186
|
+
if module_node.loc > 0:
|
|
187
|
+
comment_ratio = module_node.single_comments / module_node.loc
|
|
188
|
+
comment_weight = 50 * math.sin(math.sqrt(2.4 * comment_ratio))
|
|
189
|
+
|
|
190
|
+
return mi_base + comment_weight
|
|
191
|
+
|
|
192
|
+
def get_metrics(self) -> list[FileMetrics]:
|
|
193
|
+
return super().get_metrics()
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class PhpBasicAstParser:
|
|
5
|
+
|
|
6
|
+
@staticmethod
|
|
7
|
+
def parse_php_structure(code: str):
|
|
8
|
+
|
|
9
|
+
# Regex patterns
|
|
10
|
+
class_pattern = re.compile(r"class\s+(\w*)")
|
|
11
|
+
interface_pattern = re.compile(r"interface\s+(\w*)")
|
|
12
|
+
method_pattern = re.compile(r"function\s+(\w+)\s*\([^)]*\)?")
|
|
13
|
+
function_pattern = re.compile(r"function\s+(\w+)\s*\([^)]*\)?")
|
|
14
|
+
|
|
15
|
+
lines = code.split("\n")
|
|
16
|
+
structure = []
|
|
17
|
+
current_class = None
|
|
18
|
+
|
|
19
|
+
for i, line in enumerate(lines):
|
|
20
|
+
class_match = class_pattern.search(line)
|
|
21
|
+
if class_match:
|
|
22
|
+
current_class = class_match.group(1)
|
|
23
|
+
structure.append(
|
|
24
|
+
{"type": "class", "name": current_class, "line": i + 1}
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
interface_match = interface_pattern.search(line)
|
|
28
|
+
if interface_match:
|
|
29
|
+
current_class = interface_match.group(1)
|
|
30
|
+
structure.append(
|
|
31
|
+
{
|
|
32
|
+
"type": "class",
|
|
33
|
+
"type_type": "interface",
|
|
34
|
+
"name": current_class,
|
|
35
|
+
"line": i + 1,
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
method_match = method_pattern.search(line)
|
|
40
|
+
if method_match and current_class:
|
|
41
|
+
structure.append(
|
|
42
|
+
{
|
|
43
|
+
"type": "method",
|
|
44
|
+
"name": method_match.group(1),
|
|
45
|
+
"class": current_class,
|
|
46
|
+
"line": i + 1,
|
|
47
|
+
}
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
elif function_pattern.search(line) and not current_class:
|
|
51
|
+
function_name = function_pattern.search(line).group(1)
|
|
52
|
+
structure.append(
|
|
53
|
+
{"type": "function", "name": function_name, "line": i + 1}
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
return structure
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class PhpBasicLocAnalyzer:
|
|
5
|
+
@staticmethod
|
|
6
|
+
def get_loc_metrics(code: str, filename: str) -> dict:
|
|
7
|
+
"""Fallback LOC calculation using manual analysis"""
|
|
8
|
+
try:
|
|
9
|
+
lines = code.split("\n")
|
|
10
|
+
|
|
11
|
+
total_lines = len(lines)
|
|
12
|
+
blank_lines = PhpBasicLocAnalyzer._count_blank_lines(lines)
|
|
13
|
+
comment_lines = PhpBasicLocAnalyzer._count_comment_lines(lines)
|
|
14
|
+
code_lines = total_lines - blank_lines - comment_lines
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
"lines": total_lines,
|
|
18
|
+
"linesOfCode": code_lines,
|
|
19
|
+
"logicalLinesOfCode": code_lines,
|
|
20
|
+
"commentLines": comment_lines,
|
|
21
|
+
"blankLines": blank_lines,
|
|
22
|
+
}
|
|
23
|
+
except Exception as e:
|
|
24
|
+
print(f"Fallback LOC analysis failed: {e}")
|
|
25
|
+
return {
|
|
26
|
+
"lines": 0,
|
|
27
|
+
"linesOfCode": 0,
|
|
28
|
+
"logicalLinesOfCode": 0,
|
|
29
|
+
"commentLines": 0,
|
|
30
|
+
"blankLines": 0,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@staticmethod
|
|
34
|
+
def _count_blank_lines(lines: list) -> int:
|
|
35
|
+
"""Count blank lines"""
|
|
36
|
+
return sum(1 for line in lines if not line.strip())
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def _count_comment_lines(lines: list) -> int:
|
|
40
|
+
"""Count comment lines (single-line and multi-line)"""
|
|
41
|
+
comment_count = 0
|
|
42
|
+
in_multiline_comment = False
|
|
43
|
+
|
|
44
|
+
for line in lines:
|
|
45
|
+
stripped = line.strip()
|
|
46
|
+
|
|
47
|
+
# Handle multi-line comments
|
|
48
|
+
if "/*" in stripped:
|
|
49
|
+
in_multiline_comment = True
|
|
50
|
+
comment_count += 1
|
|
51
|
+
# Check if comment closes on same line
|
|
52
|
+
if "*/" in stripped:
|
|
53
|
+
in_multiline_comment = False
|
|
54
|
+
continue
|
|
55
|
+
|
|
56
|
+
if in_multiline_comment:
|
|
57
|
+
comment_count += 1
|
|
58
|
+
if "*/" in stripped:
|
|
59
|
+
in_multiline_comment = False
|
|
60
|
+
continue
|
|
61
|
+
|
|
62
|
+
# Handle single-line comments
|
|
63
|
+
if stripped.startswith(("//")) or stripped.startswith("#"):
|
|
64
|
+
comment_count += 1
|
|
65
|
+
continue
|
|
66
|
+
|
|
67
|
+
# Handle doc comments
|
|
68
|
+
if stripped.startswith("*") and not stripped.startswith("*/"):
|
|
69
|
+
comment_count += 1
|
|
70
|
+
continue
|
|
71
|
+
|
|
72
|
+
return comment_count
|
|
73
|
+
|
|
74
|
+
def _count_classes(self, lines: list) -> int:
|
|
75
|
+
"""Count class definitions"""
|
|
76
|
+
content = "".join(lines)
|
|
77
|
+
class_pattern = r"(?:abstract\s+)?class\s+\w+"
|
|
78
|
+
return len(re.findall(class_pattern, content, re.IGNORECASE))
|
|
79
|
+
|
|
80
|
+
def _count_functions(self, lines: list) -> int:
|
|
81
|
+
"""Count all function definitions (including methods)"""
|
|
82
|
+
content = "".join(lines)
|
|
83
|
+
func_pattern = (
|
|
84
|
+
r"(?:public\s+|private\s+|protected\s+)?(?:static\s+)?function\s+\w+"
|
|
85
|
+
)
|
|
86
|
+
return len(re.findall(func_pattern, content, re.IGNORECASE))
|
|
87
|
+
|
|
88
|
+
def _count_methods(self, lines: list) -> int:
|
|
89
|
+
"""Count class methods (functions inside classes)"""
|
|
90
|
+
content = "".join(lines)
|
|
91
|
+
|
|
92
|
+
# This is a simplified approach - count functions that appear after class declarations
|
|
93
|
+
class_positions = []
|
|
94
|
+
for match in re.finditer(r"class\s+\w+", content, re.IGNORECASE):
|
|
95
|
+
class_positions.append(match.start())
|
|
96
|
+
|
|
97
|
+
if not class_positions:
|
|
98
|
+
return 0
|
|
99
|
+
|
|
100
|
+
method_count = 0
|
|
101
|
+
for match in re.finditer(
|
|
102
|
+
r"(?:public\s+|private\s+|protected\s+)?(?:static\s+)?function\s+\w+",
|
|
103
|
+
content,
|
|
104
|
+
re.IGNORECASE,
|
|
105
|
+
):
|
|
106
|
+
func_pos = match.start()
|
|
107
|
+
# Check if this function is after any class declaration
|
|
108
|
+
for class_pos in class_positions:
|
|
109
|
+
if func_pos > class_pos:
|
|
110
|
+
method_count += 1
|
|
111
|
+
break
|
|
112
|
+
|
|
113
|
+
return method_count
|
|
114
|
+
|
|
115
|
+
def _count_constants(self, lines: list) -> int:
|
|
116
|
+
"""Count constants (const and define)"""
|
|
117
|
+
content = "".join(lines)
|
|
118
|
+
|
|
119
|
+
# Count const declarations
|
|
120
|
+
const_pattern = r"\bconst\s+\w+"
|
|
121
|
+
const_count = len(re.findall(const_pattern, content, re.IGNORECASE))
|
|
122
|
+
|
|
123
|
+
# Count define() calls
|
|
124
|
+
define_pattern = r"\bdefine\s*\("
|
|
125
|
+
define_count = len(re.findall(define_pattern, content, re.IGNORECASE))
|
|
126
|
+
|
|
127
|
+
return const_count + define_count
|
|
128
|
+
|
|
129
|
+
def analyze_code_structure(self, filepath: str) -> dict:
|
|
130
|
+
"""Analyze the overall structure of the PHP file"""
|
|
131
|
+
try:
|
|
132
|
+
with open(filepath, "r") as f:
|
|
133
|
+
content = f.read()
|
|
134
|
+
|
|
135
|
+
# Count various PHP constructs
|
|
136
|
+
structure = {
|
|
137
|
+
"namespaces": len(
|
|
138
|
+
re.findall(r"namespace\s+[\w\\]+", content, re.IGNORECASE)
|
|
139
|
+
),
|
|
140
|
+
"use_statements": len(
|
|
141
|
+
re.findall(r"use\s+[\w\\]+", content, re.IGNORECASE)
|
|
142
|
+
),
|
|
143
|
+
"interfaces": len(
|
|
144
|
+
re.findall(r"interface\s+\w+", content, re.IGNORECASE)
|
|
145
|
+
),
|
|
146
|
+
"traits": len(re.findall(r"trait\s+\w+", content, re.IGNORECASE)),
|
|
147
|
+
"abstract_classes": len(
|
|
148
|
+
re.findall(r"abstract\s+class\s+\w+", content, re.IGNORECASE)
|
|
149
|
+
),
|
|
150
|
+
"final_classes": len(
|
|
151
|
+
re.findall(r"final\s+class\s+\w+", content, re.IGNORECASE)
|
|
152
|
+
),
|
|
153
|
+
"magic_methods": len(
|
|
154
|
+
re.findall(r"function\s+__\w+", content, re.IGNORECASE)
|
|
155
|
+
),
|
|
156
|
+
"static_methods": len(
|
|
157
|
+
re.findall(r"static\s+function\s+\w+", content, re.IGNORECASE)
|
|
158
|
+
),
|
|
159
|
+
"private_methods": len(
|
|
160
|
+
re.findall(r"private\s+function\s+\w+", content, re.IGNORECASE)
|
|
161
|
+
),
|
|
162
|
+
"protected_methods": len(
|
|
163
|
+
re.findall(r"protected\s+function\s+\w+", content, re.IGNORECASE)
|
|
164
|
+
),
|
|
165
|
+
"public_methods": len(
|
|
166
|
+
re.findall(r"public\s+function\s+\w+", content, re.IGNORECASE)
|
|
167
|
+
),
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return structure
|
|
171
|
+
|
|
172
|
+
except Exception as e:
|
|
173
|
+
print(f"Code structure analysis failed: {e}")
|
|
174
|
+
return {}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from LangAnalyzer.Generic.HalSteadAnalyzer import HalSteadAnalyzer
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class PhpHalSteadAnalyzer:
|
|
5
|
+
def __init__(self):
|
|
6
|
+
self.operators = set(
|
|
7
|
+
[
|
|
8
|
+
"+",
|
|
9
|
+
"-",
|
|
10
|
+
"*",
|
|
11
|
+
"/",
|
|
12
|
+
"%",
|
|
13
|
+
"++",
|
|
14
|
+
"--",
|
|
15
|
+
"=",
|
|
16
|
+
"==",
|
|
17
|
+
"!=",
|
|
18
|
+
"===",
|
|
19
|
+
"!==",
|
|
20
|
+
"<",
|
|
21
|
+
">",
|
|
22
|
+
"<=",
|
|
23
|
+
">=",
|
|
24
|
+
"&&",
|
|
25
|
+
"||",
|
|
26
|
+
"!",
|
|
27
|
+
".",
|
|
28
|
+
"->",
|
|
29
|
+
"=>",
|
|
30
|
+
"::",
|
|
31
|
+
"?",
|
|
32
|
+
":",
|
|
33
|
+
"&",
|
|
34
|
+
"|",
|
|
35
|
+
"^",
|
|
36
|
+
"~",
|
|
37
|
+
"<<",
|
|
38
|
+
">>",
|
|
39
|
+
]
|
|
40
|
+
)
|
|
41
|
+
self.analyzer = HalSteadAnalyzer(self.operators)
|
|
42
|
+
|
|
43
|
+
def calculate_halstead_metrics(self, code: str):
|
|
44
|
+
return self.analyzer.calculate_halstead_metrics(code)
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
from radon.complexity import cc_visit
|
|
2
|
+
from radon.metrics import Halstead, HalsteadReport, h_visit, mi_visit
|
|
3
|
+
from radon.raw import analyze
|
|
4
|
+
from radon.visitors import Class, Function
|
|
5
|
+
|
|
6
|
+
from Component.Output.ProgressBar import ProgressBar
|
|
7
|
+
from LangAnalyzer.AbstractLangAnalyzer import AbstractLangAnalyzer
|
|
8
|
+
from Metric.Code.FileMetrics import FileMetrics
|
|
9
|
+
from Tree.ClassNode import ClassNode
|
|
10
|
+
from Tree.FunctionNode import FunctionNode
|
|
11
|
+
from Tree.ModuleNode import ModuleNode
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PythonAnalyzer(AbstractLangAnalyzer):
|
|
15
|
+
|
|
16
|
+
def __init__(self):
|
|
17
|
+
super().__init__()
|
|
18
|
+
|
|
19
|
+
def get_lang_name(self) -> str:
|
|
20
|
+
return "Python"
|
|
21
|
+
|
|
22
|
+
def set_files(self, files: list[str]) -> None:
|
|
23
|
+
self.files = list(filter(lambda file: file.endswith(".py"), files))
|
|
24
|
+
|
|
25
|
+
def is_needed(self) -> bool:
|
|
26
|
+
return len(self.files) > 0
|
|
27
|
+
|
|
28
|
+
def run(self, progress_bar: ProgressBar) -> None:
|
|
29
|
+
for file in self.files:
|
|
30
|
+
with open(file, "r") as f:
|
|
31
|
+
code = f.read()
|
|
32
|
+
self.analyze(code, file)
|
|
33
|
+
progress_bar.advance()
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def full_name(
|
|
37
|
+
filename: str, item_name: str | None = None, class_name: str | None = None
|
|
38
|
+
) -> str:
|
|
39
|
+
if class_name is None:
|
|
40
|
+
if item_name is None:
|
|
41
|
+
return filename
|
|
42
|
+
return f"{filename}:{item_name}"
|
|
43
|
+
return f"{filename}:{class_name}:{item_name}"
|
|
44
|
+
|
|
45
|
+
def analyze(self, code: str, filename: str) -> None:
|
|
46
|
+
classes: dict[str, ClassNode] = {}
|
|
47
|
+
functions: dict[str, FunctionNode] = {}
|
|
48
|
+
cc = cc_visit(code)
|
|
49
|
+
for item in cc:
|
|
50
|
+
if isinstance(item, Class):
|
|
51
|
+
full_name = self.full_name(filename, item.name)
|
|
52
|
+
classes[full_name] = ClassNode(
|
|
53
|
+
full_name,
|
|
54
|
+
item.name,
|
|
55
|
+
item.lineno,
|
|
56
|
+
item.col_offset,
|
|
57
|
+
item.real_complexity,
|
|
58
|
+
)
|
|
59
|
+
elif isinstance(item, Function):
|
|
60
|
+
full_class_name = self.full_name(filename, item.classname)
|
|
61
|
+
full_name = self.full_name(filename, item.name)
|
|
62
|
+
function_node = FunctionNode(
|
|
63
|
+
full_name, item.name, item.lineno, item.col_offset, item.complexity
|
|
64
|
+
)
|
|
65
|
+
if item.is_method:
|
|
66
|
+
class_node = classes.get(full_class_name)
|
|
67
|
+
if class_node is not None:
|
|
68
|
+
class_node.functions.append(function_node)
|
|
69
|
+
else:
|
|
70
|
+
raise ValueError(
|
|
71
|
+
f"Class node not found for function {full_class_name}"
|
|
72
|
+
)
|
|
73
|
+
functions[full_name] = function_node
|
|
74
|
+
else:
|
|
75
|
+
raise ValueError(f"Unknown item type: {type(item)}")
|
|
76
|
+
|
|
77
|
+
# print("--------------------------------")
|
|
78
|
+
# print(json.dumps([c.__dict__() for c in classes.values()], indent=4))
|
|
79
|
+
# print("--------------------------------")
|
|
80
|
+
# print(json.dumps([f.__dict__() for f in functions.values()], indent=4))
|
|
81
|
+
# exit()
|
|
82
|
+
module = analyze(code)
|
|
83
|
+
full_name = self.full_name(filename)
|
|
84
|
+
module_node = ModuleNode(
|
|
85
|
+
full_name,
|
|
86
|
+
module.loc,
|
|
87
|
+
module.lloc,
|
|
88
|
+
module.sloc,
|
|
89
|
+
module.comments,
|
|
90
|
+
module.multi,
|
|
91
|
+
module.blank,
|
|
92
|
+
module.single_comments,
|
|
93
|
+
)
|
|
94
|
+
module_node.classes.extend(classes.values())
|
|
95
|
+
module_node.functions.extend(functions.values())
|
|
96
|
+
# print(module)
|
|
97
|
+
# print(json.dumps([m.to_dict() for m in modules.values()], indent=4))
|
|
98
|
+
# exit()
|
|
99
|
+
h = h_visit(code)
|
|
100
|
+
assert isinstance(h, Halstead)
|
|
101
|
+
# print(h.total)
|
|
102
|
+
function_name: str
|
|
103
|
+
report: HalsteadReport
|
|
104
|
+
for function_name, report in h.functions:
|
|
105
|
+
full_name = self.full_name(filename, function_name)
|
|
106
|
+
function_node = functions.get(full_name)
|
|
107
|
+
if function_node is not None:
|
|
108
|
+
function_node.h1 = report.h1
|
|
109
|
+
function_node.h2 = report.h2
|
|
110
|
+
function_node.N1 = report.N1
|
|
111
|
+
function_node.N2 = report.N2
|
|
112
|
+
function_node.vocabulary = report.vocabulary
|
|
113
|
+
function_node.length = report.length
|
|
114
|
+
function_node.calculated_length = report.calculated_length
|
|
115
|
+
function_node.volume = report.volume
|
|
116
|
+
function_node.difficulty = report.difficulty
|
|
117
|
+
function_node.effort = report.effort
|
|
118
|
+
function_node.bugs = report.bugs
|
|
119
|
+
function_node.time = report.time
|
|
120
|
+
else:
|
|
121
|
+
raise ValueError(f"Function node not found for function {full_name}")
|
|
122
|
+
|
|
123
|
+
maintainability_index = mi_visit(code, True)
|
|
124
|
+
module_node.maintainability_index = maintainability_index
|
|
125
|
+
|
|
126
|
+
self.modules[full_name] = module_node
|
|
127
|
+
|
|
128
|
+
def get_metrics(self) -> list[FileMetrics]:
|
|
129
|
+
return super().get_metrics()
|