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