metripy 0.2.8__py3-none-any.whl → 0.3.0__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.
- metripy/Application/Analyzer.py +23 -3
- metripy/Application/Application.py +16 -1
- metripy/Application/Config/Config.py +33 -0
- metripy/Application/Config/File/ConfigFileReaderFactory.py +4 -4
- metripy/Application/Config/File/JsonConfigFileReader.py +7 -2
- metripy/Application/Config/Parser.py +26 -11
- metripy/Application/Config/ProjectConfig.py +63 -0
- metripy/Application/Info.py +29 -0
- metripy/Dependency/Dependency.py +2 -1
- metripy/Dependency/Pip/Pip.py +1 -2
- 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 +42 -0
- metripy/Metric/FileTree/FileTreeParser.py +0 -4
- metripy/Metric/Git/GitMetrics.py +1 -1
- metripy/Metric/ProjectMetrics.py +17 -2
- 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/Reporter.py +247 -28
- metripy/Report/Json/GitJsonReporter.py +3 -1
- metripy/Report/Json/JsonReporter.py +4 -1
- metripy/Tree/ClassNode.py +21 -0
- metripy/Tree/FunctionNode.py +66 -1
- metripy/Trend/TrendAnalyzer.py +150 -0
- {metripy-0.2.8.dist-info → metripy-0.3.0.dist-info}/METADATA +3 -3
- metripy-0.3.0.dist-info/RECORD +76 -0
- metripy-0.2.8.dist-info/RECORD +0 -66
- {metripy-0.2.8.dist-info → metripy-0.3.0.dist-info}/WHEEL +0 -0
- {metripy-0.2.8.dist-info → metripy-0.3.0.dist-info}/entry_points.txt +0 -0
- {metripy-0.2.8.dist-info → metripy-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {metripy-0.2.8.dist-info → metripy-0.3.0.dist-info}/top_level.txt +0 -0
metripy/Report/Html/Reporter.py
CHANGED
|
@@ -7,10 +7,12 @@ 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.Dependency.Dependency import Dependency
|
|
11
|
+
from metripy.Metric.Code.FileMetrics import FileMetrics
|
|
12
|
+
from metripy.Metric.Code.Segmentor import Segmentor
|
|
10
13
|
from metripy.Metric.FileTree.FileTreeParser import FileTreeParser
|
|
11
14
|
from metripy.Metric.ProjectMetrics import ProjectMetrics
|
|
12
15
|
from metripy.Report.ReporterInterface import ReporterInterface
|
|
13
|
-
from metripy.Dependency.Dependency import Dependency
|
|
14
16
|
|
|
15
17
|
|
|
16
18
|
class Reporter(ReporterInterface):
|
|
@@ -54,24 +56,173 @@ class Reporter(ReporterInterface):
|
|
|
54
56
|
os.path.join(self.config.path, "css"),
|
|
55
57
|
dirs_exist_ok=True,
|
|
56
58
|
)
|
|
57
|
-
|
|
59
|
+
shutil.copytree(
|
|
60
|
+
os.path.join(self.template_dir, "images"),
|
|
61
|
+
os.path.join(self.config.path, "images"),
|
|
62
|
+
dirs_exist_ok=True,
|
|
63
|
+
)
|
|
58
64
|
# shutil.copytree(os.path.join(self.template_dir, "fonts"), os.path.join(self.config.path, "fonts"), dirs_exist_ok=True)
|
|
59
65
|
|
|
60
|
-
#
|
|
66
|
+
# copy logo, lies 2 down from the templates directory
|
|
67
|
+
shutil.copy(os.path.join(self.template_dir, "../..", "logo.svg"), os.path.join(self.config.path, "images", "logo.svg"))
|
|
68
|
+
|
|
69
|
+
# Render main pages
|
|
70
|
+
self.render_index_page(metrics)
|
|
71
|
+
self.render_files_page(metrics)
|
|
72
|
+
self.render_git_analysis_page(metrics)
|
|
73
|
+
self.render_dependencies_page(metrics)
|
|
74
|
+
self.render_top_offenders_page(metrics)
|
|
75
|
+
self.render_trends_page(metrics)
|
|
76
|
+
|
|
77
|
+
self.output.writeln(
|
|
78
|
+
f"<success>HTML report generated in {self.config.path} directory</success>"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def render_template(self, template_name: str, data: dict) -> str:
|
|
82
|
+
engine = TemplateEngine(os.path.join(self.template_dir, template_name))
|
|
83
|
+
content = engine.render(**data)
|
|
84
|
+
with open(os.path.join(self.config.path, template_name), "w") as file:
|
|
85
|
+
file.write(content)
|
|
86
|
+
|
|
87
|
+
def render_trends_page(self, metrics: ProjectMetrics):
|
|
88
|
+
def compile(file: FileMetrics) -> dict:
|
|
89
|
+
return {
|
|
90
|
+
"name": file.full_name,
|
|
91
|
+
"path": file.full_name,
|
|
92
|
+
"complexity_current": file.totalCc,
|
|
93
|
+
"complexity_prev": round(file.trend.historical_totalCc, 2),
|
|
94
|
+
"complexity_delta": round(file.trend.totalCc_delta, 2),
|
|
95
|
+
"maintainability_current": round(file.maintainabilityIndex, 2),
|
|
96
|
+
"maintainability_prev": round(
|
|
97
|
+
file.trend.historical_maintainabilityIndex, 2
|
|
98
|
+
),
|
|
99
|
+
"maintainability_delta": round(
|
|
100
|
+
file.trend.maintainabilityIndex_delta, 2
|
|
101
|
+
),
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# Top improved complexity (complexity went down - negative delta)
|
|
105
|
+
top_improved_complexity = [
|
|
106
|
+
x
|
|
107
|
+
for x in metrics.file_metrics
|
|
108
|
+
if x.trend is not None and x.trend.totalCc_delta < 0
|
|
109
|
+
]
|
|
110
|
+
top_improved_complexity = sorted(
|
|
111
|
+
top_improved_complexity, key=lambda x: x.trend.totalCc_delta
|
|
112
|
+
)[:10]
|
|
113
|
+
|
|
114
|
+
# Top worsened complexity (complexity went up - positive delta)
|
|
115
|
+
top_worsened_complexity = [
|
|
116
|
+
x
|
|
117
|
+
for x in metrics.file_metrics
|
|
118
|
+
if x.trend is not None and x.trend.totalCc_delta > 0
|
|
119
|
+
]
|
|
120
|
+
top_worsened_complexity = sorted(
|
|
121
|
+
top_worsened_complexity, key=lambda x: x.trend.totalCc_delta, reverse=True
|
|
122
|
+
)[:10]
|
|
123
|
+
|
|
124
|
+
# Top improved maintainability (maintainability went up - positive delta)
|
|
125
|
+
top_improved_maintainability = [
|
|
126
|
+
x
|
|
127
|
+
for x in metrics.file_metrics
|
|
128
|
+
if x.trend is not None and round(x.trend.maintainabilityIndex_delta, 2) > 0
|
|
129
|
+
]
|
|
130
|
+
top_improved_maintainability = sorted(
|
|
131
|
+
top_improved_maintainability,
|
|
132
|
+
key=lambda x: x.trend.maintainabilityIndex_delta,
|
|
133
|
+
reverse=True,
|
|
134
|
+
)[:10]
|
|
135
|
+
|
|
136
|
+
# Top worsened maintainability (maintainability went down - negative delta)
|
|
137
|
+
top_worsened_maintainability = [
|
|
138
|
+
x
|
|
139
|
+
for x in metrics.file_metrics
|
|
140
|
+
if x.trend is not None and round(x.trend.maintainabilityIndex_delta, 2) < 0
|
|
141
|
+
]
|
|
142
|
+
top_worsened_maintainability = sorted(
|
|
143
|
+
top_worsened_maintainability,
|
|
144
|
+
key=lambda x: x.trend.maintainabilityIndex_delta,
|
|
145
|
+
)[:10]
|
|
146
|
+
|
|
147
|
+
trend_data = {
|
|
148
|
+
# Segment distributions for each metric
|
|
149
|
+
"loc_segments_current": metrics.total_code_metrics.segmentation_data[
|
|
150
|
+
"loc"
|
|
151
|
+
].to_dict_with_percent(),
|
|
152
|
+
"loc_segments_prev": metrics.total_code_metrics.trend.historical_segmentation_data[
|
|
153
|
+
"loc"
|
|
154
|
+
].to_dict_with_percent(),
|
|
155
|
+
"complexity_segments_current": metrics.total_code_metrics.segmentation_data[
|
|
156
|
+
"complexity"
|
|
157
|
+
].to_dict_with_percent(),
|
|
158
|
+
"complexity_segments_prev": metrics.total_code_metrics.trend.historical_segmentation_data[
|
|
159
|
+
"complexity"
|
|
160
|
+
].to_dict_with_percent(),
|
|
161
|
+
"maintainability_segments_current": metrics.total_code_metrics.segmentation_data[
|
|
162
|
+
"maintainability"
|
|
163
|
+
].to_dict_with_percent(),
|
|
164
|
+
"maintainability_segments_prev": metrics.total_code_metrics.trend.historical_segmentation_data[
|
|
165
|
+
"maintainability"
|
|
166
|
+
].to_dict_with_percent(),
|
|
167
|
+
"method_size_segments_current": metrics.total_code_metrics.segmentation_data[
|
|
168
|
+
"methodSize"
|
|
169
|
+
].to_dict_with_percent(),
|
|
170
|
+
"method_size_segments_prev": metrics.total_code_metrics.trend.historical_segmentation_data[
|
|
171
|
+
"methodSize"
|
|
172
|
+
].to_dict_with_percent(),
|
|
173
|
+
"top_improved_complexity": [compile(x) for x in top_improved_complexity],
|
|
174
|
+
"top_improved_maintainability": [
|
|
175
|
+
compile(x) for x in top_improved_maintainability
|
|
176
|
+
],
|
|
177
|
+
"top_worsened_complexity": [compile(x) for x in top_worsened_complexity],
|
|
178
|
+
"top_worsened_maintainability": [
|
|
179
|
+
compile(x) for x in top_worsened_maintainability
|
|
180
|
+
],
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
self.output.writeln("<info>Rendering trends page</info>")
|
|
184
|
+
self.render_template(
|
|
185
|
+
"trends.html",
|
|
186
|
+
{
|
|
187
|
+
"has_trend_data": metrics.total_code_metrics.trend is not None,
|
|
188
|
+
"trend_data": trend_data,
|
|
189
|
+
"project_name": "Metripy",
|
|
190
|
+
"last_updated": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
191
|
+
"date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
192
|
+
"author": "Metripy",
|
|
193
|
+
"version": "1.0.0",
|
|
194
|
+
},
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
def render_index_page(self, metrics: ProjectMetrics):
|
|
61
198
|
git_stats_data = {}
|
|
62
199
|
if metrics.git_metrics:
|
|
63
200
|
git_stats_data = metrics.git_metrics.get_commit_stats_per_month()
|
|
64
201
|
|
|
65
202
|
self.output.writeln("<info>Rendering index page</info>")
|
|
66
|
-
# Render main index page
|
|
67
203
|
self.render_template(
|
|
68
204
|
"index.html",
|
|
69
205
|
{
|
|
70
206
|
"git_stats_data": json.dumps(git_stats_data, indent=4),
|
|
71
207
|
"total_code_metrics": metrics.total_code_metrics.to_dict(),
|
|
208
|
+
"has_total_code_metrics_trend": metrics.total_code_metrics.trend
|
|
209
|
+
is not None,
|
|
210
|
+
"total_code_metrics_trend": (
|
|
211
|
+
metrics.total_code_metrics.trend.to_dict()
|
|
212
|
+
if metrics.total_code_metrics.trend
|
|
213
|
+
else None
|
|
214
|
+
),
|
|
72
215
|
"segmentation_data": json.dumps(
|
|
73
216
|
metrics.total_code_metrics.to_dict_segmentation(), indent=4
|
|
74
217
|
),
|
|
218
|
+
"segmentation_data_trend": (
|
|
219
|
+
json.dumps(
|
|
220
|
+
metrics.total_code_metrics.trend.to_dict_segmentation(),
|
|
221
|
+
indent=4,
|
|
222
|
+
)
|
|
223
|
+
if metrics.total_code_metrics.trend
|
|
224
|
+
else None
|
|
225
|
+
),
|
|
75
226
|
"project_name": "Metripy",
|
|
76
227
|
"last_updated": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
77
228
|
"date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
@@ -81,23 +232,91 @@ class Reporter(ReporterInterface):
|
|
|
81
232
|
)
|
|
82
233
|
self.output.writeln("<success>Done rendering index page</success>")
|
|
83
234
|
|
|
84
|
-
|
|
85
|
-
self.
|
|
86
|
-
# Render git analysis page
|
|
87
|
-
self.render_git_analysis_page(metrics)
|
|
235
|
+
def render_top_offenders_page(self, metrics: ProjectMetrics):
|
|
236
|
+
self.output.writeln("<info>Rendering top offenders page</info>")
|
|
88
237
|
|
|
89
|
-
|
|
238
|
+
orderedByTotalCc = sorted(
|
|
239
|
+
metrics.file_metrics, key=lambda x: x.totalCc, reverse=True
|
|
240
|
+
)[:10]
|
|
241
|
+
orderedByMI = sorted(
|
|
242
|
+
metrics.file_metrics, key=lambda x: x.maintainabilityIndex, reverse=False
|
|
243
|
+
)[:10]
|
|
244
|
+
orderedByLoc = sorted(metrics.file_metrics, key=lambda x: x.loc, reverse=True)[
|
|
245
|
+
:10
|
|
246
|
+
]
|
|
247
|
+
|
|
248
|
+
all_functions: list = []
|
|
249
|
+
for fm in metrics.file_metrics:
|
|
250
|
+
all_functions.extend(fm.function_nodes)
|
|
90
251
|
|
|
252
|
+
functionsOrderedByCc = sorted(
|
|
253
|
+
all_functions, key=lambda x: x.complexity, reverse=True
|
|
254
|
+
)[:10]
|
|
255
|
+
functionsOrderedByMi = sorted(
|
|
256
|
+
all_functions, key=lambda x: x.maintainability_index, reverse=False
|
|
257
|
+
)[:10]
|
|
258
|
+
functionsOrderedByLoc = sorted(
|
|
259
|
+
all_functions, key=lambda x: x.get_loc(), reverse=True
|
|
260
|
+
)[:10]
|
|
261
|
+
|
|
262
|
+
# TODO maintainability index per function, we dont calc yet
|
|
263
|
+
|
|
264
|
+
self.render_template(
|
|
265
|
+
"top_offenders.html",
|
|
266
|
+
Reporter._stringify_values(
|
|
267
|
+
{
|
|
268
|
+
"file_loc_offenders": [
|
|
269
|
+
{**e.to_dict(), "status": Segmentor.get_loc_segment(e.loc)}
|
|
270
|
+
for e in orderedByLoc
|
|
271
|
+
],
|
|
272
|
+
"file_cc_offenders": [
|
|
273
|
+
{
|
|
274
|
+
**e.to_dict(),
|
|
275
|
+
"status": Segmentor.get_complexity_segment(e.totalCc),
|
|
276
|
+
}
|
|
277
|
+
for e in orderedByTotalCc
|
|
278
|
+
],
|
|
279
|
+
"file_mi_offenders": [
|
|
280
|
+
{
|
|
281
|
+
**e.to_dict(),
|
|
282
|
+
"status": Segmentor.get_maintainability_segment(
|
|
283
|
+
e.maintainabilityIndex
|
|
284
|
+
),
|
|
285
|
+
}
|
|
286
|
+
for e in orderedByMI
|
|
287
|
+
],
|
|
288
|
+
"function_size_offenders": [
|
|
289
|
+
{
|
|
290
|
+
**e.to_dict(),
|
|
291
|
+
"status": Segmentor.get_method_size_segment(e.get_loc()),
|
|
292
|
+
}
|
|
293
|
+
for e in functionsOrderedByLoc
|
|
294
|
+
],
|
|
295
|
+
"function_cc_offenders": [
|
|
296
|
+
{
|
|
297
|
+
**e.to_dict(),
|
|
298
|
+
"status": Segmentor.get_complexity_segment(e.complexity),
|
|
299
|
+
}
|
|
300
|
+
for e in functionsOrderedByCc
|
|
301
|
+
],
|
|
302
|
+
"function_mi_offenders": [
|
|
303
|
+
{
|
|
304
|
+
**e.to_dict(),
|
|
305
|
+
"status": Segmentor.get_maintainability_segment(
|
|
306
|
+
e.maintainability_index
|
|
307
|
+
),
|
|
308
|
+
}
|
|
309
|
+
for e in functionsOrderedByMi
|
|
310
|
+
],
|
|
311
|
+
"project_name": "Metripy",
|
|
312
|
+
"last_updated": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
313
|
+
}
|
|
314
|
+
),
|
|
315
|
+
)
|
|
91
316
|
self.output.writeln(
|
|
92
|
-
|
|
317
|
+
"<success>Top offenders page generated successfully</success>"
|
|
93
318
|
)
|
|
94
319
|
|
|
95
|
-
def render_template(self, template_name: str, data: dict) -> str:
|
|
96
|
-
engine = TemplateEngine(os.path.join(self.template_dir, template_name))
|
|
97
|
-
content = engine.render(**data)
|
|
98
|
-
with open(os.path.join(self.config.path, template_name), "w") as file:
|
|
99
|
-
file.write(content)
|
|
100
|
-
|
|
101
320
|
def render_dependencies_page(self, metrics: ProjectMetrics):
|
|
102
321
|
"""Render the dependencies page with dependency details and stats"""
|
|
103
322
|
if not metrics.dependencies:
|
|
@@ -108,11 +327,8 @@ class Reporter(ReporterInterface):
|
|
|
108
327
|
|
|
109
328
|
dependencies = metrics.dependencies if metrics.dependencies is not None else []
|
|
110
329
|
|
|
111
|
-
# TODO render a pie chart
|
|
112
330
|
license_by_type = Dependency.get_lisence_distribution(dependencies)
|
|
113
331
|
|
|
114
|
-
print(json.dumps(license_by_type, indent=2))
|
|
115
|
-
|
|
116
332
|
self.render_template(
|
|
117
333
|
"dependencies.html",
|
|
118
334
|
{
|
|
@@ -149,26 +365,29 @@ class Reporter(ReporterInterface):
|
|
|
149
365
|
)
|
|
150
366
|
self.output.writeln("<success>Files page generated successfully</success>")
|
|
151
367
|
|
|
368
|
+
@staticmethod
|
|
369
|
+
def _stringify_values(obj):
|
|
370
|
+
if isinstance(obj, dict):
|
|
371
|
+
return {
|
|
372
|
+
key: Reporter._stringify_values(value) for key, value in obj.items()
|
|
373
|
+
}
|
|
374
|
+
elif isinstance(obj, list):
|
|
375
|
+
return [Reporter._stringify_values(item) for item in obj]
|
|
376
|
+
else:
|
|
377
|
+
return str(obj)
|
|
378
|
+
|
|
152
379
|
def render_git_analysis_page(self, metrics: ProjectMetrics):
|
|
153
380
|
"""Render the git analysis page with comprehensive git data"""
|
|
154
381
|
if not metrics.git_metrics:
|
|
155
382
|
self.output.writeln("<success>No git metrics to render</success>")
|
|
156
383
|
return
|
|
157
384
|
|
|
158
|
-
def stringify_values(obj):
|
|
159
|
-
if isinstance(obj, dict):
|
|
160
|
-
return {key: stringify_values(value) for key, value in obj.items()}
|
|
161
|
-
elif isinstance(obj, list):
|
|
162
|
-
return [stringify_values(item) for item in obj]
|
|
163
|
-
else:
|
|
164
|
-
return str(obj)
|
|
165
|
-
|
|
166
385
|
self.output.writeln("<info>Rendering git analysis page</info>")
|
|
167
386
|
try:
|
|
168
387
|
# Render git analysis template
|
|
169
388
|
self.render_template(
|
|
170
389
|
"git_analysis.html",
|
|
171
|
-
|
|
390
|
+
Reporter._stringify_values(
|
|
172
391
|
{
|
|
173
392
|
"git_analysis": metrics.git_metrics.to_dict(),
|
|
174
393
|
"git_analysis_json": json.dumps(
|
|
@@ -19,4 +19,6 @@ class GitJsonReporter(AbstractJsonReporter):
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
self.put_data(data)
|
|
22
|
-
self.output.writeln(
|
|
22
|
+
self.output.writeln(
|
|
23
|
+
f"<success>Create git json report in {self.config.path}</success>"
|
|
24
|
+
)
|
|
@@ -3,6 +3,7 @@ from metripy.Component.Output.CliOutput import CliOutput
|
|
|
3
3
|
from metripy.Metric.ProjectMetrics import ProjectMetrics
|
|
4
4
|
from metripy.Report.Json.AbstractJsonReporter import AbstractJsonReporter
|
|
5
5
|
|
|
6
|
+
|
|
6
7
|
class JsonReporter(AbstractJsonReporter):
|
|
7
8
|
def __init__(self, config: Config, output: CliOutput):
|
|
8
9
|
self.config = config
|
|
@@ -10,4 +11,6 @@ class JsonReporter(AbstractJsonReporter):
|
|
|
10
11
|
|
|
11
12
|
def generate(self, metrics: ProjectMetrics):
|
|
12
13
|
self.put_data(metrics.to_dict())
|
|
13
|
-
self.output.writeln(
|
|
14
|
+
self.output.writeln(
|
|
15
|
+
f"<success>Create json report in {self.config.path}</success>"
|
|
16
|
+
)
|
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
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
from metripy.Metric.Code.AggregatedMetrics import AggregatedMetrics
|
|
2
|
+
from metripy.Metric.Code.FileMetrics import FileMetrics
|
|
3
|
+
from metripy.Metric.ProjectMetrics import ProjectMetrics
|
|
4
|
+
from metripy.Metric.Trend.AggregatedTrendMetric import AggregatedTrendMetric
|
|
5
|
+
from metripy.Metric.Trend.ClassTrendMetric import ClassTrendMetric
|
|
6
|
+
from metripy.Metric.Trend.FileTrendMetric import FileTrendMetric
|
|
7
|
+
from metripy.Metric.Trend.FunctionTrendMetric import FunctionTrendMetric
|
|
8
|
+
from metripy.Tree.ClassNode import ClassNode
|
|
9
|
+
from metripy.Tree.FunctionNode import FunctionNode
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TrendAnalyzer:
|
|
13
|
+
def create_file_trend_metric(
|
|
14
|
+
self, file_metric: FileMetrics, historical_file_metric: FileMetrics
|
|
15
|
+
) -> FileTrendMetric:
|
|
16
|
+
return FileTrendMetric(
|
|
17
|
+
historical_loc=historical_file_metric.loc,
|
|
18
|
+
loc=file_metric.loc,
|
|
19
|
+
historical_totalCc=historical_file_metric.totalCc,
|
|
20
|
+
totalCc=file_metric.totalCc,
|
|
21
|
+
historical_avgCcPerFunction=historical_file_metric.avgCcPerFunction,
|
|
22
|
+
avgCcPerFunction=file_metric.avgCcPerFunction,
|
|
23
|
+
historical_maintainabilityIndex=historical_file_metric.maintainabilityIndex,
|
|
24
|
+
maintainabilityIndex=file_metric.maintainabilityIndex,
|
|
25
|
+
historical_avgLocPerFunction=historical_file_metric.avgLocPerFunction,
|
|
26
|
+
avgLocPerFunction=file_metric.avgLocPerFunction,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
def create_class_trend_metric(
|
|
30
|
+
self, class_metric: ClassNode, historical_class_metric: ClassNode
|
|
31
|
+
) -> ClassTrendMetric:
|
|
32
|
+
return ClassTrendMetric(
|
|
33
|
+
historical_lineno=historical_class_metric.lineno,
|
|
34
|
+
lineno=class_metric.lineno,
|
|
35
|
+
historical_real_complexity=historical_class_metric.real_complexity,
|
|
36
|
+
real_complexity=class_metric.real_complexity,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def create_function_trend_metric(
|
|
40
|
+
self, function_metric: FunctionNode, historical_function_metric: FunctionNode
|
|
41
|
+
) -> FunctionTrendMetric:
|
|
42
|
+
return FunctionTrendMetric(
|
|
43
|
+
historical_loc=historical_function_metric.get_loc(),
|
|
44
|
+
loc=function_metric.get_loc(),
|
|
45
|
+
historical_complexity=historical_function_metric.complexity,
|
|
46
|
+
complexity=function_metric.complexity,
|
|
47
|
+
historical_maintainability_index=historical_function_metric.maintainability_index,
|
|
48
|
+
maintainability_index=function_metric.maintainability_index,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
def add_historical_file_trends(
|
|
52
|
+
self,
|
|
53
|
+
file_metrics: list[FileMetrics],
|
|
54
|
+
historical_file_metrics: list[FileMetrics],
|
|
55
|
+
):
|
|
56
|
+
indexed_file_metrics = {m.full_name: m for m in file_metrics}
|
|
57
|
+
indexed_historical_file_metrics = {
|
|
58
|
+
m.full_name: m for m in historical_file_metrics
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
for full_name, file_metric in indexed_file_metrics.items():
|
|
62
|
+
historical_file_metric = indexed_historical_file_metrics.get(full_name)
|
|
63
|
+
if not historical_file_metric:
|
|
64
|
+
continue
|
|
65
|
+
file_metric.trend = self.create_file_trend_metric(
|
|
66
|
+
file_metric, historical_file_metric
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
indexed_class_nodes = {
|
|
70
|
+
n.full_name: n for n in historical_file_metric.class_nodes
|
|
71
|
+
}
|
|
72
|
+
for class_node in file_metric.class_nodes:
|
|
73
|
+
historical_class_node = indexed_class_nodes.get(class_node.full_name)
|
|
74
|
+
if not historical_class_node:
|
|
75
|
+
continue
|
|
76
|
+
class_node.trend = self.create_class_trend_metric(
|
|
77
|
+
class_node, historical_class_node
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
indexed_function_nodes = {
|
|
81
|
+
n.full_name: n for n in historical_class_node.functions
|
|
82
|
+
}
|
|
83
|
+
for function_node in class_node.functions:
|
|
84
|
+
historical_function_node = indexed_function_nodes.get(
|
|
85
|
+
function_node.full_name
|
|
86
|
+
)
|
|
87
|
+
if not historical_function_node:
|
|
88
|
+
continue
|
|
89
|
+
function_node.trend = self.create_function_trend_metric(
|
|
90
|
+
function_node, historical_function_node
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
indexed_function_nodes = {
|
|
94
|
+
n.full_name: n for n in historical_file_metric.function_nodes
|
|
95
|
+
}
|
|
96
|
+
for function_node in file_metric.function_nodes:
|
|
97
|
+
historical_function_node = indexed_function_nodes.get(
|
|
98
|
+
function_node.full_name
|
|
99
|
+
)
|
|
100
|
+
if not historical_function_node:
|
|
101
|
+
continue
|
|
102
|
+
function_node.trend = self.create_function_trend_metric(
|
|
103
|
+
function_node, historical_function_node
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
def create_aggregated_trend_metric(
|
|
107
|
+
self,
|
|
108
|
+
aggregated_metric: AggregatedMetrics,
|
|
109
|
+
historical_aggregated_metric: AggregatedMetrics,
|
|
110
|
+
) -> AggregatedTrendMetric:
|
|
111
|
+
return AggregatedTrendMetric(
|
|
112
|
+
historical_loc=historical_aggregated_metric.loc,
|
|
113
|
+
loc=aggregated_metric.loc,
|
|
114
|
+
historical_avgCcPerFunction=historical_aggregated_metric.avgCcPerFunction,
|
|
115
|
+
avgCcPerFunction=aggregated_metric.avgCcPerFunction,
|
|
116
|
+
historical_maintainabilityIndex=historical_aggregated_metric.maintainabilityIndex,
|
|
117
|
+
maintainabilityIndex=aggregated_metric.maintainabilityIndex,
|
|
118
|
+
historical_avgLocPerFunction=historical_aggregated_metric.avgLocPerFunction,
|
|
119
|
+
avgLocPerFunction=aggregated_metric.avgLocPerFunction,
|
|
120
|
+
historical_num_files=historical_aggregated_metric.num_files,
|
|
121
|
+
num_files=aggregated_metric.num_files,
|
|
122
|
+
historical_segmented_loc=historical_aggregated_metric.segmentation_data[
|
|
123
|
+
"loc"
|
|
124
|
+
],
|
|
125
|
+
segmented_loc=aggregated_metric.segmentation_data["loc"],
|
|
126
|
+
historical_segmented_complexity=historical_aggregated_metric.segmentation_data[
|
|
127
|
+
"complexity"
|
|
128
|
+
],
|
|
129
|
+
segmented_complexity=aggregated_metric.segmentation_data["complexity"],
|
|
130
|
+
historical_segmented_maintainability=historical_aggregated_metric.segmentation_data[
|
|
131
|
+
"maintainability"
|
|
132
|
+
],
|
|
133
|
+
segmented_maintainability=aggregated_metric.segmentation_data[
|
|
134
|
+
"maintainability"
|
|
135
|
+
],
|
|
136
|
+
historical_segmented_method_size=historical_aggregated_metric.segmentation_data[
|
|
137
|
+
"methodSize"
|
|
138
|
+
],
|
|
139
|
+
segmented_method_size=aggregated_metric.segmentation_data["methodSize"],
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def add_historical_project_trends(
|
|
143
|
+
self,
|
|
144
|
+
project_metrics: ProjectMetrics,
|
|
145
|
+
historical_project_metrics: ProjectMetrics,
|
|
146
|
+
):
|
|
147
|
+
project_metrics.total_code_metrics.trend = self.create_aggregated_trend_metric(
|
|
148
|
+
project_metrics.total_code_metrics,
|
|
149
|
+
historical_project_metrics.total_code_metrics,
|
|
150
|
+
)
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: metripy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: A Python tool to generate multi project, multi language code metric reports
|
|
5
5
|
Author-email: Yannick Zimmermann <yannick.zimmermann@proton.me>
|
|
6
6
|
License: MIT
|
|
7
|
-
Project-URL: Homepage, https://
|
|
7
|
+
Project-URL: Homepage, https://zimmer-yan.github.io/metripy/
|
|
8
8
|
Project-URL: Repository, https://github.com/zimmer-yan/metripy
|
|
9
|
-
Project-URL: Documentation, https://
|
|
9
|
+
Project-URL: Documentation, https://zimmer-yan.github.io/metripy/
|
|
10
10
|
Project-URL: Bug Tracker, https://github.com/zimmer-yan/metripy/issues
|
|
11
11
|
Keywords: code metrics,multi-language,code analysis,git metrics,code visualization,software quality,static analysis,repository insights,developer productivity,codebase health,technical debt,language-agnostic
|
|
12
12
|
Classifier: Development Status :: 3 - Alpha
|