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", "functions": [], } # 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 # Extract function details for detailed reporting func_details = [] for f in funcs: func_details.append( { "name": getattr(f, "name", "unknown"), "cc": getattr(f, "cyclomatic_complexity", 0), "line": getattr(f, "start_line", 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, "functions": func_details, # Add function-level details }