This commit is contained in:
gre-ilya 2026-05-21 08:09:36 +05:00
parent 950c984542
commit b9d6a13b58
11 changed files with 8095 additions and 811 deletions

View File

@ -0,0 +1,13 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
pyqt5 = "*"
networkx = "*"
[dev-packages]
[requires]
python_version = "3.12"

83
prog/arch-researcher/Pipfile.lock generated Normal file
View File

@ -0,0 +1,83 @@
{
"_meta": {
"hash": {
"sha256": "7949d56dd1701e13de32a8fe849526075967be4665409687dde3cbe9d4dc32d9"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.12"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"networkx": {
"hashes": [
"sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509",
"sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762"
],
"index": "pypi",
"markers": "python_version >= '3.11' and python_full_version != '3.14.1'",
"version": "==3.6.1"
},
"pyqt5": {
"hashes": [
"sha256:6cd75628f6e732b1ffcfe709ab833a0716c0445d7aec8046a48d5843352becb6",
"sha256:76be0322ceda5deecd1708a8d628e698089a1cea80d1a49d242a6d579a40babd",
"sha256:bdde598a3bb95022131a5c9ea62e0a96bd6fb28932cc1619fd7ba211531b7517",
"sha256:c8b03dd9380bb13c804f0bdb0f4956067f281785b5e12303d529f0462f9afdc2",
"sha256:cd672a6738d1ae33ef7d9efa8e6cb0a1525ecf53ec86da80a9e1b6ec38c8d0f1",
"sha256:fda45743ebb4a27b4b1a51c6d8ef455c4c1b5d610c90d2934c7802b5c1557c52"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==5.15.11"
},
"pyqt5-qt5": {
"hashes": [
"sha256:8bb997eb903afa9da3221a0c9e6eaa00413bbeb4394d5706118ad05375684767",
"sha256:bf2457e6371969736b4f660a0c153258fa03dbc6a181348218e6f05421682af7",
"sha256:c656af9c1e6aaa7f59bf3d8995f2fa09adbf6762b470ed284c31dca80d686a26"
],
"version": "==5.15.18"
},
"pyqt5-sip": {
"hashes": [
"sha256:13c32e9025d0ab5fe960ef64dcb4c6f7ec26156ddce2cf2f195600aa26e8f9fe",
"sha256:163c2bba5e637c2222ec17d82a2c5aa158184a191923eb7d137cf4cfa0399529",
"sha256:521729e4ce44db555005f4e6987c4a5164d45ea03f5a7d08519813d4776acd15",
"sha256:65f257e28d06a01c2a9a7c0b2faf2af1dbf25a1420a5a9d905a54aaddce74d79",
"sha256:6d948f1be619c645cd3bda54952bfdc1aef7c79242dccea6a6858748e61114b9",
"sha256:71c37db75a0664325de149f43e2a712ec5fa1f90429a21dafbca005cb6767f94",
"sha256:8372ec8704bfd5e09942d0d055a1657eb4f702f4b30847a5e59df0496f99d67f",
"sha256:9254e5dd7676b76503ba20edcc919e7ac4a97b6c70a6fb2f9dba9e13b4c60509",
"sha256:92abe984becbde768954d6d0951f56d80a9868d2fd9e738e61fc944f0ff83dd6",
"sha256:95bba4670ecf5cba73958b85aa2087c17838a402ed251c38e68060c7665c998b",
"sha256:993fe3ed9a62a92e770f32d5344e3df56c2cacf1471f01b7feaf04818a2df1c4",
"sha256:9b689e02e400abd1ce0a30cd6eae8eceabcf1bbba0395cb5c86e64ba74351d68",
"sha256:a2a0278091d067b17855047ed8bd557e5ca62308a7d9fb5200451d0bd9090afc",
"sha256:aac4adc37df2f2ac1dc259409be1900f07332d140a12c9db7c84112cef64ff59",
"sha256:ac0be1a8ce145ed78c7e8d45243749405bee7e3a87e9810b0542bca010c50bd7",
"sha256:b29e4cda24748e59e5bd1bdad4812091a86b4b5b08c38b7f781eb55a5166f2b7",
"sha256:b4b743566f67cabfa7b0ba2da30b46d9d635d16cd5bc8cdcec83f620ed14dd93",
"sha256:bbec4c14903bcd585b4e1ee50edccfe224c8c0ee8004a072143993b4eb954442",
"sha256:bd9e3c6f81346f1b08d6db02305cdee20c009b43aa083d44ee2de47a7da0e123",
"sha256:c738949863d88b78f86f28e13151989ca3b1a302934811af41856dc8a27838bd",
"sha256:c969631ada7293a81e1012b2264a62d69a91995b517586489dfe24421b87b9af",
"sha256:d84ac384a63285132e67762c87681191c25e28a1df7560287ec3889d9eb223b5",
"sha256:dc5c30bb9bd8cca6e13d2fedac6e2a75fe2168d798d66f76e74835546b025027",
"sha256:dd855149c724634eb1f92d10e04e1be0751068a8521d5f9f06e5c2dbe32fd89f",
"sha256:ead5e0a64ad852ac60797989d8444a6a5bd834768536b04a07b40b2937d922f6",
"sha256:fdb45c7cd2af7eccd7370b994d432bfc7965079f845392760724f26771bb59dc"
],
"markers": "python_version >= '3.10'",
"version": "==12.18.0"
}
},
"develop": {}
}

View File

@ -49,9 +49,6 @@ class AnalyzerConfig:
cpp_extensions: List[str] = field(default_factory=lambda: [ cpp_extensions: List[str] = field(default_factory=lambda: [
".cpp", ".cxx", ".cc", ".c", ".hpp", ".hxx", ".h" ".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: [ ignored_dirs: List[str] = field(default_factory=lambda: [

View File

@ -30,7 +30,6 @@ class Collector:
def __init__(self, config: AnalyzerConfig = DEFAULT_CONFIG): def __init__(self, config: AnalyzerConfig = DEFAULT_CONFIG):
self.config = config self.config = config
self._cpp_exts = set(config.cpp_extensions) self._cpp_exts = set(config.cpp_extensions)
self._js_exts = set(config.js_extensions)
self._ignored = set(config.ignored_dirs) self._ignored = set(config.ignored_dirs)
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@ -96,12 +95,9 @@ class Collector:
) )
def _detect_language(self, path: Path) -> str | None: def _detect_language(self, path: Path) -> str | None:
"""Возвращает 'cpp', 'js' или None если файл не анализируется.""" """Возвращает 'cpp' или None если файл не анализируется."""
suffix = path.suffix.lower() if path.suffix.lower() in self._cpp_exts:
if suffix in self._cpp_exts:
return "cpp" return "cpp"
if suffix in self._js_exts:
return "js"
return None return None
def _resolve_module_name(self, root: Path, file: Path) -> str: def _resolve_module_name(self, root: Path, file: Path) -> str:
@ -124,8 +120,6 @@ class Collector:
@staticmethod @staticmethod
def _merge_language(existing: str, new_lang: str) -> str: def _merge_language(existing: str, new_lang: str) -> str:
"""Объединяет языки при добавлении нового файла в модуль.""" """Объединяет языки при добавлении нового файла в модуль."""
if existing == "mixed" or existing == new_lang: if not existing:
return existing if existing != "mixed" else "mixed" return new_lang
if existing in ("cpp", "js") and existing != new_lang: return existing
return "mixed"
return new_lang # existing == "" — первый файл

View File

@ -2,14 +2,13 @@
Модуль DependencyAnalyzer построение и анализ графа зависимостей. Модуль DependencyAnalyzer построение и анализ графа зависимостей.
Алгоритм: Алгоритм:
1. Для C++ файлов извлекаются #include-директивы: 1. Для C/C++ файлов извлекаются #include-директивы:
- через libclang AST (если установлен) точный разбор - через libclang AST (если установлен) точный разбор
- через regex (fallback) лексический разбор - через regex (fallback) лексический разбор
2. Для JS файлов regex-парсинг require() и import ... from 2. #include разрешаются до имени модуля (директории)
3. #include / import разрешаются до имени модуля (директории) 3. Строится ориентированный граф зависимостей (networkx DiGraph)
4. Строится ориентированный граф зависимостей (networkx DiGraph) 4. Вычисляются метрики: coupling_in, coupling_out, instability
5. Вычисляются метрики: coupling_in, coupling_out, instability 5. Обнаруживаются циклические зависимости (strongly connected components)
6. Обнаруживаются циклические зависимости (strongly connected components)
Метрика нестабильности (Instability): Метрика нестабильности (Instability):
I = C_out / (C_in + C_out), где I = C_out / (C_in + C_out), где
@ -51,12 +50,6 @@ except ImportError:
# Regex для fallback-разбора #include # Regex для fallback-разбора #include
_RE_INCLUDE = re.compile(r'^\s*#\s*include\s+[<"]([^>"]+)[>"]', re.MULTILINE) _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: class DependencyAnalyzer:
""" """
@ -169,21 +162,13 @@ class DependencyAnalyzer:
root: Path, root: Path,
) -> Set[str]: ) -> Set[str]:
"""Извлекает зависимости из одного файла.""" """Извлекает зависимости из одного файла."""
suffix = Path(file_path).suffix.lower() if Path(file_path).suffix.lower() not in self.config.cpp_extensions:
return set()
if suffix in self.config.cpp_extensions: raw_includes = self._extract_cpp_includes(file_path)
raw_includes = self._extract_cpp_includes(file_path) return self._resolve_includes_to_modules(
return self._resolve_includes_to_modules( raw_includes, file_path, file_to_module, root
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]: def _extract_cpp_includes(self, file_path: str) -> List[str]:
""" """
@ -219,14 +204,6 @@ class DependencyAnalyzer:
return [] return []
return _RE_INCLUDE.findall(text) 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)
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Разрешение имён в модули # Разрешение имён в модули
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@ -261,39 +238,6 @@ class DependencyAnalyzer:
return deps 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
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Вспомогательные методы # Вспомогательные методы
# ------------------------------------------------------------------ # ------------------------------------------------------------------

View File

@ -140,9 +140,6 @@ class MetricsEngine:
def _detect_language(self, file_path: str) -> str: def _detect_language(self, file_path: str) -> str:
"""Определяет язык по расширению файла.""" """Определяет язык по расширению файла."""
suffix = Path(file_path).suffix.lower() if Path(file_path).suffix.lower() in self.config.cpp_extensions:
if suffix in self.config.cpp_extensions:
return "cpp" return "cpp"
if suffix in self.config.js_extensions:
return "js"
return "unknown" return "unknown"

482
prog/arch-researcher/gui.py Normal file
View File

@ -0,0 +1,482 @@
#!/usr/bin/env python3
"""
PyQt5 GUI для Legacy Analyzer.
Запуск:
python gui.py
pip install PyQt5 # если не установлен
"""
import logging
import os
import sys
from pathlib import Path
from PyQt5.QtCore import QThread, pyqtSignal, Qt
from PyQt5.QtGui import QColor, QFont, QTextCursor
from PyQt5.QtWidgets import (
QApplication, QCheckBox, QFileDialog, QFormLayout, QGroupBox,
QHBoxLayout, QHeaderView, QLabel, QLineEdit, QMainWindow,
QPushButton, QSizePolicy, QSpinBox, QSplitter, QTableWidget,
QTableWidgetItem, QTextEdit, QVBoxLayout, QWidget,
)
# Добавляем директорию скрипта в sys.path, чтобы импорты работали при запуске
# gui.py из любой рабочей директории
_HERE = Path(__file__).parent
if str(_HERE) not in sys.path:
sys.path.insert(0, str(_HERE))
from config import AnalyzerConfig, ThresholdConfig
from analyzer import Analyzer
from models import RiskLevel, ModernizationStrategy
# ---------------------------------------------------------------------------
# Цвета уровней риска
# ---------------------------------------------------------------------------
RISK_COLORS = {
RiskLevel.LOW: "#27ae60",
RiskLevel.MEDIUM: "#f39c12",
RiskLevel.HIGH: "#e67e22",
RiskLevel.CRITICAL: "#e74c3c",
}
RISK_BG = {
RiskLevel.LOW: "#eafaf1",
RiskLevel.MEDIUM: "#fef9e7",
RiskLevel.HIGH: "#fef0e6",
RiskLevel.CRITICAL: "#fdedec",
}
STRATEGY_RU = {
ModernizationStrategy.KEEP: "Оставить",
ModernizationStrategy.REFACTOR: "Рефакторинг",
ModernizationStrategy.REENGINEER: "Реинжиниринг",
ModernizationStrategy.REPLACE: "Замена",
}
RISK_RU = {
RiskLevel.LOW: "НИЗКИЙ",
RiskLevel.MEDIUM: "СРЕДНИЙ",
RiskLevel.HIGH: "ВЫСОКИЙ",
RiskLevel.CRITICAL: "КРИТИЧЕСКИЙ",
}
LOG_LEVEL_COLORS = {
"DEBUG": "#888888",
"INFO": "#ecf0f1",
"WARNING": "#f39c12",
"ERROR": "#e74c3c",
"CRITICAL":"#e74c3c",
}
# ---------------------------------------------------------------------------
# Фоновый поток анализа
# ---------------------------------------------------------------------------
class _QtLogHandler(logging.Handler):
"""Перехватывает logging и отправляет сообщения в Qt-сигнал."""
def __init__(self, signal):
super().__init__()
self._signal = signal
def emit(self, record):
try:
self._signal.emit(self.format(record), record.levelname)
except Exception:
pass
class AnalysisWorker(QThread):
log_signal = pyqtSignal(str, str) # (message, levelname)
finished_signal = pyqtSignal(object) # AnalysisReport
error_signal = pyqtSignal(str)
def __init__(self, project_root: str, config: AnalyzerConfig):
super().__init__()
self._project_root = project_root
self._config = config
def run(self):
handler = _QtLogHandler(self.log_signal)
handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(message)s",
datefmt="%H:%M:%S"))
root_logger = logging.getLogger()
root_logger.addHandler(handler)
root_logger.setLevel(logging.DEBUG)
try:
analyzer = Analyzer(self._config)
report = analyzer.run(self._project_root)
self.finished_signal.emit(report)
except Exception as exc:
self.error_signal.emit(str(exc))
finally:
root_logger.removeHandler(handler)
# ---------------------------------------------------------------------------
# Панель настроек (левая)
# ---------------------------------------------------------------------------
class _SettingsPanel(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self._build_ui()
def _build_ui(self):
layout = QVBoxLayout(self)
layout.setContentsMargins(8, 8, 8, 8)
layout.setSpacing(10)
# --- Проект ---
proj_box = QGroupBox("Проект")
proj_form = QFormLayout(proj_box)
proj_form.setLabelAlignment(Qt.AlignRight)
self.project_edit = QLineEdit()
self.project_edit.setPlaceholderText("Путь к анализируемому проекту")
proj_btn = QPushButton("")
proj_btn.setFixedWidth(30)
proj_btn.clicked.connect(self._browse_project)
proj_row = QHBoxLayout()
proj_row.addWidget(self.project_edit)
proj_row.addWidget(proj_btn)
self.output_edit = QLineEdit("legacy_report")
self.output_edit.setPlaceholderText("Директория для отчётов")
out_btn = QPushButton("")
out_btn.setFixedWidth(30)
out_btn.clicked.connect(self._browse_output)
out_row = QHBoxLayout()
out_row.addWidget(self.output_edit)
out_row.addWidget(out_btn)
self.libclang_edit = QLineEdit()
self.libclang_edit.setPlaceholderText("Авто (опционально)")
lc_btn = QPushButton("")
lc_btn.setFixedWidth(30)
lc_btn.clicked.connect(self._browse_libclang)
lc_row = QHBoxLayout()
lc_row.addWidget(self.libclang_edit)
lc_row.addWidget(lc_btn)
proj_form.addRow("Проект:", proj_row)
proj_form.addRow("Вывод:", out_row)
proj_form.addRow("libclang:", lc_row)
# --- Форматы ---
fmt_box = QGroupBox("Форматы отчётов")
fmt_layout = QHBoxLayout(fmt_box)
self.fmt_json = QCheckBox("JSON")
self.fmt_json.setChecked(True)
self.fmt_html = QCheckBox("HTML")
self.fmt_html.setChecked(True)
fmt_layout.addWidget(self.fmt_json)
fmt_layout.addWidget(self.fmt_html)
fmt_layout.addStretch()
# --- Пороги ---
thr_box = QGroupBox("Пороговые значения")
thr_form = QFormLayout(thr_box)
thr_form.setLabelAlignment(Qt.AlignRight)
self.ccn_warn = self._spinbox(1, 100, 10)
self.ccn_crit = self._spinbox(1, 100, 20)
self.coup_warn = self._spinbox(1, 100, 5)
self.coup_crit = self._spinbox(1, 100, 10)
thr_form.addRow("CCN предупрежд.:", self.ccn_warn)
thr_form.addRow("CCN критическое:", self.ccn_crit)
thr_form.addRow("Связность вых. (пред.):", self.coup_warn)
thr_form.addRow("Связность вых. (крит.):", self.coup_crit)
# --- Кнопка запуска ---
self.run_btn = QPushButton("▶ Запустить анализ")
self.run_btn.setMinimumHeight(40)
self.run_btn.setStyleSheet(
"QPushButton { background: #2980b9; color: white; font-weight: bold; "
"border-radius: 4px; font-size: 13px; }"
"QPushButton:hover { background: #3498db; }"
"QPushButton:disabled { background: #7f8c8d; }"
)
layout.addWidget(proj_box)
layout.addWidget(fmt_box)
layout.addWidget(thr_box)
layout.addStretch()
layout.addWidget(self.run_btn)
@staticmethod
def _spinbox(minimum, maximum, default) -> QSpinBox:
sb = QSpinBox()
sb.setRange(minimum, maximum)
sb.setValue(default)
return sb
def _browse_project(self):
d = QFileDialog.getExistingDirectory(self, "Выберите директорию проекта")
if d:
self.project_edit.setText(d)
def _browse_output(self):
d = QFileDialog.getExistingDirectory(self, "Директория для отчётов")
if d:
self.output_edit.setText(d)
def _browse_libclang(self):
f, _ = QFileDialog.getOpenFileName(self, "Путь к libclang", "",
"Библиотеки (*.so *.dylib *.dll);;Все файлы (*)")
if f:
self.libclang_edit.setText(f)
def build_config(self) -> AnalyzerConfig:
formats = []
if self.fmt_json.isChecked():
formats.append("json")
if self.fmt_html.isChecked():
formats.append("html")
if not formats:
formats = ["json"]
thresholds = ThresholdConfig(
ccn_warn=self.ccn_warn.value(),
ccn_critical=self.ccn_crit.value(),
coupling_out_warn=self.coup_warn.value(),
coupling_out_critical=self.coup_crit.value(),
)
libclang = self.libclang_edit.text().strip() or None
return AnalyzerConfig(
output_formats=formats,
output_dir=self.output_edit.text().strip() or "legacy_report",
libclang_path=libclang,
thresholds=thresholds,
)
# ---------------------------------------------------------------------------
# Панель лога (правая верхняя)
# ---------------------------------------------------------------------------
class _LogPanel(QTextEdit):
def __init__(self, parent=None):
super().__init__(parent)
self.setReadOnly(True)
self.setFont(QFont("Monospace", 9))
self.setStyleSheet("background: #1e1e1e; color: #ecf0f1;")
def append_log(self, message: str, level: str):
color = LOG_LEVEL_COLORS.get(level, "#ecf0f1")
html = f'<span style="color:{color};">{message}</span>'
self.append(html)
self.moveCursor(QTextCursor.End)
def clear_log(self):
self.clear()
# ---------------------------------------------------------------------------
# Панель результатов (правая нижняя)
# ---------------------------------------------------------------------------
class _ResultsPanel(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self._html_path: str = ""
self._build_ui()
self.setVisible(False)
def _build_ui(self):
layout = QVBoxLayout(self)
layout.setContentsMargins(8, 4, 8, 8)
layout.setSpacing(6)
# Заголовок
title = QLabel("Результаты анализа")
title.setStyleSheet("font-weight: bold; font-size: 13px;")
layout.addWidget(title)
# Сводка
self._summary = QLabel()
self._summary.setWordWrap(True)
self._summary.setStyleSheet("font-size: 12px;")
layout.addWidget(self._summary)
# Таблица решений
self._table = QTableWidget()
self._table.setColumnCount(4)
self._table.setHorizontalHeaderLabels(["Модуль", "Риск", "Стратегия", "Причины"])
self._table.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch)
self._table.horizontalHeader().setSectionResizeMode(3, QHeaderView.Stretch)
self._table.setEditTriggers(QTableWidget.NoEditTriggers)
self._table.setSelectionBehavior(QTableWidget.SelectRows)
self._table.setAlternatingRowColors(True)
layout.addWidget(self._table)
# Кнопка открытия отчёта
self._open_btn = QPushButton("Открыть HTML отчёт")
self._open_btn.setStyleSheet(
"QPushButton { background: #27ae60; color: white; border-radius: 4px; padding: 6px; }"
"QPushButton:hover { background: #2ecc71; }"
"QPushButton:disabled { background: #7f8c8d; }"
)
self._open_btn.clicked.connect(self._open_html)
self._open_btn.setEnabled(False)
layout.addWidget(self._open_btn)
def show_report(self, report, output_dir: str):
from models import RiskLevel
risk = report.system_risk_level
strategy = report.recommended_system_strategy
color = RISK_COLORS.get(risk, "#ecf0f1")
risk_ru = RISK_RU.get(risk, risk.name)
strategy_ru = STRATEGY_RU.get(strategy, strategy.name)
summary_html = (
f"<b>Системный риск:</b> <span style='color:{color};font-weight:bold;'>{risk_ru}</span> &nbsp;|&nbsp; "
f"<b>Стратегия:</b> {strategy_ru}<br>"
f"<b>Модулей:</b> {len(report.modules)} &nbsp;|&nbsp; "
f"<b>Функций:</b> {report.total_functions} &nbsp;|&nbsp; "
f"<b>Ср. CCN:</b> {report.avg_system_ccn:.2f} &nbsp;|&nbsp; "
f"<b>Циклов зависим.:</b> {len(report.dependency_cycles)}"
)
self._summary.setText(summary_html)
# Заполняем таблицу
self._table.setRowCount(len(report.decisions))
for row, decision in enumerate(report.decisions):
r = decision.risk_level
bg = QColor(RISK_BG.get(r, "#ffffff"))
fg = QColor(RISK_COLORS.get(r, "#000000"))
items = [
decision.module_name,
RISK_RU.get(r, r.name),
STRATEGY_RU.get(decision.strategy, decision.strategy.name),
"; ".join(decision.reasons) if decision.reasons else "",
]
for col, text in enumerate(items):
item = QTableWidgetItem(text)
item.setBackground(bg)
if col == 1:
item.setForeground(fg)
item.setFont(QFont("", -1, QFont.Bold))
self._table.setItem(row, col, item)
self._table.resizeRowsToContents()
# HTML отчёт
html_path = Path(output_dir) / "report.html"
if not html_path.is_absolute():
html_path = Path.cwd() / html_path
if html_path.exists():
self._html_path = str(html_path)
self._open_btn.setEnabled(True)
else:
self._html_path = ""
self._open_btn.setEnabled(False)
self.setVisible(True)
def _open_html(self):
if self._html_path:
import webbrowser
webbrowser.open(f"file://{self._html_path}")
# ---------------------------------------------------------------------------
# Главное окно
# ---------------------------------------------------------------------------
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Legacy Analyzer")
self.resize(1100, 700)
self._worker: AnalysisWorker | None = None
self._build_ui()
def _build_ui(self):
central = QWidget()
self.setCentralWidget(central)
root_layout = QHBoxLayout(central)
root_layout.setContentsMargins(6, 6, 6, 6)
root_layout.setSpacing(6)
# Левая панель — настройки
self._settings = _SettingsPanel()
self._settings.setFixedWidth(320)
self._settings.run_btn.clicked.connect(self._run_analysis)
# Правая панель — лог + результаты
right_splitter = QSplitter(Qt.Vertical)
self._log = _LogPanel()
self._log.setMinimumHeight(150)
self._results = _ResultsPanel()
self._results.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
right_splitter.addWidget(self._log)
right_splitter.addWidget(self._results)
right_splitter.setSizes([300, 300])
root_layout.addWidget(self._settings)
root_layout.addWidget(right_splitter, stretch=1)
def _run_analysis(self):
project_root = self._settings.project_edit.text().strip()
if not project_root:
self._log.append_log("Укажите путь к проекту.", "ERROR")
return
if not Path(project_root).exists():
self._log.append_log(f"Директория не найдена: {project_root}", "ERROR")
return
config = self._settings.build_config()
self._log.clear_log()
self._results.setVisible(False)
self._settings.run_btn.setEnabled(False)
self._settings.run_btn.setText("⏳ Анализ...")
self._worker = AnalysisWorker(project_root, config)
self._worker.log_signal.connect(self._log.append_log)
self._worker.finished_signal.connect(self._on_finished)
self._worker.error_signal.connect(self._on_error)
self._worker.start()
def _on_finished(self, report):
self._reset_run_btn()
output_dir = self._settings.output_edit.text().strip() or "legacy_report"
self._results.show_report(report, output_dir)
self._log.append_log("Анализ завершён.", "INFO")
def _on_error(self, message: str):
self._reset_run_btn()
self._log.append_log(f"Ошибка: {message}", "ERROR")
def _reset_run_btn(self):
self._settings.run_btn.setEnabled(True)
self._settings.run_btn.setText("▶ Запустить анализ")
# ---------------------------------------------------------------------------
# Точка входа
# ---------------------------------------------------------------------------
def main():
app = QApplication(sys.argv)
app.setStyle("Fusion")
window = MainWindow()
window.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()

View File

@ -0,0 +1,271 @@
<!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>/home/ilya/A</code> &nbsp;|&nbsp;
Сгенерировано: 21.05.2026 08:08 &nbsp;|&nbsp;
Системный риск: <strong style="color:#ef4444">Высокий</strong>
</p>
<h2>Сводка по системе</h2>
<div class="grid-4">
<div class="stat-card">
<div class="stat-value">126</div>
<div class="stat-label">Файлов</div>
</div>
<div class="stat-card">
<div class="stat-value">5,186</div>
<div class="stat-label">Строк кода (NLOC)</div>
</div>
<div class="stat-card">
<div class="stat-value">875</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">1</div>
<div class="stat-label">Циклов зависимостей</div>
</div>
<div class="stat-card">
<div class="stat-value" style="color:#ef4444">Высокий</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><th>Причины</th>
</tr></thead>
<tbody><tr>
<td><code>helpers/memenv</code></td>
<td><span class="badge" style="color:#ef4444;background:#fee2e2">Высокий</span></td>
<td>Реинжиниринг</td>
<td><span style="font-size:0.8rem;color:#1d4ed8">Anti-Corruption Layer: изоляция модуля от циклических зависимостей</span></td>
<td>390</td>
<td><ul class="reasons-list"><li>Высокая нестабильность: I=0.75 (порог 0.7)</li><li>Участвует в циклических зависимостях — признак архитектурной деградации</li></ul></td>
</tr><tr>
<td><code>table</code></td>
<td><span class="badge" style="color:#ef4444;background:#fee2e2">Высокий</span></td>
<td>Реинжиниринг</td>
<td><span style="font-size:0.8rem;color:#1d4ed8">Anti-Corruption Layer: изоляция модуля от циклических зависимостей</span></td>
<td>390</td>
<td><ul class="reasons-list"><li>Высокая нестабильность: I=0.75 (порог 0.7)</li><li>Участвует в циклических зависимостях — признак архитектурной деградации</li></ul></td>
</tr><tr>
<td><code>util</code></td>
<td><span class="badge" style="color:#ef4444;background:#fee2e2">Высокий</span></td>
<td>Реинжиниринг</td>
<td><span style="font-size:0.8rem;color:#1d4ed8">Anti-Corruption Layer: изоляция модуля от циклических зависимостей</span></td>
<td>470</td>
<td><ul class="reasons-list"><li>Участвует в циклических зависимостях — признак архитектурной деградации</li></ul></td>
</tr><tr>
<td><code>db</code></td>
<td><span class="badge" style="color:#ef4444;background:#fee2e2">Высокий</span></td>
<td>Реинжиниринг</td>
<td><span style="font-size:0.8rem;color:#1d4ed8">Anti-Corruption Layer: изоляция модуля от циклических зависимостей</span></td>
<td>480</td>
<td><ul class="reasons-list"><li>Участвует в циклических зависимостях — признак архитектурной деградации</li></ul></td>
</tr><tr>
<td><code>port</code></td>
<td><span class="badge" style="color:#22c55e;background:#dcfce7">Низкий</span></td>
<td>Оставить</td>
<td></td>
<td>960</td>
<td></td>
</tr><tr>
<td><code>include/leveldb</code></td>
<td><span class="badge" style="color:#22c55e;background:#dcfce7">Низкий</span></td>
<td>Оставить</td>
<td></td>
<td>1000</td>
<td></td>
</tr></tbody>
</table>
</div>
<h2>Циклические зависимости (1)</h2>
<div class="card"><p><strong>Цикл 1:</strong> <span class="cycle-badge">db</span><span class="cycle-badge">helpers/memenv</span><span class="cycle-badge">table</span><span class="cycle-badge">util</span></p></div>
<h2>Детализация по модулям</h2>
<div class="card module-card">
<div class="module-header">
<h3><code>db</code></h3>
<span class="badge" style="color:#ef4444;background:#fee2e2">Высокий</span>
</div>
<span style="color:#ef4444;font-size:0.8rem">⚠ Участвует в цикле зависимостей</span>
<table style="width:auto;margin:8px 0">
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Язык</td><td>cpp</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">NLOC</td><td>2,664</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Функций</td><td>455</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Avg CCN</td><td>1.7</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Max CCN</td><td>12</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">C_out / C_in</td><td>3 / 2</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Нестабильность (I)</td><td>0.60</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>CompareIterators</code></td><td>12</td><td>71</td><td>5</td><td><code>db_test.cc:1843</code></td></tr><tr><td><code>BuildTable</code></td><td>11</td><td>49</td><td>6</td><td><code>builder.cc:2</code></td></tr><tr><td><code>NewWritableFile</code></td><td>11</td><td>71</td><td>2</td><td><code>db_test.cc:116</code></td></tr><tr><td><code>ParseFileName</code></td><td>11</td><td>39</td><td>3</td><td><code>filename.cc:55</code></td></tr><tr><td><code>ScanTable</code></td><td>10</td><td>51</td><td>1</td><td><code>repair.cc:166</code></td></tr></tbody>
</table></details>
</div><div class="card module-card">
<div class="module-header">
<h3><code>helpers/memenv</code></h3>
<span class="badge" style="color:#ef4444;background:#fee2e2">Высокий</span>
</div>
<span style="color:#ef4444;font-size:0.8rem">⚠ Участвует в цикле зависимостей</span>
<table style="width:auto;margin:8px 0">
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Язык</td><td>cpp</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">NLOC</td><td>176</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Функций</td><td>31</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Avg CCN</td><td>1.8</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Max CCN</td><td>6</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">C_out / C_in</td><td>3 / 1</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Нестабильность (I)</td><td>0.75</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>Read</code></td><td>6</td><td>30</td><td>4</td><td><code>memenv.cc:39</code></td></tr><tr><td><code>Append</code></td><td>4</td><td>21</td><td>1</td><td><code>memenv.cc:85</code></td></tr><tr><td><code>Unref</code></td><td>3</td><td>12</td><td>0</td><td><code>memenv.cc:16</code></td></tr><tr><td><code>Skip</code></td><td>3</td><td>9</td><td>1</td><td><code>memenv.cc:139</code></td></tr><tr><td><code>GetChildren</code></td><td>3</td><td>10</td><td>2</td><td><code>memenv.cc:259</code></td></tr></tbody>
</table></details>
</div><div class="card module-card">
<div class="module-header">
<h3><code>include/leveldb</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>cpp</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">NLOC</td><td>43</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Функций</td><td>41</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Avg CCN</td><td>1.0</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Max CCN</td><td>1</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">C_out / C_in</td><td>0 / 0</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Нестабильность (I)</td><td>0.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>Prune</code></td><td>1</td><td>0</td><td>0</td><td><code>cache.h:16</code></td></tr><tr><td><code>target</code></td><td>1</td><td>1</td><td>0</td><td><code>env.h:116</code></td></tr><tr><td><code>NewSequentialFile</code></td><td>1</td><td>1</td><td>2</td><td><code>env.h:116</code></td></tr><tr><td><code>NewRandomAccessFile</code></td><td>1</td><td>1</td><td>2</td><td><code>env.h:119</code></td></tr><tr><td><code>NewWritableFile</code></td><td>1</td><td>1</td><td>2</td><td><code>env.h:123</code></td></tr></tbody>
</table></details>
</div><div class="card module-card">
<div class="module-header">
<h3><code>port</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>cpp</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">NLOC</td><td>94</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Функций</td><td>10</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Avg CCN</td><td>2.2</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Max CCN</td><td>4</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">C_out / C_in</td><td>0 / 4</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Нестабильность (I)</td><td>0.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>Zstd_Compress</code></td><td>4</td><td>24</td><td>4</td><td><code>port_stdcxx.h:93</code></td></tr><tr><td><code>Zstd_Uncompress</code></td><td>4</td><td>18</td><td>3</td><td><code>port_stdcxx.h:136</code></td></tr><tr><td><code>Zstd_GetUncompressedLength</code></td><td>3</td><td>11</td><td>3</td><td><code>port_stdcxx.h:123</code></td></tr><tr><td><code>Snappy_Compress</code></td><td>2</td><td>12</td><td>3</td><td><code>port_stdcxx.h:57</code></td></tr><tr><td><code>Snappy_GetUncompressedLength</code></td><td>2</td><td>8</td><td>3</td><td><code>port_stdcxx.h:74</code></td></tr></tbody>
</table></details>
</div><div class="card module-card">
<div class="module-header">
<h3><code>table</code></h3>
<span class="badge" style="color:#ef4444;background:#fee2e2">Высокий</span>
</div>
<span style="color:#ef4444;font-size:0.8rem">⚠ Участвует в цикле зависимостей</span>
<table style="width:auto;margin:8px 0">
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Язык</td><td>cpp</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">NLOC</td><td>692</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Функций</td><td>132</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Avg CCN</td><td>1.8</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Max CCN</td><td>17</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">C_out / C_in</td><td>3 / 1</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Нестабильность (I)</td><td>0.75</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>TestRandomAccess</code></td><td>17</td><td>61</td><td>3</td><td><code>table_test.cc:434</code></td></tr><tr><td><code>ReadBlock</code></td><td>13</td><td>79</td><td>4</td><td><code>format.cc:51</code></td></tr><tr><td><code>Seek</code></td><td>11</td><td>44</td><td>1</td><td><code>block.cc:110</code></td></tr><tr><td><code>DecodeEntry</code></td><td>7</td><td>15</td><td>5</td><td><code>block.cc:29</code></td></tr><tr><td><code>Init</code></td><td>6</td><td>22</td><td>1</td><td><code>table_test.cc:360</code></td></tr></tbody>
</table></details>
</div><div class="card module-card">
<div class="module-header">
<h3><code>util</code></h3>
<span class="badge" style="color:#ef4444;background:#fee2e2">Высокий</span>
</div>
<span style="color:#ef4444;font-size:0.8rem">⚠ Участвует в цикле зависимостей</span>
<table style="width:auto;margin:8px 0">
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Язык</td><td>cpp</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">NLOC</td><td>1,517</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Функций</td><td>206</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Avg CCN</td><td>2.0</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Max CCN</td><td>17</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">C_out / C_in</td><td>2 / 3</td></tr>
<tr><td style="padding:2px 16px 2px 0;color:var(--muted)">Нестабильность (I)</td><td>0.40</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>Extend</code></td><td>17</td><td>73</td><td>3</td><td><code>crc32c.cc:254</code></td></tr><tr><td><code>GetChildren</code></td><td>7</td><td>24</td><td>2</td><td><code>env_windows.cc:417</code></td></tr><tr><td><code>Logv</code></td><td>7</td><td>58</td><td>2</td><td><code>posix_logger.h:13</code></td></tr><tr><td><code>Logv</code></td><td>7</td><td>55</td><td>2</td><td><code>windows_logger.h:13</code></td></tr><tr><td><code>NewRandomAccessFile</code></td><td>6</td><td>27</td><td>2</td><td><code>env_posix.cc:372</code></td></tr></tbody>
</table></details>
</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>

File diff suppressed because it is too large Load Diff

View File

@ -23,7 +23,7 @@ class FunctionMetrics:
ccn: int # Цикломатическая сложность (McCabe) ccn: int # Цикломатическая сложность (McCabe)
nloc: int # Строк кода без комментариев nloc: int # Строк кода без комментариев
params: int # Число параметров params: int # Число параметров
language: str # "cpp" | "js" language: str # "cpp"
@property @property
def is_complex(self) -> bool: def is_complex(self) -> bool:
@ -40,7 +40,7 @@ class ModuleMetrics:
name: str # Имя модуля (путь относительно корня) name: str # Имя модуля (путь относительно корня)
files: List[str] = field(default_factory=list) files: List[str] = field(default_factory=list)
functions: List[FunctionMetrics] = field(default_factory=list) functions: List[FunctionMetrics] = field(default_factory=list)
language: str = "mixed" # "cpp" | "js" | "mixed" language: str = "" # "cpp"
# Агрегированные показатели (заполняются MetricsEngine) # Агрегированные показатели (заполняются MetricsEngine)
total_nloc: int = 0 total_nloc: int = 0

View File

@ -5,7 +5,7 @@
методом подсчёта ветвлений в тексте кода (лексический подход), методом подсчёта ветвлений в тексте кода (лексический подход),
аналогично тому, как это делает библиотека lizard. аналогично тому, как это делает библиотека lizard.
Поддерживаемые языки: C, C++, JavaScript, TypeScript. Поддерживаемые языки: C, C++.
Точность: comparable с lizard при отсутствии установленного Clang. Точность: comparable с lizard при отсутствии установленного Clang.
Для продакшн-использования рекомендуется установить lizard: Для продакшн-использования рекомендуется установить lizard:
@ -15,8 +15,6 @@
from __future__ import annotations from __future__ import annotations
import re import re
import tokenize
import io
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import List, Optional from typing import List, Optional
@ -29,35 +27,17 @@ _BRANCH_KEYWORDS_CPP = re.compile(
re.MULTILINE, re.MULTILINE,
) )
_BRANCH_KEYWORDS_JS = re.compile(
r'\b(if|else\s+if|for|while|do|switch|case|catch|&&|\|\||\?(?!:))\b',
re.MULTILINE,
)
# Сигнатуры функций C/C++ # Сигнатуры функций C/C++
_CPP_FUNC = re.compile( _CPP_FUNC = re.compile(
r'^(?:[\w:*&<>\s]+?)\s+(\w+)\s*\(([^)]*)\)\s*(?:const\s*)?(?:override\s*)?(?:noexcept[^{]*)?\{', r'^(?:[\w:*&<>\s]+?)\s+(\w+)\s*\(([^)]*)\)\s*(?:const\s*)?(?:override\s*)?(?:noexcept[^{]*)?\{',
re.MULTILINE, 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_LINE = re.compile(r'//[^\n]*')
_CPP_COMMENT_BLOCK = re.compile(r'/\*.*?\*/', re.DOTALL) _CPP_COMMENT_BLOCK = re.compile(r'/\*.*?\*/', re.DOTALL)
_CPP_STRING = re.compile(r'"(?:[^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)*\'') _CPP_STRING = re.compile(r'"(?:[^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)*\'')
_JS_TEMPLATE = re.compile(r'`[^`]*`', re.DOTALL)
@dataclass @dataclass
class FunctionInfo: class FunctionInfo:
@ -90,18 +70,15 @@ def analyze_file(file_path: str) -> Optional[FileAnalysisResult]:
path = Path(file_path) path = Path(file_path)
suffix = path.suffix.lower() suffix = path.suffix.lower()
if suffix not in (".cpp", ".cxx", ".cc", ".c", ".hpp", ".hxx", ".h"):
return None
try: try:
text = path.read_text(encoding="utf-8", errors="replace") text = path.read_text(encoding="utf-8", errors="replace")
except OSError: except OSError:
return None return None
if suffix in (".cpp", ".cxx", ".cc", ".c", ".hpp", ".hxx", ".h"): functions = _analyze_cpp(text, file_path)
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) return FileAnalysisResult(filename=file_path, function_list=functions)
@ -149,63 +126,6 @@ def _analyze_cpp(text: str, filename: str) -> List[FunctionInfo]:
return functions 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
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Вспомогательные функции # Вспомогательные функции
# ------------------------------------------------------------------ # ------------------------------------------------------------------