metripy 0.2.7__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.

Files changed (66) hide show
  1. metripy/Application/Analyzer.py +106 -0
  2. metripy/Application/Application.py +54 -0
  3. metripy/Application/Config/Config.py +13 -0
  4. metripy/Application/Config/File/ConfigFileReaderFactory.py +24 -0
  5. metripy/Application/Config/File/ConfigFileReaderInterface.py +14 -0
  6. metripy/Application/Config/File/JsonConfigFileReader.py +82 -0
  7. metripy/Application/Config/GitConfig.py +10 -0
  8. metripy/Application/Config/Parser.py +31 -0
  9. metripy/Application/Config/ProjectConfig.py +27 -0
  10. metripy/Application/Config/ReportConfig.py +10 -0
  11. metripy/Application/__init__.py +0 -0
  12. metripy/Component/Debug/Debugger.py +20 -0
  13. metripy/Component/File/Finder.py +37 -0
  14. metripy/Component/Output/CliOutput.py +49 -0
  15. metripy/Component/Output/ProgressBar.py +27 -0
  16. metripy/Dependency/Composer/Composer.py +30 -0
  17. metripy/Dependency/Composer/Packegist.py +55 -0
  18. metripy/Dependency/Dependency.py +30 -0
  19. metripy/Dependency/Npm/Npm.py +30 -0
  20. metripy/Dependency/Npm/NpmOrg.py +47 -0
  21. metripy/Dependency/Pip/Pip.py +69 -0
  22. metripy/Dependency/Pip/PyPi.py +49 -0
  23. metripy/Git/GitAnalyzer.py +86 -0
  24. metripy/LangAnalyzer/AbstractLangAnalyzer.py +65 -0
  25. metripy/LangAnalyzer/Generic/HalSteadAnalyzer.py +58 -0
  26. metripy/LangAnalyzer/Generic/__init__.py +0 -0
  27. metripy/LangAnalyzer/Php/PhpAnalyzer.py +193 -0
  28. metripy/LangAnalyzer/Php/PhpBasicAstParser.py +56 -0
  29. metripy/LangAnalyzer/Php/PhpBasicLocAnalyzer.py +174 -0
  30. metripy/LangAnalyzer/Php/PhpHalSteadAnalyzer.py +44 -0
  31. metripy/LangAnalyzer/Python/PythonAnalyzer.py +129 -0
  32. metripy/LangAnalyzer/Typescript/TypescriptAnalyzer.py +208 -0
  33. metripy/LangAnalyzer/Typescript/TypescriptAstParser.py +68 -0
  34. metripy/LangAnalyzer/Typescript/TypescriptBasicComplexityAnalyzer.py +114 -0
  35. metripy/LangAnalyzer/Typescript/TypescriptBasicLocAnalyzer.py +69 -0
  36. metripy/LangAnalyzer/Typescript/TypescriptHalSteadAnalyzer.py +55 -0
  37. metripy/LangAnalyzer/__init__.py +0 -0
  38. metripy/Metric/Code/AggregatedMetrics.py +42 -0
  39. metripy/Metric/Code/FileMetrics.py +33 -0
  40. metripy/Metric/Code/ModuleMetrics.py +32 -0
  41. metripy/Metric/Code/SegmentedMetrics.py +65 -0
  42. metripy/Metric/FileTree/FileTree.py +15 -0
  43. metripy/Metric/FileTree/FileTreeParser.py +42 -0
  44. metripy/Metric/Git/GitCodeHotspot.py +37 -0
  45. metripy/Metric/Git/GitContributor.py +37 -0
  46. metripy/Metric/Git/GitKnowledgeSilo.py +27 -0
  47. metripy/Metric/Git/GitMetrics.py +148 -0
  48. metripy/Metric/ProjectMetrics.py +55 -0
  49. metripy/Report/Csv/Reporter.py +12 -0
  50. metripy/Report/Html/Reporter.py +210 -0
  51. metripy/Report/Json/AbstractJsonReporter.py +11 -0
  52. metripy/Report/Json/GitJsonReporter.py +21 -0
  53. metripy/Report/Json/JsonReporter.py +12 -0
  54. metripy/Report/ReporterFactory.py +22 -0
  55. metripy/Report/ReporterInterface.py +17 -0
  56. metripy/Tree/ClassNode.py +32 -0
  57. metripy/Tree/FunctionNode.py +49 -0
  58. metripy/Tree/ModuleNode.py +42 -0
  59. metripy/__init__.py +0 -0
  60. metripy/metripy.py +15 -0
  61. metripy-0.2.7.dist-info/METADATA +113 -0
  62. metripy-0.2.7.dist-info/RECORD +66 -0
  63. metripy-0.2.7.dist-info/WHEEL +5 -0
  64. metripy-0.2.7.dist-info/entry_points.txt +2 -0
  65. metripy-0.2.7.dist-info/licenses/LICENSE +21 -0
  66. metripy-0.2.7.dist-info/top_level.txt +1 -0
@@ -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 metripy.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 metripy.Component.Output.ProgressBar import ProgressBar
7
+ from metripy.LangAnalyzer.AbstractLangAnalyzer import AbstractLangAnalyzer
8
+ from metripy.Metric.Code.FileMetrics import FileMetrics
9
+ from metripy.Tree.ClassNode import ClassNode
10
+ from metripy.Tree.FunctionNode import FunctionNode
11
+ from metripy.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()
@@ -0,0 +1,208 @@
1
+ import math
2
+
3
+ import lizard
4
+
5
+ from metripy.Component.Output.ProgressBar import ProgressBar
6
+ from metripy.LangAnalyzer.AbstractLangAnalyzer import AbstractLangAnalyzer
7
+ from metripy.LangAnalyzer.Typescript.TypescriptAstParser import \
8
+ TypescriptAstParser
9
+ from metripy.LangAnalyzer.Typescript.TypescriptBasicComplexityAnalyzer import \
10
+ TypescriptBasicComplexityAnalzyer
11
+ from metripy.LangAnalyzer.Typescript.TypescriptBasicLocAnalyzer import \
12
+ TypescriptBasicLocAnalyzer
13
+ from metripy.LangAnalyzer.Typescript.TypescriptHalSteadAnalyzer import \
14
+ TypeScriptHalSteadAnalyzer
15
+ from metripy.Tree.ClassNode import ClassNode
16
+ from metripy.Tree.FunctionNode import FunctionNode
17
+ from metripy.Tree.ModuleNode import ModuleNode
18
+
19
+
20
+ class TypescriptAnalyzer(AbstractLangAnalyzer):
21
+
22
+ def __init__(self):
23
+ super().__init__()
24
+ self.ast_parser = TypescriptAstParser()
25
+ self.halstead_analyzer = TypeScriptHalSteadAnalyzer()
26
+ self.basic_complexity_analyzer = TypescriptBasicComplexityAnalzyer()
27
+
28
+ def get_lang_name(self) -> str:
29
+ return "Typescript"
30
+
31
+ def set_files(self, files: list[str]) -> None:
32
+ self.files = list(filter(lambda file: file.endswith((".ts", "js")), files))
33
+
34
+ def is_needed(self) -> bool:
35
+ return len(self.files) > 0
36
+
37
+ def run(self, progress_bar: ProgressBar) -> None:
38
+ for file in self.files:
39
+ with open(file, "r") as f:
40
+ code = f.read()
41
+ self.analyze(code, file)
42
+ progress_bar.advance()
43
+
44
+ @staticmethod
45
+ def full_name(
46
+ filename: str, item_name: str | None = None, class_name: str | None = None
47
+ ) -> str:
48
+ if class_name is None:
49
+ if item_name is None:
50
+ return filename
51
+ return f"{filename}:{item_name}"
52
+ return f"{filename}:{class_name}:{item_name}"
53
+
54
+ def analyze(self, code: str, filename: str) -> None:
55
+ structure = self.ast_parser.extract_structure(code)
56
+
57
+ lizard_result = lizard.analyze_file(filename)
58
+ complexity_data = {
59
+ func.name: {
60
+ "complexity": func.cyclomatic_complexity,
61
+ "start_line": func.start_line,
62
+ "end_line": func.end_line,
63
+ }
64
+ for func in lizard_result.function_list
65
+ }
66
+
67
+ classes: dict[str, ClassNode] = {}
68
+ functions: dict[str, FunctionNode] = {}
69
+
70
+ for func_name in structure["functions"]:
71
+ full_name = self.full_name(filename, func_name)
72
+ try:
73
+ function_complexity_data = complexity_data[func_name]
74
+ except KeyError as e:
75
+ function_complexity_data = (
76
+ self.basic_complexity_analyzer.get_complexity(code, func_name)
77
+ )
78
+ if function_complexity_data is None:
79
+ print(
80
+ f"error for function {full_name}: no lizard complexity data, basic analyzer also failed: '{e}'"
81
+ )
82
+ continue
83
+ function_node = FunctionNode(
84
+ full_name,
85
+ func_name,
86
+ function_complexity_data["start_line"],
87
+ 0,
88
+ function_complexity_data["complexity"],
89
+ )
90
+ functions[full_name] = function_node
91
+
92
+ for class_name, method_names in structure["classes"].items():
93
+ full_name = self.full_name(filename, class_name)
94
+ class_node = ClassNode(
95
+ full_name,
96
+ class_name,
97
+ 0,
98
+ 0,
99
+ 0.0, # gets filled in later based on methods
100
+ )
101
+ classes[full_name] = class_node
102
+ for func_name in method_names:
103
+ full_name = self.full_name(filename, func_name, class_name)
104
+ try:
105
+ function_complexity_data = complexity_data[func_name]
106
+ except KeyError as e:
107
+ function_complexity_data = (
108
+ self.basic_complexity_analyzer.get_complexity(code, func_name)
109
+ )
110
+ if function_complexity_data is None:
111
+ print(
112
+ f"error for method {full_name}: no lizard complexity data, basic analyzer also failed: '{e}'"
113
+ )
114
+ continue
115
+ function_node = FunctionNode(
116
+ full_name,
117
+ func_name,
118
+ function_complexity_data["start_line"],
119
+ function_complexity_data["end_line"],
120
+ function_complexity_data["complexity"],
121
+ )
122
+ class_node.functions.append(function_node)
123
+ functions[full_name] = function_node
124
+
125
+ # complexity of classes
126
+ for class_node in classes.values():
127
+ class_node.real_complexity = sum(
128
+ [func.complexity for func in class_node.functions]
129
+ )
130
+
131
+ loc_data = TypescriptBasicLocAnalyzer.get_loc_metrics(code, filename)
132
+
133
+ full_name = self.full_name(filename)
134
+ module_node = ModuleNode(
135
+ full_name,
136
+ loc_data.get("lines", 0),
137
+ loc_data.get("linesOfCode", 0),
138
+ loc_data.get("logicalLinesOfCode", 0),
139
+ loc_data.get("commentLines", 0),
140
+ 0, # multi-line comments - not directly available
141
+ loc_data.get("linesOfCode", 0) - loc_data.get("logicalLinesOfCode", 0),
142
+ loc_data.get("commentLines", 0),
143
+ )
144
+ module_node.classes.extend(classes.values())
145
+ module_node.functions.extend(functions.values())
146
+
147
+ code_lines = code.split("\n")
148
+ for func_name, function_node in functions.items():
149
+ lines = code_lines[function_node.lineno:function_node.line_end]
150
+ function_metrics = self.halstead_analyzer.calculate_halstead_metrics(
151
+ "\n".join(lines)
152
+ )
153
+ function_node.h1 = function_metrics["n1"]
154
+ function_node.h2 = function_metrics["n2"]
155
+ function_node.N1 = function_metrics["N1"]
156
+ function_node.N2 = function_metrics["N2"]
157
+ function_node.vocabulary = function_metrics["vocabulary"]
158
+ function_node.length = function_metrics["length"]
159
+ function_node.volume = function_metrics["volume"]
160
+ function_node.difficulty = function_metrics["difficulty"]
161
+ function_node.effort = function_metrics["effort"]
162
+ function_node.calculated_length = function_metrics["calculated_length"]
163
+ function_node.bugs = function_metrics["bugs"]
164
+ function_node.time = function_metrics["time"]
165
+
166
+ maintainability_index = self._calculate_maintainability_index(
167
+ functions.values(), module_node
168
+ )
169
+ module_node.maintainability_index = maintainability_index
170
+ self.modules[full_name] = module_node
171
+
172
+ def _calculate_maintainability_index(
173
+ self, functions: list[FunctionNode], module_node: ModuleNode
174
+ ) -> float:
175
+ """Calculate maintainability index for PHP"""
176
+ if not functions:
177
+ return 100.0
178
+
179
+ total_volume = sum(func.volume for func in functions)
180
+ total_complexity = sum(func.complexity for func in functions)
181
+ total_length = sum(func.length for func in functions)
182
+
183
+ if total_volume == 0 or total_length == 0:
184
+ return 100.0
185
+
186
+ # PHP maintainability index calculation
187
+ mi_base = max(
188
+ (
189
+ 171
190
+ - 5.2 * math.log(total_volume)
191
+ - 0.23 * total_complexity
192
+ - 16.2 * math.log(total_length)
193
+ )
194
+ * 100
195
+ / 171,
196
+ 0,
197
+ )
198
+
199
+ # Comment weight
200
+ comment_weight = 0
201
+ if module_node.loc > 0:
202
+ comment_ratio = module_node.single_comments / module_node.loc
203
+ comment_weight = 50 * math.sin(math.sqrt(2.4 * comment_ratio))
204
+
205
+ return mi_base + comment_weight
206
+
207
+ def get_metrics(self):
208
+ return super().get_metrics()
@@ -0,0 +1,68 @@
1
+ from collections import defaultdict
2
+
3
+ from tree_sitter_languages import get_parser
4
+
5
+
6
+ class TypescriptAstParser:
7
+ def __init__(self):
8
+ self.parser = get_parser("typescript")
9
+
10
+ def _get_node_text(self, code: str, node) -> str:
11
+ return code[node.start_byte:node.end_byte].decode("utf-8")
12
+
13
+ def extract_structure(self, code: str) -> dict:
14
+ tree = self.parser.parse(bytes(code, "utf8"))
15
+ root_node = tree.root_node
16
+ structure = defaultdict(list)
17
+ structure["classes"] = {}
18
+ structure["functions"] = []
19
+ structure["enums"] = []
20
+
21
+ def traverse(node, parent_class=None):
22
+ if node.type == "class_declaration":
23
+ class_name = None
24
+ for child in node.children:
25
+ if child.type == "type_identifier":
26
+ class_name = self._get_node_text(code.encode(), child)
27
+ structure["classes"][class_name] = []
28
+ for child in node.children:
29
+ traverse(child, class_name)
30
+
31
+ elif node.type == "method_definition" and parent_class:
32
+ for child in node.children:
33
+ if child.type == "property_identifier":
34
+ method_name = self._get_node_text(code.encode(), child)
35
+ structure["classes"][parent_class].append(method_name)
36
+
37
+ elif node.type == "function_declaration":
38
+ for child in node.children:
39
+ if child.type == "identifier":
40
+ function_name = self._get_node_text(code.encode(), child)
41
+ structure["functions"].append(function_name)
42
+
43
+ elif node.type == "lexical_declaration":
44
+ # Handle exported arrow functions like: export const foo = (...) => {...}
45
+ for child in node.children:
46
+ if child.type == "variable_declarator":
47
+ identifier = None
48
+ for grandchild in child.children:
49
+ if grandchild.type == "identifier":
50
+ identifier = self._get_node_text(
51
+ code.encode(), grandchild
52
+ )
53
+ elif grandchild.type == "arrow_function":
54
+ if identifier:
55
+ structure["functions"].append(identifier)
56
+
57
+ elif node.type == "enum_declaration":
58
+ enum_name = None
59
+ for child in node.children:
60
+ if child.type == "identifier":
61
+ enum_name = self._get_node_text(code.encode(), child)
62
+ structure["enums"].append(enum_name)
63
+
64
+ for child in node.children:
65
+ traverse(child, parent_class)
66
+
67
+ traverse(root_node)
68
+ return dict(structure)