410 lines
17 KiB
Python
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))
|