91 lines
2.7 KiB
Python
91 lines
2.7 KiB
Python
import math
|
|
from pathlib import Path
|
|
from typing import Dict, Any
|
|
|
|
|
|
def analyze_file_metrics(path) -> Dict[str, Any]:
|
|
"""Analyze a single file and return metrics dict.
|
|
|
|
Returns:
|
|
{"file": str(path), "name": filename, "avg_cc": float, "max_cc": int,
|
|
"func_count": int, "mi": float, "language": str}
|
|
"""
|
|
p = Path(path)
|
|
# lazy import lizard to avoid hard dependency until needed
|
|
try:
|
|
import lizard as lizard_mod
|
|
except Exception:
|
|
lizard_mod = None
|
|
|
|
# If lizard is not available, attempt to approximate with simple counts
|
|
if lizard_mod is None:
|
|
# fallback: minimal values using line counts
|
|
loc = 0
|
|
try:
|
|
with p.open("r", encoding="utf-8", errors="ignore") as fh:
|
|
loc = sum(1 for _ in fh)
|
|
except Exception:
|
|
loc = 0
|
|
return {
|
|
"file": str(p),
|
|
"name": p.name,
|
|
"avg_cc": 0.0,
|
|
"max_cc": 0,
|
|
"func_count": 0,
|
|
"mi": 0.0,
|
|
"language": "unknown",
|
|
}
|
|
|
|
# use lizard when available
|
|
res = lizard_mod.analyze_file(str(p))
|
|
funcs = res.function_list if hasattr(res, "function_list") else []
|
|
cc_values = [getattr(f, "cyclomatic_complexity", 0) for f in funcs]
|
|
func_count = len(cc_values)
|
|
avg_cc = float(sum(cc_values) / func_count) if func_count else 0.0
|
|
max_cc = int(max(cc_values)) if cc_values else 0
|
|
|
|
# Maintainability Index (approximate): use Coleman-Oman formula when possible.
|
|
# MI = 171 - 5.2 * ln(HV) - 0.23 * CC - 16.2 * ln(LOC)
|
|
loc = getattr(res, "nloc", None) or 0
|
|
if not loc:
|
|
try:
|
|
with p.open("r", encoding="utf-8", errors="ignore") as fh:
|
|
loc = sum(1 for _ in fh)
|
|
except Exception:
|
|
loc = 0
|
|
hv = None
|
|
try:
|
|
hv = getattr(res, "halstead_volume", None)
|
|
except Exception:
|
|
hv = None
|
|
|
|
mi_raw = 171.0
|
|
try:
|
|
if hv and hv > 0 and loc and loc > 0:
|
|
mi_raw = (
|
|
171
|
|
- 5.2 * math.log(max(1.0, hv))
|
|
- 0.23 * avg_cc
|
|
- 16.2 * math.log(max(1.0, loc))
|
|
)
|
|
elif loc and loc > 0:
|
|
mi_raw = 171 - 0.23 * avg_cc - 16.2 * math.log(max(1.0, loc))
|
|
else:
|
|
mi_raw = 0.0
|
|
mi = max(0.0, min(100.0, (mi_raw * 100.0 / 171.0)))
|
|
except Exception:
|
|
mi = 0.0
|
|
|
|
# attempt to map extension to language
|
|
ext = p.suffix
|
|
language = ext.lower().lstrip(".") if ext else "unknown"
|
|
return {
|
|
"file": str(p),
|
|
"name": p.name,
|
|
"avg_cc": round(avg_cc, 2),
|
|
"max_cc": int(max_cc),
|
|
"func_count": int(func_count),
|
|
"mi": round(mi, 2),
|
|
"language": language,
|
|
}
|