149 lines
6.0 KiB
Python
149 lines
6.0 KiB
Python
"""
|
||
Модуль 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"
|