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