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"
|