SXXXXXXX_PyUCC/pyucc/utils/ucc_report_generator.py

410 lines
17 KiB
Python

"""
UCC-style report generator for PyUcc.
Generates reports in UCC format for:
- Scan results
- Counting results
- Differ results
- Metrics results
"""
from pathlib import Path
from typing import Dict, List, Any, Optional
from datetime import datetime
import os
class UCCReportGenerator:
"""Generates UCC-style text reports."""
@staticmethod
def _format_header(command_description: str) -> str:
"""Generate PyUcc-style header."""
now = datetime.now()
date_str = now.strftime("%d %m %Y")
time_str = now.strftime("%H:%M:%S")
header = []
header.append("=" * 100)
header.append("")
header.append(" " * 32 + "SLOC COUNT RESULTS")
header.append(" " * 25 + f"Generated by PyUcc on {date_str} at {time_str}")
header.append(f"{command_description}")
header.append("=" * 100)
header.append("")
return "\n".join(header)
@staticmethod
def _format_counting_table_header(language: str = "ALL") -> str:
"""Generate counting table header."""
lines = []
lines.append(" " * 35 + f"RESULTS FOR {language} FILES")
lines.append("")
lines.append("NOTE: Total Lines = all lines in file | Blank Lines = empty lines")
lines.append(" Comments (Whole) = comment-only lines | Comments (Embedded) = inline comments")
lines.append(" Compiler Directives = preprocessor commands (#include, #define, etc.)")
lines.append(" Data Declarations = variable/type declarations | Exec. Instructions = executable code")
lines.append(" Logical SLOC = statements | Physical SLOC = lines of code (excluding blank/comments)")
lines.append("")
lines.append(" Total Blank | Comments | Compiler Data Exec. | Logical Physical | File Module")
lines.append(" Lines Lines | Whole Embedded | Direct. Decl. Instr. | SLOC SLOC | Type Name")
lines.append("-" * 100 + "-" * 25)
return "\n".join(lines)
@staticmethod
def _format_counting_row(result: Dict[str, Any], base_path: str = "") -> str:
"""Format a single counting result row in UCC style."""
# Extract values
total = result.get('physical_lines', 0)
blank = result.get('blank_lines', 0)
comment_whole = result.get('comment_whole', 0)
comment_embed = result.get('comment_embedded', 0)
directives = result.get('compiler_directives', 0)
data_decl = result.get('data_declarations', 0)
exec_inst = result.get('exec_instructions', 0)
logical = result.get('logical_sloc', 0)
physical = result.get('physical_sloc', 0)
# Get file path (relative to base if provided)
file_path = result.get('file', result.get('path', ''))
if base_path and file_path:
try:
file_path = os.path.relpath(file_path, base_path)
except:
pass
# Format: align numbers right
row = (
f"{total:8d} {blank:7d} | "
f"{comment_whole:7d} {comment_embed:8d} | "
f"{directives:7d} {data_decl:6d} {exec_inst:7d} | "
f"{logical:7d} {physical:9d} | "
f"CODE {file_path}"
)
return row
@staticmethod
def _format_summary(results: List[Dict[str, Any]]) -> str:
"""Generate summary section."""
if not results:
return ""
# Calculate totals
total_lines = sum(r.get('physical_lines', 0) for r in results)
total_blank = sum(r.get('blank_lines', 0) for r in results)
total_cmt_whole = sum(r.get('comment_whole', 0) for r in results)
total_cmt_embed = sum(r.get('comment_embedded', 0) for r in results)
total_directives = sum(r.get('compiler_directives', 0) for r in results)
total_data = sum(r.get('data_declarations', 0) for r in results)
total_exec = sum(r.get('exec_instructions', 0) for r in results)
total_logical = sum(r.get('logical_sloc', 0) for r in results)
total_physical = sum(r.get('physical_sloc', 0) for r in results)
# Calculate ratio
ratio = total_physical / total_logical if total_logical > 0 else 0.0
lines = []
lines.append("")
lines.append(" " * 40 + "RESULTS SUMMARY")
lines.append("")
lines.append(" Total Blank | Comments | Compiler Data Exec. | | File SLOC")
lines.append(" Lines Lines | Whole Embedded | Direct. Decl. Instr. | SLOC | Type Definition")
lines.append("-" * 100)
# Physical SLOC row
lines.append(
f"{total_lines:8d} {total_blank:7d} | "
f"{total_cmt_whole:7d} {total_cmt_embed:8d} | "
f"{total_directives:7d} {total_data:6d} {total_exec:7d} | "
f"{total_physical:7d} | CODE Physical"
)
# Logical SLOC row
lines.append(
f"{total_lines:8d} {total_blank:7d} | "
f"{total_cmt_whole:7d} {total_cmt_embed:8d} | "
f"{total_directives:7d} {total_data:6d} {total_exec:7d} | "
f"{total_logical:7d} | CODE Logical"
)
lines.append("")
lines.append(f"Number of files successfully accessed........................ {len(results):6d} out of {len(results):6d}")
lines.append("")
lines.append(f"Ratio of Physical to Logical SLOC............................ {ratio:.2f}")
lines.append("")
return "\n".join(lines)
@staticmethod
def generate_counting_report(
results: List[Dict[str, Any]],
output_path: Path,
command_description: str = "",
base_path: str = "",
language_filter: Optional[str] = None
) -> None:
"""
Generate UCC-style counting report.
Args:
results: List of counting results
output_path: Path to save report
command_description: Description of command run
base_path: Base path for relative file paths
language_filter: Optional language to filter by
"""
# Filter by language if specified
if language_filter:
results = [r for r in results if r.get('language', '').lower() == language_filter.lower()]
# Group by language
by_language = {}
for r in results:
lang = r.get('language', 'unknown').upper()
if lang not in by_language:
by_language[lang] = []
by_language[lang].append(r)
# Build report
report_lines = []
report_lines.append(UCCReportGenerator._format_header(command_description))
report_lines.append("")
# Add sections for each language
for lang, lang_results in sorted(by_language.items()):
report_lines.append(UCCReportGenerator._format_counting_table_header(lang))
for result in lang_results:
report_lines.append(UCCReportGenerator._format_counting_row(result, base_path))
report_lines.append("")
# Add summary
report_lines.append(UCCReportGenerator._format_summary(results))
# Write to file
output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, 'w', encoding='utf-8') as f:
f.write("\n".join(report_lines))
@staticmethod
def generate_differ_report(
diff_results: List[Dict[str, Any]],
output_path: Path,
baseline_id: str,
command_description: str = ""
) -> None:
"""
Generate UCC-style differ report showing Baseline-A vs Baseline-B.
Args:
diff_results: List of differ results
output_path: Path to save report
baseline_id: Baseline identifier
command_description: Description of command run
"""
report_lines = []
report_lines.append(UCCReportGenerator._format_header(command_description))
report_lines.append("")
report_lines.append(" " * 30 + "DIFFERENTIAL RESULTS")
report_lines.append("")
report_lines.append("NOTE: This report compares Baseline-A (previous) vs Baseline-B (current)")
report_lines.append(" MODIFIED = files changed between baselines")
report_lines.append(" ADDED = files added in Baseline-B | DELETED = files removed from Baseline-A")
report_lines.append(" Delta = change in Physical SLOC (positive = code added, negative = code removed)")
report_lines.append("")
report_lines.append(" " * 25 + f"Baseline-A: {baseline_id}")
report_lines.append(" " * 25 + "Baseline-B: Current")
report_lines.append("")
report_lines.append(" File Status | Baseline-A | Baseline-B | Delta Lines")
report_lines.append(" | (Physical) | (Physical) | (Code/Cmt/Blank)")
report_lines.append("-" * 80)
for result in diff_results:
status = "MODIFIED" if result.get('modified', 0) > 0 else \
"ADDED" if result.get('added', 0) > 0 else \
"DELETED" if result.get('deleted', 0) > 0 else \
"UNCHANGED"
baseline_file = result.get('fileA', '')
current_file = result.get('fileB', '')
# Get metrics
baseline_counts = result.get('baseline_countings', {})
current_counts = result.get('current_countings', {})
deltas = result.get('countings_delta', {})
baseline_phys = baseline_counts.get('physical_lines', 0) if baseline_counts else 0
current_phys = current_counts.get('physical_lines', 0) if current_counts else 0
delta_code = deltas.get('code_lines', 0) if deltas else 0
delta_comment = deltas.get('comment_lines', 0) if deltas else 0
delta_blank = deltas.get('blank_lines', 0) if deltas else 0
file_name = current_file or baseline_file
report_lines.append(
f"{status:15s} | {baseline_phys:10d} | {current_phys:10d} | "
f"{delta_code:+6d}/{delta_comment:+6d}/{delta_blank:+6d} {file_name}"
)
report_lines.append("")
# Write to file
output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, 'w', encoding='utf-8') as f:
f.write("\n".join(report_lines))
@staticmethod
def generate_metrics_report(
results: List[Dict[str, Any]],
output_path: Path,
command_description: str = "",
base_path: str = ""
) -> None:
"""
Generate UCC-style metrics report (Cyclomatic Complexity).
Args:
results: List of metrics results
output_path: Path to save report
command_description: Description of command run
base_path: Base path for relative file paths
"""
report_lines = []
report_lines.append(UCCReportGenerator._format_header(command_description))
report_lines.append("")
report_lines.append(" " * 35 + "CYCLOMATIC COMPLEXITY RESULTS")
report_lines.append("")
report_lines.append("NOTE: CC1 = McCabe Cyclomatic Complexity (Standard) - measures code complexity")
report_lines.append(" Total_CC = sum of complexity for all functions in file")
report_lines.append(" Average_CC = average complexity per function")
report_lines.append(" Risk: Low (≤10) | Medium (≤20) | High (≤50) | Very High (>50)")
report_lines.append(" MI = Maintainability Index (0-100): higher is better")
report_lines.append(" 85-100 = Excellent | 65-84 = Good | 0-64 = Needs attention")
report_lines.append("")
report_lines.append(" " * 40 + "RESULTS BY FILE")
report_lines.append("")
report_lines.append("Cyclomatic Complexity and Maintainability Index")
report_lines.append(" Total_CC Average_CC Risk MI | File Name")
report_lines.append("-" * 56 + "+" + "-" * 50)
total_cc = 0
total_funcs = 0
for result in results:
file_path = result.get('file', '')
if base_path:
try:
file_path = str(Path(file_path).relative_to(base_path))
except ValueError:
pass
# Get metrics
avg_cc = result.get('avg_cc', 0.0)
func_count = result.get('func_count', 0)
max_cc = result.get('max_cc', 0)
mi = result.get('mi', 0.0)
# Calculate total CC (avg * func_count)
file_total_cc = int(avg_cc * func_count) if func_count > 0 else 0
total_cc += file_total_cc
total_funcs += func_count
# Determine risk level based on average CC
if avg_cc <= 10:
risk = "Low"
elif avg_cc <= 20:
risk = "Medium"
elif avg_cc <= 50:
risk = "High"
else:
risk = "Very High"
report_lines.append(
f"{file_total_cc:10d} {avg_cc:11.2f} {risk:11s} {mi:6.2f} | {file_path}"
)
report_lines.append("-" * 56 + "+" + "-" * 50)
# Overall average
overall_avg = total_cc / total_funcs if total_funcs > 0 else 0.0
avg_funcs_per_file = total_funcs / len(results) if results else 0.0
avg_mi = sum(r.get('mi', 0.0) for r in results) / len(results) if results else 0.0
report_lines.append(
f"{total_cc:10d} {overall_avg:11.2f} {avg_mi:6.2f} Totals | {total_funcs} Functions in {len(results)} File(s)"
)
report_lines.append(
f"{'':10s} {avg_funcs_per_file:11.1f} {'':6s} Averages | {avg_funcs_per_file:.1f} Functions per File (Averages = Totals/Functions)"
)
# Add RESULTS BY FUNCTION section
report_lines.append("")
report_lines.append("")
report_lines.append(" " * 40 + "RESULTS BY FUNCTION")
report_lines.append("")
report_lines.append("Cyclomatic Complexity (CC1 = McCabe Standard)")
report_lines.append(" CC1 Risk Function Name" + " " * 50 + "| File Name")
report_lines.append("-" * 98 + "+" + "-" * 23)
# Collect all functions across all files
all_functions = []
for result in results:
file_path = result.get('file', '')
if base_path:
try:
file_path = str(Path(file_path).relative_to(base_path))
except ValueError:
pass
functions = result.get('functions', [])
for func in functions:
func_name = func.get('name', 'unknown')
cc = func.get('cc', 0)
# Determine risk level
if cc <= 10:
risk = "Low"
elif cc <= 20:
risk = "Medium"
elif cc <= 50:
risk = "High"
else:
risk = "Very High"
all_functions.append({
'name': func_name,
'cc': cc,
'risk': risk,
'file': file_path
})
# Sort functions by CC descending (most complex first)
all_functions.sort(key=lambda x: x['cc'], reverse=True)
# Write function details
for func in all_functions:
func_name_trunc = func['name'][:60] # Limit function name length
report_lines.append(
f"{func['cc']:10d} {func['risk']:11s} {func_name_trunc:60s} | {func['file']}"
)
report_lines.append("-" * 98 + "+" + "-" * 23)
report_lines.append(
f"{total_cc:10d} Totals {total_funcs} Functions" + " " * 50 + f"| {len(results)} File(s)"
)
report_lines.append(
f"{overall_avg:10.2f} Averages {avg_funcs_per_file:.1f} Functions per File (Averages = Totals/Functions)" + " " * 6 + "|"
)
report_lines.append("")
report_lines.append("")
# Write to file
output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, 'w', encoding='utf-8') as f:
f.write("\n".join(report_lines))