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/Application/Analyzer.py
CHANGED
|
@@ -7,14 +7,15 @@ from metripy.Dependency.Dependency import Dependency
|
|
|
7
7
|
from metripy.Dependency.Npm.Npm import Npm
|
|
8
8
|
from metripy.Dependency.Pip.Pip import Pip
|
|
9
9
|
from metripy.Git.GitAnalyzer import GitAnalyzer
|
|
10
|
+
from metripy.Import.Json.JsonImporter import JsonImporter
|
|
10
11
|
from metripy.LangAnalyzer.AbstractLangAnalyzer import AbstractLangAnalyzer
|
|
11
12
|
from metripy.LangAnalyzer.Php.PhpAnalyzer import PhpAnalyzer
|
|
12
13
|
from metripy.LangAnalyzer.Python.PythonAnalyzer import PythonAnalyzer
|
|
13
|
-
from metripy.LangAnalyzer.Typescript.TypescriptAnalyzer import
|
|
14
|
-
TypescriptAnalyzer
|
|
14
|
+
from metripy.LangAnalyzer.Typescript.TypescriptAnalyzer import TypescriptAnalyzer
|
|
15
15
|
from metripy.Metric.Code.FileMetrics import FileMetrics
|
|
16
16
|
from metripy.Metric.Git.GitMetrics import GitMetrics
|
|
17
17
|
from metripy.Metric.ProjectMetrics import ProjectMetrics
|
|
18
|
+
from metripy.Trend.TrendAnalyzer import TrendAnalyzer
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
class Analyzer:
|
|
@@ -85,6 +86,18 @@ class Analyzer:
|
|
|
85
86
|
|
|
86
87
|
return dependencies
|
|
87
88
|
|
|
89
|
+
def add_trends(self, project_metrics: ProjectMetrics):
|
|
90
|
+
self.output.writeln("<info>Analyzing trends...</info>")
|
|
91
|
+
importer = JsonImporter(self.output)
|
|
92
|
+
historical_project_metrics = importer.import_data(self.config.history_path)
|
|
93
|
+
TrendAnalyzer().add_historical_file_trends(
|
|
94
|
+
project_metrics.file_metrics, historical_project_metrics.file_metrics
|
|
95
|
+
)
|
|
96
|
+
TrendAnalyzer().add_historical_project_trends(
|
|
97
|
+
project_metrics, historical_project_metrics
|
|
98
|
+
)
|
|
99
|
+
self.output.writeln("<success>Trends analyzed</success>")
|
|
100
|
+
|
|
88
101
|
def run(self, files: list[str]) -> ProjectMetrics:
|
|
89
102
|
git_stats = None
|
|
90
103
|
if self.config.git:
|
|
@@ -103,4 +116,11 @@ class Analyzer:
|
|
|
103
116
|
elif self.config.npm:
|
|
104
117
|
packages = self.analyze_npm()
|
|
105
118
|
|
|
106
|
-
|
|
119
|
+
if not self.config.history_path:
|
|
120
|
+
return ProjectMetrics(file_metrics, git_stats, packages)
|
|
121
|
+
|
|
122
|
+
# analyze trends
|
|
123
|
+
project_metrics = ProjectMetrics(file_metrics, git_stats, packages)
|
|
124
|
+
self.add_trends(project_metrics)
|
|
125
|
+
|
|
126
|
+
return project_metrics
|
|
@@ -8,15 +8,30 @@ from metripy.Component.Output.CliOutput import CliOutput
|
|
|
8
8
|
from metripy.Report.ReporterFactory import ReporterFactory
|
|
9
9
|
from metripy.Report.ReporterInterface import ReporterInterface
|
|
10
10
|
|
|
11
|
+
from metripy.Application.Info import Info
|
|
12
|
+
|
|
11
13
|
|
|
12
14
|
class Application:
|
|
13
15
|
def run(self, argv) -> None:
|
|
14
16
|
output = CliOutput()
|
|
15
17
|
|
|
16
18
|
# issues and debug
|
|
17
|
-
debugger = Debugger(output)
|
|
19
|
+
debugger = Debugger(output)
|
|
18
20
|
|
|
19
21
|
config = Parser().parse(argv)
|
|
22
|
+
if config.debug:
|
|
23
|
+
debugger.enable()
|
|
24
|
+
|
|
25
|
+
if config.version:
|
|
26
|
+
output.writeln(Info().get_version_info())
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
if config.help:
|
|
30
|
+
output.writeln(Info().get_help())
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
if config.quiet:
|
|
34
|
+
output.set_quiet(True)
|
|
20
35
|
|
|
21
36
|
finder = Finder()
|
|
22
37
|
files = finder.fetch(config.project_configs)
|
|
@@ -4,6 +4,10 @@ from metripy.Application.Config.ProjectConfig import ProjectConfig
|
|
|
4
4
|
class Config:
|
|
5
5
|
def __init__(self):
|
|
6
6
|
self.project_configs: list[ProjectConfig] = []
|
|
7
|
+
self.quiet: bool = False
|
|
8
|
+
self.version: bool = False
|
|
9
|
+
self.help: bool = False
|
|
10
|
+
self.debug: bool = False
|
|
7
11
|
|
|
8
12
|
def to_dict(self) -> dict:
|
|
9
13
|
return {
|
|
@@ -11,3 +15,32 @@ class Config:
|
|
|
11
15
|
project_config.to_dict() for project_config in self.project_configs
|
|
12
16
|
],
|
|
13
17
|
}
|
|
18
|
+
|
|
19
|
+
def set(self, param: str, value: any) -> None:
|
|
20
|
+
print(f"Setting {param} to {value}")
|
|
21
|
+
if param == "quiet":
|
|
22
|
+
self.quiet = value
|
|
23
|
+
elif param == "version":
|
|
24
|
+
self.version = value
|
|
25
|
+
elif param == "help":
|
|
26
|
+
self.help = value
|
|
27
|
+
elif param == "debug":
|
|
28
|
+
self.debug = value
|
|
29
|
+
elif param.startswith("configs."):
|
|
30
|
+
self._set_project_value(param[len("configs."):], value)
|
|
31
|
+
else:
|
|
32
|
+
# ignore unknown parameters
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
def _set_project_value(self, param: str, value: any) -> None:
|
|
36
|
+
keys = param.split(".")
|
|
37
|
+
project_name = keys[0]
|
|
38
|
+
project_config = next((pc for pc in self.project_configs if pc.name == project_name), None)
|
|
39
|
+
if not project_config:
|
|
40
|
+
project_config = ProjectConfig(project_name)
|
|
41
|
+
self.project_configs.append(project_config)
|
|
42
|
+
if len(keys) > 1:
|
|
43
|
+
project_config.set(keys[1:], value)
|
|
44
|
+
else:
|
|
45
|
+
# weird but okay
|
|
46
|
+
return
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import pathlib
|
|
3
3
|
|
|
4
|
-
from metripy.Application.Config.File.ConfigFileReaderInterface import
|
|
5
|
-
ConfigFileReaderInterface
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
from metripy.Application.Config.File.ConfigFileReaderInterface import (
|
|
5
|
+
ConfigFileReaderInterface,
|
|
6
|
+
)
|
|
7
|
+
from metripy.Application.Config.File.JsonConfigFileReader import JsonConfigFileReader
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class ConfigFileReaderFactory:
|
|
@@ -2,8 +2,9 @@ import json
|
|
|
2
2
|
import os
|
|
3
3
|
|
|
4
4
|
from metripy.Application.Config.Config import Config
|
|
5
|
-
from metripy.Application.Config.File.ConfigFileReaderInterface import
|
|
6
|
-
ConfigFileReaderInterface
|
|
5
|
+
from metripy.Application.Config.File.ConfigFileReaderInterface import (
|
|
6
|
+
ConfigFileReaderInterface,
|
|
7
|
+
)
|
|
7
8
|
from metripy.Application.Config.GitConfig import GitConfig
|
|
8
9
|
from metripy.Application.Config.ProjectConfig import ProjectConfig
|
|
9
10
|
from metripy.Application.Config.ReportConfig import ReportConfig
|
|
@@ -79,4 +80,8 @@ class JsonConfigFileReader(ConfigFileReaderInterface):
|
|
|
79
80
|
if npm := json_data.get("npm"):
|
|
80
81
|
project_config.npm = npm
|
|
81
82
|
|
|
83
|
+
# trends
|
|
84
|
+
if history_path := json_data.get("trends"):
|
|
85
|
+
project_config.history_path = self.resolve_path(history_path)
|
|
86
|
+
|
|
82
87
|
return project_config
|
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
import re
|
|
2
2
|
|
|
3
3
|
from metripy.Application.Config.Config import Config
|
|
4
|
-
from metripy.Application.Config.File.ConfigFileReaderFactory import
|
|
5
|
-
ConfigFileReaderFactory
|
|
4
|
+
from metripy.Application.Config.File.ConfigFileReaderFactory import (
|
|
5
|
+
ConfigFileReaderFactory,
|
|
6
|
+
)
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
class Parser:
|
|
9
10
|
def parse(self, argv: list[str]) -> Config:
|
|
10
11
|
config = Config()
|
|
12
|
+
print(argv)
|
|
11
13
|
|
|
12
|
-
if argv[0]
|
|
13
|
-
|
|
14
|
-
pass
|
|
15
|
-
argv.pop(0)
|
|
14
|
+
if argv[0].endswith("metripy.py") or argv[0].endswith("metripy"):
|
|
15
|
+
argv.pop(0)
|
|
16
16
|
|
|
17
17
|
# check for a config file
|
|
18
|
-
|
|
18
|
+
key = 0
|
|
19
|
+
while key < len(argv):
|
|
20
|
+
arg = argv[key]
|
|
21
|
+
print(f"Key: {key} Arg: '{arg}'")
|
|
19
22
|
if matches := re.search(r"^--config=(.+)$", arg):
|
|
20
23
|
fileReader = ConfigFileReaderFactory.createFromFileName(
|
|
21
24
|
matches.group(1)
|
|
@@ -23,9 +26,21 @@ class Parser:
|
|
|
23
26
|
fileReader.read(config)
|
|
24
27
|
argv.pop(key)
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
# arguments with options
|
|
30
|
+
elif matches := re.search(r"^--([\w]+(?:\.[\w]+)*)=(.*)$", arg):
|
|
31
|
+
param = matches.group(1)
|
|
32
|
+
value = matches.group(2)
|
|
33
|
+
config.set(param, value)
|
|
34
|
+
argv.pop(key)
|
|
35
|
+
|
|
36
|
+
# arguments without options
|
|
37
|
+
elif matches := re.search(r"^--([\w]+(?:\.[\w]+)*)$", arg):
|
|
38
|
+
param = matches.group(1)
|
|
39
|
+
config.set(param, True)
|
|
40
|
+
argv.pop(key)
|
|
41
|
+
else:
|
|
42
|
+
key += 1
|
|
43
|
+
|
|
44
|
+
# TODO handle remaining arguments
|
|
30
45
|
|
|
31
46
|
return config
|
|
@@ -14,6 +14,7 @@ class ProjectConfig:
|
|
|
14
14
|
self.pip: bool = False
|
|
15
15
|
self.npm: bool = False
|
|
16
16
|
self.reports: list[ReportConfig] = []
|
|
17
|
+
self.history_path: str | None = None
|
|
17
18
|
|
|
18
19
|
def to_dict(self) -> dict:
|
|
19
20
|
return {
|
|
@@ -22,6 +23,68 @@ class ProjectConfig:
|
|
|
22
23
|
"includes": self.includes,
|
|
23
24
|
"excludes": self.excludes,
|
|
24
25
|
"extensions": self.extensions,
|
|
26
|
+
"composer": self.composer,
|
|
27
|
+
"pip": self.pip,
|
|
28
|
+
"npm": self.npm,
|
|
25
29
|
"git": self.git.to_dict() if self.git else None,
|
|
26
30
|
"reports": [report.to_dict() for report in self.reports],
|
|
31
|
+
"history_path": self.history_path,
|
|
27
32
|
}
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def str_to_bool(value):
|
|
36
|
+
if isinstance(value, bool):
|
|
37
|
+
return value
|
|
38
|
+
return str(value).lower() in ("true", "1", "yes")
|
|
39
|
+
|
|
40
|
+
def set(self, keys: list[str], value: any) -> None:
|
|
41
|
+
if len(keys) == 0:
|
|
42
|
+
return
|
|
43
|
+
primary_key = keys[0]
|
|
44
|
+
print(f"Setting primary {primary_key} to {value}")
|
|
45
|
+
# single value
|
|
46
|
+
if primary_key == "base_path":
|
|
47
|
+
self.base_path = value
|
|
48
|
+
elif primary_key == "pip":
|
|
49
|
+
self.pip = self.str_to_bool(value)
|
|
50
|
+
elif primary_key == "npm":
|
|
51
|
+
self.npm = self.str_to_bool(value)
|
|
52
|
+
elif primary_key == "composer":
|
|
53
|
+
self.composer = self.str_to_bool(value)
|
|
54
|
+
elif primary_key == "trends":
|
|
55
|
+
self.history_path = value
|
|
56
|
+
elif primary_key == "git":
|
|
57
|
+
self.git = GitConfig()
|
|
58
|
+
self.git.repo = self.base_path
|
|
59
|
+
self.git.branch = value
|
|
60
|
+
|
|
61
|
+
# list values
|
|
62
|
+
elif primary_key == "includes":
|
|
63
|
+
if value == "":
|
|
64
|
+
self.includes = []
|
|
65
|
+
else:
|
|
66
|
+
self.includes.append(value)
|
|
67
|
+
elif primary_key == "excludes":
|
|
68
|
+
if value == "":
|
|
69
|
+
self.excludes = []
|
|
70
|
+
else:
|
|
71
|
+
self.excludes.append(value)
|
|
72
|
+
elif primary_key == "extensions":
|
|
73
|
+
if value == "":
|
|
74
|
+
self.extensions = []
|
|
75
|
+
else:
|
|
76
|
+
self.extensions.append(value)
|
|
77
|
+
|
|
78
|
+
# dict values
|
|
79
|
+
elif primary_key == "reports":
|
|
80
|
+
if len(keys) == 1:
|
|
81
|
+
return
|
|
82
|
+
report_type = keys[1]
|
|
83
|
+
report_path = value
|
|
84
|
+
if value != "":
|
|
85
|
+
self.reports.append(ReportConfig(report_type, report_path))
|
|
86
|
+
else:
|
|
87
|
+
report_config = next((rc for rc in self.reports if rc.type == report_type), None)
|
|
88
|
+
if not report_config:
|
|
89
|
+
return
|
|
90
|
+
self.reports.remove(report_config)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import toml
|
|
2
|
+
|
|
3
|
+
class Info:
|
|
4
|
+
def __init__(self):
|
|
5
|
+
data = self._get_data()
|
|
6
|
+
self.version = data["project"]["version"]
|
|
7
|
+
self.url = data["project"]["urls"]["Homepage"]
|
|
8
|
+
|
|
9
|
+
def _get_data(self) -> dict:
|
|
10
|
+
with open("pyproject.toml", "r") as file:
|
|
11
|
+
data = toml.load(file)
|
|
12
|
+
return data
|
|
13
|
+
|
|
14
|
+
def get_version_info(self) -> str:
|
|
15
|
+
return f"""
|
|
16
|
+
Metripy {self.version}
|
|
17
|
+
{self.url}
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def get_help(self) -> str:
|
|
21
|
+
return self.get_version_info() + f"""
|
|
22
|
+
Usage: metripy [options]
|
|
23
|
+
Options:
|
|
24
|
+
--config=<file> Use a custom config file
|
|
25
|
+
--version Show the version and exit
|
|
26
|
+
--help Show this help message and exit
|
|
27
|
+
--debug Enable debug mode
|
|
28
|
+
--quiet Disable output
|
|
29
|
+
"""
|
metripy/Dependency/Dependency.py
CHANGED
metripy/Dependency/Pip/Pip.py
CHANGED
metripy/Dependency/Pip/PyPi.py
CHANGED
|
@@ -16,6 +16,7 @@ class PyPi:
|
|
|
16
16
|
print(f"Package '{dependency.name}' has no info section")
|
|
17
17
|
return dependency
|
|
18
18
|
|
|
19
|
+
dependency.type = "pip"
|
|
19
20
|
dependency.description = info.get("summary")
|
|
20
21
|
dependency.repository = info.get("project_url") or info.get("home_page")
|
|
21
22
|
if info.get("license"):
|
metripy/Git/GitAnalyzer.py
CHANGED
|
@@ -9,8 +9,6 @@ from metripy.Metric.Git.GitMetrics import GitMetrics
|
|
|
9
9
|
|
|
10
10
|
class GitAnalyzer:
|
|
11
11
|
def __init__(self, git_config: GitConfig):
|
|
12
|
-
print(git_config.repo)
|
|
13
|
-
print(git_config.branch)
|
|
14
12
|
self.repo = Repo(git_config.repo)
|
|
15
13
|
self.branch_name = git_config.branch
|
|
16
14
|
|
|
@@ -22,7 +20,6 @@ class GitAnalyzer:
|
|
|
22
20
|
first_of_month_last_year = datetime(now.year - 1, now.month, 1)
|
|
23
21
|
# first_of_month_last_year = datetime(now.year, now.month, 1)
|
|
24
22
|
after_date = first_of_month_last_year.strftime("%Y-%m-%d")
|
|
25
|
-
print(f"analyzing from {after_date}")
|
|
26
23
|
|
|
27
24
|
return self.get_metrics(after_date)
|
|
28
25
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from metripy.Component.Output.CliOutput import CliOutput
|
|
4
|
+
from metripy.Metric.ProjectMetrics import ProjectMetrics
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class JsonImporter:
|
|
8
|
+
def __init__(self, output: CliOutput):
|
|
9
|
+
self.output = output
|
|
10
|
+
|
|
11
|
+
def import_data(self, path: str) -> ProjectMetrics:
|
|
12
|
+
self.output.writeln(f"<info>Importing data from {path}...</info>")
|
|
13
|
+
with open(path, "r") as file:
|
|
14
|
+
data = json.load(file)
|
|
15
|
+
project_metrics = ProjectMetrics.from_dict(data)
|
|
16
|
+
self.output.writeln("<success>Data imported successfuly</success>")
|
|
17
|
+
return project_metrics
|
|
@@ -40,13 +40,13 @@ class AbstractLangAnalyzer(ABC):
|
|
|
40
40
|
full_name = module.full_name
|
|
41
41
|
|
|
42
42
|
if len(module.functions) > 0:
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
) / len(module.functions)
|
|
43
|
+
totalCc = sum(function.complexity for function in module.functions)
|
|
44
|
+
avgCcPerFunction = totalCc / len(module.functions)
|
|
46
45
|
avgLocPerFunction = (
|
|
47
46
|
module.lloc - module.comments - len(module.functions)
|
|
48
47
|
) / len(module.functions)
|
|
49
48
|
else:
|
|
49
|
+
totalCc = 0
|
|
50
50
|
avgCcPerFunction = 0
|
|
51
51
|
avgLocPerFunction = 0
|
|
52
52
|
maintainabilityIndex = module.maintainability_index
|
|
@@ -54,6 +54,7 @@ class AbstractLangAnalyzer(ABC):
|
|
|
54
54
|
file_metric = FileMetrics(
|
|
55
55
|
full_name=full_name,
|
|
56
56
|
loc=module.loc,
|
|
57
|
+
totalCc=totalCc,
|
|
57
58
|
avgCcPerFunction=avgCcPerFunction,
|
|
58
59
|
maintainabilityIndex=maintainabilityIndex,
|
|
59
60
|
avgLocPerFunction=avgLocPerFunction,
|
|
@@ -131,7 +131,7 @@ class PhpAnalyzer(AbstractLangAnalyzer):
|
|
|
131
131
|
|
|
132
132
|
code_lines = code.split("\n")
|
|
133
133
|
for func_name, function_node in functions.items():
|
|
134
|
-
lines = code_lines[function_node.lineno:function_node.line_end]
|
|
134
|
+
lines = code_lines[function_node.lineno : function_node.line_end]
|
|
135
135
|
function_metrics = self.halstead_analyzer.calculate_halstead_metrics(
|
|
136
136
|
"\n".join(lines)
|
|
137
137
|
)
|
|
@@ -147,6 +147,7 @@ class PhpAnalyzer(AbstractLangAnalyzer):
|
|
|
147
147
|
function_node.calculated_length = function_metrics["calculated_length"]
|
|
148
148
|
function_node.bugs = function_metrics["bugs"]
|
|
149
149
|
function_node.time = function_metrics["time"]
|
|
150
|
+
function_node.calc_mi()
|
|
150
151
|
|
|
151
152
|
maintainability_index = self._calculate_maintainability_index(
|
|
152
153
|
functions.values(), module_node
|
|
@@ -5,6 +5,7 @@ from radon.visitors import Class, Function
|
|
|
5
5
|
|
|
6
6
|
from metripy.Component.Output.ProgressBar import ProgressBar
|
|
7
7
|
from metripy.LangAnalyzer.AbstractLangAnalyzer import AbstractLangAnalyzer
|
|
8
|
+
from metripy.LangAnalyzer.Python.PythonHalSteadAnalyzer import PythonHalSteadAnalyzer
|
|
8
9
|
from metripy.Metric.Code.FileMetrics import FileMetrics
|
|
9
10
|
from metripy.Tree.ClassNode import ClassNode
|
|
10
11
|
from metripy.Tree.FunctionNode import FunctionNode
|
|
@@ -15,6 +16,7 @@ class PythonAnalyzer(AbstractLangAnalyzer):
|
|
|
15
16
|
|
|
16
17
|
def __init__(self):
|
|
17
18
|
super().__init__()
|
|
19
|
+
self.fallback_halstead_analyzer = PythonHalSteadAnalyzer()
|
|
18
20
|
|
|
19
21
|
def get_lang_name(self) -> str:
|
|
20
22
|
return "Python"
|
|
@@ -62,6 +64,7 @@ class PythonAnalyzer(AbstractLangAnalyzer):
|
|
|
62
64
|
function_node = FunctionNode(
|
|
63
65
|
full_name, item.name, item.lineno, item.col_offset, item.complexity
|
|
64
66
|
)
|
|
67
|
+
function_node.line_end = item.endline
|
|
65
68
|
if item.is_method:
|
|
66
69
|
class_node = classes.get(full_class_name)
|
|
67
70
|
if class_node is not None:
|
|
@@ -74,11 +77,6 @@ class PythonAnalyzer(AbstractLangAnalyzer):
|
|
|
74
77
|
else:
|
|
75
78
|
raise ValueError(f"Unknown item type: {type(item)}")
|
|
76
79
|
|
|
77
|
-
# print("--------------------------------")
|
|
78
|
-
# print(json.dumps([c.__dict__() for c in classes.values()], indent=4))
|
|
79
|
-
# print("--------------------------------")
|
|
80
|
-
# print(json.dumps([f.__dict__() for f in functions.values()], indent=4))
|
|
81
|
-
# exit()
|
|
82
80
|
module = analyze(code)
|
|
83
81
|
full_name = self.full_name(filename)
|
|
84
82
|
module_node = ModuleNode(
|
|
@@ -93,12 +91,9 @@ class PythonAnalyzer(AbstractLangAnalyzer):
|
|
|
93
91
|
)
|
|
94
92
|
module_node.classes.extend(classes.values())
|
|
95
93
|
module_node.functions.extend(functions.values())
|
|
96
|
-
|
|
97
|
-
# print(json.dumps([m.to_dict() for m in modules.values()], indent=4))
|
|
98
|
-
# exit()
|
|
94
|
+
|
|
99
95
|
h = h_visit(code)
|
|
100
96
|
assert isinstance(h, Halstead)
|
|
101
|
-
# print(h.total)
|
|
102
97
|
function_name: str
|
|
103
98
|
report: HalsteadReport
|
|
104
99
|
for function_name, report in h.functions:
|
|
@@ -117,9 +112,36 @@ class PythonAnalyzer(AbstractLangAnalyzer):
|
|
|
117
112
|
function_node.effort = report.effort
|
|
118
113
|
function_node.bugs = report.bugs
|
|
119
114
|
function_node.time = report.time
|
|
115
|
+
function_node.calc_mi()
|
|
120
116
|
else:
|
|
121
117
|
raise ValueError(f"Function node not found for function {full_name}")
|
|
122
118
|
|
|
119
|
+
code_lines = code.split("\n")
|
|
120
|
+
for func_name, function_node in functions.items():
|
|
121
|
+
if function_node.maintainability_index != 0:
|
|
122
|
+
continue
|
|
123
|
+
# if MI is 0, we want to take another look, radon does not like boring functions
|
|
124
|
+
|
|
125
|
+
lines = code_lines[function_node.lineno:function_node.line_end]
|
|
126
|
+
function_metrics = (
|
|
127
|
+
self.fallback_halstead_analyzer.calculate_halstead_metrics(
|
|
128
|
+
"\n".join(lines)
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
function_node.h1 = function_metrics["n1"]
|
|
132
|
+
function_node.h2 = function_metrics["n2"]
|
|
133
|
+
function_node.N1 = function_metrics["N1"]
|
|
134
|
+
function_node.N2 = function_metrics["N2"]
|
|
135
|
+
function_node.vocabulary = function_metrics["vocabulary"]
|
|
136
|
+
function_node.length = function_metrics["length"]
|
|
137
|
+
function_node.volume = function_metrics["volume"]
|
|
138
|
+
function_node.difficulty = function_metrics["difficulty"]
|
|
139
|
+
function_node.effort = function_metrics["effort"]
|
|
140
|
+
function_node.calculated_length = function_metrics["calculated_length"]
|
|
141
|
+
function_node.bugs = function_metrics["bugs"]
|
|
142
|
+
function_node.time = function_metrics["time"]
|
|
143
|
+
function_node.calc_mi()
|
|
144
|
+
|
|
123
145
|
maintainability_index = mi_visit(code, True)
|
|
124
146
|
module_node.maintainability_index = maintainability_index
|
|
125
147
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from metripy.LangAnalyzer.Generic.HalSteadAnalyzer import HalSteadAnalyzer
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class PythonHalSteadAnalyzer:
|
|
5
|
+
def __init__(self):
|
|
6
|
+
self.operators = [
|
|
7
|
+
"+",
|
|
8
|
+
"-",
|
|
9
|
+
"*",
|
|
10
|
+
"/",
|
|
11
|
+
"//",
|
|
12
|
+
"%",
|
|
13
|
+
"**",
|
|
14
|
+
"==",
|
|
15
|
+
"!=",
|
|
16
|
+
">",
|
|
17
|
+
"<",
|
|
18
|
+
">=",
|
|
19
|
+
"<=",
|
|
20
|
+
"=",
|
|
21
|
+
"+=",
|
|
22
|
+
"-=",
|
|
23
|
+
"*=",
|
|
24
|
+
"/=",
|
|
25
|
+
"%=",
|
|
26
|
+
"//=",
|
|
27
|
+
"**=",
|
|
28
|
+
"and",
|
|
29
|
+
"or",
|
|
30
|
+
"not",
|
|
31
|
+
"&",
|
|
32
|
+
"|",
|
|
33
|
+
"^",
|
|
34
|
+
"~",
|
|
35
|
+
"<<",
|
|
36
|
+
">>",
|
|
37
|
+
"in",
|
|
38
|
+
"not in",
|
|
39
|
+
"is",
|
|
40
|
+
"is not",
|
|
41
|
+
":",
|
|
42
|
+
",",
|
|
43
|
+
".",
|
|
44
|
+
"(",
|
|
45
|
+
")",
|
|
46
|
+
"[",
|
|
47
|
+
"]",
|
|
48
|
+
"{",
|
|
49
|
+
"}",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
self.analyzer = HalSteadAnalyzer(self.operators)
|
|
53
|
+
|
|
54
|
+
def calculate_halstead_metrics(self, code: str):
|
|
55
|
+
return self.analyzer.calculate_halstead_metrics(code)
|
|
@@ -4,14 +4,16 @@ import lizard
|
|
|
4
4
|
|
|
5
5
|
from metripy.Component.Output.ProgressBar import ProgressBar
|
|
6
6
|
from metripy.LangAnalyzer.AbstractLangAnalyzer import AbstractLangAnalyzer
|
|
7
|
-
from metripy.LangAnalyzer.Typescript.TypescriptAstParser import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
from metripy.LangAnalyzer.Typescript.TypescriptBasicLocAnalyzer import
|
|
12
|
-
TypescriptBasicLocAnalyzer
|
|
13
|
-
|
|
14
|
-
|
|
7
|
+
from metripy.LangAnalyzer.Typescript.TypescriptAstParser import TypescriptAstParser
|
|
8
|
+
from metripy.LangAnalyzer.Typescript.TypescriptBasicComplexityAnalyzer import (
|
|
9
|
+
TypescriptBasicComplexityAnalzyer,
|
|
10
|
+
)
|
|
11
|
+
from metripy.LangAnalyzer.Typescript.TypescriptBasicLocAnalyzer import (
|
|
12
|
+
TypescriptBasicLocAnalyzer,
|
|
13
|
+
)
|
|
14
|
+
from metripy.LangAnalyzer.Typescript.TypescriptHalSteadAnalyzer import (
|
|
15
|
+
TypeScriptHalSteadAnalyzer,
|
|
16
|
+
)
|
|
15
17
|
from metripy.Tree.ClassNode import ClassNode
|
|
16
18
|
from metripy.Tree.FunctionNode import FunctionNode
|
|
17
19
|
from metripy.Tree.ModuleNode import ModuleNode
|
|
@@ -146,7 +148,7 @@ class TypescriptAnalyzer(AbstractLangAnalyzer):
|
|
|
146
148
|
|
|
147
149
|
code_lines = code.split("\n")
|
|
148
150
|
for func_name, function_node in functions.items():
|
|
149
|
-
lines = code_lines[function_node.lineno:function_node.line_end]
|
|
151
|
+
lines = code_lines[function_node.lineno : function_node.line_end]
|
|
150
152
|
function_metrics = self.halstead_analyzer.calculate_halstead_metrics(
|
|
151
153
|
"\n".join(lines)
|
|
152
154
|
)
|
|
@@ -162,6 +164,7 @@ class TypescriptAnalyzer(AbstractLangAnalyzer):
|
|
|
162
164
|
function_node.calculated_length = function_metrics["calculated_length"]
|
|
163
165
|
function_node.bugs = function_metrics["bugs"]
|
|
164
166
|
function_node.time = function_metrics["time"]
|
|
167
|
+
function_node.calc_mi()
|
|
165
168
|
|
|
166
169
|
maintainability_index = self._calculate_maintainability_index(
|
|
167
170
|
functions.values(), module_node
|
|
@@ -8,7 +8,7 @@ class TypescriptAstParser:
|
|
|
8
8
|
self.parser = get_parser("typescript")
|
|
9
9
|
|
|
10
10
|
def _get_node_text(self, code: str, node) -> str:
|
|
11
|
-
return code[node.start_byte:node.end_byte].decode("utf-8")
|
|
11
|
+
return code[node.start_byte : node.end_byte].decode("utf-8")
|
|
12
12
|
|
|
13
13
|
def extract_structure(self, code: str) -> dict:
|
|
14
14
|
tree = self.parser.parse(bytes(code, "utf8"))
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from metripy.Metric.Code.SegmentedMetrics import SegmentedMetrics
|
|
2
|
+
from metripy.Metric.Trend.AggregatedTrendMetric import AggregatedTrendMetric
|
|
2
3
|
|
|
3
4
|
|
|
4
5
|
class AggregatedMetrics:
|
|
@@ -29,13 +30,19 @@ class AggregatedMetrics:
|
|
|
29
30
|
"methodSize": segmented_method_size,
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
self.trend: AggregatedTrendMetric | None = None
|
|
34
|
+
|
|
32
35
|
def to_dict(self) -> dict:
|
|
33
36
|
return {
|
|
34
|
-
"loc":
|
|
35
|
-
"avgCcPerFunction":
|
|
36
|
-
"maintainabilityIndex":
|
|
37
|
-
"avgLocPerFunction":
|
|
38
|
-
"num_files":
|
|
37
|
+
"loc": self.loc,
|
|
38
|
+
"avgCcPerFunction": round(self.avgCcPerFunction, 2),
|
|
39
|
+
"maintainabilityIndex": round(self.maintainabilityIndex, 2),
|
|
40
|
+
"avgLocPerFunction": round(self.avgLocPerFunction, 2),
|
|
41
|
+
"num_files": self.num_files,
|
|
42
|
+
"trend": self.trend.to_dict() if self.trend else None,
|
|
43
|
+
"trend_segmentation": (
|
|
44
|
+
self.trend.to_dict_segmentation() if self.trend else None
|
|
45
|
+
),
|
|
39
46
|
}
|
|
40
47
|
|
|
41
48
|
def to_dict_segmentation(self) -> dict:
|