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", ".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: [

View File

@ -30,7 +30,6 @@ class Collector:
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)
# ------------------------------------------------------------------
@ -96,12 +95,9 @@ class Collector:
)
def _detect_language(self, path: Path) -> str | None:
"""Возвращает 'cpp', 'js' или None если файл не анализируется."""
suffix = path.suffix.lower()
if suffix in self._cpp_exts:
"""Возвращает 'cpp' или None если файл не анализируется."""
if path.suffix.lower() 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:
@ -124,8 +120,6 @@ class Collector:
@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 == "" — первый файл
if not existing:
return new_lang
return existing

View File

@ -2,14 +2,13 @@
Модуль DependencyAnalyzer построение и анализ графа зависимостей.
Алгоритм:
1. Для C++ файлов извлекаются #include-директивы:
1. Для C/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)
2. #include разрешаются до имени модуля (директории)
3. Строится ориентированный граф зависимостей (networkx DiGraph)
4. Вычисляются метрики: coupling_in, coupling_out, instability
5. Обнаруживаются циклические зависимости (strongly connected components)
Метрика нестабильности (Instability):
I = C_out / (C_in + C_out), где
@ -51,12 +50,6 @@ except ImportError:
# 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:
"""
@ -169,21 +162,13 @@ class DependencyAnalyzer:
root: Path,
) -> 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)
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()
raw_includes = self._extract_cpp_includes(file_path)
return self._resolve_includes_to_modules(
raw_includes, file_path, file_to_module, root
)
def _extract_cpp_includes(self, file_path: str) -> List[str]:
"""
@ -219,14 +204,6 @@ class DependencyAnalyzer:
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)
# ------------------------------------------------------------------
# Разрешение имён в модули
# ------------------------------------------------------------------
@ -261,39 +238,6 @@ class DependencyAnalyzer:
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:
"""Определяет язык по расширению файла."""
suffix = Path(file_path).suffix.lower()
if suffix in self.config.cpp_extensions:
if Path(file_path).suffix.lower() in self.config.cpp_extensions:
return "cpp"
if suffix in self.config.js_extensions:
return "js"
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)
nloc: int # Строк кода без комментариев
params: int # Число параметров
language: str # "cpp" | "js"
language: str # "cpp"
@property
def is_complex(self) -> bool:
@ -40,7 +40,7 @@ class ModuleMetrics:
name: str # Имя модуля (путь относительно корня)
files: List[str] = field(default_factory=list)
functions: List[FunctionMetrics] = field(default_factory=list)
language: str = "mixed" # "cpp" | "js" | "mixed"
language: str = "" # "cpp"
# Агрегированные показатели (заполняются MetricsEngine)
total_nloc: int = 0

View File

@ -5,7 +5,7 @@
методом подсчёта ветвлений в тексте кода (лексический подход),
аналогично тому, как это делает библиотека lizard.
Поддерживаемые языки: C, C++, JavaScript, TypeScript.
Поддерживаемые языки: C, C++.
Точность: comparable с lizard при отсутствии установленного Clang.
Для продакшн-использования рекомендуется установить lizard:
@ -15,8 +15,6 @@
from __future__ import annotations
import re
import tokenize
import io
from dataclasses import dataclass
from pathlib import Path
from typing import List, Optional
@ -29,35 +27,17 @@ _BRANCH_KEYWORDS_CPP = re.compile(
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:
@ -90,18 +70,15 @@ def analyze_file(file_path: str) -> Optional[FileAnalysisResult]:
path = Path(file_path)
suffix = path.suffix.lower()
if suffix not in (".cpp", ".cxx", ".cc", ".c", ".hpp", ".hxx", ".h"):
return None
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
functions = _analyze_cpp(text, file_path)
return FileAnalysisResult(filename=file_path, function_list=functions)
@ -149,63 +126,6 @@ def _analyze_cpp(text: str, filename: str) -> List[FunctionInfo]:
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
# ------------------------------------------------------------------
# Вспомогательные функции
# ------------------------------------------------------------------