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_extensions: List[str] = field(default_factory=lambda: [
|
||||||
".cpp", ".cxx", ".cc", ".c", ".hpp", ".hxx", ".h"
|
".cpp", ".cxx", ".cc", ".c", ".hpp", ".hxx", ".h"
|
||||||
])
|
])
|
||||||
js_extensions: List[str] = field(default_factory=lambda: [
|
|
||||||
".js", ".mjs", ".cjs", ".ts"
|
|
||||||
])
|
|
||||||
|
|
||||||
# Директории, которые игнорируются при обходе
|
# Директории, которые игнорируются при обходе
|
||||||
ignored_dirs: List[str] = field(default_factory=lambda: [
|
ignored_dirs: List[str] = field(default_factory=lambda: [
|
||||||
|
|||||||
@ -30,7 +30,6 @@ class Collector:
|
|||||||
def __init__(self, config: AnalyzerConfig = DEFAULT_CONFIG):
|
def __init__(self, config: AnalyzerConfig = DEFAULT_CONFIG):
|
||||||
self.config = config
|
self.config = config
|
||||||
self._cpp_exts = set(config.cpp_extensions)
|
self._cpp_exts = set(config.cpp_extensions)
|
||||||
self._js_exts = set(config.js_extensions)
|
|
||||||
self._ignored = set(config.ignored_dirs)
|
self._ignored = set(config.ignored_dirs)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@ -96,12 +95,9 @@ class Collector:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _detect_language(self, path: Path) -> str | None:
|
def _detect_language(self, path: Path) -> str | None:
|
||||||
"""Возвращает 'cpp', 'js' или None если файл не анализируется."""
|
"""Возвращает 'cpp' или None если файл не анализируется."""
|
||||||
suffix = path.suffix.lower()
|
if path.suffix.lower() in self._cpp_exts:
|
||||||
if suffix in self._cpp_exts:
|
|
||||||
return "cpp"
|
return "cpp"
|
||||||
if suffix in self._js_exts:
|
|
||||||
return "js"
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _resolve_module_name(self, root: Path, file: Path) -> str:
|
def _resolve_module_name(self, root: Path, file: Path) -> str:
|
||||||
@ -124,8 +120,6 @@ class Collector:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _merge_language(existing: str, new_lang: str) -> str:
|
def _merge_language(existing: str, new_lang: str) -> str:
|
||||||
"""Объединяет языки при добавлении нового файла в модуль."""
|
"""Объединяет языки при добавлении нового файла в модуль."""
|
||||||
if existing == "mixed" or existing == new_lang:
|
if not existing:
|
||||||
return existing if existing != "mixed" else "mixed"
|
return new_lang
|
||||||
if existing in ("cpp", "js") and existing != new_lang:
|
return existing
|
||||||
return "mixed"
|
|
||||||
return new_lang # existing == "" — первый файл
|
|
||||||
|
|||||||
@ -2,14 +2,13 @@
|
|||||||
Модуль DependencyAnalyzer — построение и анализ графа зависимостей.
|
Модуль DependencyAnalyzer — построение и анализ графа зависимостей.
|
||||||
|
|
||||||
Алгоритм:
|
Алгоритм:
|
||||||
1. Для C++ файлов извлекаются #include-директивы:
|
1. Для C/C++ файлов извлекаются #include-директивы:
|
||||||
- через libclang AST (если установлен) — точный разбор
|
- через libclang AST (если установлен) — точный разбор
|
||||||
- через regex (fallback) — лексический разбор
|
- через regex (fallback) — лексический разбор
|
||||||
2. Для JS файлов — regex-парсинг require() и import ... from
|
2. #include разрешаются до имени модуля (директории)
|
||||||
3. #include / import разрешаются до имени модуля (директории)
|
3. Строится ориентированный граф зависимостей (networkx DiGraph)
|
||||||
4. Строится ориентированный граф зависимостей (networkx DiGraph)
|
4. Вычисляются метрики: coupling_in, coupling_out, instability
|
||||||
5. Вычисляются метрики: coupling_in, coupling_out, instability
|
5. Обнаруживаются циклические зависимости (strongly connected components)
|
||||||
6. Обнаруживаются циклические зависимости (strongly connected components)
|
|
||||||
|
|
||||||
Метрика нестабильности (Instability):
|
Метрика нестабильности (Instability):
|
||||||
I = C_out / (C_in + C_out), где
|
I = C_out / (C_in + C_out), где
|
||||||
@ -51,12 +50,6 @@ except ImportError:
|
|||||||
# Regex для fallback-разбора #include
|
# Regex для fallback-разбора #include
|
||||||
_RE_INCLUDE = re.compile(r'^\s*#\s*include\s+[<"]([^>"]+)[>"]', re.MULTILINE)
|
_RE_INCLUDE = re.compile(r'^\s*#\s*include\s+[<"]([^>"]+)[>"]', re.MULTILINE)
|
||||||
|
|
||||||
# Regex для JS import / require
|
|
||||||
_RE_JS_IMPORT = re.compile(
|
|
||||||
r'(?:import\s+.*?\s+from\s+|require\s*\(\s*)[\'"]([^\'"]+)[\'"]',
|
|
||||||
re.MULTILINE,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class DependencyAnalyzer:
|
class DependencyAnalyzer:
|
||||||
"""
|
"""
|
||||||
@ -169,21 +162,13 @@ class DependencyAnalyzer:
|
|||||||
root: Path,
|
root: Path,
|
||||||
) -> Set[str]:
|
) -> Set[str]:
|
||||||
"""Извлекает зависимости из одного файла."""
|
"""Извлекает зависимости из одного файла."""
|
||||||
suffix = Path(file_path).suffix.lower()
|
if Path(file_path).suffix.lower() not in self.config.cpp_extensions:
|
||||||
|
return set()
|
||||||
|
|
||||||
if suffix in self.config.cpp_extensions:
|
raw_includes = self._extract_cpp_includes(file_path)
|
||||||
raw_includes = self._extract_cpp_includes(file_path)
|
return self._resolve_includes_to_modules(
|
||||||
return self._resolve_includes_to_modules(
|
raw_includes, file_path, file_to_module, root
|
||||||
raw_includes, file_path, file_to_module, root
|
)
|
||||||
)
|
|
||||||
|
|
||||||
if suffix in self.config.js_extensions:
|
|
||||||
raw_imports = self._extract_js_imports(file_path)
|
|
||||||
return self._resolve_js_imports_to_modules(
|
|
||||||
raw_imports, file_path, file_to_module, root
|
|
||||||
)
|
|
||||||
|
|
||||||
return set()
|
|
||||||
|
|
||||||
def _extract_cpp_includes(self, file_path: str) -> List[str]:
|
def _extract_cpp_includes(self, file_path: str) -> List[str]:
|
||||||
"""
|
"""
|
||||||
@ -219,14 +204,6 @@ class DependencyAnalyzer:
|
|||||||
return []
|
return []
|
||||||
return _RE_INCLUDE.findall(text)
|
return _RE_INCLUDE.findall(text)
|
||||||
|
|
||||||
def _extract_js_imports(self, file_path: str) -> List[str]:
|
|
||||||
"""Извлечение import/require из JS/TS файла через regex."""
|
|
||||||
try:
|
|
||||||
text = Path(file_path).read_text(encoding="utf-8", errors="replace")
|
|
||||||
except OSError:
|
|
||||||
return []
|
|
||||||
return _RE_JS_IMPORT.findall(text)
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Разрешение имён в модули
|
# Разрешение имён в модули
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@ -261,39 +238,6 @@ class DependencyAnalyzer:
|
|||||||
|
|
||||||
return deps
|
return deps
|
||||||
|
|
||||||
def _resolve_js_imports_to_modules(
|
|
||||||
self,
|
|
||||||
imports: List[str],
|
|
||||||
source_file: str,
|
|
||||||
file_to_module: Dict[str, str],
|
|
||||||
root: Path,
|
|
||||||
) -> Set[str]:
|
|
||||||
"""
|
|
||||||
Разрешает JS import-пути в имена модулей.
|
|
||||||
Внешние пакеты (не начинаются с '.') игнорируются.
|
|
||||||
"""
|
|
||||||
deps: Set[str] = set()
|
|
||||||
source_dir = Path(source_file).parent
|
|
||||||
|
|
||||||
for imp in imports:
|
|
||||||
if not imp.startswith("."):
|
|
||||||
continue # внешний пакет — не наш модуль
|
|
||||||
|
|
||||||
# Пробуем добавить расширения
|
|
||||||
base = (source_dir / imp).resolve()
|
|
||||||
candidates = [base]
|
|
||||||
for ext in self.config.js_extensions:
|
|
||||||
candidates.append(base.with_suffix(ext))
|
|
||||||
candidates.append(base / f"index{ext}")
|
|
||||||
|
|
||||||
for candidate in candidates:
|
|
||||||
key = str(candidate)
|
|
||||||
if key in file_to_module:
|
|
||||||
deps.add(file_to_module[key])
|
|
||||||
break
|
|
||||||
|
|
||||||
return deps
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Вспомогательные методы
|
# Вспомогательные методы
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|||||||
@ -140,9 +140,6 @@ class MetricsEngine:
|
|||||||
|
|
||||||
def _detect_language(self, file_path: str) -> str:
|
def _detect_language(self, file_path: str) -> str:
|
||||||
"""Определяет язык по расширению файла."""
|
"""Определяет язык по расширению файла."""
|
||||||
suffix = Path(file_path).suffix.lower()
|
if Path(file_path).suffix.lower() in self.config.cpp_extensions:
|
||||||
if suffix in self.config.cpp_extensions:
|
|
||||||
return "cpp"
|
return "cpp"
|
||||||
if suffix in self.config.js_extensions:
|
|
||||||
return "js"
|
|
||||||
return "unknown"
|
return "unknown"
|
||||||
|
|||||||
482
prog/arch-researcher/gui.py
Normal file
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)
|
ccn: int # Цикломатическая сложность (McCabe)
|
||||||
nloc: int # Строк кода без комментариев
|
nloc: int # Строк кода без комментариев
|
||||||
params: int # Число параметров
|
params: int # Число параметров
|
||||||
language: str # "cpp" | "js"
|
language: str # "cpp"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_complex(self) -> bool:
|
def is_complex(self) -> bool:
|
||||||
@ -40,7 +40,7 @@ class ModuleMetrics:
|
|||||||
name: str # Имя модуля (путь относительно корня)
|
name: str # Имя модуля (путь относительно корня)
|
||||||
files: List[str] = field(default_factory=list)
|
files: List[str] = field(default_factory=list)
|
||||||
functions: List[FunctionMetrics] = field(default_factory=list)
|
functions: List[FunctionMetrics] = field(default_factory=list)
|
||||||
language: str = "mixed" # "cpp" | "js" | "mixed"
|
language: str = "" # "cpp"
|
||||||
|
|
||||||
# Агрегированные показатели (заполняются MetricsEngine)
|
# Агрегированные показатели (заполняются MetricsEngine)
|
||||||
total_nloc: int = 0
|
total_nloc: int = 0
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
методом подсчёта ветвлений в тексте кода (лексический подход),
|
методом подсчёта ветвлений в тексте кода (лексический подход),
|
||||||
аналогично тому, как это делает библиотека lizard.
|
аналогично тому, как это делает библиотека lizard.
|
||||||
|
|
||||||
Поддерживаемые языки: C, C++, JavaScript, TypeScript.
|
Поддерживаемые языки: C, C++.
|
||||||
|
|
||||||
Точность: comparable с lizard при отсутствии установленного Clang.
|
Точность: comparable с lizard при отсутствии установленного Clang.
|
||||||
Для продакшн-использования рекомендуется установить lizard:
|
Для продакшн-использования рекомендуется установить lizard:
|
||||||
@ -15,8 +15,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import tokenize
|
|
||||||
import io
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
@ -29,35 +27,17 @@ _BRANCH_KEYWORDS_CPP = re.compile(
|
|||||||
re.MULTILINE,
|
re.MULTILINE,
|
||||||
)
|
)
|
||||||
|
|
||||||
_BRANCH_KEYWORDS_JS = re.compile(
|
|
||||||
r'\b(if|else\s+if|for|while|do|switch|case|catch|&&|\|\||\?(?!:))\b',
|
|
||||||
re.MULTILINE,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Сигнатуры функций C/C++
|
# Сигнатуры функций C/C++
|
||||||
_CPP_FUNC = re.compile(
|
_CPP_FUNC = re.compile(
|
||||||
r'^(?:[\w:*&<>\s]+?)\s+(\w+)\s*\(([^)]*)\)\s*(?:const\s*)?(?:override\s*)?(?:noexcept[^{]*)?\{',
|
r'^(?:[\w:*&<>\s]+?)\s+(\w+)\s*\(([^)]*)\)\s*(?:const\s*)?(?:override\s*)?(?:noexcept[^{]*)?\{',
|
||||||
re.MULTILINE,
|
re.MULTILINE,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Сигнатуры функций JS/TS
|
|
||||||
_JS_FUNC = re.compile(
|
|
||||||
r'(?:'
|
|
||||||
r'function\s+(\w+)\s*\(([^)]*)\)' # function foo(...)
|
|
||||||
r'|(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s*)?\(([^)]*)\)\s*=>' # arrow
|
|
||||||
r'|(\w+)\s*:\s*(?:async\s*)?function\s*\(([^)]*)\)' # method: function
|
|
||||||
r'|(?:async\s+)?(\w+)\s*\(([^)]*)\)\s*\{' # method shorthand
|
|
||||||
r')',
|
|
||||||
re.MULTILINE,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Строки и комментарии — для удаления перед анализом
|
# Строки и комментарии — для удаления перед анализом
|
||||||
_CPP_COMMENT_LINE = re.compile(r'//[^\n]*')
|
_CPP_COMMENT_LINE = re.compile(r'//[^\n]*')
|
||||||
_CPP_COMMENT_BLOCK = re.compile(r'/\*.*?\*/', re.DOTALL)
|
_CPP_COMMENT_BLOCK = re.compile(r'/\*.*?\*/', re.DOTALL)
|
||||||
_CPP_STRING = re.compile(r'"(?:[^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)*\'')
|
_CPP_STRING = re.compile(r'"(?:[^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)*\'')
|
||||||
|
|
||||||
_JS_TEMPLATE = re.compile(r'`[^`]*`', re.DOTALL)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class FunctionInfo:
|
class FunctionInfo:
|
||||||
@ -90,18 +70,15 @@ def analyze_file(file_path: str) -> Optional[FileAnalysisResult]:
|
|||||||
path = Path(file_path)
|
path = Path(file_path)
|
||||||
suffix = path.suffix.lower()
|
suffix = path.suffix.lower()
|
||||||
|
|
||||||
|
if suffix not in (".cpp", ".cxx", ".cc", ".c", ".hpp", ".hxx", ".h"):
|
||||||
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
text = path.read_text(encoding="utf-8", errors="replace")
|
text = path.read_text(encoding="utf-8", errors="replace")
|
||||||
except OSError:
|
except OSError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if suffix in (".cpp", ".cxx", ".cc", ".c", ".hpp", ".hxx", ".h"):
|
functions = _analyze_cpp(text, file_path)
|
||||||
functions = _analyze_cpp(text, file_path)
|
|
||||||
elif suffix in (".js", ".mjs", ".cjs", ".ts"):
|
|
||||||
functions = _analyze_js(text, file_path)
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return FileAnalysisResult(filename=file_path, function_list=functions)
|
return FileAnalysisResult(filename=file_path, function_list=functions)
|
||||||
|
|
||||||
|
|
||||||
@ -149,63 +126,6 @@ def _analyze_cpp(text: str, filename: str) -> List[FunctionInfo]:
|
|||||||
return functions
|
return functions
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# JavaScript / TypeScript анализ
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
|
|
||||||
def _analyze_js(text: str, filename: str) -> List[FunctionInfo]:
|
|
||||||
"""Анализирует JS/TS код."""
|
|
||||||
# Удаляем шаблонные строки и комментарии
|
|
||||||
clean = _CPP_COMMENT_BLOCK.sub(' ', text)
|
|
||||||
clean = _CPP_COMMENT_LINE.sub('', clean)
|
|
||||||
clean = _JS_TEMPLATE.sub('``', clean)
|
|
||||||
clean = _CPP_STRING.sub('""', clean)
|
|
||||||
|
|
||||||
functions: List[FunctionInfo] = []
|
|
||||||
seen_positions: set = set()
|
|
||||||
|
|
||||||
for match in _JS_FUNC.finditer(clean):
|
|
||||||
# Определяем имя и параметры из разных групп
|
|
||||||
name = (
|
|
||||||
match.group(1) or match.group(3) or
|
|
||||||
match.group(5) or match.group(7) or "anonymous"
|
|
||||||
)
|
|
||||||
params_str = (
|
|
||||||
match.group(2) or match.group(4) or
|
|
||||||
match.group(6) or match.group(8) or ""
|
|
||||||
)
|
|
||||||
|
|
||||||
start_pos = match.start()
|
|
||||||
if start_pos in seen_positions:
|
|
||||||
continue
|
|
||||||
seen_positions.add(start_pos)
|
|
||||||
|
|
||||||
start_line = text[:start_pos].count('\n') + 1
|
|
||||||
|
|
||||||
# Ищем открывающую фигурную скобку
|
|
||||||
brace_pos = clean.find('{', match.end())
|
|
||||||
if brace_pos == -1:
|
|
||||||
continue
|
|
||||||
|
|
||||||
body = _extract_body(clean, brace_pos)
|
|
||||||
if body is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
nloc = _count_nloc(body)
|
|
||||||
ccn = 1 + len(_BRANCH_KEYWORDS_JS.findall(body))
|
|
||||||
params = _count_params(params_str)
|
|
||||||
|
|
||||||
functions.append(FunctionInfo(
|
|
||||||
name=name,
|
|
||||||
start_line=start_line,
|
|
||||||
nloc=nloc,
|
|
||||||
cyclomatic_complexity=ccn,
|
|
||||||
parameter_count=params,
|
|
||||||
))
|
|
||||||
|
|
||||||
return functions
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Вспомогательные функции
|
# Вспомогательные функции
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user