""" Модуль MetricsEngine — вычисление метрик сложности и объёма кода. Для обоих языков (C++ и JS) используется библиотека lizard как основной источник метрик сложности (CCN, NLOC, число параметров). libclang используется только DependencyAnalyzer для извлечения #include. Ссылки: McCabe T.J., A Complexity Measure, IEEE TSE, 1976 Yin T., Lizard: A Simple Code Complexity Analyser, github.com/terryyin/lizard """ import logging from pathlib import Path from typing import Dict, List from config import AnalyzerConfig, DEFAULT_CONFIG from models import FunctionMetrics, ModuleMetrics logger = logging.getLogger(__name__) # Пытаемся импортировать lizard — предпочтительный вариант. # Если недоступен — используем встроенный анализатор (utils/code_analyzer.py). try: import lizard as _lizard_module _LIZARD_AVAILABLE = True logger.debug("Используется lizard для анализа метрик") except ImportError: _LIZARD_AVAILABLE = False from utils import code_analyzer as _builtin_analyzer # noqa: E402 logger.info( "lizard не установлен — используется встроенный анализатор. " "Для повышения точности: pip install lizard" ) def _analyze_file_unified(file_path: str): """ Единая точка вызова анализа файла — lizard если доступен, иначе встроенный. Возвращает объект с полем function_list (список объектов с атрибутами name, start_line, nloc, cyclomatic_complexity, parameter_count). """ if _LIZARD_AVAILABLE: return _lizard_module.analyze_file(file_path) else: return _builtin_analyzer.analyze_file(file_path) class MetricsEngine: """ Вычисляет метрики сложности и объёма для каждого модуля. Использует lizard для анализа C++ и JavaScript файлов, агрегирует результаты на уровне модулей. """ def __init__(self, config: AnalyzerConfig = DEFAULT_CONFIG): self.config = config self.thresholds = config.thresholds # ------------------------------------------------------------------ # Публичный API # ------------------------------------------------------------------ def analyze(self, modules: Dict[str, ModuleMetrics]) -> Dict[str, ModuleMetrics]: """ Заполняет метрики для каждого модуля. :param modules: словарь модулей от Collector (с заполненными files) :returns: тот же словарь с заполненными метриками """ for module_name, module in modules.items(): logger.info("MetricsEngine: анализируем модуль '%s'", module_name) self._analyze_module(module) return modules # ------------------------------------------------------------------ # Внутренние методы # ------------------------------------------------------------------ def _analyze_module(self, module: ModuleMetrics) -> None: """Анализирует все файлы модуля и агрегирует метрики.""" all_functions: List[FunctionMetrics] = [] for file_path in module.files: funcs = self._analyze_file(file_path) all_functions.extend(funcs) module.functions = all_functions self._aggregate(module) def _analyze_file(self, file_path: str) -> List[FunctionMetrics]: """Запускает анализатор на один файл и возвращает список FunctionMetrics.""" try: result = _analyze_file_unified(file_path) except Exception as exc: logger.warning("Ошибка анализа файла %s: %s", file_path, exc) return [] if result is None: return [] lang = self._detect_language(file_path) functions: List[FunctionMetrics] = [] for func in result.function_list: fm = FunctionMetrics( name=func.name, file=file_path, line=func.start_line, ccn=func.cyclomatic_complexity, nloc=func.nloc, params=func.parameter_count, language=lang, ) functions.append(fm) return functions def _aggregate(self, module: ModuleMetrics) -> None: """Считает агрегированные показатели из списка функций.""" funcs = module.functions if not funcs: module.total_nloc = 0 module.avg_ccn = 0.0 module.max_ccn = 0 module.total_functions = 0 module.heavy_functions_count = 0 module.heavy_functions_ratio = 0.0 return warn_ccn = self.thresholds.ccn_warn module.total_functions = len(funcs) module.total_nloc = sum(f.nloc for f in funcs) module.max_ccn = max(f.ccn for f in funcs) module.avg_ccn = sum(f.ccn for f in funcs) / len(funcs) module.heavy_functions_count = sum(1 for f in funcs if f.ccn > warn_ccn) module.heavy_functions_ratio = module.heavy_functions_count / module.total_functions def _detect_language(self, file_path: str) -> str: """Определяет язык по расширению файла.""" suffix = Path(file_path).suffix.lower() if suffix in self.config.cpp_extensions: return "cpp" if suffix in self.config.js_extensions: return "js" return "unknown"