This commit is contained in:
gre-ilya 2026-05-20 07:12:02 +05:00
commit 950c984542
36 changed files with 3415 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
__pycache__
*.pyc

4
prog/arch-researcher/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
*.swp
*__pycache__*

View File

@ -0,0 +1,112 @@
# Legacy Analyzer
Методический инструментарий диагностики архитектурного состояния
унаследованных программных систем на языках **C++** и **JavaScript/TypeScript**.
## Назначение
Инструмент реализует следующий pipeline анализа:
```
Collector → MetricsEngine → DependencyAnalyzer → DecisionEngine → Reporter
```
| Модуль | Ответственность |
|----------------------|----------------------------------------------------------------|
| `Collector` | Обход файловой структуры, идентификация модулей |
| `MetricsEngine` | CCN, NLOC, число параметров (через `lizard`) |
| `DependencyAnalyzer` | Граф зависимостей, coupling_in/out, instability, циклы (через `networkx`) |
| `DecisionEngine` | Классификация риска, выбор стратегии модернизации |
| `Reporter` | Генерация JSON и HTML отчётов |
## Установка
```bash
pip install lizard networkx
# Опционально — AST-анализ C++ через libclang:
# pip install libclang
# Ubuntu: sudo apt install clang libclang-dev
```
## Использование
```bash
# Базовый запуск
python main.py /path/to/your/project
# С кастомными параметрами
python main.py /path/to/project \
--output my_report \
--formats json html \
--ccn-warn 15 \
--ccn-critical 25 \
--verbose
# Только JSON
python main.py /path/to/project --formats json
```
## Программный API
```python
from analyzer import Analyzer
from config import AnalyzerConfig, ThresholdConfig
config = AnalyzerConfig(
output_dir="report",
thresholds=ThresholdConfig(ccn_warn=12, ccn_critical=25),
)
report = Analyzer(config).run("/path/to/project")
print(f"Системный риск: {report.system_risk_level.name}")
print(f"Стратегия: {report.recommended_system_strategy.name}")
for decision in report.decisions[:10]: # Топ-10 проблемных модулей
print(f" {decision.module_name}: {decision.risk_level.name}")
```
## Выходные файлы
После анализа в директории `legacy_report/` (или указанной через `--output`) создаются:
- `report.json` — машинно-читаемый отчёт (для CI/CD)
- `report.html` — человекочитаемый отчёт с таблицами и карточками
## Метрики
| Метрика | Формула / источник | Порог warn / critical |
|---------------------|---------------------------------------------|-----------------------|
| CCN | McCabe (1976) | > 10 / > 20 |
| NLOC | Строк без комментариев | > 50 / > 100 |
| Параметры функции | — | > 5 / > 8 |
| C_out (fan-out) | Число исходящих рёбер модуля | > 5 / > 10 |
| C_in (fan-in) | Число входящих рёбер модуля | > 10 / > 20 |
| Instability (I) | C_out / (C_in + C_out), Martin (2018) | > 0.7 / > 0.9 |
## Стратегии модернизации
| Уровень риска | Стратегия | Описание |
|---------------|----------------|--------------------------------------------------|
| LOW | Оставить | Модуль в удовлетворительном состоянии |
| MEDIUM | Рефакторинг | Локальное улучшение без изменения архитектуры |
| HIGH | Реинжиниринг | Перестройка архитектуры с сохранением логики |
| CRITICAL | Замена | Полная переработка или замена модуля |
## Структура проекта
```
legacy_analyzer/
├── main.py # CLI точка входа
├── analyzer.py # Главный оркестратор (фасад)
├── config.py # Конфигурация и пороговые значения
├── models.py # Доменные модели данных
├── core/
│ ├── collector.py # Сбор файловой структуры
│ ├── metrics_engine.py # Вычисление метрик
│ └── dependency_analyzer.py # Граф зависимостей
└── reporters/
├── reporter.py # Фасад репортеров
├── json_reporter.py # JSON отчёт
└── html_reporter.py # HTML отчёт
```

View File

@ -0,0 +1,90 @@
"""
Главный фасад инструментария Analyzer.
Orchestrates полный pipeline анализа:
Collector MetricsEngine DependencyAnalyzer DecisionEngine Reporter
Использование:
from analyzer import Analyzer, AnalyzerConfig
config = AnalyzerConfig(output_dir="my_report")
report = Analyzer(config).run("/path/to/project")
"""
import logging
import sys
from pathlib import Path
from config import AnalyzerConfig, DEFAULT_CONFIG
from models import AnalysisReport
from core.collector import Collector
from core.metrics_engine import MetricsEngine
from core.dependency_analyzer import DependencyAnalyzer
from core.decision_engine import DecisionEngine
from reporters.reporter import Reporter
logger = logging.getLogger(__name__)
class Analyzer:
"""
Главный оркестратор анализа унаследованной системы.
Реализует принцип разделения ответственности (SRP):
каждый шаг анализа выполняется специализированным модулем,
Analyzer лишь координирует их взаимодействие.
"""
def __init__(self, config: AnalyzerConfig = DEFAULT_CONFIG):
self.config = config
self._collector = Collector(config)
self._metrics = MetricsEngine(config)
self._dependencies = DependencyAnalyzer(config)
self._decisions = DecisionEngine(config)
self._reporter = Reporter(config)
def run(self, project_root: str) -> AnalysisReport:
"""
Выполняет полный цикл анализа проекта.
:param project_root: путь к корневой директории анализируемой системы
:returns: заполненный AnalysisReport
"""
root = str(Path(project_root).resolve())
logger.info("=" * 60)
logger.info("Запуск анализа: %s", root)
logger.info("=" * 60)
# 1. Сбор файловой структуры
logger.info("[1/4] Collector — обход файловой структуры...")
modules = self._collector.collect(root)
if not modules:
logger.warning("Не найдено ни одного анализируемого файла в %s", root)
# 2. Вычисление метрик сложности
logger.info("[2/4] MetricsEngine — вычисление метрик сложности...")
modules = self._metrics.analyze(modules)
# 3. Построение графа зависимостей
logger.info("[3/4] DependencyAnalyzer — построение графа зависимостей...")
modules, cycles = self._dependencies.analyze(modules, root)
# 4. Принятие решений
logger.info("[4/4] DecisionEngine — классификация риска и выбор стратегии...")
report = AnalysisReport(project_root=root, modules=modules)
report = self._decisions.analyze(report, cycles)
# 5. Формирование отчётов
logger.info("[5/5] Reporter — формирование отчётов...")
created_files = self._reporter.generate(report)
logger.info("=" * 60)
logger.info("Анализ завершён. Системный риск: %s", report.system_risk_level.name)
logger.info("Рекомендуемая стратегия: %s", report.recommended_system_strategy.name)
logger.info("Отчёты созданы:")
for f in created_files:
logger.info(" %s", f)
logger.info("=" * 60)
return report

View File

@ -0,0 +1,74 @@
"""
Конфигурация инструментария анализа legacy-систем.
Все пороговые значения вынесены сюда для удобной настройки под конкретный проект.
"""
from dataclasses import dataclass, field
from typing import List
@dataclass
class ThresholdConfig:
"""Пороговые значения метрик для классификации риска."""
# Цикломатическая сложность (McCabe, 1976)
ccn_warn: int = 10 # > 10 — функция требует внимания
ccn_critical: int = 20 # > 20 — функция является кандидатом на рефакторинг
# Число строк кода без комментариев в функции
nloc_warn: int = 50
nloc_critical: int = 100
# Число параметров функции
params_warn: int = 5
params_critical: int = 8
# Исходящая связность модуля (fan-out)
coupling_out_warn: int = 5
coupling_out_critical: int = 10
# Входящая связность модуля (fan-in) — высокое значение = нестабильный центральный модуль
coupling_in_warn: int = 10
coupling_in_critical: int = 20
# Нестабильность (Instability = C_out / (C_in + C_out)), метрика Р. Мартина
# 0 — абсолютно стабильный, 1 — абсолютно нестабильный
instability_warn: float = 0.7
instability_critical: float = 0.9
# Доля "тяжёлых" функций в модуле (с CCN > ccn_warn)
heavy_functions_ratio_warn: float = 0.3
heavy_functions_ratio_critical: float = 0.6
@dataclass
class AnalyzerConfig:
"""Основная конфигурация анализатора."""
# Расширения файлов для анализа
cpp_extensions: List[str] = field(default_factory=lambda: [
".cpp", ".cxx", ".cc", ".c", ".hpp", ".hxx", ".h"
])
js_extensions: List[str] = field(default_factory=lambda: [
".js", ".mjs", ".cjs", ".ts"
])
# Директории, которые игнорируются при обходе
ignored_dirs: List[str] = field(default_factory=lambda: [
"node_modules", ".git", "build", "dist", "out",
"__pycache__", ".venv", "venv", "vendor", "third_party"
])
# Путь к libclang (.so / .dylib / .dll).
# None — автоматическое определение; если не найден — fallback на regex.
libclang_path: str = None
thresholds: ThresholdConfig = field(default_factory=ThresholdConfig)
# Форматы отчётов
output_formats: List[str] = field(default_factory=lambda: ["json", "html"])
output_dir: str = "legacy_report"
# Инстанция по умолчанию — используется если конфиг не передан явно
DEFAULT_CONFIG = AnalyzerConfig()

View File

View File

@ -0,0 +1,131 @@
"""
Модуль Collector обход файловой структуры проекта и сбор исходных данных.
Отвечает за:
- рекурсивный обход дерева каталогов
- идентификацию модулей и их границ
- подготовку списков файлов для MetricsEngine и DependencyAnalyzer
"""
import logging
from pathlib import Path
from typing import Dict, List, Tuple
from config import AnalyzerConfig, DEFAULT_CONFIG
from models import ModuleMetrics
logger = logging.getLogger(__name__)
class Collector:
"""
Собирает файловую структуру проекта и формирует первичное
представление модулей.
Стратегия разбивки на модули: каждая директория первого уровня
является отдельным модулем. Файлы непосредственно в корне
помещаются в псевдомодуль "<root>".
"""
def __init__(self, config: AnalyzerConfig = DEFAULT_CONFIG):
self.config = config
self._cpp_exts = set(config.cpp_extensions)
self._js_exts = set(config.js_extensions)
self._ignored = set(config.ignored_dirs)
# ------------------------------------------------------------------
# Публичный API
# ------------------------------------------------------------------
def collect(self, project_root: str) -> Dict[str, ModuleMetrics]:
"""
Обходит дерево проекта и возвращает словарь модулей.
:param project_root: абсолютный или относительный путь к корню проекта
:returns: {module_name: ModuleMetrics} с заполненными полями files и language
"""
root = Path(project_root).resolve()
if not root.exists():
raise FileNotFoundError(f"Корень проекта не найден: {root}")
logger.info("Начало сбора данных из: %s", root)
modules: Dict[str, ModuleMetrics] = {}
self._walk(root, root, modules)
# Убираем пустые модули (нет анализируемых файлов)
modules = {k: v for k, v in modules.items() if v.files}
logger.info("Обнаружено модулей: %d", len(modules))
for name, mod in modules.items():
logger.debug(" %s: %d файл(ов) [%s]", name, len(mod.files), mod.language)
return modules
# ------------------------------------------------------------------
# Внутренние методы
# ------------------------------------------------------------------
def _walk(self, root: Path, current: Path, modules: Dict[str, ModuleMetrics]) -> None:
"""Рекурсивный обход директории."""
try:
entries = sorted(current.iterdir())
except PermissionError:
logger.warning("Нет прав на чтение: %s", current)
return
for entry in entries:
if entry.name.startswith("."):
continue
if entry.name in self._ignored:
logger.debug("Пропускаем директорию: %s", entry)
continue
if entry.is_dir():
self._walk(root, entry, modules)
elif entry.is_file():
lang = self._detect_language(entry)
if lang is None:
continue
module_name = self._resolve_module_name(root, entry)
if module_name not in modules:
modules[module_name] = ModuleMetrics(name=module_name)
modules[module_name].files.append(str(entry))
modules[module_name].language = self._merge_language(
modules[module_name].language, lang
)
def _detect_language(self, path: Path) -> str | None:
"""Возвращает 'cpp', 'js' или None если файл не анализируется."""
suffix = path.suffix.lower()
if suffix in self._cpp_exts:
return "cpp"
if suffix in self._js_exts:
return "js"
return None
def _resolve_module_name(self, root: Path, file: Path) -> str:
"""
Определяет имя модуля для файла.
Файлы в корне "<root>".
Файлы в поддиректории относительный путь к директории первого уровня.
"""
try:
rel = file.relative_to(root)
except ValueError:
return "<root>"
parts = rel.parts
if len(parts) == 1:
return "<root>"
# Берём путь относительно корня до файла (вся директория как модуль)
return str(Path(*parts[:-1]))
@staticmethod
def _merge_language(existing: str, new_lang: str) -> str:
"""Объединяет языки при добавлении нового файла в модуль."""
if existing == "mixed" or existing == new_lang:
return existing if existing != "mixed" else "mixed"
if existing in ("cpp", "js") and existing != new_lang:
return "mixed"
return new_lang # existing == "" — первый файл

View File

@ -0,0 +1,287 @@
"""
Модуль DecisionEngine классификация архитектурного риска и
выбор стратегии модернизации.
Реализует многокритериальный анализ на основе набора метрик,
полученных от MetricsEngine и DependencyAnalyzer.
Логика принятия решений:
Каждый модуль получает «очки риска» по нескольким независимым
критериям. Итоговый уровень риска определяется числом набранных
очков, что обеспечивает интерпретируемость и прозрачность решения.
Стратегии (по Lehman & Belady, 1985; Fowler, 2018):
LOW / MEDIUM KEEP / REFACTOR
HIGH REENGINEER
CRITICAL REPLACE
"""
import logging
from typing import Dict, List, Tuple
from config import AnalyzerConfig, DEFAULT_CONFIG
from models import (
AnalysisReport,
ModuleDecision,
ModuleMetrics,
ModernizationStrategy,
RiskLevel,
)
logger = logging.getLogger(__name__)
class DecisionEngine:
"""
Классифицирует модули по уровню архитектурного риска
и формирует рекомендации по стратегии модернизации.
"""
def __init__(self, config: AnalyzerConfig = DEFAULT_CONFIG):
self.config = config
self.t = config.thresholds
# ------------------------------------------------------------------
# Публичный API
# ------------------------------------------------------------------
def analyze(
self,
report: AnalysisReport,
cycles: List[List[str]],
) -> AnalysisReport:
"""
Заполняет report.decisions и системные агрегаты.
:param report: отчёт с заполненными modules
:param cycles: список циклов зависимостей от DependencyAnalyzer
:returns: обновлённый отчёт
"""
decisions: List[ModuleDecision] = []
for module_name, module in report.modules.items():
score, reasons = self._score_module(module)
risk = self._score_to_risk(score)
strategy = self._risk_to_strategy(risk)
decision = ModuleDecision(
module_name=module_name,
risk_level=risk,
strategy=strategy,
reasons=reasons,
priority=self._compute_priority(score, module),
recommended_pattern=self._recommend_pattern(risk, module),
)
decisions.append(decision)
# Сортируем по приоритету (меньше = важнее)
decisions.sort(key=lambda d: d.priority)
report.decisions = decisions
report.dependency_cycles = cycles
# Системные агрегаты
report.total_files = sum(len(m.files) for m in report.modules.values())
report.total_nloc = sum(m.total_nloc for m in report.modules.values())
report.total_functions = sum(m.total_functions for m in report.modules.values())
all_ccn = [
f.ccn
for m in report.modules.values()
for f in m.functions
]
report.avg_system_ccn = sum(all_ccn) / len(all_ccn) if all_ccn else 0.0
report.system_risk_level = self._system_risk(decisions)
report.recommended_system_strategy = self._risk_to_strategy(report.system_risk_level)
logger.info(
"DecisionEngine: системный риск=%s, стратегия=%s",
report.system_risk_level.value,
report.recommended_system_strategy.name,
)
return report
# ------------------------------------------------------------------
# Оценка отдельного модуля
# ------------------------------------------------------------------
def _score_module(self, module: ModuleMetrics) -> Tuple[int, List[str]]:
"""
Начисляет очки риска по каждому критерию.
Возвращает (суммарный_счёт, список_причин).
Каждый критерий даёт 1 (warn) или 2 (critical) очка.
"""
score = 0
reasons: List[str] = []
# --- Цикломатическая сложность ---
if module.max_ccn > self.t.ccn_critical:
score += 2
reasons.append(
f"Критическая CCN: max={module.max_ccn} (порог {self.t.ccn_critical})"
)
elif module.avg_ccn > self.t.ccn_warn:
score += 1
reasons.append(
f"Повышенная средняя CCN: avg={module.avg_ccn:.1f} (порог {self.t.ccn_warn})"
)
# --- Доля сложных функций ---
if module.heavy_functions_ratio > self.t.heavy_functions_ratio_critical:
score += 2
reasons.append(
f"Критическая доля сложных функций: "
f"{module.heavy_functions_ratio:.0%} (порог {self.t.heavy_functions_ratio_critical:.0%})"
)
elif module.heavy_functions_ratio > self.t.heavy_functions_ratio_warn:
score += 1
reasons.append(
f"Высокая доля сложных функций: "
f"{module.heavy_functions_ratio:.0%} (порог {self.t.heavy_functions_ratio_warn:.0%})"
)
# --- Исходящая связность (fan-out) ---
if module.coupling_out > self.t.coupling_out_critical:
score += 2
reasons.append(
f"Критическая исходящая связность: C_out={module.coupling_out} "
f"(порог {self.t.coupling_out_critical})"
)
elif module.coupling_out > self.t.coupling_out_warn:
score += 1
reasons.append(
f"Высокая исходящая связность: C_out={module.coupling_out} "
f"(порог {self.t.coupling_out_warn})"
)
# --- Входящая связность (fan-in) ---
if module.coupling_in > self.t.coupling_in_critical:
score += 2
reasons.append(
f"Критическая входящая связность: C_in={module.coupling_in} "
f"(порог {self.t.coupling_in_critical}) — нестабильный центральный узел"
)
elif module.coupling_in > self.t.coupling_in_warn:
score += 1
reasons.append(
f"Высокая входящая связность: C_in={module.coupling_in} "
f"(порог {self.t.coupling_in_warn})"
)
# --- Нестабильность (Instability, метрика Р. Мартина) ---
if module.instability > self.t.instability_critical:
score += 2
reasons.append(
f"Критическая нестабильность: I={module.instability:.2f} "
f"(порог {self.t.instability_critical})"
)
elif module.instability > self.t.instability_warn:
score += 1
reasons.append(
f"Высокая нестабильность: I={module.instability:.2f} "
f"(порог {self.t.instability_warn})"
)
# --- Циклические зависимости ---
if module.has_cycles:
score += 3
reasons.append("Участвует в циклических зависимостях — признак архитектурной деградации")
return score, reasons
# ------------------------------------------------------------------
# Преобразование очков в решения
# ------------------------------------------------------------------
@staticmethod
def _score_to_risk(score: int) -> RiskLevel:
"""Преобразует суммарный счёт в уровень риска."""
if score == 0:
return RiskLevel.LOW
if score <= 2:
return RiskLevel.MEDIUM
if score <= 5:
return RiskLevel.HIGH
return RiskLevel.CRITICAL
@staticmethod
def _risk_to_strategy(risk: RiskLevel) -> ModernizationStrategy:
"""
Определяет стратегию модернизации по уровню риска.
Логика основана на классификации Fowler (2018) и
Lehman & Belady (1985).
"""
return {
RiskLevel.LOW: ModernizationStrategy.KEEP,
RiskLevel.MEDIUM: ModernizationStrategy.REFACTOR,
RiskLevel.HIGH: ModernizationStrategy.REENGINEER,
RiskLevel.CRITICAL: ModernizationStrategy.REPLACE,
}[risk]
@staticmethod
def _compute_priority(score: int, module: ModuleMetrics) -> int:
"""
Приоритет в плане рефакторинга: меньше = выше приоритет.
Модули с циклами и высоким C_in получают наивысший приоритет
(они блокируют изменения во множестве других модулей).
"""
priority = 1000 - score * 100
# Модули с высоким fan-in важнее менять первыми —
# они наиболее болезненные для всей системы
if module.coupling_in > 0:
priority -= module.coupling_in * 10
if module.has_cycles:
priority -= 200
return max(priority, 1)
@staticmethod
def _recommend_pattern(risk: RiskLevel, module: ModuleMetrics) -> str:
"""
Рекомендует архитектурный паттерн на основе уровня риска и метрик.
Паттерны по Fowler (2018), Martin (2018):
LOW без изменений
MEDIUM Extract Method/Module, Модульный монолит
HIGH Strangler Fig, Anti-Corruption Layer
CRITICAL Hexagonal Architecture (порты и адаптеры)
"""
if risk == RiskLevel.LOW:
return ""
if risk == RiskLevel.MEDIUM:
if module.has_cycles:
return "Модульный монолит: явные границы модулей для разрыва цикла"
return "Рефакторинг: Extract Method / Extract Module"
if risk == RiskLevel.HIGH:
if module.has_cycles:
return "Anti-Corruption Layer: изоляция модуля от циклических зависимостей"
if module.coupling_in > module.coupling_out:
return "Strangler Fig: постепенное замещение — модуль активно используется"
return "Strangler Fig / Anti-Corruption Layer: постепенный реинжиниринг"
# CRITICAL
if module.has_cycles:
return "Strangler Fig + Anti-Corruption Layer: замена с изоляцией от цикла"
return "Hexagonal Architecture (порты и адаптеры): полная замена с чёткими границами"
@staticmethod
def _system_risk(decisions: List[ModuleDecision]) -> RiskLevel:
"""Определяет системный уровень риска на основе распределения модулей."""
if not decisions:
return RiskLevel.LOW
total = len(decisions)
critical_ratio = sum(1 for d in decisions if d.risk_level == RiskLevel.CRITICAL) / total
high_ratio = sum(1 for d in decisions if d.risk_level in (RiskLevel.HIGH, RiskLevel.CRITICAL)) / total
if critical_ratio > 0.3:
return RiskLevel.CRITICAL
if high_ratio > 0.5:
return RiskLevel.HIGH
if high_ratio > 0.2:
return RiskLevel.MEDIUM
return RiskLevel.LOW

View File

@ -0,0 +1,318 @@
"""
Модуль DependencyAnalyzer построение и анализ графа зависимостей.
Алгоритм:
1. Для C++ файлов извлекаются #include-директивы:
- через libclang AST (если установлен) точный разбор
- через regex (fallback) лексический разбор
2. Для JS файлов regex-парсинг require() и import ... from
3. #include / import разрешаются до имени модуля (директории)
4. Строится ориентированный граф зависимостей (networkx DiGraph)
5. Вычисляются метрики: coupling_in, coupling_out, instability
6. Обнаруживаются циклические зависимости (strongly connected components)
Метрика нестабильности (Instability):
I = C_out / (C_in + C_out), где
C_out исходящая связность (fan-out),
C_in входящая связность (fan-in).
I = 0 абсолютно стабильный модуль (ничего не импортирует)
I = 1 абсолютно нестабильный модуль (никто не зависит от него)
Ссылки:
Martin R., Clean Architecture, 2018 (принципы стабильности)
NetworkX: https://networkx.org
"""
import logging
import re
from pathlib import Path
from typing import Dict, List, Optional, Set, Tuple
from config import AnalyzerConfig, DEFAULT_CONFIG
from models import ModuleMetrics
logger = logging.getLogger(__name__)
# Пытаемся подключить networkx
try:
import networkx as nx
_NX_AVAILABLE = True
except ImportError:
_NX_AVAILABLE = False
# Пытаемся подключить libclang
try:
import clang.cindex as clang
_CLANG_AVAILABLE = True
except ImportError:
_CLANG_AVAILABLE = False
# Regex для fallback-разбора #include
_RE_INCLUDE = re.compile(r'^\s*#\s*include\s+[<"]([^>"]+)[>"]', re.MULTILINE)
# Regex для JS import / require
_RE_JS_IMPORT = re.compile(
r'(?:import\s+.*?\s+from\s+|require\s*\(\s*)[\'"]([^\'"]+)[\'"]',
re.MULTILINE,
)
class DependencyAnalyzer:
"""
Строит граф зависимостей между модулями и вычисляет метрики связности.
"""
def __init__(self, config: AnalyzerConfig = DEFAULT_CONFIG):
self.config = config
if not _NX_AVAILABLE:
raise RuntimeError(
"Библиотека 'networkx' не установлена. "
"Выполните: pip install networkx"
)
# Инициализируем libclang если доступен и указан путь
self._clang_index: Optional[object] = None
if _CLANG_AVAILABLE:
try:
if config.libclang_path:
clang.Config.set_library_file(config.libclang_path)
self._clang_index = clang.Index.create()
logger.info("libclang инициализирован — используется AST-анализ для C++")
except Exception as exc:
logger.warning("libclang недоступен (%s), используется regex fallback", exc)
else:
logger.info("libclang не установлен — используется regex fallback для #include")
# ------------------------------------------------------------------
# Публичный API
# ------------------------------------------------------------------
def analyze(
self,
modules: Dict[str, ModuleMetrics],
project_root: str,
) -> Tuple[Dict[str, ModuleMetrics], List[List[str]]]:
"""
Строит граф зависимостей и заполняет coupling-метрики в модулях.
:returns: (обновлённые модули, список циклов зависимостей)
"""
root = Path(project_root).resolve()
# --- Построение файл → модуль ---
file_to_module = self._build_file_to_module_map(modules)
# --- Граф зависимостей ---
graph: nx.DiGraph = nx.DiGraph()
graph.add_nodes_from(modules.keys())
for module_name, module in modules.items():
deps = self._collect_module_dependencies(module, file_to_module, root)
module.dependencies = deps - {module_name} # убираем самозависимость
for dep in deps:
if dep != module_name and dep in modules:
graph.add_edge(module_name, dep)
# --- Вычисляем coupling ---
for module_name, module in modules.items():
module.coupling_out = graph.out_degree(module_name)
module.coupling_in = graph.in_degree(module_name)
total = module.coupling_in + module.coupling_out
module.instability = module.coupling_out / total if total > 0 else 0.0
# --- Dependents (обратные зависимости) ---
for module_name, module in modules.items():
module.dependents = set(graph.predecessors(module_name))
# --- Циклические зависимости (SCC с размером > 1) ---
cycles = self._find_cycles(graph)
# Отмечаем модули, участвующие в циклах
cyclic_modules: Set[str] = set()
for cycle in cycles:
cyclic_modules.update(cycle)
for module_name in cyclic_modules:
if module_name in modules:
modules[module_name].has_cycles = True
logger.info(
"DependencyAnalyzer: рёбер в графе=%d, циклов=%d",
graph.number_of_edges(),
len(cycles),
)
return modules, cycles
# ------------------------------------------------------------------
# Извлечение зависимостей
# ------------------------------------------------------------------
def _collect_module_dependencies(
self,
module: ModuleMetrics,
file_to_module: Dict[str, str],
root: Path,
) -> Set[str]:
"""Собирает зависимости модуля из всех его файлов."""
deps: Set[str] = set()
for file_path in module.files:
file_deps = self._extract_file_dependencies(file_path, file_to_module, root)
deps.update(file_deps)
return deps
def _extract_file_dependencies(
self,
file_path: str,
file_to_module: Dict[str, str],
root: Path,
) -> Set[str]:
"""Извлекает зависимости из одного файла."""
suffix = Path(file_path).suffix.lower()
if suffix in self.config.cpp_extensions:
raw_includes = self._extract_cpp_includes(file_path)
return self._resolve_includes_to_modules(
raw_includes, file_path, file_to_module, root
)
if suffix in self.config.js_extensions:
raw_imports = self._extract_js_imports(file_path)
return self._resolve_js_imports_to_modules(
raw_imports, file_path, file_to_module, root
)
return set()
def _extract_cpp_includes(self, file_path: str) -> List[str]:
"""
Извлекает #include-директивы из C++ файла.
Предпочтительно через libclang AST, fallback regex.
"""
if self._clang_index is not None:
return self._extract_includes_clang(file_path)
return self._extract_includes_regex(file_path)
def _extract_includes_clang(self, file_path: str) -> List[str]:
"""Извлечение #include через libclang AST."""
includes: List[str] = []
try:
tu = self._clang_index.parse(
file_path,
args=["-std=c++17"],
options=clang.TranslationUnit.PARSE_SKIP_FUNCTION_BODIES,
)
for inc in tu.get_includes():
if inc.depth == 1: # только прямые зависимости
includes.append(inc.include.name)
except Exception as exc:
logger.debug("clang ошибка для %s: %s, использую regex", file_path, exc)
return self._extract_includes_regex(file_path)
return includes
def _extract_includes_regex(self, file_path: str) -> List[str]:
"""Fallback: извлечение #include через regex."""
try:
text = Path(file_path).read_text(encoding="utf-8", errors="replace")
except OSError:
return []
return _RE_INCLUDE.findall(text)
def _extract_js_imports(self, file_path: str) -> List[str]:
"""Извлечение import/require из JS/TS файла через regex."""
try:
text = Path(file_path).read_text(encoding="utf-8", errors="replace")
except OSError:
return []
return _RE_JS_IMPORT.findall(text)
# ------------------------------------------------------------------
# Разрешение имён в модули
# ------------------------------------------------------------------
def _resolve_includes_to_modules(
self,
includes: List[str],
source_file: str,
file_to_module: Dict[str, str],
root: Path,
) -> Set[str]:
"""
Пытается сопоставить #include с именем модуля в проекте.
Системные заголовки (без '/' и с < >) игнорируются.
"""
deps: Set[str] = set()
source_dir = Path(source_file).parent
for inc in includes:
# Пробуем относительно директории файла
candidate = (source_dir / inc).resolve()
key = str(candidate)
if key in file_to_module:
deps.add(file_to_module[key])
continue
# Пробуем относительно корня проекта
candidate = (root / inc).resolve()
key = str(candidate)
if key in file_to_module:
deps.add(file_to_module[key])
return deps
def _resolve_js_imports_to_modules(
self,
imports: List[str],
source_file: str,
file_to_module: Dict[str, str],
root: Path,
) -> Set[str]:
"""
Разрешает JS import-пути в имена модулей.
Внешние пакеты (не начинаются с '.') игнорируются.
"""
deps: Set[str] = set()
source_dir = Path(source_file).parent
for imp in imports:
if not imp.startswith("."):
continue # внешний пакет — не наш модуль
# Пробуем добавить расширения
base = (source_dir / imp).resolve()
candidates = [base]
for ext in self.config.js_extensions:
candidates.append(base.with_suffix(ext))
candidates.append(base / f"index{ext}")
for candidate in candidates:
key = str(candidate)
if key in file_to_module:
deps.add(file_to_module[key])
break
return deps
# ------------------------------------------------------------------
# Вспомогательные методы
# ------------------------------------------------------------------
@staticmethod
def _build_file_to_module_map(modules: Dict[str, ModuleMetrics]) -> Dict[str, str]:
"""Строит обратный индекс: абсолютный путь файла → имя модуля."""
mapping: Dict[str, str] = {}
for module_name, module in modules.items():
for file_path in module.files:
resolved = str(Path(file_path).resolve())
mapping[resolved] = module_name
return mapping
@staticmethod
def _find_cycles(graph: "nx.DiGraph") -> List[List[str]]:
"""Находит все циклы в графе через strongly connected components."""
cycles: List[List[str]] = []
for scc in nx.strongly_connected_components(graph):
if len(scc) > 1:
cycles.append(sorted(scc))
return cycles

View File

@ -0,0 +1,148 @@
"""
Модуль 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"

View File

@ -0,0 +1,771 @@
{
"generated_at": "2026-05-11T13:38:33.090534",
"project_root": "/home/claude/vkr/B",
"summary": {
"total_files": 22,
"total_nloc": 1137,
"total_functions": 68,
"avg_system_ccn": 3.78,
"system_risk_level": "HIGH",
"recommended_system_strategy": "REENGINEER",
"dependency_cycles_count": 1
},
"dependency_cycles": [
[
"src",
"src/database",
"src/error-handlers",
"src/middlewares",
"src/routes"
]
],
"decisions": [
{
"module": "src/routes",
"risk_level": "CRITICAL",
"strategy": "REPLACE",
"recommended_pattern": "Strangler Fig + Anti-Corruption Layer: замена с изоляцией от цикла",
"priority": 190,
"reasons": [
"Критическая CCN: max=21 (порог 20)",
"Высокая нестабильность: I=0.75 (порог 0.7)",
"Участвует в циклических зависимостях — признак архитектурной деградации"
]
},
{
"module": "src",
"risk_level": "HIGH",
"strategy": "REENGINEER",
"recommended_pattern": "Anti-Corruption Layer: изоляция модуля от циклических зависимостей",
"priority": 460,
"reasons": [
"Участвует в циклических зависимостях — признак архитектурной деградации"
]
},
{
"module": "src/middlewares",
"risk_level": "HIGH",
"strategy": "REENGINEER",
"recommended_pattern": "Anti-Corruption Layer: изоляция модуля от циклических зависимостей",
"priority": 470,
"reasons": [
"Участвует в циклических зависимостях — признак архитектурной деградации"
]
},
{
"module": "src/database",
"risk_level": "HIGH",
"strategy": "REENGINEER",
"recommended_pattern": "Anti-Corruption Layer: изоляция модуля от циклических зависимостей",
"priority": 480,
"reasons": [
"Участвует в циклических зависимостях — признак архитектурной деградации"
]
},
{
"module": "src/error-handlers",
"risk_level": "HIGH",
"strategy": "REENGINEER",
"recommended_pattern": "Anti-Corruption Layer: изоляция модуля от циклических зависимостей",
"priority": 490,
"reasons": [
"Участвует в циклических зависимостях — признак архитектурной деградации"
]
}
],
"modules": {
"src": {
"files": [
"/home/claude/vkr/B/src/config.mjs",
"/home/claude/vkr/B/src/index.mjs",
"/home/claude/vkr/B/src/utils.mjs"
],
"language": "mixed",
"total_nloc": 166,
"total_functions": 22,
"avg_ccn": 1.77,
"max_ccn": 8,
"heavy_functions_count": 0,
"heavy_functions_ratio": 0.0,
"coupling_out": 4,
"coupling_in": 4,
"instability": 0.5,
"has_cycles": true,
"dependencies": [
"src/database",
"src/error-handlers",
"src/middlewares",
"src/routes"
],
"dependents": [
"src/database",
"src/error-handlers",
"src/middlewares",
"src/routes"
],
"functions": [
{
"name": "isPng",
"file": "/home/claude/vkr/B/src/utils.mjs",
"line": 10,
"ccn": 8,
"nloc": 11,
"params": 1
},
{
"name": "warmupExtract",
"file": "/home/claude/vkr/B/src/index.mjs",
"line": 101,
"ccn": 3,
"nloc": 12,
"params": 4
},
{
"name": "isJpeg",
"file": "/home/claude/vkr/B/src/utils.mjs",
"line": 3,
"ccn": 3,
"nloc": 6,
"params": 1
},
{
"name": "createDbConnection",
"file": "/home/claude/vkr/B/src/index.mjs",
"line": 29,
"ccn": 2,
"nloc": 24,
"params": 0
},
{
"name": "generateApiPrefix",
"file": "/home/claude/vkr/B/src/index.mjs",
"line": 54,
"ccn": 2,
"nloc": 5,
"params": 2
},
{
"name": "(anonymous)",
"file": "/home/claude/vkr/B/src/index.mjs",
"line": 123,
"ccn": 2,
"nloc": 11,
"params": 0
},
{
"name": "main",
"file": "/home/claude/vkr/B/src/index.mjs",
"line": 139,
"ccn": 2,
"nloc": 19,
"params": 0
},
{
"name": "toFixedOne",
"file": "/home/claude/vkr/B/src/utils.mjs",
"line": 31,
"ccn": 2,
"nloc": 7,
"params": 2
},
{
"name": "float32ArrayFromBuffer",
"file": "/home/claude/vkr/B/src/utils.mjs",
"line": 43,
"ccn": 2,
"nloc": 9,
"params": 1
},
{
"name": "(anonymous)",
"file": "/home/claude/vkr/B/src/index.mjs",
"line": 26,
"ccn": 1,
"nloc": 1,
"params": 0
},
{
"name": "sleep",
"file": "/home/claude/vkr/B/src/index.mjs",
"line": 24,
"ccn": 1,
"nloc": 4,
"params": 1
},
{
"name": "(anonymous)",
"file": "/home/claude/vkr/B/src/index.mjs",
"line": 64,
"ccn": 1,
"nloc": 3,
"params": 0
},
{
"name": "addModuleMiddlewares",
"file": "/home/claude/vkr/B/src/index.mjs",
"line": 60,
"ccn": 1,
"nloc": 7,
"params": 2
},
{
"name": "(anonymous)",
"file": "/home/claude/vkr/B/src/index.mjs",
"line": 74,
"ccn": 1,
"nloc": 3,
"params": 0
},
{
"name": "addModuleRoutes",
"file": "/home/claude/vkr/B/src/index.mjs",
"line": 70,
"ccn": 1,
"nloc": 7,
"params": 3
},
{
"name": "(anonymous)",
"file": "/home/claude/vkr/B/src/index.mjs",
"line": 85,
"ccn": 1,
"nloc": 3,
"params": 0
},
{
"name": "addUknownRoute",
"file": "/home/claude/vkr/B/src/index.mjs",
"line": 80,
"ccn": 1,
"nloc": 8,
"params": 1
},
{
"name": "(anonymous)",
"file": "/home/claude/vkr/B/src/index.mjs",
"line": 95,
"ccn": 1,
"nloc": 3,
"params": 0
},
{
"name": "addErrHandler",
"file": "/home/claude/vkr/B/src/index.mjs",
"line": 91,
"ccn": 1,
"nloc": 7,
"params": 1
},
{
"name": "runExpressServer",
"file": "/home/claude/vkr/B/src/index.mjs",
"line": 116,
"ccn": 1,
"nloc": 11,
"params": 1
},
{
"name": "float32ArrayToBuffer",
"file": "/home/claude/vkr/B/src/utils.mjs",
"line": 56,
"ccn": 1,
"nloc": 4,
"params": 1
},
{
"name": "clamp",
"file": "/home/claude/vkr/B/src/utils.mjs",
"line": 61,
"ccn": 1,
"nloc": 1,
"params": 3
}
]
},
"src/database": {
"files": [
"/home/claude/vkr/B/src/database/dbQueries.mjs",
"/home/claude/vkr/B/src/database/dbRepository.mjs"
],
"language": "mixed",
"total_nloc": 32,
"total_functions": 4,
"avg_ccn": 1.0,
"max_ccn": 1,
"heavy_functions_count": 0,
"heavy_functions_ratio": 0.0,
"coupling_out": 1,
"coupling_in": 2,
"instability": 0.333,
"has_cycles": true,
"dependencies": [
"src"
],
"dependents": [
"src",
"src/routes"
],
"functions": [
{
"name": "addVector",
"file": "/home/claude/vkr/B/src/database/dbRepository.mjs",
"line": 11,
"ccn": 1,
"nloc": 9,
"params": 3
},
{
"name": "updateVector",
"file": "/home/claude/vkr/B/src/database/dbRepository.mjs",
"line": 21,
"ccn": 1,
"nloc": 9,
"params": 3
},
{
"name": "deleteVector",
"file": "/home/claude/vkr/B/src/database/dbRepository.mjs",
"line": 31,
"ccn": 1,
"nloc": 4,
"params": 1
},
{
"name": "getNearestVector",
"file": "/home/claude/vkr/B/src/database/dbRepository.mjs",
"line": 36,
"ccn": 1,
"nloc": 10,
"params": 4
}
]
},
"src/error-handlers": {
"files": [
"/home/claude/vkr/B/src/error-handlers/errorHandler.mjs"
],
"language": "mixed",
"total_nloc": 37,
"total_functions": 3,
"avg_ccn": 2.0,
"max_ccn": 3,
"heavy_functions_count": 0,
"heavy_functions_ratio": 0.0,
"coupling_out": 2,
"coupling_in": 1,
"instability": 0.667,
"has_cycles": true,
"dependencies": [
"src",
"src/middlewares"
],
"dependents": [
"src"
],
"functions": [
{
"name": "generateResponse",
"file": "/home/claude/vkr/B/src/error-handlers/errorHandler.mjs",
"line": 90,
"ccn": 3,
"nloc": 16,
"params": 2
},
{
"name": "handleErrorCase",
"file": "/home/claude/vkr/B/src/error-handlers/errorHandler.mjs",
"line": 107,
"ccn": 2,
"nloc": 10,
"params": 3
},
{
"name": "logError",
"file": "/home/claude/vkr/B/src/error-handlers/errorHandler.mjs",
"line": 78,
"ccn": 1,
"nloc": 11,
"params": 3
}
]
},
"src/middlewares": {
"files": [
"/home/claude/vkr/B/src/middlewares/addLogInfo.mjs",
"/home/claude/vkr/B/src/middlewares/builder.mjs",
"/home/claude/vkr/B/src/middlewares/index.mjs",
"/home/claude/vkr/B/src/middlewares/logger.mjs",
"/home/claude/vkr/B/src/middlewares/parser.mjs"
],
"language": "mixed",
"total_nloc": 267,
"total_functions": 15,
"avg_ccn": 3.53,
"max_ccn": 8,
"heavy_functions_count": 0,
"heavy_functions_ratio": 0.0,
"coupling_out": 1,
"coupling_in": 3,
"instability": 0.25,
"has_cycles": true,
"dependencies": [
"src"
],
"dependents": [
"src",
"src/error-handlers",
"src/routes"
],
"functions": [
{
"name": "findBoundary",
"file": "/home/claude/vkr/B/src/middlewares/parser.mjs",
"line": 78,
"ccn": 8,
"nloc": 27,
"params": 3
},
{
"name": "parsePart",
"file": "/home/claude/vkr/B/src/middlewares/parser.mjs",
"line": 106,
"ccn": 7,
"nloc": 34,
"params": 1
},
{
"name": "parse",
"file": "/home/claude/vkr/B/src/middlewares/parser.mjs",
"line": 14,
"ccn": 6,
"nloc": 38,
"params": 3
},
{
"name": "getNextPart",
"file": "/home/claude/vkr/B/src/middlewares/parser.mjs",
"line": 159,
"ccn": 6,
"nloc": 39,
"params": 3
},
{
"name": "parseMultipart",
"file": "/home/claude/vkr/B/src/middlewares/parser.mjs",
"line": 205,
"ccn": 6,
"nloc": 35,
"params": 2
},
{
"name": "getBoundary",
"file": "/home/claude/vkr/B/src/middlewares/parser.mjs",
"line": 146,
"ccn": 4,
"nloc": 12,
"params": 1
},
{
"name": "(anonymous)",
"file": "/home/claude/vkr/B/src/middlewares/parser.mjs",
"line": 42,
"ccn": 3,
"nloc": 6,
"params": 0
},
{
"name": "boundaryValid",
"file": "/home/claude/vkr/B/src/middlewares/parser.mjs",
"line": 68,
"ccn": 3,
"nloc": 9,
"params": 3
},
{
"name": "(anonymous)",
"file": "/home/claude/vkr/B/src/middlewares/builder.mjs",
"line": 18,
"ccn": 2,
"nloc": 12,
"params": 0
},
{
"name": "(anonymous)",
"file": "/home/claude/vkr/B/src/middlewares/builder.mjs",
"line": 40,
"ccn": 2,
"nloc": 15,
"params": 0
},
{
"name": "(anonymous)",
"file": "/home/claude/vkr/B/src/middlewares/parser.mjs",
"line": 32,
"ccn": 2,
"nloc": 6,
"params": 0
},
{
"name": "addLogInfo",
"file": "/home/claude/vkr/B/src/middlewares/addLogInfo.mjs",
"line": 3,
"ccn": 1,
"nloc": 6,
"params": 3
},
{
"name": "buildMultipart",
"file": "/home/claude/vkr/B/src/middlewares/builder.mjs",
"line": 12,
"ccn": 1,
"nloc": 15,
"params": 2
},
{
"name": "customSuccessMessage",
"file": "/home/claude/vkr/B/src/middlewares/logger.mjs",
"line": 26,
"ccn": 1,
"nloc": 9,
"params": 3
},
{
"name": "(anonymous)",
"file": "/home/claude/vkr/B/src/middlewares/parser.mjs",
"line": 51,
"ccn": 1,
"nloc": 4,
"params": 0
}
]
},
"src/routes": {
"files": [
"/home/claude/vkr/B/src/routes/add.mjs",
"/home/claude/vkr/B/src/routes/compare.mjs",
"/home/claude/vkr/B/src/routes/delete.mjs",
"/home/claude/vkr/B/src/routes/extract.mjs",
"/home/claude/vkr/B/src/routes/health.mjs",
"/home/claude/vkr/B/src/routes/identify.mjs",
"/home/claude/vkr/B/src/routes/index.mjs",
"/home/claude/vkr/B/src/routes/match.mjs",
"/home/claude/vkr/B/src/routes/memory.mjs",
"/home/claude/vkr/B/src/routes/update.mjs",
"/home/claude/vkr/B/src/routes/verify.mjs"
],
"language": "mixed",
"total_nloc": 635,
"total_functions": 24,
"avg_ccn": 6.46,
"max_ccn": 21,
"heavy_functions_count": 5,
"heavy_functions_ratio": 0.208,
"coupling_out": 3,
"coupling_in": 1,
"instability": 0.75,
"has_cycles": true,
"dependencies": [
"src",
"src/database",
"src/middlewares"
],
"dependents": [
"src"
],
"functions": [
{
"name": "(anonymous)",
"file": "/home/claude/vkr/B/src/routes/identify.mjs",
"line": 19,
"ccn": 21,
"nloc": 79,
"params": 0
},
{
"name": "(anonymous)",
"file": "/home/claude/vkr/B/src/routes/add.mjs",
"line": 13,
"ccn": 16,
"nloc": 65,
"params": 0
},
{
"name": "(anonymous)",
"file": "/home/claude/vkr/B/src/routes/match.mjs",
"line": 13,
"ccn": 16,
"nloc": 71,
"params": 0
},
{
"name": "(anonymous)",
"file": "/home/claude/vkr/B/src/routes/update.mjs",
"line": 11,
"ccn": 16,
"nloc": 61,
"params": 0
},
{
"name": "(anonymous)",
"file": "/home/claude/vkr/B/src/routes/verify.mjs",
"line": 19,
"ccn": 15,
"nloc": 62,
"params": 0
},
{
"name": "(anonymous)",
"file": "/home/claude/vkr/B/src/routes/compare.mjs",
"line": 9,
"ccn": 9,
"nloc": 46,
"params": 0
},
{
"name": "(anonymous)",
"file": "/home/claude/vkr/B/src/routes/extract.mjs",
"line": 8,
"ccn": 9,
"nloc": 27,
"params": 0
},
{
"name": "(anonymous)",
"file": "/home/claude/vkr/B/src/routes/health.mjs",
"line": 19,
"ccn": 7,
"nloc": 47,
"params": 0
},
{
"name": "(anonymous)",
"file": "/home/claude/vkr/B/src/routes/delete.mjs",
"line": 8,
"ccn": 5,
"nloc": 21,
"params": 0
},
{
"name": "verifyParams",
"file": "/home/claude/vkr/B/src/routes/identify.mjs",
"line": 121,
"ccn": 5,
"nloc": 11,
"params": 1
},
{
"name": "verifyParams",
"file": "/home/claude/vkr/B/src/routes/match.mjs",
"line": 125,
"ccn": 5,
"nloc": 11,
"params": 1
},
{
"name": "getId",
"file": "/home/claude/vkr/B/src/routes/add.mjs",
"line": 95,
"ccn": 4,
"nloc": 10,
"params": 1
},
{
"name": "getId",
"file": "/home/claude/vkr/B/src/routes/delete.mjs",
"line": 44,
"ccn": 4,
"nloc": 10,
"params": 1
},
{
"name": "getId",
"file": "/home/claude/vkr/B/src/routes/update.mjs",
"line": 108,
"ccn": 4,
"nloc": 10,
"params": 1
},
{
"name": "(anonymous)",
"file": "/home/claude/vkr/B/src/routes/memory.mjs",
"line": 6,
"ccn": 3,
"nloc": 16,
"params": 0
},
{
"name": "parseTemplate",
"file": "/home/claude/vkr/B/src/routes/add.mjs",
"line": 83,
"ccn": 2,
"nloc": 11,
"params": 1
},
{
"name": "parseTemplate",
"file": "/home/claude/vkr/B/src/routes/delete.mjs",
"line": 32,
"ccn": 2,
"nloc": 11,
"params": 1
},
{
"name": "parseMeta",
"file": "/home/claude/vkr/B/src/routes/identify.mjs",
"line": 109,
"ccn": 2,
"nloc": 11,
"params": 1
},
{
"name": "parseMeta",
"file": "/home/claude/vkr/B/src/routes/match.mjs",
"line": 100,
"ccn": 2,
"nloc": 11,
"params": 1
},
{
"name": "parseVectors",
"file": "/home/claude/vkr/B/src/routes/match.mjs",
"line": 112,
"ccn": 2,
"nloc": 11,
"params": 1
},
{
"name": "parseTemplate",
"file": "/home/claude/vkr/B/src/routes/update.mjs",
"line": 84,
"ccn": 2,
"nloc": 11,
"params": 1
},
{
"name": "parseVectors",
"file": "/home/claude/vkr/B/src/routes/update.mjs",
"line": 96,
"ccn": 2,
"nloc": 11,
"params": 1
},
{
"name": "(anonymous)",
"file": "/home/claude/vkr/B/src/routes/identify.mjs",
"line": 87,
"ccn": 1,
"nloc": 6,
"params": 0
},
{
"name": "(anonymous)",
"file": "/home/claude/vkr/B/src/routes/match.mjs",
"line": 85,
"ccn": 1,
"nloc": 5,
"params": 0
}
]
}
}
}

View File

@ -0,0 +1,249 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Legacy Analyzer — Отчёт</title>
<style>
:root {
--font: 'Segoe UI', system-ui, sans-serif;
--bg: #f8fafc;
--card: #ffffff;
--border: #e2e8f0;
--text: #1e293b;
--muted: #64748b;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: var(--font); background: var(--bg); color: var(--text); padding: 24px; }
h1 { font-size: 1.75rem; margin-bottom: 4px; }
h2 { font-size: 1.25rem; margin: 24px 0 12px; border-bottom: 2px solid var(--border); padding-bottom: 6px; }
h3 { font-size: 1rem; margin-bottom: 8px; }
.subtitle { color: var(--muted); font-size: 0.875rem; margin-bottom: 24px; }
.card { background: var(--card); border: 1px solid var(--border); border-radius: 8px; padding: 20px; margin-bottom: 16px; }
.grid-4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 24px; }
.stat-card { background: var(--card); border: 1px solid var(--border); border-radius: 8px; padding: 16px; text-align: center; }
.stat-value { font-size: 2rem; font-weight: 700; }
.stat-label { font-size: 0.75rem; color: var(--muted); margin-top: 4px; }
table { width: 100%; border-collapse: collapse; font-size: 0.875rem; }
th { background: #f1f5f9; text-align: left; padding: 8px 12px; border-bottom: 2px solid var(--border); }
td { padding: 8px 12px; border-bottom: 1px solid var(--border); vertical-align: top; }
tr:hover td { background: #f8fafc; }
.badge { display: inline-block; padding: 2px 8px; border-radius: 9999px; font-size: 0.75rem; font-weight: 600; }
.module-card { margin-bottom: 12px; }
.module-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
.reasons-list { margin: 0; padding-left: 20px; color: var(--muted); font-size: 0.8rem; }
.reasons-list li { margin-bottom: 2px; }
.func-table { margin-top: 8px; }
code { font-family: monospace; background: #f1f5f9; padding: 1px 4px; border-radius: 3px; }
.cycle-badge { background: #fca5a5; color: #7f1d1d; border-radius: 4px; padding: 2px 6px; font-size: 0.75rem; margin: 2px; display: inline-block; }
details summary { cursor: pointer; user-select: none; }
details summary:hover { color: #3b82f6; }
@media (max-width: 768px) { .grid-4 { grid-template-columns: repeat(2, 1fr); } }
</style>
</head>
<body>
<h1>📊 Legacy Analyzer</h1>
<p class="subtitle">
Проект: <code>/tmp/tp</code> &nbsp;|&nbsp;
Сгенерировано: 27.04.2026 18:39 &nbsp;|&nbsp;
Системный риск: <strong style="color:#22c55e">Низкий</strong>
</p>
<h2>Сводка по системе</h2>
<div class="grid-4">
<div class="stat-card">
<div class="stat-value">9</div>
<div class="stat-label">Файлов</div>
</div>
<div class="stat-card">
<div class="stat-value">26</div>
<div class="stat-label">Строк кода (NLOC)</div>
</div>
<div class="stat-card">
<div class="stat-value">9</div>
<div class="stat-label">Функций</div>
</div>
<div class="stat-card">
<div class="stat-value">1.8</div>
<div class="stat-label">Средняя CCN</div>
</div>
<div class="stat-card">
<div class="stat-value">6</div>
<div class="stat-label">Модулей</div>
</div>
<div class="stat-card">
<div class="stat-value">0</div>
<div class="stat-label">Циклов зависимостей</div>
</div>
<div class="stat-card">
<div class="stat-value" style="color:#22c55e">Низкий</div>
<div class="stat-label">Системный риск</div>
</div>
<div class="stat-card">
<div class="stat-value" style="font-size:1.2rem">Оставить</div>
<div class="stat-label">Рекомендуемая стратегия</div>
</div>
</div>
<h2>Таблица решений по модулям</h2>
<div class="card" style="padding:0;overflow:auto">
<table>
<thead><tr>
<th>Модуль</th><th>Риск</th><th>Стратегия</th><th>Приоритет</th><th>Причины</th>
</tr></thead>
<tbody><tr>
<td><code>service/routes</code></td>
<td><span class="badge" style="color:#f59e0b;background:#fef3c7">Средний</span></td>
<td>Рефакторинг</td>
<td>800</td>
<td><ul class="reasons-list"><li>Критическая нестабильность: I=1.00 (порог 0.9)</li></ul></td>
</tr><tr>
<td><code>ui_widget</code></td>
<td><span class="badge" style="color:#f59e0b;background:#fef3c7">Средний</span></td>
<td>Рефакторинг</td>
<td>800</td>
<td><ul class="reasons-list"><li>Критическая нестабильность: I=1.00 (порог 0.9)</li></ul></td>
</tr><tr>
<td><code>&lt;root&gt;</code></td>
<td><span class="badge" style="color:#22c55e;background:#dcfce7">Низкий</span></td>
<td>Оставить</td>
<td>980</td>
<td></td>
</tr><tr>
<td><code>utils</code></td>
<td><span class="badge" style="color:#22c55e;background:#dcfce7">Низкий</span></td>
<td>Оставить</td>
<td>980</td>
<td></td>
</tr><tr>
<td><code>database</code></td>
<td><span class="badge" style="color:#22c55e;background:#dcfce7">Низкий</span></td>
<td>Оставить</td>
<td>990</td>
<td></td>
</tr><tr>
<td><code>service/database</code></td>
<td><span class="badge" style="color:#22c55e;background:#dcfce7">Низкий</span></td>
<td>Оставить</td>
<td>990</td>
<td></td>
</tr></tbody>
</table>
</div>
<h2>Циклические зависимости</h2>
<div class="card"><p style="color:#22c55e">✓ Циклических зависимостей не обнаружено</p></div>
<h2>Детализация по модулям</h2>
<div class="card module-card">
<div class="module-header">
<h3><code>&lt;root&gt;</code></h3>
<span class="badge" style="color:#22c55e;background:#dcfce7">Низкий</span>
</div>
<table style="width:auto;margin:8px 0">
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Язык</td><td>mixed</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">NLOC</td><td>0</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Функций</td><td>0</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Avg CCN</td><td>0.0</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Max CCN</td><td>0</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">C_out / C_in</td><td>0 / 2</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Нестабильность (I)</td><td>0.00</td></tr>
</table>
</div><div class="card module-card">
<div class="module-header">
<h3><code>database</code></h3>
<span class="badge" style="color:#22c55e;background:#dcfce7">Низкий</span>
</div>
<table style="width:auto;margin:8px 0">
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Язык</td><td>mixed</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">NLOC</td><td>0</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Функций</td><td>0</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Avg CCN</td><td>0.0</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Max CCN</td><td>0</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">C_out / C_in</td><td>1 / 1</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Нестабильность (I)</td><td>0.50</td></tr>
</table>
</div><div class="card module-card">
<div class="module-header">
<h3><code>service/database</code></h3>
<span class="badge" style="color:#22c55e;background:#dcfce7">Низкий</span>
</div>
<table style="width:auto;margin:8px 0">
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Язык</td><td>mixed</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">NLOC</td><td>3</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Функций</td><td>2</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Avg CCN</td><td>1.5</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Max CCN</td><td>2</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">C_out / C_in</td><td>1 / 1</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Нестабильность (I)</td><td>0.50</td></tr>
</table>
<details><summary>Топ функций по сложности</summary>
<table class="func-table">
<thead><tr><th>Функция</th><th>CCN</th><th>NLOC</th><th>Параметры</th><th>Расположение</th></tr></thead>
<tbody><tr><td><code>query</code></td><td>2</td><td>2</td><td>2</td><td><code>db.js:3</code></td></tr><tr><td><code>if</code></td><td>1</td><td>1</td><td>1</td><td><code>db.js:4</code></td></tr></tbody>
</table></details>
</div><div class="card module-card">
<div class="module-header">
<h3><code>service/routes</code></h3>
<span class="badge" style="color:#f59e0b;background:#fef3c7">Средний</span>
</div>
<table style="width:auto;margin:8px 0">
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Язык</td><td>mixed</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">NLOC</td><td>23</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Функций</td><td>7</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Avg CCN</td><td>1.9</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Max CCN</td><td>7</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">C_out / C_in</td><td>2 / 0</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Нестабильность (I)</td><td>1.00</td></tr>
</table>
<details><summary>Топ функций по сложности</summary>
<table class="func-table">
<thead><tr><th>Функция</th><th>CCN</th><th>NLOC</th><th>Параметры</th><th>Расположение</th></tr></thead>
<tbody><tr><td><code>authenticate</code></td><td>7</td><td>17</td><td>2</td><td><code>auth.js:3</code></td></tr><tr><td><code>if</code></td><td>1</td><td>1</td><td>1</td><td><code>auth.js:6</code></td></tr><tr><td><code>if</code></td><td>1</td><td>1</td><td>1</td><td><code>auth.js:9</code></td></tr><tr><td><code>if</code></td><td>1</td><td>1</td><td>1</td><td><code>auth.js:10</code></td></tr><tr><td><code>if</code></td><td>1</td><td>1</td><td>1</td><td><code>auth.js:10</code></td></tr></tbody>
</table></details>
</div><div class="card module-card">
<div class="module-header">
<h3><code>ui_widget</code></h3>
<span class="badge" style="color:#f59e0b;background:#fef3c7">Средний</span>
</div>
<table style="width:auto;margin:8px 0">
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Язык</td><td>mixed</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">NLOC</td><td>0</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Функций</td><td>0</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Avg CCN</td><td>0.0</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Max CCN</td><td>0</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">C_out / C_in</td><td>2 / 0</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Нестабильность (I)</td><td>1.00</td></tr>
</table>
</div><div class="card module-card">
<div class="module-header">
<h3><code>utils</code></h3>
<span class="badge" style="color:#22c55e;background:#dcfce7">Низкий</span>
</div>
<table style="width:auto;margin:8px 0">
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Язык</td><td>mixed</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">NLOC</td><td>0</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Функций</td><td>0</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Avg CCN</td><td>0.0</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Max CCN</td><td>0</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">C_out / C_in</td><td>0 / 2</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Нестабильность (I)</td><td>0.00</td></tr>
</table>
</div>
<hr style="margin:32px 0;border:none;border-top:1px solid var(--border)">
<p style="color:var(--muted);font-size:0.75rem;text-align:center">
Legacy Analyzer — методический инструментарий модернизации унаследованных систем
</p>
</body></html>

View File

@ -0,0 +1,279 @@
{
"generated_at": "2026-04-27T18:39:05.636749",
"project_root": "/tmp/tp",
"summary": {
"total_files": 9,
"total_nloc": 26,
"total_functions": 9,
"avg_system_ccn": 1.78,
"system_risk_level": "LOW",
"recommended_system_strategy": "KEEP",
"dependency_cycles_count": 0
},
"dependency_cycles": [],
"decisions": [
{
"module": "service/routes",
"risk_level": "MEDIUM",
"strategy": "REFACTOR",
"priority": 800,
"reasons": [
"Критическая нестабильность: I=1.00 (порог 0.9)"
]
},
{
"module": "ui_widget",
"risk_level": "MEDIUM",
"strategy": "REFACTOR",
"priority": 800,
"reasons": [
"Критическая нестабильность: I=1.00 (порог 0.9)"
]
},
{
"module": "<root>",
"risk_level": "LOW",
"strategy": "KEEP",
"priority": 980,
"reasons": []
},
{
"module": "utils",
"risk_level": "LOW",
"strategy": "KEEP",
"priority": 980,
"reasons": []
},
{
"module": "database",
"risk_level": "LOW",
"strategy": "KEEP",
"priority": 990,
"reasons": []
},
{
"module": "service/database",
"risk_level": "LOW",
"strategy": "KEEP",
"priority": 990,
"reasons": []
}
],
"modules": {
"<root>": {
"files": [
"/tmp/tp/config.js"
],
"language": "mixed",
"total_nloc": 0,
"total_functions": 0,
"avg_ccn": 0.0,
"max_ccn": 0,
"heavy_functions_count": 0,
"heavy_functions_ratio": 0.0,
"coupling_out": 0,
"coupling_in": 2,
"instability": 0.0,
"has_cycles": false,
"dependencies": [],
"dependents": [
"service/database",
"service/routes"
],
"functions": []
},
"database": {
"files": [
"/tmp/tp/database/DatabaseManager.cpp",
"/tmp/tp/database/DatabaseManager.h"
],
"language": "mixed",
"total_nloc": 0,
"total_functions": 0,
"avg_ccn": 0.0,
"max_ccn": 0,
"heavy_functions_count": 0,
"heavy_functions_ratio": 0.0,
"coupling_out": 1,
"coupling_in": 1,
"instability": 0.5,
"has_cycles": false,
"dependencies": [
"database",
"utils"
],
"dependents": [
"ui_widget"
],
"functions": []
},
"service/database": {
"files": [
"/tmp/tp/service/database/db.js"
],
"language": "mixed",
"total_nloc": 3,
"total_functions": 2,
"avg_ccn": 1.5,
"max_ccn": 2,
"heavy_functions_count": 0,
"heavy_functions_ratio": 0.0,
"coupling_out": 1,
"coupling_in": 1,
"instability": 0.5,
"has_cycles": false,
"dependencies": [
"<root>"
],
"dependents": [
"service/routes"
],
"functions": [
{
"name": "query",
"file": "/tmp/tp/service/database/db.js",
"line": 3,
"ccn": 2,
"nloc": 2,
"params": 2
},
{
"name": "if",
"file": "/tmp/tp/service/database/db.js",
"line": 4,
"ccn": 1,
"nloc": 1,
"params": 1
}
]
},
"service/routes": {
"files": [
"/tmp/tp/service/routes/auth.js"
],
"language": "mixed",
"total_nloc": 23,
"total_functions": 7,
"avg_ccn": 1.86,
"max_ccn": 7,
"heavy_functions_count": 0,
"heavy_functions_ratio": 0.0,
"coupling_out": 2,
"coupling_in": 0,
"instability": 1.0,
"has_cycles": false,
"dependencies": [
"<root>",
"service/database"
],
"dependents": [],
"functions": [
{
"name": "authenticate",
"file": "/tmp/tp/service/routes/auth.js",
"line": 3,
"ccn": 7,
"nloc": 17,
"params": 2
},
{
"name": "if",
"file": "/tmp/tp/service/routes/auth.js",
"line": 6,
"ccn": 1,
"nloc": 1,
"params": 1
},
{
"name": "if",
"file": "/tmp/tp/service/routes/auth.js",
"line": 9,
"ccn": 1,
"nloc": 1,
"params": 1
},
{
"name": "if",
"file": "/tmp/tp/service/routes/auth.js",
"line": 10,
"ccn": 1,
"nloc": 1,
"params": 1
},
{
"name": "if",
"file": "/tmp/tp/service/routes/auth.js",
"line": 10,
"ccn": 1,
"nloc": 1,
"params": 1
},
{
"name": "if",
"file": "/tmp/tp/service/routes/auth.js",
"line": 13,
"ccn": 1,
"nloc": 1,
"params": 1
},
{
"name": "catch",
"file": "/tmp/tp/service/routes/auth.js",
"line": 17,
"ccn": 1,
"nloc": 1,
"params": 1
}
]
},
"ui_widget": {
"files": [
"/tmp/tp/ui_widget/UserWidget.cpp",
"/tmp/tp/ui_widget/UserWidget.h"
],
"language": "mixed",
"total_nloc": 0,
"total_functions": 0,
"avg_ccn": 0.0,
"max_ccn": 0,
"heavy_functions_count": 0,
"heavy_functions_ratio": 0.0,
"coupling_out": 2,
"coupling_in": 0,
"instability": 1.0,
"has_cycles": false,
"dependencies": [
"database",
"ui_widget",
"utils"
],
"dependents": [],
"functions": []
},
"utils": {
"files": [
"/tmp/tp/utils/Logger.cpp",
"/tmp/tp/utils/Logger.h"
],
"language": "mixed",
"total_nloc": 0,
"total_functions": 0,
"avg_ccn": 0.0,
"max_ccn": 0,
"heavy_functions_count": 0,
"heavy_functions_ratio": 0.0,
"coupling_out": 0,
"coupling_in": 2,
"instability": 0.0,
"has_cycles": false,
"dependencies": [
"utils"
],
"dependents": [
"database",
"ui_widget"
],
"functions": []
}
}
}

View File

@ -0,0 +1,131 @@
#!/usr/bin/env python3
"""
CLI точка входа для запуска из командной строки.
Использование:
python main.py /path/to/project
python main.py /path/to/project --output my_report --formats json html
python main.py /path/to/project --ccn-warn 15 --ccn-critical 25
python main.py /path/to/project --libclang /usr/lib/llvm-14/lib/libclang.so.1
"""
import argparse
import logging
import sys
from pathlib import Path
from config import AnalyzerConfig, ThresholdConfig
from analyzer import Analyzer
def build_arg_parser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser(
prog="legacy_analyzer",
description=(
"Методический инструментарий модернизации унаследованных систем.\n"
"Анализирует C++ и JavaScript проекты, выявляет архитектурные проблемы\n"
"и рекомендует стратегию модернизации."
),
formatter_class=argparse.RawDescriptionHelpFormatter,
)
p.add_argument(
"project_root",
help="Путь к корневой директории анализируемого проекта",
)
p.add_argument(
"--output", "-o",
default="legacy_report",
help="Директория для сохранения отчётов (по умолчанию: legacy_report)",
)
p.add_argument(
"--formats", "-f",
nargs="+",
choices=["json", "html"],
default=["json", "html"],
help="Форматы отчётов (по умолчанию: json html)",
)
p.add_argument(
"--libclang",
default=None,
help="Путь к libclang (.so/.dylib/.dll). Если не указан — используется автоопределение или regex-fallback",
)
# Пороговые значения
thresholds = p.add_argument_group("Пороговые значения метрик")
thresholds.add_argument("--ccn-warn", type=int, default=10, metavar="N",
help="CCN порог предупреждения (по умолчанию: 10)")
thresholds.add_argument("--ccn-critical", type=int, default=20, metavar="N",
help="CCN критический порог (по умолчанию: 20)")
thresholds.add_argument("--coupling-out-warn", type=int, default=5, metavar="N",
help="Порог исходящей связности — предупреждение (по умолчанию: 5)")
thresholds.add_argument("--coupling-out-critical", type=int, default=10, metavar="N",
help="Порог исходящей связности — критический (по умолчанию: 10)")
p.add_argument(
"--verbose", "-v",
action="store_true",
help="Подробный вывод (DEBUG)",
)
return p
def main() -> None:
parser = build_arg_parser()
args = parser.parse_args()
# --- Настройка логирования ---
level = logging.DEBUG if args.verbose else logging.INFO
logging.basicConfig(
level=level,
format="%(asctime)s [%(levelname)s] %(message)s",
datefmt="%H:%M:%S",
)
# --- Проверка входного пути ---
root = Path(args.project_root)
if not root.exists():
print(f"Ошибка: директория не найдена: {root}", file=sys.stderr)
sys.exit(1)
# --- Сборка конфигурации ---
thresholds = ThresholdConfig(
ccn_warn=args.ccn_warn,
ccn_critical=args.ccn_critical,
coupling_out_warn=args.coupling_out_warn,
coupling_out_critical=args.coupling_out_critical,
)
config = AnalyzerConfig(
output_formats=args.formats,
output_dir=args.output,
libclang_path=args.libclang,
thresholds=thresholds,
)
# --- Запуск ---
try:
analyzer = Analyzer(config)
report = analyzer.run(str(root))
# Краткий итог в stdout
print("\n" + "=" * 50)
print(f" Системный риск: {report.system_risk_level.name}")
print(f" Стратегия: {report.recommended_system_strategy.name}")
print(f" Модулей: {len(report.modules)}")
print(f" Функций: {report.total_functions}")
print(f" Средняя CCN: {report.avg_system_ccn:.2f}")
print(f" Циклов зависим.: {len(report.dependency_cycles)}")
print(f" Отчёты в: {args.output}/")
print("=" * 50)
except RuntimeError as exc:
print(f"Ошибка: {exc}", file=sys.stderr)
sys.exit(1)
except KeyboardInterrupt:
print("\nПрервано пользователем", file=sys.stderr)
sys.exit(130)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,121 @@
"""
Доменные модели инструментария.
Все типы данных, которыми обмениваются модули системы.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from enum import Enum, auto
from pathlib import Path
from typing import Dict, List, Optional, Set
# ---------------------------------------------------------------------------
# Метрики
# ---------------------------------------------------------------------------
@dataclass
class FunctionMetrics:
"""Метрики одной функции / метода."""
name: str
file: str
line: int
ccn: int # Цикломатическая сложность (McCabe)
nloc: int # Строк кода без комментариев
params: int # Число параметров
language: str # "cpp" | "js"
@property
def is_complex(self) -> bool:
return self.ccn > 10
@property
def is_large(self) -> bool:
return self.nloc > 50
@dataclass
class ModuleMetrics:
"""Агрегированные метрики модуля (директории или файла)."""
name: str # Имя модуля (путь относительно корня)
files: List[str] = field(default_factory=list)
functions: List[FunctionMetrics] = field(default_factory=list)
language: str = "mixed" # "cpp" | "js" | "mixed"
# Агрегированные показатели (заполняются MetricsEngine)
total_nloc: int = 0
avg_ccn: float = 0.0
max_ccn: int = 0
total_functions: int = 0
heavy_functions_count: int = 0 # Функций с CCN > порога
heavy_functions_ratio: float = 0.0
# Граф зависимостей (заполняется DependencyAnalyzer)
coupling_out: int = 0 # Исходящая связность (fan-out)
coupling_in: int = 0 # Входящая связность (fan-in)
instability: float = 0.0 # C_out / (C_in + C_out), метрика Р. Мартина
has_cycles: bool = False # Участвует в циклической зависимости
# Зависимости
dependencies: Set[str] = field(default_factory=set) # Модули, от которых зависит этот
dependents: Set[str] = field(default_factory=set) # Модули, зависящие от этого
# ---------------------------------------------------------------------------
# Классификация риска и стратегия
# ---------------------------------------------------------------------------
class RiskLevel(Enum):
"""Уровень архитектурного риска модуля."""
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
CRITICAL = "critical"
class ModernizationStrategy(Enum):
"""Рекомендуемая стратегия модернизации (Lehman & Belady, 1985)."""
KEEP = auto() # Оставить без изменений
REFACTOR = auto() # Локальный рефакторинг (Fowler, 2018)
REENGINEER = auto() # Реинжиниринг архитектуры
REPLACE = auto() # Полная замена
@dataclass
class ModuleDecision:
"""Решение DecisionEngine по конкретному модулю."""
module_name: str
risk_level: RiskLevel
strategy: ModernizationStrategy
reasons: List[str] = field(default_factory=list) # Причины в человекочитаемом виде
priority: int = 0 # Приоритет в плане рефакторинга (1 = высший)
recommended_pattern: str = "" # Архитектурный паттерн (Fowler, Strangler Fig и т.д.)
# ---------------------------------------------------------------------------
# Итоговый отчёт
# ---------------------------------------------------------------------------
@dataclass
class AnalysisReport:
"""Итоговый отчёт по всей системе."""
project_root: str
modules: Dict[str, ModuleMetrics] = field(default_factory=dict)
decisions: List[ModuleDecision] = field(default_factory=list)
dependency_cycles: List[List[str]] = field(default_factory=list)
# Системные агрегаты
total_files: int = 0
total_nloc: int = 0
total_functions: int = 0
avg_system_ccn: float = 0.0
system_risk_level: RiskLevel = RiskLevel.LOW
recommended_system_strategy: ModernizationStrategy = ModernizationStrategy.KEEP
@property
def critical_modules(self) -> List[ModuleDecision]:
return [d for d in self.decisions if d.risk_level == RiskLevel.CRITICAL]
@property
def high_risk_modules(self) -> List[ModuleDecision]:
return [d for d in self.decisions if d.risk_level in (RiskLevel.HIGH, RiskLevel.CRITICAL)]

View File

@ -0,0 +1,292 @@
"""
HTML Reporter человекочитаемый отчёт для архитекторов и менеджеров.
Формирует самодостаточный HTML-файл (без внешних зависимостей):
- Сводная таблица по системе
- Таблица модулей с цветовой индикацией риска
- Детальные карточки по каждому модулю
- Граф зависимостей в SVG (через библиотеку Mermaid.js из CDN)
- Список циклических зависимостей
"""
import html
import logging
from datetime import datetime
from pathlib import Path
from typing import List
from models import AnalysisReport, ModuleDecision, ModernizationStrategy, RiskLevel
logger = logging.getLogger(__name__)
# Цвета по уровням риска
_RISK_COLORS = {
RiskLevel.LOW: ("#22c55e", "#dcfce7"), # зелёный
RiskLevel.MEDIUM: ("#f59e0b", "#fef3c7"), # жёлтый
RiskLevel.HIGH: ("#ef4444", "#fee2e2"), # красный
RiskLevel.CRITICAL: ("#7f1d1d", "#fca5a5"), # тёмно-красный
}
_STRATEGY_LABELS = {
ModernizationStrategy.KEEP: "Оставить",
ModernizationStrategy.REFACTOR: "Рефакторинг",
ModernizationStrategy.REENGINEER: "Реинжиниринг",
ModernizationStrategy.REPLACE: "Замена",
}
_RISK_LABELS = {
RiskLevel.LOW: "Низкий",
RiskLevel.MEDIUM: "Средний",
RiskLevel.HIGH: "Высокий",
RiskLevel.CRITICAL: "Критический",
}
class HtmlReporter:
"""Формирует HTML-отчёт по результатам анализа."""
def write(self, report: AnalysisReport, output_dir: str) -> str:
"""
Записывает отчёт в файл report.html внутри output_dir.
:returns: путь к созданному файлу
"""
out = Path(output_dir)
out.mkdir(parents=True, exist_ok=True)
file_path = out / "report.html"
content = self._build_html(report)
with open(file_path, "w", encoding="utf-8") as fh:
fh.write(content)
logger.info("HTML отчёт записан: %s", file_path)
return str(file_path)
# ------------------------------------------------------------------
# Сборка HTML
# ------------------------------------------------------------------
def _build_html(self, report: AnalysisReport) -> str:
sections = [
self._head(),
"<body>",
self._header(report),
self._summary_section(report),
self._decisions_table(report),
self._cycles_section(report),
self._modules_detail(report),
self._footer(),
"</body></html>",
]
return "\n".join(sections)
def _head(self) -> str:
return """<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Legacy Analyzer Отчёт</title>
<style>
:root {
--font: 'Segoe UI', system-ui, sans-serif;
--bg: #f8fafc;
--card: #ffffff;
--border: #e2e8f0;
--text: #1e293b;
--muted: #64748b;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: var(--font); background: var(--bg); color: var(--text); padding: 24px; }
h1 { font-size: 1.75rem; margin-bottom: 4px; }
h2 { font-size: 1.25rem; margin: 24px 0 12px; border-bottom: 2px solid var(--border); padding-bottom: 6px; }
h3 { font-size: 1rem; margin-bottom: 8px; }
.subtitle { color: var(--muted); font-size: 0.875rem; margin-bottom: 24px; }
.card { background: var(--card); border: 1px solid var(--border); border-radius: 8px; padding: 20px; margin-bottom: 16px; }
.grid-4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 24px; }
.stat-card { background: var(--card); border: 1px solid var(--border); border-radius: 8px; padding: 16px; text-align: center; }
.stat-value { font-size: 2rem; font-weight: 700; }
.stat-label { font-size: 0.75rem; color: var(--muted); margin-top: 4px; }
table { width: 100%; border-collapse: collapse; font-size: 0.875rem; }
th { background: #f1f5f9; text-align: left; padding: 8px 12px; border-bottom: 2px solid var(--border); }
td { padding: 8px 12px; border-bottom: 1px solid var(--border); vertical-align: top; }
tr:hover td { background: #f8fafc; }
.badge { display: inline-block; padding: 2px 8px; border-radius: 9999px; font-size: 0.75rem; font-weight: 600; }
.module-card { margin-bottom: 12px; }
.module-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
.reasons-list { margin: 0; padding-left: 20px; color: var(--muted); font-size: 0.8rem; }
.reasons-list li { margin-bottom: 2px; }
.func-table { margin-top: 8px; }
code { font-family: monospace; background: #f1f5f9; padding: 1px 4px; border-radius: 3px; }
.cycle-badge { background: #fca5a5; color: #7f1d1d; border-radius: 4px; padding: 2px 6px; font-size: 0.75rem; margin: 2px; display: inline-block; }
details summary { cursor: pointer; user-select: none; }
details summary:hover { color: #3b82f6; }
@media (max-width: 768px) { .grid-4 { grid-template-columns: repeat(2, 1fr); } }
</style>
</head>"""
def _header(self, report: AnalysisReport) -> str:
ts = datetime.now().strftime("%d.%m.%Y %H:%M")
risk_color = _RISK_COLORS[report.system_risk_level][0]
return f"""
<h1>📊 Legacy Analyzer</h1>
<p class="subtitle">
Проект: <code>{html.escape(report.project_root)}</code> &nbsp;|&nbsp;
Сгенерировано: {ts} &nbsp;|&nbsp;
Системный риск: <strong style="color:{risk_color}">{_RISK_LABELS[report.system_risk_level]}</strong>
</p>"""
def _summary_section(self, report: AnalysisReport) -> str:
strategy_label = _STRATEGY_LABELS[report.recommended_system_strategy]
risk_color = _RISK_COLORS[report.system_risk_level][0]
return f"""
<h2>Сводка по системе</h2>
<div class="grid-4">
<div class="stat-card">
<div class="stat-value">{report.total_files}</div>
<div class="stat-label">Файлов</div>
</div>
<div class="stat-card">
<div class="stat-value">{report.total_nloc:,}</div>
<div class="stat-label">Строк кода (NLOC)</div>
</div>
<div class="stat-card">
<div class="stat-value">{report.total_functions}</div>
<div class="stat-label">Функций</div>
</div>
<div class="stat-card">
<div class="stat-value">{report.avg_system_ccn:.1f}</div>
<div class="stat-label">Средняя CCN</div>
</div>
<div class="stat-card">
<div class="stat-value">{len(report.modules)}</div>
<div class="stat-label">Модулей</div>
</div>
<div class="stat-card">
<div class="stat-value">{len(report.dependency_cycles)}</div>
<div class="stat-label">Циклов зависимостей</div>
</div>
<div class="stat-card">
<div class="stat-value" style="color:{risk_color}">{_RISK_LABELS[report.system_risk_level]}</div>
<div class="stat-label">Системный риск</div>
</div>
<div class="stat-card">
<div class="stat-value" style="font-size:1.2rem">{strategy_label}</div>
<div class="stat-label">Рекомендуемая стратегия</div>
</div>
</div>"""
def _decisions_table(self, report: AnalysisReport) -> str:
rows = []
for d in report.decisions:
fg, bg = _RISK_COLORS[d.risk_level]
risk_badge = (
f'<span class="badge" style="color:{fg};background:{bg}">'
f'{_RISK_LABELS[d.risk_level]}</span>'
)
reasons_html = ""
if d.reasons:
items = "".join(f"<li>{html.escape(r)}</li>" for r in d.reasons)
reasons_html = f'<ul class="reasons-list">{items}</ul>'
pattern_html = (
f'<span style="font-size:0.8rem;color:#1d4ed8">{html.escape(d.recommended_pattern)}</span>'
if d.recommended_pattern else ""
)
rows.append(f"""<tr>
<td><code>{html.escape(d.module_name)}</code></td>
<td>{risk_badge}</td>
<td>{html.escape(_STRATEGY_LABELS[d.strategy])}</td>
<td>{pattern_html}</td>
<td>{d.priority}</td>
<td>{reasons_html or ''}</td>
</tr>""")
return f"""
<h2>Таблица решений по модулям</h2>
<div class="card" style="padding:0;overflow:auto">
<table>
<thead><tr>
<th>Модуль</th><th>Риск</th><th>Стратегия</th><th>Паттерн</th><th>Приоритет</th><th>Причины</th>
</tr></thead>
<tbody>{"".join(rows)}</tbody>
</table>
</div>"""
def _cycles_section(self, report: AnalysisReport) -> str:
if not report.dependency_cycles:
return """
<h2>Циклические зависимости</h2>
<div class="card"><p style="color:#22c55e"> Циклических зависимостей не обнаружено</p></div>"""
items = []
for i, cycle in enumerate(report.dependency_cycles, 1):
badges = "".join(f'<span class="cycle-badge">{html.escape(m)}</span>' for m in cycle)
items.append(f"<p><strong>Цикл {i}:</strong> {badges}</p>")
return f"""
<h2>Циклические зависимости ({len(report.dependency_cycles)})</h2>
<div class="card">{"".join(items)}</div>"""
def _modules_detail(self, report: AnalysisReport) -> str:
# Строим индекс decision по имени модуля
decision_map = {d.module_name: d for d in report.decisions}
cards = []
for mod_name, mod in sorted(report.modules.items()):
decision = decision_map.get(mod_name)
risk = decision.risk_level if decision else RiskLevel.LOW
fg, bg = _RISK_COLORS[risk]
# Топ-5 самых сложных функций
top_funcs = sorted(mod.functions, key=lambda f: f.ccn, reverse=True)[:5]
func_rows = "".join(
f"<tr><td><code>{html.escape(f.name)}</code></td>"
f"<td>{f.ccn}</td><td>{f.nloc}</td><td>{f.params}</td>"
f"<td><code>{html.escape(Path(f.file).name)}:{f.line}</code></td></tr>"
for f in top_funcs
)
func_table = ""
if func_rows:
func_table = f"""<details><summary>Топ функций по сложности</summary>
<table class="func-table">
<thead><tr><th>Функция</th><th>CCN</th><th>NLOC</th><th>Параметры</th><th>Расположение</th></tr></thead>
<tbody>{func_rows}</tbody>
</table></details>"""
cycle_warn = (
'<span style="color:#ef4444;font-size:0.8rem">⚠ Участвует в цикле зависимостей</span>'
if mod.has_cycles else ""
)
cards.append(f"""<div class="card module-card">
<div class="module-header">
<h3><code>{html.escape(mod_name)}</code></h3>
<span class="badge" style="color:{fg};background:{bg}">{_RISK_LABELS[risk]}</span>
</div>
{cycle_warn}
<table style="width:auto;margin:8px 0">
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Язык</td><td>{mod.language}</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">NLOC</td><td>{mod.total_nloc:,}</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Функций</td><td>{mod.total_functions}</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Avg CCN</td><td>{mod.avg_ccn:.1f}</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Max CCN</td><td>{mod.max_ccn}</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">C_out / C_in</td><td>{mod.coupling_out} / {mod.coupling_in}</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Нестабильность (I)</td><td>{mod.instability:.2f}</td></tr>
</table>
{func_table}
</div>""")
return f"""
<h2>Детализация по модулям</h2>
{"".join(cards)}"""
def _footer(self) -> str:
return """
<hr style="margin:32px 0;border:none;border-top:1px solid var(--border)">
<p style="color:var(--muted);font-size:0.75rem;text-align:center">
Legacy Analyzer методический инструментарий модернизации унаследованных систем
</p>"""

View File

@ -0,0 +1,107 @@
"""
JSON Reporter машинно-читаемый отчёт в формате JSON.
Подходит для интеграции с CI/CD конвейерами и внешними инструментами.
"""
import json
import logging
from dataclasses import asdict, is_dataclass
from datetime import datetime
from pathlib import Path
from typing import Any
from models import AnalysisReport, ModernizationStrategy, RiskLevel
logger = logging.getLogger(__name__)
def _serialize(obj: Any) -> Any:
"""Рекурсивный сериализатор для dataclasses, Enum и set."""
if is_dataclass(obj) and not isinstance(obj, type):
return {k: _serialize(v) for k, v in asdict(obj).items()}
if isinstance(obj, (RiskLevel, ModernizationStrategy)):
return obj.name
if isinstance(obj, set):
return sorted(obj)
if isinstance(obj, list):
return [_serialize(i) for i in obj]
if isinstance(obj, dict):
return {k: _serialize(v) for k, v in obj.items()}
return obj
class JsonReporter:
"""Формирует JSON-отчёт по результатам анализа."""
def write(self, report: AnalysisReport, output_dir: str) -> str:
"""
Записывает отчёт в файл report.json внутри output_dir.
:returns: путь к созданному файлу
"""
out = Path(output_dir)
out.mkdir(parents=True, exist_ok=True)
file_path = out / "report.json"
payload = {
"generated_at": datetime.now().isoformat(),
"project_root": report.project_root,
"summary": {
"total_files": report.total_files,
"total_nloc": report.total_nloc,
"total_functions": report.total_functions,
"avg_system_ccn": round(report.avg_system_ccn, 2),
"system_risk_level": report.system_risk_level.name,
"recommended_system_strategy": report.recommended_system_strategy.name,
"dependency_cycles_count": len(report.dependency_cycles),
},
"dependency_cycles": report.dependency_cycles,
"decisions": [
{
"module": d.module_name,
"risk_level": d.risk_level.name,
"strategy": d.strategy.name,
"recommended_pattern": d.recommended_pattern,
"priority": d.priority,
"reasons": d.reasons,
}
for d in report.decisions
],
"modules": {
name: {
"files": mod.files,
"language": mod.language,
"total_nloc": mod.total_nloc,
"total_functions": mod.total_functions,
"avg_ccn": round(mod.avg_ccn, 2),
"max_ccn": mod.max_ccn,
"heavy_functions_count": mod.heavy_functions_count,
"heavy_functions_ratio": round(mod.heavy_functions_ratio, 3),
"coupling_out": mod.coupling_out,
"coupling_in": mod.coupling_in,
"instability": round(mod.instability, 3),
"has_cycles": mod.has_cycles,
"dependencies": sorted(mod.dependencies),
"dependents": sorted(mod.dependents),
"functions": [
{
"name": f.name,
"file": f.file,
"line": f.line,
"ccn": f.ccn,
"nloc": f.nloc,
"params": f.params,
}
for f in sorted(mod.functions, key=lambda x: x.ccn, reverse=True)
],
}
for name, mod in report.modules.items()
},
}
with open(file_path, "w", encoding="utf-8") as fh:
json.dump(payload, fh, ensure_ascii=False, indent=2)
logger.info("JSON отчёт записан: %s", file_path)
return str(file_path)

View File

@ -0,0 +1,41 @@
"""
Reporter фасад для всех форматов отчётности.
"""
import logging
from typing import List
from config import AnalyzerConfig, DEFAULT_CONFIG
from models import AnalysisReport
from .json_reporter import JsonReporter
from .html_reporter import HtmlReporter
logger = logging.getLogger(__name__)
class Reporter:
"""Управляет формированием отчётов в нескольких форматах."""
def __init__(self, config: AnalyzerConfig = DEFAULT_CONFIG):
self.config = config
self._json = JsonReporter()
self._html = HtmlReporter()
def generate(self, report: AnalysisReport) -> List[str]:
"""
Формирует все отчёты согласно конфигурации.
:returns: список путей к созданным файлам
"""
output_dir = self.config.output_dir
created: List[str] = []
if "json" in self.config.output_formats:
path = self._json.write(report, output_dir)
created.append(path)
if "html" in self.config.output_formats:
path = self._html.write(report, output_dir)
created.append(path)
return created

View File

View File

@ -0,0 +1,257 @@
"""
Встроенный анализатор сложности кода не требует lizard.
Реализует подсчёт цикломатической сложности (McCabe, 1976)
методом подсчёта ветвлений в тексте кода (лексический подход),
аналогично тому, как это делает библиотека lizard.
Поддерживаемые языки: C, C++, JavaScript, TypeScript.
Точность: comparable с lizard при отсутствии установленного Clang.
Для продакшн-использования рекомендуется установить lizard:
pip install lizard
"""
from __future__ import annotations
import re
import tokenize
import io
from dataclasses import dataclass
from pathlib import Path
from typing import List, Optional
# Ключевые слова ветвления, каждое увеличивает CCN на 1
# По методу McCabe: CCN = 1 + число точек ветвления
_BRANCH_KEYWORDS_CPP = re.compile(
r'\b(if|else\s+if|for|while|do|case|catch|&&|\|\||and|or|\?)\b',
re.MULTILINE,
)
_BRANCH_KEYWORDS_JS = re.compile(
r'\b(if|else\s+if|for|while|do|switch|case|catch|&&|\|\||\?(?!:))\b',
re.MULTILINE,
)
# Сигнатуры функций C/C++
_CPP_FUNC = re.compile(
r'^(?:[\w:*&<>\s]+?)\s+(\w+)\s*\(([^)]*)\)\s*(?:const\s*)?(?:override\s*)?(?:noexcept[^{]*)?\{',
re.MULTILINE,
)
# Сигнатуры функций JS/TS
_JS_FUNC = re.compile(
r'(?:'
r'function\s+(\w+)\s*\(([^)]*)\)' # function foo(...)
r'|(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s*)?\(([^)]*)\)\s*=>' # arrow
r'|(\w+)\s*:\s*(?:async\s*)?function\s*\(([^)]*)\)' # method: function
r'|(?:async\s+)?(\w+)\s*\(([^)]*)\)\s*\{' # method shorthand
r')',
re.MULTILINE,
)
# Строки и комментарии — для удаления перед анализом
_CPP_COMMENT_LINE = re.compile(r'//[^\n]*')
_CPP_COMMENT_BLOCK = re.compile(r'/\*.*?\*/', re.DOTALL)
_CPP_STRING = re.compile(r'"(?:[^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)*\'')
_JS_TEMPLATE = re.compile(r'`[^`]*`', re.DOTALL)
@dataclass
class FunctionInfo:
"""Результат разбора одной функции."""
name: str
start_line: int
nloc: int
cyclomatic_complexity: int
parameter_count: int
@dataclass
class FileAnalysisResult:
"""Результат анализа одного файла."""
filename: str
function_list: List[FunctionInfo]
@property
def average_ccn(self) -> float:
if not self.function_list:
return 0.0
return sum(f.cyclomatic_complexity for f in self.function_list) / len(self.function_list)
def analyze_file(file_path: str) -> Optional[FileAnalysisResult]:
"""
Анализирует один файл и возвращает метрики по функциям.
Возвращает None если файл нечитаем или неподдерживаемого типа.
"""
path = Path(file_path)
suffix = path.suffix.lower()
try:
text = path.read_text(encoding="utf-8", errors="replace")
except OSError:
return None
if suffix in (".cpp", ".cxx", ".cc", ".c", ".hpp", ".hxx", ".h"):
functions = _analyze_cpp(text, file_path)
elif suffix in (".js", ".mjs", ".cjs", ".ts"):
functions = _analyze_js(text, file_path)
else:
return None
return FileAnalysisResult(filename=file_path, function_list=functions)
# ------------------------------------------------------------------
# C / C++ анализ
# ------------------------------------------------------------------
def _analyze_cpp(text: str, filename: str) -> List[FunctionInfo]:
"""Анализирует C/C++ код."""
# Удаляем строковые литералы и комментарии
clean = _CPP_COMMENT_BLOCK.sub(' ', text)
clean = _CPP_COMMENT_LINE.sub('', clean)
clean = _CPP_STRING.sub('""', clean)
functions: List[FunctionInfo] = []
for match in _CPP_FUNC.finditer(clean):
func_name = match.group(1)
params_str = match.group(2).strip()
# Пропускаем системные/macro-like имена
if func_name in ('if', 'for', 'while', 'switch', 'catch', 'return'):
continue
start_pos = match.start()
start_line = text[:start_pos].count('\n') + 1
# Извлекаем тело функции
body = _extract_body(clean, match.end() - 1)
if body is None:
continue
nloc = _count_nloc(body)
ccn = 1 + len(_BRANCH_KEYWORDS_CPP.findall(body))
params = _count_params(params_str)
functions.append(FunctionInfo(
name=func_name,
start_line=start_line,
nloc=nloc,
cyclomatic_complexity=ccn,
parameter_count=params,
))
return functions
# ------------------------------------------------------------------
# JavaScript / TypeScript анализ
# ------------------------------------------------------------------
def _analyze_js(text: str, filename: str) -> List[FunctionInfo]:
"""Анализирует JS/TS код."""
# Удаляем шаблонные строки и комментарии
clean = _CPP_COMMENT_BLOCK.sub(' ', text)
clean = _CPP_COMMENT_LINE.sub('', clean)
clean = _JS_TEMPLATE.sub('``', clean)
clean = _CPP_STRING.sub('""', clean)
functions: List[FunctionInfo] = []
seen_positions: set = set()
for match in _JS_FUNC.finditer(clean):
# Определяем имя и параметры из разных групп
name = (
match.group(1) or match.group(3) or
match.group(5) or match.group(7) or "anonymous"
)
params_str = (
match.group(2) or match.group(4) or
match.group(6) or match.group(8) or ""
)
start_pos = match.start()
if start_pos in seen_positions:
continue
seen_positions.add(start_pos)
start_line = text[:start_pos].count('\n') + 1
# Ищем открывающую фигурную скобку
brace_pos = clean.find('{', match.end())
if brace_pos == -1:
continue
body = _extract_body(clean, brace_pos)
if body is None:
continue
nloc = _count_nloc(body)
ccn = 1 + len(_BRANCH_KEYWORDS_JS.findall(body))
params = _count_params(params_str)
functions.append(FunctionInfo(
name=name,
start_line=start_line,
nloc=nloc,
cyclomatic_complexity=ccn,
parameter_count=params,
))
return functions
# ------------------------------------------------------------------
# Вспомогательные функции
# ------------------------------------------------------------------
def _extract_body(text: str, open_brace_pos: int) -> Optional[str]:
"""
Извлекает тело функции, начиная с позиции открывающей скобки.
Возвращает содержимое между { и соответствующей }.
"""
depth = 0
i = open_brace_pos
while i < len(text):
ch = text[i]
if ch == '{':
depth += 1
elif ch == '}':
depth -= 1
if depth == 0:
return text[open_brace_pos + 1:i]
i += 1
return None # Незакрытая скобка
def _count_nloc(body: str) -> int:
"""Считает непустые, неккомментарные строки."""
return sum(
1 for line in body.splitlines()
if line.strip() and not line.strip().startswith('//')
)
def _count_params(params_str: str) -> int:
"""Считает число параметров по строке параметров."""
params_str = params_str.strip()
if not params_str or params_str in ('void', '...'):
return 0
# Разбиваем по запятой, учитывая вложенные угловые скобки
depth = 0
count = 1
for ch in params_str:
if ch in '<(':
depth += 1
elif ch in '>)':
depth -= 1
elif ch == ',' and depth == 0:
count += 1
return count

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.