arch-researcher/metrics_engine.py
2026-04-27 23:44:22 +05:00

149 lines
6.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Модуль 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"