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,47 @@
1
+ import requests
2
+
3
+ from metripy.Dependency.Dependency import Dependency
4
+
5
+
6
+ class NpmOrg:
7
+ def get_info(self, dependency: Dependency) -> Dependency | None:
8
+ if not dependency.name:
9
+ return None
10
+
11
+ uri = f"https://registry.npmjs.org/{dependency.name}"
12
+ response = requests.get(uri)
13
+
14
+ if response.status_code != 200:
15
+ print(f"Package {dependency.name} not found on npm.org")
16
+ return None
17
+
18
+ data = response.json()
19
+
20
+ # Basic metadata
21
+ dependency.type = "npm"
22
+ dependency.description = data.get("description", "")
23
+ dependency.repository = data.get("repository", {}).get("url", "")
24
+ dependency.homepage = data.get("homepage", "")
25
+ dependency.license = [data.get("license")] if data.get("license") else []
26
+
27
+ # Version info
28
+ latest_version = data.get("dist-tags", {}).get("latest", "")
29
+ dependency.latest = latest_version
30
+
31
+ # npm doesn't provide download stats in the registry API
32
+ dependency.github_stars = "??"
33
+ dependency.downloads_total = "??"
34
+ dependency.downloads_monthly = "??"
35
+ dependency.downloads_daily = "??"
36
+
37
+ # Determine status
38
+ if dependency.version == dependency.latest:
39
+ dependency.status = "latest"
40
+ else:
41
+ dependency.status = "outdated"
42
+
43
+ # build zip url
44
+ if latest_version:
45
+ dependency.zip = f"https://registry.npmjs.org/{dependency.name}/-/{dependency.name}-{latest_version}.tgz"
46
+
47
+ return dependency
@@ -0,0 +1,69 @@
1
+ import os
2
+ import re
3
+
4
+ import toml
5
+
6
+ from metripy.Dependency.Dependency import Dependency
7
+ from metripy.Dependency.Pip.PyPi import PyPi
8
+
9
+
10
+ class Pip:
11
+ def get_dependencies(self, path: str) -> list[Dependency]:
12
+ try:
13
+ requirements = self.get_from_requirements_txt(path)
14
+ except FileNotFoundError:
15
+ requirements = self.get_from_pyproject_toml(path)
16
+
17
+ pypi = PyPi()
18
+ packages = []
19
+ for dependency in requirements:
20
+ package = pypi.get_info(dependency)
21
+ packages.append(package)
22
+
23
+ return [item for item in packages if item is not None]
24
+
25
+ def get_from_requirements_txt(self, path: str) -> list[Dependency]:
26
+ requirements = []
27
+
28
+ pattern = re.compile(r"([a-zA-Z0-9_\-]+)([<>=!~]+[^\s]+)?")
29
+ with open(os.path.join(path, "requirements.txt"), "r") as file:
30
+ lines = file.readlines()
31
+ for line in lines:
32
+
33
+ line = line.strip()
34
+ if line and not line.startswith("#"):
35
+ match = pattern.match(line)
36
+ if match:
37
+ name = match.group(1)
38
+ version = match.group(2) if match.group(2) else None
39
+ requirements.append(Dependency(name, version))
40
+ return requirements
41
+
42
+ def get_from_pyproject_toml(self, path: str) -> list[Dependency]:
43
+ dependencies = []
44
+
45
+ with open(os.path.join(path, "pyproject.toml"), "r") as f:
46
+ data = toml.load(f)
47
+
48
+ # For PEP 621 / setuptools projects
49
+ if "project" in data:
50
+ deps = data["project"].get("dependencies", [])
51
+ for dep in deps:
52
+ # dep is a string like "requests>=2.32.5"
53
+ # You can split it if needed
54
+ if "==" in dep:
55
+ name, version = dep.split("==")
56
+ elif ">=" in dep:
57
+ name, version = dep.split(">=")
58
+ else:
59
+ name, version = dep, None
60
+ dependencies.append(
61
+ Dependency(name.strip(), version.strip() if version else None)
62
+ )
63
+
64
+ return dependencies
65
+
66
+
67
+ if __name__ == "__main__":
68
+ pip = Pip()
69
+ pip.get_dependencies("./")
@@ -0,0 +1,49 @@
1
+ import requests
2
+
3
+ from metripy.Dependency.Dependency import Dependency
4
+
5
+
6
+ class PyPi:
7
+ def get_info(self, dependency: Dependency) -> Dependency | None:
8
+ uri = f"https://pypi.org/pypi/{dependency.name}/json"
9
+ x = requests.get(uri)
10
+ data = x.json()
11
+
12
+ info = data.get("info", {})
13
+ releases = data.get("releases", {})
14
+
15
+ if not info:
16
+ print(f"Package '{dependency.name}' has no info section")
17
+ return dependency
18
+
19
+ dependency.description = info.get("summary")
20
+ dependency.repository = info.get("project_url") or info.get("home_page")
21
+ if info.get("license"):
22
+ dependency.license = [info.get("license")]
23
+ else:
24
+ dependency.license = []
25
+ dependency.homepage = info.get("home_page")
26
+
27
+ # PyPI doesn't provide GitHub stars or download counts directly
28
+ dependency.github_stars = "??"
29
+ dependency.downloads_total = "??"
30
+ dependency.downloads_monthly = "??"
31
+ dependency.downloads_daily = "??"
32
+
33
+ # Determine latest version
34
+ latest_version = info.get("version")
35
+ dependency.latest = latest_version
36
+
37
+ # Compare with current version
38
+ if dependency.version == latest_version:
39
+ dependency.status = "latest"
40
+ else:
41
+ dependency.status = "outdated"
42
+
43
+ # Get distribution URL (e.g., wheel or sdist)
44
+ if latest_version in releases:
45
+ release_files = releases[latest_version]
46
+ if release_files:
47
+ dependency.zip = release_files[0].get("url")
48
+
49
+ return dependency
@@ -0,0 +1,86 @@
1
+ from collections import defaultdict
2
+ from datetime import datetime
3
+
4
+ from git import Repo
5
+
6
+ from metripy.Application.Config.GitConfig import GitConfig
7
+ from metripy.Metric.Git.GitMetrics import GitMetrics
8
+
9
+
10
+ class GitAnalyzer:
11
+ def __init__(self, git_config: GitConfig):
12
+ print(git_config.repo)
13
+ print(git_config.branch)
14
+ self.repo = Repo(git_config.repo)
15
+ self.branch_name = git_config.branch
16
+
17
+ def analyze(self) -> GitMetrics:
18
+ """Main analysis method with comprehensive output"""
19
+
20
+ # Calculate first day of this month last year
21
+ now = datetime.now()
22
+ first_of_month_last_year = datetime(now.year - 1, now.month, 1)
23
+ # first_of_month_last_year = datetime(now.year, now.month, 1)
24
+ after_date = first_of_month_last_year.strftime("%Y-%m-%d")
25
+ print(f"analyzing from {after_date}")
26
+
27
+ return self.get_metrics(after_date)
28
+
29
+ def _is_source_file(self, file_path: str) -> bool:
30
+ """Check if file is a source code file we want to analyze"""
31
+ source_extensions = {
32
+ ".py",
33
+ ".js",
34
+ ".ts",
35
+ ".tsx",
36
+ ".php",
37
+ ".java",
38
+ ".cpp",
39
+ ".c",
40
+ ".h",
41
+ ".rb",
42
+ ".go",
43
+ ".rs",
44
+ }
45
+ return any(file_path.endswith(ext) for ext in source_extensions)
46
+
47
+ def get_metrics(self, after: str) -> GitMetrics:
48
+ commits_per_month = {}
49
+ chrun_per_month = defaultdict(lambda: {"added": 0, "removed": 0})
50
+ file_contributors = defaultdict(lambda: {"contributors": set(), "commits": 0})
51
+ contributor_stats = defaultdict(
52
+ lambda: {"commits": 0, "lines_added": 0, "lines_removed": 0}
53
+ )
54
+
55
+ for commit in self.repo.iter_commits(
56
+ self.branch_name, no_merges=True, after=after
57
+ ):
58
+ month = commit.committed_datetime.strftime("%Y-%m")
59
+ author = commit.author.name
60
+
61
+ if month not in commits_per_month.keys():
62
+ commits_per_month[month] = 0
63
+ commits_per_month[month] += 1
64
+
65
+ stats = commit.stats.total
66
+ insertions = stats.get("insertions", 0)
67
+ deletions = stats.get("deletions", 0)
68
+ chrun_per_month[month]["added"] += insertions
69
+ chrun_per_month[month]["removed"] += deletions
70
+
71
+ contributor_stats[author]["commits"] += 1
72
+ contributor_stats[author]["lines_added"] += insertions
73
+ contributor_stats[author]["lines_removed"] += deletions
74
+
75
+ for file_path in commit.stats.files:
76
+ if self._is_source_file(file_path):
77
+ file_contributors[file_path]["contributors"].add(author)
78
+ file_contributors[file_path]["commits"] += 1
79
+
80
+ return GitMetrics(
81
+ analysis_start_date=after,
82
+ commit_stats_per_month=commits_per_month,
83
+ churn_per_month=chrun_per_month,
84
+ contributor_stats=contributor_stats,
85
+ file_contributors=file_contributors,
86
+ )
@@ -0,0 +1,65 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ from metripy.Metric.Code.FileMetrics import FileMetrics
4
+ from metripy.Tree.ModuleNode import ModuleNode
5
+
6
+
7
+ class AbstractLangAnalyzer(ABC):
8
+ def __init__(self):
9
+ self.files: list[str] = []
10
+ self.modules: dict[str, ModuleNode] = {}
11
+
12
+ @abstractmethod
13
+ def set_files(self, files: list[str]) -> None:
14
+ raise NotImplementedError
15
+
16
+ @abstractmethod
17
+ def is_needed(self) -> bool:
18
+ pass
19
+
20
+ def before_run(self) -> None:
21
+ # build cache
22
+ pass
23
+
24
+ def after_run(self) -> None:
25
+ # clear cache
26
+ pass
27
+
28
+ @abstractmethod
29
+ def get_lang_name(self) -> str:
30
+ raise NotImplementedError
31
+
32
+ @abstractmethod
33
+ def run(self) -> None:
34
+ raise NotImplementedError
35
+
36
+ @abstractmethod
37
+ def get_metrics(self) -> list[FileMetrics]:
38
+ metrics = []
39
+ for module in self.modules.values():
40
+ full_name = module.full_name
41
+
42
+ if len(module.functions) > 0:
43
+ avgCcPerFunction = sum(
44
+ function.complexity for function in module.functions
45
+ ) / len(module.functions)
46
+ avgLocPerFunction = (
47
+ module.lloc - module.comments - len(module.functions)
48
+ ) / len(module.functions)
49
+ else:
50
+ avgCcPerFunction = 0
51
+ avgLocPerFunction = 0
52
+ maintainabilityIndex = module.maintainability_index
53
+
54
+ file_metric = FileMetrics(
55
+ full_name=full_name,
56
+ loc=module.loc,
57
+ avgCcPerFunction=avgCcPerFunction,
58
+ maintainabilityIndex=maintainabilityIndex,
59
+ avgLocPerFunction=avgLocPerFunction,
60
+ class_nodes=module.classes,
61
+ function_nodes=module.functions,
62
+ )
63
+ metrics.append(file_metric)
64
+
65
+ return metrics
@@ -0,0 +1,58 @@
1
+ import math
2
+ import re
3
+ from collections import Counter
4
+
5
+
6
+ class HalSteadAnalyzer:
7
+ def __init__(self, operators: set):
8
+ self.operators: set = operators
9
+
10
+ def calculate_halstead_metrics(self, code: str):
11
+ # Tokenize the code
12
+ tokens = re.findall(r"\b\w+\b|[^\s\w]", code)
13
+
14
+ # Count operators and operands
15
+ operator_counts = Counter()
16
+ operand_counts = Counter()
17
+
18
+ for token in tokens:
19
+ if token in self.operators:
20
+ operator_counts[token] += 1
21
+ elif re.match(r"\b\w+\b", token):
22
+ operand_counts[token] += 1
23
+
24
+ # Halstead metrics
25
+ n1 = len(operator_counts) # distinct operators
26
+ n2 = len(operand_counts) # distinct operands
27
+ N1 = sum(operator_counts.values()) # total operators
28
+ N2 = sum(operand_counts.values()) # total operands
29
+
30
+ vocabulary = n1 + n2
31
+ length = N1 + N2
32
+ volume = length * math.log2(vocabulary) if vocabulary > 0 else 0
33
+ difficulty = (n1 / 2) * (N2 / n2) if n2 > 0 else 0
34
+ effort = difficulty * volume
35
+
36
+ calculated_length = 0
37
+ if n1 > 0:
38
+ calculated_length += n1 * math.log2(n1)
39
+ if n2 > 0:
40
+ calculated_length += n2 * math.log2(n2)
41
+
42
+ bugs = (effort ** (2 / 3)) / 3000 if effort > 0 else 0
43
+ time = effort / 18 if effort > 0 else 0
44
+
45
+ return {
46
+ "n1": n1, # distinct operators
47
+ "n2": n2, # distinct operands
48
+ "N1": N1, # total operators
49
+ "N2": N2, # total operands
50
+ "vocabulary": vocabulary,
51
+ "length": length,
52
+ "volume": volume,
53
+ "difficulty": difficulty,
54
+ "effort": effort,
55
+ "calculated_length": calculated_length,
56
+ "bugs": bugs,
57
+ "time": time,
58
+ }
File without changes
@@ -0,0 +1,193 @@
1
+ import math
2
+ from pathlib import Path
3
+
4
+ import lizard
5
+
6
+ from metripy.Component.Output.ProgressBar import ProgressBar
7
+ from metripy.LangAnalyzer.AbstractLangAnalyzer import AbstractLangAnalyzer
8
+ from metripy.LangAnalyzer.Php.PhpBasicAstParser import PhpBasicAstParser
9
+ from metripy.LangAnalyzer.Php.PhpBasicLocAnalyzer import PhpBasicLocAnalyzer
10
+ from metripy.LangAnalyzer.Php.PhpHalSteadAnalyzer import PhpHalSteadAnalyzer
11
+ from metripy.Metric.Code.FileMetrics import FileMetrics
12
+ from metripy.Tree.ClassNode import ClassNode
13
+ from metripy.Tree.FunctionNode import FunctionNode
14
+ from metripy.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