metripy 0.2.7__py3-none-any.whl → 0.3.6__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/Application/Analyzer.py +23 -3
- metripy/Application/Application.py +16 -2
- metripy/Application/Config/Config.py +34 -0
- metripy/Application/Config/File/ConfigFileReaderFactory.py +6 -5
- metripy/Application/Config/File/ConfigFileReaderInterface.py +70 -3
- metripy/Application/Config/File/JsonConfigFileReader.py +5 -70
- metripy/Application/Config/File/YamlConfigFileReader.py +17 -0
- metripy/Application/Config/Parser.py +24 -11
- metripy/Application/Config/ProjectConfig.py +64 -0
- metripy/Application/Info.py +61 -0
- metripy/Dependency/Dependency.py +17 -1
- metripy/Dependency/Pip/Pip.py +21 -31
- metripy/Dependency/Pip/PyPi.py +1 -0
- metripy/Git/GitAnalyzer.py +0 -3
- metripy/Import/Json/JsonImporter.py +17 -0
- metripy/LangAnalyzer/AbstractLangAnalyzer.py +4 -3
- metripy/LangAnalyzer/Php/PhpAnalyzer.py +2 -1
- metripy/LangAnalyzer/Python/PythonAnalyzer.py +31 -9
- metripy/LangAnalyzer/Python/PythonHalSteadAnalyzer.py +55 -0
- metripy/LangAnalyzer/Typescript/TypescriptAnalyzer.py +12 -9
- metripy/LangAnalyzer/Typescript/TypescriptAstParser.py +1 -1
- metripy/Metric/Code/AggregatedMetrics.py +12 -5
- metripy/Metric/Code/FileMetrics.py +32 -1
- metripy/Metric/Code/ModuleMetrics.py +5 -5
- metripy/Metric/Code/SegmentedMetrics.py +72 -36
- metripy/Metric/Code/Segmentor.py +44 -0
- metripy/Metric/FileTree/FileTreeParser.py +0 -4
- metripy/Metric/Git/GitMetrics.py +1 -1
- metripy/Metric/ProjectMetrics.py +29 -0
- metripy/Metric/Trend/AggregatedTrendMetric.py +101 -0
- metripy/Metric/Trend/ClassTrendMetric.py +20 -0
- metripy/Metric/Trend/FileTrendMetric.py +46 -0
- metripy/Metric/Trend/FunctionTrendMetric.py +28 -0
- metripy/Metric/Trend/SegmentedTrendMetric.py +29 -0
- metripy/Report/Html/DependencyPageRenderer.py +21 -0
- metripy/Report/Html/FilesPageRenderer.py +28 -0
- metripy/Report/Html/GitAnalysisPageRenderer.py +55 -0
- metripy/Report/Html/IndexPageRenderer.py +47 -0
- metripy/Report/Html/PageRenderer.py +43 -0
- metripy/Report/Html/PageRendererFactory.py +37 -0
- metripy/Report/Html/Reporter.py +78 -137
- metripy/Report/Html/TopOffendersPageRenderer.py +84 -0
- metripy/Report/Html/TrendsPageRenderer.py +137 -0
- metripy/Report/Json/GitJsonReporter.py +3 -0
- metripy/Report/Json/JsonReporter.py +6 -2
- metripy/Report/ReporterFactory.py +6 -3
- metripy/Tree/ClassNode.py +21 -0
- metripy/Tree/FunctionNode.py +66 -1
- metripy/Trend/TrendAnalyzer.py +150 -0
- metripy/templates/html_report/css/styles.css +1386 -0
- metripy/templates/html_report/dependencies.html +411 -0
- metripy/templates/html_report/files.html +1080 -0
- metripy/templates/html_report/git_analysis.html +325 -0
- metripy/templates/html_report/images/logo.svg +31 -0
- metripy/templates/html_report/index.html +374 -0
- metripy/templates/html_report/js/charts.js +313 -0
- metripy/templates/html_report/js/dashboard.js +546 -0
- metripy/templates/html_report/js/git_analysis.js +383 -0
- metripy/templates/html_report/top_offenders.html +267 -0
- metripy/templates/html_report/trends.html +468 -0
- {metripy-0.2.7.dist-info → metripy-0.3.6.dist-info}/METADATA +27 -9
- metripy-0.3.6.dist-info/RECORD +96 -0
- {metripy-0.2.7.dist-info → metripy-0.3.6.dist-info}/licenses/LICENSE +1 -1
- metripy-0.2.7.dist-info/RECORD +0 -66
- {metripy-0.2.7.dist-info → metripy-0.3.6.dist-info}/WHEEL +0 -0
- {metripy-0.2.7.dist-info → metripy-0.3.6.dist-info}/entry_points.txt +0 -0
- {metripy-0.2.7.dist-info → metripy-0.3.6.dist-info}/top_level.txt +0 -0
metripy/Report/Html/Reporter.py
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import json
|
|
2
1
|
import os
|
|
3
2
|
import shutil
|
|
3
|
+
import sys
|
|
4
4
|
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
5
6
|
|
|
6
|
-
from py_template_engine import TemplateEngine
|
|
7
7
|
|
|
8
8
|
from metripy.Application.Config.ReportConfig import ReportConfig
|
|
9
9
|
from metripy.Component.Output.CliOutput import CliOutput
|
|
10
|
-
from metripy.Metric.FileTree.FileTreeParser import FileTreeParser
|
|
11
10
|
from metripy.Metric.ProjectMetrics import ProjectMetrics
|
|
11
|
+
from metripy.Report.Html.PageRendererFactory import PageRendererFactory
|
|
12
12
|
from metripy.Report.ReporterInterface import ReporterInterface
|
|
13
13
|
|
|
14
14
|
|
|
@@ -18,12 +18,48 @@ class Reporter(ReporterInterface):
|
|
|
18
18
|
):
|
|
19
19
|
self.config: ReportConfig = config
|
|
20
20
|
self.output = output
|
|
21
|
-
|
|
21
|
+
|
|
22
|
+
# Find templates directory - works both in development and when installed
|
|
23
|
+
template_dir = self._find_template_dir()
|
|
24
|
+
if not template_dir.exists():
|
|
25
|
+
raise FileNotFoundError(
|
|
26
|
+
f"Could not find templates directory. Searched in: "
|
|
27
|
+
f"{template_dir}"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
self.template_dir = str(template_dir)
|
|
31
|
+
self.project_name = project_name
|
|
32
|
+
|
|
33
|
+
self.page_renderer_factory = PageRendererFactory(
|
|
34
|
+
self.template_dir, self.config.path, self.project_name
|
|
35
|
+
)
|
|
22
36
|
|
|
23
37
|
self.global_template_args = {
|
|
24
38
|
"project_name": project_name,
|
|
25
39
|
"last_updated": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
26
40
|
}
|
|
41
|
+
|
|
42
|
+
def _find_template_dir(self) -> Path:
|
|
43
|
+
"""Find the templates directory, checking multiple possible locations"""
|
|
44
|
+
package_dir = Path(__file__).parent.parent.parent # metripy package root
|
|
45
|
+
|
|
46
|
+
# List of possible locations to check
|
|
47
|
+
possible_locations = [
|
|
48
|
+
# Development: templates at project root
|
|
49
|
+
package_dir.parent / "templates" / "html_report",
|
|
50
|
+
# Alternative: templates inside metripy package
|
|
51
|
+
package_dir / "templates" / "html_report",
|
|
52
|
+
# System install location
|
|
53
|
+
Path(sys.prefix) / "share" / "metripy" / "templates" / "html_report",
|
|
54
|
+
# Fallback to cwd (for development)
|
|
55
|
+
Path.cwd() / "metripy" / "templates" / "html_report",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
for location in possible_locations:
|
|
59
|
+
if location.exists() and (location / "index.html").exists():
|
|
60
|
+
return location
|
|
61
|
+
|
|
62
|
+
return possible_locations[0]
|
|
27
63
|
|
|
28
64
|
def generate(self, metrics: ProjectMetrics):
|
|
29
65
|
|
|
@@ -53,158 +89,63 @@ class Reporter(ReporterInterface):
|
|
|
53
89
|
os.path.join(self.config.path, "css"),
|
|
54
90
|
dirs_exist_ok=True,
|
|
55
91
|
)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
},
|
|
92
|
+
shutil.copytree(
|
|
93
|
+
os.path.join(self.template_dir, "images"),
|
|
94
|
+
os.path.join(self.config.path, "images"),
|
|
95
|
+
dirs_exist_ok=True,
|
|
80
96
|
)
|
|
81
|
-
|
|
97
|
+
# shutil.copytree(os.path.join(self.template_dir, "fonts"), os.path.join(self.config.path, "fonts"), dirs_exist_ok=True)
|
|
82
98
|
|
|
83
|
-
# Render
|
|
99
|
+
# Render main pages
|
|
100
|
+
self.render_index_page(metrics)
|
|
84
101
|
self.render_files_page(metrics)
|
|
85
|
-
|
|
102
|
+
self.render_top_offenders_page(metrics)
|
|
86
103
|
self.render_git_analysis_page(metrics)
|
|
87
|
-
|
|
88
104
|
self.render_dependencies_page(metrics)
|
|
105
|
+
self.render_trends_page(metrics)
|
|
89
106
|
|
|
90
107
|
self.output.writeln(
|
|
91
108
|
f"<success>HTML report generated in {self.config.path} directory</success>"
|
|
92
109
|
)
|
|
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
110
|
self.output.writeln(
|
|
130
|
-
"<success>
|
|
111
|
+
f"<success>Open HTML report: {self.config.path}/index.html</success>"
|
|
131
112
|
)
|
|
132
113
|
|
|
114
|
+
def render_index_page(self, metrics: ProjectMetrics):
|
|
115
|
+
self.output.writeln("<info>Rendering index page</info>")
|
|
116
|
+
self.page_renderer_factory.create_index_page_renderer().render(metrics)
|
|
117
|
+
self.output.writeln("<success>Done rendering index page</success>")
|
|
118
|
+
|
|
133
119
|
def render_files_page(self, metrics: ProjectMetrics):
|
|
134
120
|
"""Render the files page with file details and analysis"""
|
|
135
121
|
self.output.writeln("<info>Rendering files page</info>")
|
|
122
|
+
self.page_renderer_factory.create_files_page_renderer().render(metrics)
|
|
123
|
+
self.output.writeln("<success>Files page generated successfully</success>")
|
|
136
124
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
},
|
|
125
|
+
def render_top_offenders_page(self, metrics: ProjectMetrics):
|
|
126
|
+
self.output.writeln("<info>Rendering top offenders page</info>")
|
|
127
|
+
self.page_renderer_factory.create_top_offenders_page_renderer().render(metrics)
|
|
128
|
+
self.output.writeln(
|
|
129
|
+
"<success>Top offenders page generated successfully</success>"
|
|
154
130
|
)
|
|
155
|
-
self.output.writeln("<success>Files page generated successfully</success>")
|
|
156
131
|
|
|
157
132
|
def render_git_analysis_page(self, metrics: ProjectMetrics):
|
|
158
133
|
"""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
134
|
self.output.writeln("<info>Rendering git analysis page</info>")
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
)
|
|
135
|
+
self.page_renderer_factory.create_git_analysis_page_renderer().render(metrics)
|
|
136
|
+
self.output.writeln(
|
|
137
|
+
"<success>Git analysis page generated successfully</success>"
|
|
138
|
+
)
|
|
202
139
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
140
|
+
def render_dependencies_page(self, metrics: ProjectMetrics):
|
|
141
|
+
"""Render the dependencies page with dependency details and stats"""
|
|
142
|
+
self.output.writeln("<info>Rendering dependencies page</info>")
|
|
143
|
+
self.page_renderer_factory.create_dependency_page_renderer().render(metrics)
|
|
144
|
+
self.output.writeln(
|
|
145
|
+
"<success>Dependencies page generated successfully</success>"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
def render_trends_page(self, metrics: ProjectMetrics):
|
|
149
|
+
self.output.writeln("<info>Rendering trends page</info>")
|
|
150
|
+
self.page_renderer_factory.create_trends_page_renderer().render(metrics)
|
|
151
|
+
self.output.writeln("<success>Trends page generated successfully</success>")
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from metripy.Metric.Code.Segmentor import Segmentor
|
|
2
|
+
from metripy.Metric.ProjectMetrics import ProjectMetrics
|
|
3
|
+
from metripy.Report.Html.PageRenderer import PageRenderer
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TopOffendersPageRenderer(PageRenderer):
|
|
7
|
+
def __init__(self, template_dir: str, output_dir: str, project_name: str):
|
|
8
|
+
super().__init__(template_dir, output_dir, project_name)
|
|
9
|
+
|
|
10
|
+
def render(self, metrics: ProjectMetrics):
|
|
11
|
+
orderedByTotalCc = sorted(
|
|
12
|
+
metrics.file_metrics, key=lambda x: x.totalCc, reverse=True
|
|
13
|
+
)[:10]
|
|
14
|
+
orderedByMI = sorted(
|
|
15
|
+
metrics.file_metrics, key=lambda x: x.maintainabilityIndex, reverse=False
|
|
16
|
+
)[:10]
|
|
17
|
+
orderedByLoc = sorted(metrics.file_metrics, key=lambda x: x.loc, reverse=True)[
|
|
18
|
+
:10
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
all_functions: list = []
|
|
22
|
+
for fm in metrics.file_metrics:
|
|
23
|
+
all_functions.extend(fm.function_nodes)
|
|
24
|
+
|
|
25
|
+
functionsOrderedByCc = sorted(
|
|
26
|
+
all_functions, key=lambda x: x.complexity, reverse=True
|
|
27
|
+
)[:10]
|
|
28
|
+
functionsOrderedByMi = sorted(
|
|
29
|
+
all_functions, key=lambda x: x.maintainability_index, reverse=False
|
|
30
|
+
)[:10]
|
|
31
|
+
functionsOrderedByLoc = sorted(
|
|
32
|
+
all_functions, key=lambda x: x.get_loc(), reverse=True
|
|
33
|
+
)[:10]
|
|
34
|
+
|
|
35
|
+
# TODO maintainability index per function, we dont calc yet
|
|
36
|
+
|
|
37
|
+
self.render_template(
|
|
38
|
+
"top_offenders.html",
|
|
39
|
+
{
|
|
40
|
+
"file_loc_offenders": [
|
|
41
|
+
{**e.to_dict(), "status": Segmentor.get_loc_segment(e.loc)}
|
|
42
|
+
for e in orderedByLoc
|
|
43
|
+
],
|
|
44
|
+
"file_cc_offenders": [
|
|
45
|
+
{
|
|
46
|
+
**e.to_dict(),
|
|
47
|
+
"status": Segmentor.get_complexity_segment(e.totalCc),
|
|
48
|
+
}
|
|
49
|
+
for e in orderedByTotalCc
|
|
50
|
+
],
|
|
51
|
+
"file_mi_offenders": [
|
|
52
|
+
{
|
|
53
|
+
**e.to_dict(),
|
|
54
|
+
"status": Segmentor.get_maintainability_segment(
|
|
55
|
+
e.maintainabilityIndex
|
|
56
|
+
),
|
|
57
|
+
}
|
|
58
|
+
for e in orderedByMI
|
|
59
|
+
],
|
|
60
|
+
"function_size_offenders": [
|
|
61
|
+
{
|
|
62
|
+
**e.to_dict(),
|
|
63
|
+
"status": Segmentor.get_method_size_segment(e.get_loc()),
|
|
64
|
+
}
|
|
65
|
+
for e in functionsOrderedByLoc
|
|
66
|
+
],
|
|
67
|
+
"function_cc_offenders": [
|
|
68
|
+
{
|
|
69
|
+
**e.to_dict(),
|
|
70
|
+
"status": Segmentor.get_complexity_segment(e.complexity),
|
|
71
|
+
}
|
|
72
|
+
for e in functionsOrderedByCc
|
|
73
|
+
],
|
|
74
|
+
"function_mi_offenders": [
|
|
75
|
+
{
|
|
76
|
+
**e.to_dict(),
|
|
77
|
+
"status": Segmentor.get_maintainability_segment(
|
|
78
|
+
e.maintainability_index
|
|
79
|
+
),
|
|
80
|
+
}
|
|
81
|
+
for e in functionsOrderedByMi
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
)
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
from metripy.Metric.Code.FileMetrics import FileMetrics
|
|
2
|
+
from metripy.Metric.ProjectMetrics import ProjectMetrics
|
|
3
|
+
from metripy.Report.Html.PageRenderer import PageRenderer
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TrendsPageRenderer(PageRenderer):
|
|
7
|
+
def __init__(self, template_dir: str, output_dir: str, project_name: str):
|
|
8
|
+
super().__init__(template_dir, output_dir, project_name)
|
|
9
|
+
|
|
10
|
+
def _compile_trend_item(self, file: FileMetrics) -> dict:
|
|
11
|
+
return {
|
|
12
|
+
"name": file.full_name,
|
|
13
|
+
"path": file.full_name,
|
|
14
|
+
"complexity_current": file.totalCc,
|
|
15
|
+
"complexity_prev": round(file.trend.historical_totalCc, 2),
|
|
16
|
+
"complexity_delta": round(file.trend.totalCc_delta, 2),
|
|
17
|
+
"maintainability_current": round(file.maintainabilityIndex, 2),
|
|
18
|
+
"maintainability_prev": round(
|
|
19
|
+
file.trend.historical_maintainabilityIndex, 2
|
|
20
|
+
),
|
|
21
|
+
"maintainability_delta": round(file.trend.maintainabilityIndex_delta, 2),
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
def render(self, metrics: ProjectMetrics):
|
|
25
|
+
if metrics.total_code_metrics.trend is None:
|
|
26
|
+
self.render_template(
|
|
27
|
+
"trends.html",
|
|
28
|
+
{
|
|
29
|
+
"has_trend_data": False,
|
|
30
|
+
"trend_data": {
|
|
31
|
+
"top_improved_complexity": [],
|
|
32
|
+
"top_improved_maintainability": [],
|
|
33
|
+
"top_worsened_complexity": [],
|
|
34
|
+
"top_worsened_maintainability": [],
|
|
35
|
+
"loc_segments_current": {},
|
|
36
|
+
"loc_segments_prev": {},
|
|
37
|
+
"complexity_segments_current": {},
|
|
38
|
+
"complexity_segments_prev": {},
|
|
39
|
+
"maintainability_segments_current": {},
|
|
40
|
+
"maintainability_segments_prev": {},
|
|
41
|
+
"method_size_segments_current": {},
|
|
42
|
+
"method_size_segments_prev": {},
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
)
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
# Top improved complexity (complexity went down - negative delta)
|
|
49
|
+
top_improved_complexity = [
|
|
50
|
+
x
|
|
51
|
+
for x in metrics.file_metrics
|
|
52
|
+
if x.trend is not None and x.trend.totalCc_delta < 0
|
|
53
|
+
]
|
|
54
|
+
top_improved_complexity = sorted(
|
|
55
|
+
top_improved_complexity, key=lambda x: x.trend.totalCc_delta
|
|
56
|
+
)[:10]
|
|
57
|
+
|
|
58
|
+
# Top worsened complexity (complexity went up - positive delta)
|
|
59
|
+
top_worsened_complexity = [
|
|
60
|
+
x
|
|
61
|
+
for x in metrics.file_metrics
|
|
62
|
+
if x.trend is not None and x.trend.totalCc_delta > 0
|
|
63
|
+
]
|
|
64
|
+
top_worsened_complexity = sorted(
|
|
65
|
+
top_worsened_complexity, key=lambda x: x.trend.totalCc_delta, reverse=True
|
|
66
|
+
)[:10]
|
|
67
|
+
|
|
68
|
+
# Top improved maintainability (maintainability went up - positive delta)
|
|
69
|
+
top_improved_maintainability = [
|
|
70
|
+
x
|
|
71
|
+
for x in metrics.file_metrics
|
|
72
|
+
if x.trend is not None and round(x.trend.maintainabilityIndex_delta, 2) > 0
|
|
73
|
+
]
|
|
74
|
+
top_improved_maintainability = sorted(
|
|
75
|
+
top_improved_maintainability,
|
|
76
|
+
key=lambda x: x.trend.maintainabilityIndex_delta,
|
|
77
|
+
reverse=True,
|
|
78
|
+
)[:10]
|
|
79
|
+
|
|
80
|
+
# Top worsened maintainability (maintainability went down - negative delta)
|
|
81
|
+
top_worsened_maintainability = [
|
|
82
|
+
x
|
|
83
|
+
for x in metrics.file_metrics
|
|
84
|
+
if x.trend is not None and round(x.trend.maintainabilityIndex_delta, 2) < 0
|
|
85
|
+
]
|
|
86
|
+
top_worsened_maintainability = sorted(
|
|
87
|
+
top_worsened_maintainability,
|
|
88
|
+
key=lambda x: x.trend.maintainabilityIndex_delta,
|
|
89
|
+
)[:10]
|
|
90
|
+
|
|
91
|
+
trend_data = {
|
|
92
|
+
# Segment distributions for each metric
|
|
93
|
+
"loc_segments_current": metrics.total_code_metrics.segmentation_data[
|
|
94
|
+
"loc"
|
|
95
|
+
].to_dict_with_percent(),
|
|
96
|
+
"loc_segments_prev": metrics.total_code_metrics.trend.historical_segmentation_data[
|
|
97
|
+
"loc"
|
|
98
|
+
].to_dict_with_percent(),
|
|
99
|
+
"complexity_segments_current": metrics.total_code_metrics.segmentation_data[
|
|
100
|
+
"complexity"
|
|
101
|
+
].to_dict_with_percent(),
|
|
102
|
+
"complexity_segments_prev": metrics.total_code_metrics.trend.historical_segmentation_data[
|
|
103
|
+
"complexity"
|
|
104
|
+
].to_dict_with_percent(),
|
|
105
|
+
"maintainability_segments_current": metrics.total_code_metrics.segmentation_data[
|
|
106
|
+
"maintainability"
|
|
107
|
+
].to_dict_with_percent(),
|
|
108
|
+
"maintainability_segments_prev": metrics.total_code_metrics.trend.historical_segmentation_data[
|
|
109
|
+
"maintainability"
|
|
110
|
+
].to_dict_with_percent(),
|
|
111
|
+
"method_size_segments_current": metrics.total_code_metrics.segmentation_data[
|
|
112
|
+
"methodSize"
|
|
113
|
+
].to_dict_with_percent(),
|
|
114
|
+
"method_size_segments_prev": metrics.total_code_metrics.trend.historical_segmentation_data[
|
|
115
|
+
"methodSize"
|
|
116
|
+
].to_dict_with_percent(),
|
|
117
|
+
"top_improved_complexity": [
|
|
118
|
+
self._compile_trend_item(x) for x in top_improved_complexity
|
|
119
|
+
],
|
|
120
|
+
"top_improved_maintainability": [
|
|
121
|
+
self._compile_trend_item(x) for x in top_improved_maintainability
|
|
122
|
+
],
|
|
123
|
+
"top_worsened_complexity": [
|
|
124
|
+
self._compile_trend_item(x) for x in top_worsened_complexity
|
|
125
|
+
],
|
|
126
|
+
"top_worsened_maintainability": [
|
|
127
|
+
self._compile_trend_item(x) for x in top_worsened_maintainability
|
|
128
|
+
],
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
self.render_template(
|
|
132
|
+
"trends.html",
|
|
133
|
+
{
|
|
134
|
+
"has_trend_data": metrics.total_code_metrics.trend is not None,
|
|
135
|
+
"trend_data": trend_data,
|
|
136
|
+
},
|
|
137
|
+
)
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
from metripy.Application.Config.Config import Config
|
|
2
2
|
from metripy.Component.Output.CliOutput import CliOutput
|
|
3
3
|
from metripy.Metric.ProjectMetrics import ProjectMetrics
|
|
4
|
+
from metripy.Report.Json.AbstractJsonReporter import AbstractJsonReporter
|
|
4
5
|
|
|
5
6
|
|
|
6
|
-
class JsonReporter:
|
|
7
|
+
class JsonReporter(AbstractJsonReporter):
|
|
7
8
|
def __init__(self, config: Config, output: CliOutput):
|
|
8
9
|
self.config = config
|
|
9
10
|
self.output = output
|
|
10
11
|
|
|
11
12
|
def generate(self, metrics: ProjectMetrics):
|
|
12
|
-
|
|
13
|
+
self.put_data(metrics.to_dict())
|
|
14
|
+
self.output.writeln(
|
|
15
|
+
f"<success>Create json report in {self.config.path}</success>"
|
|
16
|
+
)
|
|
@@ -3,15 +3,18 @@ from metripy.Component.Output.CliOutput import CliOutput
|
|
|
3
3
|
from metripy.Report import ReporterInterface
|
|
4
4
|
from metripy.Report.Html.Reporter import Reporter as HtmlReporter
|
|
5
5
|
from metripy.Report.Json.GitJsonReporter import GitJsonReporter
|
|
6
|
+
from metripy.Report.Json.JsonReporter import JsonReporter
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
class ReporterFactory:
|
|
9
10
|
@staticmethod
|
|
10
|
-
def create(
|
|
11
|
+
def create(
|
|
12
|
+
config: ReportConfig, output: CliOutput, project_name: str
|
|
13
|
+
) -> ReporterInterface:
|
|
11
14
|
if config.type == "html":
|
|
12
|
-
return HtmlReporter(config, output)
|
|
15
|
+
return HtmlReporter(config, output, project_name)
|
|
13
16
|
elif config.type == "json":
|
|
14
|
-
|
|
17
|
+
return JsonReporter(config, output)
|
|
15
18
|
elif config.type == "csv":
|
|
16
19
|
raise NotImplementedError
|
|
17
20
|
elif config.type == "cli":
|
metripy/Tree/ClassNode.py
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
from typing import Self
|
|
2
|
+
|
|
3
|
+
from metripy.Metric.Code.Segmentor import Segmentor
|
|
4
|
+
from metripy.Metric.Trend.ClassTrendMetric import ClassTrendMetric
|
|
1
5
|
from metripy.Tree.FunctionNode import FunctionNode
|
|
2
6
|
|
|
3
7
|
|
|
@@ -17,6 +21,8 @@ class ClassNode:
|
|
|
17
21
|
self.real_complexity = real_complexity
|
|
18
22
|
self.functions: list[FunctionNode] = []
|
|
19
23
|
|
|
24
|
+
self.trend: ClassTrendMetric | None = None
|
|
25
|
+
|
|
20
26
|
def to_dict(self) -> dict:
|
|
21
27
|
"""Convert ClassNode to a dictionary for JSON serialization."""
|
|
22
28
|
return {
|
|
@@ -25,8 +31,23 @@ class ClassNode:
|
|
|
25
31
|
"lineno": self.lineno,
|
|
26
32
|
"col_offset": self.col_offset,
|
|
27
33
|
"real_complexity": self.real_complexity,
|
|
34
|
+
"complexity_segment": Segmentor.get_complexity_segment(
|
|
35
|
+
self.real_complexity
|
|
36
|
+
),
|
|
28
37
|
"functions": [func.to_dict() for func in self.functions],
|
|
29
38
|
}
|
|
30
39
|
|
|
31
40
|
def __dict__(self) -> dict:
|
|
32
41
|
return self.to_dict()
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def from_dict(data: dict) -> Self:
|
|
45
|
+
node = ClassNode(
|
|
46
|
+
full_name=data["full_name"],
|
|
47
|
+
name=data["name"],
|
|
48
|
+
lineno=data["lineno"],
|
|
49
|
+
col_offset=data["col_offset"],
|
|
50
|
+
real_complexity=data["real_complexity"],
|
|
51
|
+
)
|
|
52
|
+
node.functions = [FunctionNode.from_dict(d) for d in data["functions"]]
|
|
53
|
+
return node
|
metripy/Tree/FunctionNode.py
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
import math
|
|
2
|
+
from typing import Self
|
|
3
|
+
|
|
4
|
+
from metripy.Metric.Code.Segmentor import Segmentor
|
|
5
|
+
from metripy.Metric.Trend.FunctionTrendMetric import FunctionTrendMetric
|
|
6
|
+
|
|
7
|
+
|
|
1
8
|
class FunctionNode:
|
|
2
9
|
def __init__(
|
|
3
10
|
self, full_name: str, name: str, lineno: int, col_offset: int, complexity: int
|
|
@@ -21,6 +28,35 @@ class FunctionNode:
|
|
|
21
28
|
self.time = 0
|
|
22
29
|
self.bugs = 0
|
|
23
30
|
self.maintainability_index = 0
|
|
31
|
+
self.trend: FunctionTrendMetric | None = None
|
|
32
|
+
|
|
33
|
+
def get_loc(self) -> int:
|
|
34
|
+
return self.line_end - self.lineno
|
|
35
|
+
|
|
36
|
+
def calc_mi(self):
|
|
37
|
+
|
|
38
|
+
total_volume = self.volume
|
|
39
|
+
total_complexity = self.complexity
|
|
40
|
+
total_length = self.length
|
|
41
|
+
|
|
42
|
+
if total_volume == 0 or total_length == 0:
|
|
43
|
+
return 100.0
|
|
44
|
+
|
|
45
|
+
# PHP maintainability index calculation
|
|
46
|
+
mi_base = max(
|
|
47
|
+
(
|
|
48
|
+
171
|
|
49
|
+
- 5.2 * math.log(total_volume)
|
|
50
|
+
- 0.23 * total_complexity
|
|
51
|
+
- 16.2 * math.log(total_length)
|
|
52
|
+
)
|
|
53
|
+
* 100
|
|
54
|
+
/ 171,
|
|
55
|
+
0,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# no comment weight
|
|
59
|
+
self.maintainability_index = mi_base
|
|
24
60
|
|
|
25
61
|
def to_dict(self) -> dict:
|
|
26
62
|
"""Convert FunctionNode to a dictionary for JSON serialization."""
|
|
@@ -28,8 +64,12 @@ class FunctionNode:
|
|
|
28
64
|
"full_name": self.full_name,
|
|
29
65
|
"name": self.name,
|
|
30
66
|
"lineno": self.lineno,
|
|
67
|
+
"line_end": self.line_end,
|
|
68
|
+
"loc": self.get_loc(),
|
|
69
|
+
"loc_segment": Segmentor.get_loc_segment(self.get_loc()),
|
|
31
70
|
"col_offset": self.col_offset,
|
|
32
71
|
"complexity": self.complexity,
|
|
72
|
+
"complexity_segment": Segmentor.get_complexity_segment(self.complexity),
|
|
33
73
|
"h1": self.h1,
|
|
34
74
|
"h2": self.h2,
|
|
35
75
|
"N1": self.N1,
|
|
@@ -42,8 +82,33 @@ class FunctionNode:
|
|
|
42
82
|
"effort": self.effort,
|
|
43
83
|
"time": self.time,
|
|
44
84
|
"bugs": self.bugs,
|
|
45
|
-
"maintainability_index": self.maintainability_index,
|
|
85
|
+
"maintainability_index": round(self.maintainability_index, 2),
|
|
86
|
+
"maintainability_segment": Segmentor.get_maintainability_segment(
|
|
87
|
+
self.maintainability_index
|
|
88
|
+
),
|
|
46
89
|
}
|
|
47
90
|
|
|
48
91
|
def __dict__(self) -> dict:
|
|
49
92
|
return self.to_dict()
|
|
93
|
+
|
|
94
|
+
@staticmethod
|
|
95
|
+
def from_dict(data: dict) -> Self:
|
|
96
|
+
node = FunctionNode(
|
|
97
|
+
full_name=data["full_name"],
|
|
98
|
+
name=data["name"],
|
|
99
|
+
lineno=data["lineno"],
|
|
100
|
+
col_offset=data["col_offset"],
|
|
101
|
+
complexity=data["complexity"],
|
|
102
|
+
)
|
|
103
|
+
node.line_end = data["line_end"]
|
|
104
|
+
node.vocabulary = data["vocabulary"]
|
|
105
|
+
node.length = data["length"]
|
|
106
|
+
node.calculated_length = data["calculated_length"]
|
|
107
|
+
node.volume = data["volume"]
|
|
108
|
+
node.difficulty = data["difficulty"]
|
|
109
|
+
node.effort = data["effort"]
|
|
110
|
+
node.time = data["time"]
|
|
111
|
+
node.bugs = data["bugs"]
|
|
112
|
+
node.maintainability_index = data["maintainability_index"]
|
|
113
|
+
|
|
114
|
+
return node
|