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,65 @@
|
|
|
1
|
+
from typing import Self
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class SegmentedMetrics:
|
|
5
|
+
def __init__(self):
|
|
6
|
+
self.good = 0
|
|
7
|
+
self.ok = 0
|
|
8
|
+
self.warning = 0
|
|
9
|
+
self.critical = 0
|
|
10
|
+
|
|
11
|
+
def to_dict(self) -> dict:
|
|
12
|
+
return {
|
|
13
|
+
"good": self.good,
|
|
14
|
+
"ok": self.ok,
|
|
15
|
+
"warning": self.warning,
|
|
16
|
+
"critical": self.critical,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
def set_loc(self, values: list[int]) -> Self:
|
|
20
|
+
for value in values:
|
|
21
|
+
if value <= 200:
|
|
22
|
+
self.good += 1
|
|
23
|
+
elif value <= 500:
|
|
24
|
+
self.ok += 1
|
|
25
|
+
elif value <= 1000:
|
|
26
|
+
self.warning += 1
|
|
27
|
+
else:
|
|
28
|
+
self.critical += 1
|
|
29
|
+
return self
|
|
30
|
+
|
|
31
|
+
def set_complexity(self, values: list[int]) -> Self:
|
|
32
|
+
for value in values:
|
|
33
|
+
if value <= 5:
|
|
34
|
+
self.good += 1
|
|
35
|
+
elif value <= 10:
|
|
36
|
+
self.ok += 1
|
|
37
|
+
elif value <= 20:
|
|
38
|
+
self.warning += 1
|
|
39
|
+
else:
|
|
40
|
+
self.critical += 1
|
|
41
|
+
return self
|
|
42
|
+
|
|
43
|
+
def set_maintainability(self, values: list[int]) -> Self:
|
|
44
|
+
for value in values:
|
|
45
|
+
if value <= 80:
|
|
46
|
+
self.critical += 1
|
|
47
|
+
elif value <= 60:
|
|
48
|
+
self.warning += 1
|
|
49
|
+
elif value <= 40:
|
|
50
|
+
self.ok += 1
|
|
51
|
+
else:
|
|
52
|
+
self.good += 1
|
|
53
|
+
return self
|
|
54
|
+
|
|
55
|
+
def set_method_size(self, values: list[int]) -> Self:
|
|
56
|
+
for value in values:
|
|
57
|
+
if value <= 15:
|
|
58
|
+
self.good += 1
|
|
59
|
+
elif value <= 30:
|
|
60
|
+
self.ok += 1
|
|
61
|
+
elif value <= 50:
|
|
62
|
+
self.warning += 1
|
|
63
|
+
else:
|
|
64
|
+
self.critical += 1
|
|
65
|
+
return self
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from typing import Self
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class FileTree:
|
|
5
|
+
def __init__(self, name: str, full_name: str, children: list[Self] | None = None):
|
|
6
|
+
self.name = name
|
|
7
|
+
self.full_name = full_name
|
|
8
|
+
self.children: list[Self] = children if children is not None else []
|
|
9
|
+
|
|
10
|
+
def to_dict(self) -> dict:
|
|
11
|
+
return {
|
|
12
|
+
"name": self.name,
|
|
13
|
+
"full_name": self.full_name,
|
|
14
|
+
"children": [child.to_dict() for child in self.children],
|
|
15
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from Metric.FileTree.FileTree import FileTree
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class FileTreeParser:
|
|
5
|
+
@staticmethod
|
|
6
|
+
def parse(paths: list[str], shorten: bool = False) -> FileTree:
|
|
7
|
+
root = FileTree(".", ".")
|
|
8
|
+
|
|
9
|
+
for path in paths:
|
|
10
|
+
parts = path.strip("./").split("/")
|
|
11
|
+
current = root
|
|
12
|
+
|
|
13
|
+
for part in parts:
|
|
14
|
+
# Check if part already exists in current children
|
|
15
|
+
found = next(
|
|
16
|
+
(child for child in current.children if child.name == part), None
|
|
17
|
+
)
|
|
18
|
+
if not found:
|
|
19
|
+
found = FileTree(part, path)
|
|
20
|
+
current.children.append(found)
|
|
21
|
+
current = found
|
|
22
|
+
|
|
23
|
+
if shorten:
|
|
24
|
+
FileTreeParser._shorten_tree(root)
|
|
25
|
+
|
|
26
|
+
return root
|
|
27
|
+
|
|
28
|
+
@staticmethod
|
|
29
|
+
def _shorten_tree(node: FileTree):
|
|
30
|
+
"""shorten tree nodes that only have a single child"""
|
|
31
|
+
while len(node.children) == 1:
|
|
32
|
+
child = node.children[0]
|
|
33
|
+
node.name += "/" + child.name
|
|
34
|
+
node.full_name = child.full_name
|
|
35
|
+
node.children = child.children
|
|
36
|
+
|
|
37
|
+
for child in node.children:
|
|
38
|
+
FileTreeParser._shorten_tree(child)
|
|
39
|
+
|
|
40
|
+
# print(json.dumps(root.to_dict(), indent=4))
|
|
41
|
+
# exit()
|
|
42
|
+
# return root
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
class GitCodeHotspot:
|
|
2
|
+
|
|
3
|
+
def __init__(self, file_path: str, changes_count: int, contributors_count: int):
|
|
4
|
+
self.file_path = file_path
|
|
5
|
+
self.changes_count = changes_count
|
|
6
|
+
self.contributors_count = contributors_count
|
|
7
|
+
self.risk_level = self._calc_risk_level(changes_count, contributors_count)
|
|
8
|
+
self.risk_label = self._label_from_risk_level()
|
|
9
|
+
|
|
10
|
+
def _calc_risk_level(self, changes_count: int, contributors_count: int) -> str:
|
|
11
|
+
if changes_count < 10:
|
|
12
|
+
if contributors_count < 5:
|
|
13
|
+
return "low"
|
|
14
|
+
else:
|
|
15
|
+
return "medium"
|
|
16
|
+
elif changes_count < 50:
|
|
17
|
+
if contributors_count < 5:
|
|
18
|
+
return "medium"
|
|
19
|
+
else:
|
|
20
|
+
return "high"
|
|
21
|
+
else:
|
|
22
|
+
if contributors_count < 1:
|
|
23
|
+
return "medium"
|
|
24
|
+
else:
|
|
25
|
+
return "high"
|
|
26
|
+
|
|
27
|
+
def _label_from_risk_level(self) -> str:
|
|
28
|
+
return self.risk_level.capitalize()
|
|
29
|
+
|
|
30
|
+
def to_dict(self) -> dict[str, str]:
|
|
31
|
+
return {
|
|
32
|
+
"file_path": self.file_path,
|
|
33
|
+
"changes_count": self.changes_count,
|
|
34
|
+
"risk_level": self.risk_level,
|
|
35
|
+
"risk_label": self.risk_label,
|
|
36
|
+
"contributors_count": self.contributors_count,
|
|
37
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
class GitContributor:
|
|
2
|
+
def __init__(
|
|
3
|
+
self,
|
|
4
|
+
name: str,
|
|
5
|
+
commits_count: int,
|
|
6
|
+
lines_added: int,
|
|
7
|
+
lines_removed: int,
|
|
8
|
+
contribution_percentage: int,
|
|
9
|
+
):
|
|
10
|
+
self.name = name
|
|
11
|
+
self.initials = self._get_initials(name)
|
|
12
|
+
self.commits_count = commits_count
|
|
13
|
+
self.lines_added = lines_added
|
|
14
|
+
self.lines_removed = lines_removed
|
|
15
|
+
self.contribution_percentage = contribution_percentage
|
|
16
|
+
|
|
17
|
+
def _get_initials(self, name: str) -> str:
|
|
18
|
+
try:
|
|
19
|
+
parts = name.split()
|
|
20
|
+
if len(parts) >= 2:
|
|
21
|
+
return (parts[0][0] + parts[1][0]).upper()
|
|
22
|
+
elif len(parts) == 1:
|
|
23
|
+
return parts[0][:2].upper()
|
|
24
|
+
else:
|
|
25
|
+
return "UN"
|
|
26
|
+
except Exception:
|
|
27
|
+
return "UN"
|
|
28
|
+
|
|
29
|
+
def to_dict(self) -> dict[str, int]:
|
|
30
|
+
return {
|
|
31
|
+
"name": self.name,
|
|
32
|
+
"initials": self.initials,
|
|
33
|
+
"commits_count": self.commits_count,
|
|
34
|
+
"lines_added": self.lines_added,
|
|
35
|
+
"lines_removed": self.lines_removed,
|
|
36
|
+
"contribution_percentage": self.contribution_percentage,
|
|
37
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
class GitKnowledgeSilo:
|
|
2
|
+
def __init__(self, file_path: str, owner: str, commits_count: int):
|
|
3
|
+
self.file_path = file_path
|
|
4
|
+
self.owner = owner
|
|
5
|
+
self.commits_count = commits_count
|
|
6
|
+
self.risk_level = self._calc_risk_level(commits_count)
|
|
7
|
+
self.risk_label = self._calc_risk_label(self.risk_level)
|
|
8
|
+
|
|
9
|
+
def _calc_risk_level(self, commits_count: int) -> str:
|
|
10
|
+
if commits_count >= 15:
|
|
11
|
+
return "high"
|
|
12
|
+
elif commits_count >= 8:
|
|
13
|
+
return "medium"
|
|
14
|
+
else:
|
|
15
|
+
return "low"
|
|
16
|
+
|
|
17
|
+
def _calc_risk_label(self, risk_level: str) -> str:
|
|
18
|
+
return risk_level.capitalize()
|
|
19
|
+
|
|
20
|
+
def to_dict(self) -> dict[str, int]:
|
|
21
|
+
return {
|
|
22
|
+
"file_path": f"{self.file_path}",
|
|
23
|
+
"owner": f"{self.owner}",
|
|
24
|
+
"commits_count": f"{self.commits_count}",
|
|
25
|
+
"risk_level": f"{self.risk_level}",
|
|
26
|
+
"risk_label": f"{self.risk_label}",
|
|
27
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
from Metric.Git.GitCodeHotspot import GitCodeHotspot
|
|
2
|
+
from Metric.Git.GitContributor import GitContributor
|
|
3
|
+
from Metric.Git.GitKnowledgeSilo import GitKnowledgeSilo
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class GitMetrics:
|
|
7
|
+
def __init__(
|
|
8
|
+
self,
|
|
9
|
+
analysis_start_date: str,
|
|
10
|
+
commit_stats_per_month: dict[str, int],
|
|
11
|
+
churn_per_month: dict[str, dict[str, int]],
|
|
12
|
+
contributor_stats: dict[str, dict[str, int]],
|
|
13
|
+
file_contributors: dict[str, dict[str, int]],
|
|
14
|
+
):
|
|
15
|
+
self.analysis_start_date = analysis_start_date
|
|
16
|
+
self._commit_stats_per_month: dict[str, int] = commit_stats_per_month
|
|
17
|
+
self._churn_per_month: dict[str, dict[str, int]] = churn_per_month
|
|
18
|
+
self._contributor_stats = contributor_stats
|
|
19
|
+
self.total_commits = sum(data["commits"] for data in contributor_stats.values())
|
|
20
|
+
self.contributors: list[GitContributor] = self._calc_contributors(
|
|
21
|
+
contributor_stats
|
|
22
|
+
)
|
|
23
|
+
self.core_contributors, self.core_percentage = self._calc_core_contributors(
|
|
24
|
+
self.contributors
|
|
25
|
+
)
|
|
26
|
+
self.silos: list[GitKnowledgeSilo] = self._calc_silos(file_contributors)
|
|
27
|
+
self.hotspots: list[GitCodeHotspot] = self._calc_hotspots(file_contributors)
|
|
28
|
+
|
|
29
|
+
def get_analysis_start_date(self) -> str:
|
|
30
|
+
return self.analysis_start_date
|
|
31
|
+
|
|
32
|
+
def get_commit_stats_per_month(self) -> dict[str, int]:
|
|
33
|
+
return self._commit_stats_per_month
|
|
34
|
+
|
|
35
|
+
def get_churn_per_month(self) -> dict[str, dict[str, int]]:
|
|
36
|
+
return self._churn_per_month
|
|
37
|
+
|
|
38
|
+
def get_avg_commit_size(self) -> float:
|
|
39
|
+
return (
|
|
40
|
+
sum(
|
|
41
|
+
data["lines_added"] + data["lines_removed"]
|
|
42
|
+
for data in self._contributor_stats.values()
|
|
43
|
+
)
|
|
44
|
+
/ self.total_commits
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def get_contributors_dict(self) -> dict[str, dict[str, int]]:
|
|
48
|
+
return {
|
|
49
|
+
contributor.name: contributor.to_dict() for contributor in self.contributors
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
def get_contributors_list(self) -> list[dict[str, int]]:
|
|
53
|
+
return [contributor.to_dict() for contributor in self.contributors]
|
|
54
|
+
|
|
55
|
+
def get_hotspots_list(self) -> list[dict[str, int]]:
|
|
56
|
+
return [hotspot.to_dict() for hotspot in self.hotspots]
|
|
57
|
+
|
|
58
|
+
def get_silos_dict(self) -> dict[str, dict[str, int]]:
|
|
59
|
+
return {silo.file_path: silo.to_dict() for silo in self.silos}
|
|
60
|
+
|
|
61
|
+
def get_silos_list(self) -> list[dict[str, int]]:
|
|
62
|
+
return [silo.to_dict() for silo in self.silos]
|
|
63
|
+
|
|
64
|
+
def _calc_hotspots(
|
|
65
|
+
self, file_contributors: dict[str, dict[str, int]]
|
|
66
|
+
) -> list[GitCodeHotspot]:
|
|
67
|
+
hotspots = []
|
|
68
|
+
for file_path, data in file_contributors.items():
|
|
69
|
+
changes_count = data["commits"]
|
|
70
|
+
if changes_count < 10:
|
|
71
|
+
continue
|
|
72
|
+
hotspots.append(
|
|
73
|
+
GitCodeHotspot(
|
|
74
|
+
file_path=file_path,
|
|
75
|
+
changes_count=changes_count,
|
|
76
|
+
contributors_count=len(data["contributors"]),
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
return sorted(hotspots, key=lambda x: x.changes_count, reverse=True)
|
|
80
|
+
|
|
81
|
+
def _calc_contributors(
|
|
82
|
+
self, contributor_stats: dict[str, dict[str, int]]
|
|
83
|
+
) -> list[GitContributor]:
|
|
84
|
+
# determine top contributors
|
|
85
|
+
total_commits = self.total_commits
|
|
86
|
+
contributors = []
|
|
87
|
+
|
|
88
|
+
for name, data in contributor_stats.items():
|
|
89
|
+
percentage = (
|
|
90
|
+
(data["commits"] / total_commits * 100) if total_commits > 0 else 0
|
|
91
|
+
)
|
|
92
|
+
contributors.append(
|
|
93
|
+
GitContributor(
|
|
94
|
+
name=name,
|
|
95
|
+
commits_count=data["commits"],
|
|
96
|
+
lines_added=data["lines_added"],
|
|
97
|
+
lines_removed=data["lines_removed"],
|
|
98
|
+
contribution_percentage=int(percentage),
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
return sorted(contributors, key=lambda x: x.commits_count, reverse=True)
|
|
103
|
+
|
|
104
|
+
def _calc_core_contributors(
|
|
105
|
+
self, contributors: list[GitContributor]
|
|
106
|
+
) -> tuple[int, int]:
|
|
107
|
+
# Calculate core contributors (top contributors making up 67% of commits)
|
|
108
|
+
core_contributors = 0
|
|
109
|
+
core_percentage = 0
|
|
110
|
+
for contributor in contributors:
|
|
111
|
+
core_percentage += contributor.contribution_percentage
|
|
112
|
+
core_contributors += 1
|
|
113
|
+
if core_percentage >= 67:
|
|
114
|
+
break
|
|
115
|
+
core_percentage = min(67, core_percentage)
|
|
116
|
+
|
|
117
|
+
return core_contributors, core_percentage
|
|
118
|
+
|
|
119
|
+
def _calc_silos(
|
|
120
|
+
self, file_contributors: dict[str, dict[str, int]]
|
|
121
|
+
) -> list[GitKnowledgeSilo]:
|
|
122
|
+
silos = []
|
|
123
|
+
for file_path, data in file_contributors.items():
|
|
124
|
+
if len(data["contributors"]) > 1 or data["commits"] < 3:
|
|
125
|
+
continue
|
|
126
|
+
owner = list(data["contributors"])[0]
|
|
127
|
+
silos.append(
|
|
128
|
+
GitKnowledgeSilo(
|
|
129
|
+
file_path=file_path,
|
|
130
|
+
owner=owner,
|
|
131
|
+
commits_count=data["commits"],
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
return sorted(silos, key=lambda x: x.commits_count, reverse=True)
|
|
135
|
+
|
|
136
|
+
def to_dict(self) -> dict[str, any]:
|
|
137
|
+
return {
|
|
138
|
+
"analysis_start_date": self.analysis_start_date,
|
|
139
|
+
"avg_commit_size": f"{self.get_avg_commit_size():.2f}",
|
|
140
|
+
"commit_stats_per_month": self.get_commit_stats_per_month(),
|
|
141
|
+
"churn_per_month": self.get_churn_per_month(),
|
|
142
|
+
"total_commits": self.total_commits,
|
|
143
|
+
"active_contributors": len(self.contributors),
|
|
144
|
+
"contributors": self.get_contributors_dict(),
|
|
145
|
+
"core_contributors": self.core_contributors,
|
|
146
|
+
"core_percentage": self.core_percentage,
|
|
147
|
+
"silos": self.get_silos_list(),
|
|
148
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from Dependency.Dependency import Dependency
|
|
2
|
+
from Metric.Code.AggregatedMetrics import AggregatedMetrics
|
|
3
|
+
from Metric.Code.FileMetrics import FileMetrics
|
|
4
|
+
from Metric.Code.SegmentedMetrics import SegmentedMetrics
|
|
5
|
+
from Metric.Git.GitMetrics import GitMetrics
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ProjectMetrics:
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
file_metrics: list[FileMetrics],
|
|
12
|
+
git_metrics: GitMetrics | None,
|
|
13
|
+
dependencies: list[Dependency] | None,
|
|
14
|
+
):
|
|
15
|
+
self.file_metrics = file_metrics
|
|
16
|
+
self.git_metrics = git_metrics
|
|
17
|
+
self.dependencies = dependencies
|
|
18
|
+
self.total_code_metrics = self._compile_total_metrics(self.file_metrics)
|
|
19
|
+
|
|
20
|
+
def _compile_total_metrics(
|
|
21
|
+
self, file_metrics: list[FileMetrics]
|
|
22
|
+
) -> AggregatedMetrics:
|
|
23
|
+
files = 0
|
|
24
|
+
locs = []
|
|
25
|
+
avgCcPerFunctions = []
|
|
26
|
+
maintainabilityIndices = []
|
|
27
|
+
avgLocPerFunctions = []
|
|
28
|
+
for file_metric in file_metrics:
|
|
29
|
+
files += 1
|
|
30
|
+
locs.append(file_metric.loc)
|
|
31
|
+
avgCcPerFunctions.append(file_metric.avgCcPerFunction)
|
|
32
|
+
maintainabilityIndices.append(file_metric.maintainabilityIndex)
|
|
33
|
+
avgLocPerFunctions.append(file_metric.avgLocPerFunction)
|
|
34
|
+
|
|
35
|
+
if files == 0:
|
|
36
|
+
return AggregatedMetrics()
|
|
37
|
+
|
|
38
|
+
return AggregatedMetrics(
|
|
39
|
+
loc=sum(locs),
|
|
40
|
+
avgCcPerFunction=self._avg(avgCcPerFunctions),
|
|
41
|
+
maintainabilityIndex=self._avg(maintainabilityIndices),
|
|
42
|
+
avgLocPerFunction=self._avg(avgLocPerFunctions),
|
|
43
|
+
num_files=files,
|
|
44
|
+
segmented_loc=SegmentedMetrics().set_loc(locs),
|
|
45
|
+
segmented_complexity=SegmentedMetrics().set_complexity(avgCcPerFunctions),
|
|
46
|
+
segmented_maintainability=SegmentedMetrics().set_maintainability(
|
|
47
|
+
maintainabilityIndices
|
|
48
|
+
),
|
|
49
|
+
segmented_method_size=SegmentedMetrics().set_method_size(
|
|
50
|
+
avgLocPerFunctions
|
|
51
|
+
),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def _avg(self, items: list[float | int]) -> float:
|
|
55
|
+
return sum(items) / len(items)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from Application.Config.Config import Config
|
|
2
|
+
from Component.Output.CliOutput import CliOutput
|
|
3
|
+
from Metric.ProjectMetrics import ProjectMetrics
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Reporter:
|
|
7
|
+
def __init__(self, config: Config, output: CliOutput):
|
|
8
|
+
self.config = config
|
|
9
|
+
self.output = output
|
|
10
|
+
|
|
11
|
+
def generate(self, metrics: ProjectMetrics):
|
|
12
|
+
raise NotImplementedError("CSV metrics report is not yet implemented")
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import shutil
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
from py_template_engine import TemplateEngine
|
|
7
|
+
|
|
8
|
+
from Application.Config.ReportConfig import ReportConfig
|
|
9
|
+
from Component.Output.CliOutput import CliOutput
|
|
10
|
+
from Metric.FileTree.FileTreeParser import FileTreeParser
|
|
11
|
+
from Metric.ProjectMetrics import ProjectMetrics
|
|
12
|
+
from Report.ReporterInterface import ReporterInterface
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Reporter(ReporterInterface):
|
|
16
|
+
def __init__(
|
|
17
|
+
self, config: ReportConfig, output: CliOutput, project_name: str = "foobar"
|
|
18
|
+
):
|
|
19
|
+
self.config: ReportConfig = config
|
|
20
|
+
self.output = output
|
|
21
|
+
self.template_dir = os.path.join(os.getcwd(), "templates/html_report")
|
|
22
|
+
|
|
23
|
+
self.global_template_args = {
|
|
24
|
+
"project_name": project_name,
|
|
25
|
+
"last_updated": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
def generate(self, metrics: ProjectMetrics):
|
|
29
|
+
|
|
30
|
+
self.output.writeln("<info>Generating HTML report...</info>")
|
|
31
|
+
|
|
32
|
+
# copy sources
|
|
33
|
+
if not os.path.exists(os.path.join(self.config.path, "js")):
|
|
34
|
+
os.makedirs(os.path.join(self.config.path, "js"))
|
|
35
|
+
if not os.path.exists(os.path.join(self.config.path, "css")):
|
|
36
|
+
os.makedirs(os.path.join(self.config.path, "css"))
|
|
37
|
+
if not os.path.exists(os.path.join(self.config.path, "images")):
|
|
38
|
+
os.makedirs(os.path.join(self.config.path, "images"))
|
|
39
|
+
if not os.path.exists(os.path.join(self.config.path, "fonts")):
|
|
40
|
+
os.makedirs(os.path.join(self.config.path, "fonts"))
|
|
41
|
+
if not os.path.exists(os.path.join(self.config.path, "data")):
|
|
42
|
+
os.makedirs(os.path.join(self.config.path, "data"))
|
|
43
|
+
|
|
44
|
+
# shutil.copy(os.path.join(self.template_dir, "favicon.ico"), os.path.join(self.config.path, "favicon.ico"))
|
|
45
|
+
|
|
46
|
+
shutil.copytree(
|
|
47
|
+
os.path.join(self.template_dir, "js"),
|
|
48
|
+
os.path.join(self.config.path, "js"),
|
|
49
|
+
dirs_exist_ok=True,
|
|
50
|
+
)
|
|
51
|
+
shutil.copytree(
|
|
52
|
+
os.path.join(self.template_dir, "css"),
|
|
53
|
+
os.path.join(self.config.path, "css"),
|
|
54
|
+
dirs_exist_ok=True,
|
|
55
|
+
)
|
|
56
|
+
# shutil.copytree(os.path.join(self.template_dir, "images"), os.path.join(self.config.path, "images"), dirs_exist_ok=True)
|
|
57
|
+
# shutil.copytree(os.path.join(self.template_dir, "fonts"), os.path.join(self.config.path, "fonts"), dirs_exist_ok=True)
|
|
58
|
+
|
|
59
|
+
# render templates
|
|
60
|
+
git_stats_data = {}
|
|
61
|
+
if metrics.git_metrics:
|
|
62
|
+
git_stats_data = metrics.git_metrics.get_commit_stats_per_month()
|
|
63
|
+
|
|
64
|
+
self.output.writeln("<info>Rendering index page</info>")
|
|
65
|
+
# Render main index page
|
|
66
|
+
self.render_template(
|
|
67
|
+
"index.html",
|
|
68
|
+
{
|
|
69
|
+
"git_stats_data": json.dumps(git_stats_data, indent=4),
|
|
70
|
+
"total_code_metrics": metrics.total_code_metrics.to_dict(),
|
|
71
|
+
"segmentation_data": json.dumps(
|
|
72
|
+
metrics.total_code_metrics.to_dict_segmentation(), indent=4
|
|
73
|
+
),
|
|
74
|
+
"project_name": "CodeMetrics",
|
|
75
|
+
"last_updated": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
76
|
+
"date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
77
|
+
"author": "CodeMetrics",
|
|
78
|
+
"version": "1.0.0",
|
|
79
|
+
},
|
|
80
|
+
)
|
|
81
|
+
self.output.writeln("<success>Done rendering index page</success>")
|
|
82
|
+
|
|
83
|
+
# Render files page
|
|
84
|
+
self.render_files_page(metrics)
|
|
85
|
+
# Render git analysis page
|
|
86
|
+
self.render_git_analysis_page(metrics)
|
|
87
|
+
|
|
88
|
+
self.render_dependencies_page(metrics)
|
|
89
|
+
|
|
90
|
+
self.output.writeln(
|
|
91
|
+
f"<success>HTML report generated in {self.config.path} directory</success>"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def render_template(self, template_name: str, data: dict) -> str:
|
|
95
|
+
engine = TemplateEngine(os.path.join(self.template_dir, template_name))
|
|
96
|
+
content = engine.render(**data)
|
|
97
|
+
with open(os.path.join(self.config.path, template_name), "w") as file:
|
|
98
|
+
file.write(content)
|
|
99
|
+
|
|
100
|
+
def render_dependencies_page(self, metrics: ProjectMetrics):
|
|
101
|
+
"""Render the dependencies page with dependency details and stats"""
|
|
102
|
+
if not metrics.dependencies:
|
|
103
|
+
self.output.writeln("<success>No dependencies to render</success>")
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
self.output.writeln("<info>Rendering dependencies page</info>")
|
|
107
|
+
|
|
108
|
+
dependencies = metrics.dependencies if metrics.dependencies is not None else []
|
|
109
|
+
|
|
110
|
+
license_by_type = {}
|
|
111
|
+
|
|
112
|
+
# TODO render a pie chart
|
|
113
|
+
for dependency in dependencies:
|
|
114
|
+
for license_name in dependency.license:
|
|
115
|
+
if license_name not in license_by_type.keys():
|
|
116
|
+
license_by_type[license_name] = 0
|
|
117
|
+
license_by_type[license_name] += 1
|
|
118
|
+
|
|
119
|
+
print(json.dumps(license_by_type, indent=2))
|
|
120
|
+
|
|
121
|
+
self.render_template(
|
|
122
|
+
"dependencies.html",
|
|
123
|
+
{
|
|
124
|
+
"dependencies": [d.to_dict() for d in dependencies],
|
|
125
|
+
"project_name": "CodeMetrics",
|
|
126
|
+
"last_updated": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
127
|
+
},
|
|
128
|
+
)
|
|
129
|
+
self.output.writeln(
|
|
130
|
+
"<success>Dependencies page generated successfully</success>"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
def render_files_page(self, metrics: ProjectMetrics):
|
|
134
|
+
"""Render the files page with file details and analysis"""
|
|
135
|
+
self.output.writeln("<info>Rendering files page</info>")
|
|
136
|
+
|
|
137
|
+
file_names = []
|
|
138
|
+
file_details = {}
|
|
139
|
+
for file_metrics in metrics.file_metrics:
|
|
140
|
+
file_name = file_metrics.full_name
|
|
141
|
+
file_details[file_name] = file_metrics.to_dict()
|
|
142
|
+
file_names.append(file_name)
|
|
143
|
+
|
|
144
|
+
filetree = FileTreeParser.parse(file_names, shorten=True)
|
|
145
|
+
|
|
146
|
+
self.render_template(
|
|
147
|
+
"files.html",
|
|
148
|
+
{
|
|
149
|
+
"filetree": json.dumps(filetree.to_dict()),
|
|
150
|
+
"file_details": json.dumps(file_details),
|
|
151
|
+
"project_name": "CodeMetrics",
|
|
152
|
+
"last_updated": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
153
|
+
},
|
|
154
|
+
)
|
|
155
|
+
self.output.writeln("<success>Files page generated successfully</success>")
|
|
156
|
+
|
|
157
|
+
def render_git_analysis_page(self, metrics: ProjectMetrics):
|
|
158
|
+
"""Render the git analysis page with comprehensive git data"""
|
|
159
|
+
if not metrics.git_metrics:
|
|
160
|
+
self.output.writeln("<success>No git metrics to render</success>")
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
def stringify_values(obj):
|
|
164
|
+
if isinstance(obj, dict):
|
|
165
|
+
return {key: stringify_values(value) for key, value in obj.items()}
|
|
166
|
+
elif isinstance(obj, list):
|
|
167
|
+
return [stringify_values(item) for item in obj]
|
|
168
|
+
else:
|
|
169
|
+
return str(obj)
|
|
170
|
+
|
|
171
|
+
self.output.writeln("<info>Rendering git analysis page</info>")
|
|
172
|
+
try:
|
|
173
|
+
# Render git analysis template
|
|
174
|
+
self.render_template(
|
|
175
|
+
"git_analysis.html",
|
|
176
|
+
stringify_values(
|
|
177
|
+
{
|
|
178
|
+
"git_analysis": metrics.git_metrics.to_dict(),
|
|
179
|
+
"git_analysis_json": json.dumps(
|
|
180
|
+
metrics.git_metrics.get_contributors_dict(), indent=4
|
|
181
|
+
),
|
|
182
|
+
"git_stats_data": json.dumps(
|
|
183
|
+
metrics.git_metrics.get_commit_stats_per_month(), indent=4
|
|
184
|
+
), # git commit graph
|
|
185
|
+
"git_churn_data": json.dumps(
|
|
186
|
+
metrics.git_metrics.get_churn_per_month(), indent=4
|
|
187
|
+
), # git chrun graph
|
|
188
|
+
"git_silos_data": metrics.git_metrics.get_silos_list()[
|
|
189
|
+
:10
|
|
190
|
+
], # silos list
|
|
191
|
+
"git_contributors": metrics.git_metrics.get_contributors_list()[
|
|
192
|
+
:10
|
|
193
|
+
], # contributors list
|
|
194
|
+
"git_hotspots_data": metrics.git_metrics.get_hotspots_list()[
|
|
195
|
+
:10
|
|
196
|
+
], # hotspots list
|
|
197
|
+
"project_name": "CodeMetrics",
|
|
198
|
+
"last_updated": metrics.git_metrics.get_analysis_start_date(),
|
|
199
|
+
}
|
|
200
|
+
),
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
self.output.writeln(
|
|
204
|
+
"<success>Git analysis page generated successfully</success>"
|
|
205
|
+
)
|
|
206
|
+
except Exception as e:
|
|
207
|
+
raise e
|
|
208
|
+
self.output.writeln(
|
|
209
|
+
f"<error>Error generating git analysis page: {e}</error>"
|
|
210
|
+
)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from Report.ReporterInterface import ReporterInterface
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AbstractJsonReporter(ReporterInterface):
|
|
7
|
+
def put_data(self, data: dict) -> None:
|
|
8
|
+
os.makedirs(os.path.dirname(self.config.path), exist_ok=True)
|
|
9
|
+
with open(self.config.path, "w") as file:
|
|
10
|
+
json.dump(data, file, indent=2)
|