gui
This commit is contained in:
parent
950c984542
commit
b9d6a13b58
13
prog/arch-researcher/Pipfile
Normal file
13
prog/arch-researcher/Pipfile
Normal 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
83
prog/arch-researcher/Pipfile.lock
generated
Normal 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": {}
|
||||
}
|
||||
@ -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: [
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Вспомогательные методы
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@ -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
482
prog/arch-researcher/gui.py
Normal 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> | "
|
||||
f"<b>Стратегия:</b> {strategy_ru}<br>"
|
||||
f"<b>Модулей:</b> {len(report.modules)} | "
|
||||
f"<b>Функций:</b> {report.total_functions} | "
|
||||
f"<b>Ср. CCN:</b> {report.avg_system_ccn:.2f} | "
|
||||
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()
|
||||
271
prog/arch-researcher/legacy_report/report.html
Normal file
271
prog/arch-researcher/legacy_report/report.html
Normal 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> |
|
||||
Сгенерировано: 21.05.2026 08:08 |
|
||||
Системный риск: <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
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Вспомогательные функции
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
Loading…
Reference in New Issue
Block a user