Files
u-boot/tools/codman/output.py
Simon Glass b7b6a731f5 codman: Add --html option to generate colored HTML reports
Add HTML output support with collapsible hierarchical directory
structure, color-coded metrics, and build info in the header.

Co-developed-by: Claude <noreply@anthropic.com>
Signed-off-by: Simon Glass <simon.glass@canonical.com>
2025-12-08 17:37:35 -07:00

988 lines
34 KiB
Python

# SPDX-License-Identifier: GPL-2.0
#
# Copyright 2025 Canonical Ltd
#
"""Output formatting and display functions for srcman.
This module provides functions for displaying analysis results in various
formats:
- Statistics views (file-level and line-level)
- Directory breakdowns (top-level and subdirectories)
- Per-file summaries
- Detailed line-by-line views
- File listings (used/unused)
- File copying operations
"""
import os
import shutil
import sys
from collections import defaultdict
# Import from tools directory
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from u_boot_pylib import terminal, tout # pylint: disable=wrong-import-position
class DirStats: # pylint: disable=too-few-public-methods
"""Statistics for a directory.
Attributes:
total: Total number of files in directory
used: Number of files used (compiled)
unused: Number of files not used
lines_total: Total lines of code in directory
lines_used: Number of active lines (after preprocessing)
files: List of file info dicts (for --show-files)
"""
def __init__(self):
self.total = 0
self.used = 0
self.unused = 0
self.lines_total = 0
self.lines_used = 0
self.files = []
def count_lines(file_path):
"""Count lines in a file"""
try:
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
return sum(1 for _ in f)
except IOError:
return 0
def klocs(lines):
"""Format line count in thousands, rounded to 1 decimal place.
Args:
lines (int): Line count (e.g., 3500)
Returns:
Formatted string in thousands (e.g., '3.5')
"""
kloc = round(lines / 1000, 1)
return f'{kloc:.1f}'
def percent(numerator, denominator):
"""Calculate percentage, handling division by zero.
Args:
numerator (int/float): The numerator
denominator (int/float): The denominator
Returns:
float: Percentage (0-100), or 0 if denominator is 0
"""
return 100 * numerator / denominator if denominator else 0
def print_heading(text, width=70, char='='):
"""Print a heading with separator lines.
Args:
text (str): Heading text to display (empty for separator only)
width (int): Width of the separator line
char (str): Character to use for separator
"""
print(char * width)
if text:
print(text)
print(char * width)
def show_file_detail(detail_file, file_results, srcdir):
"""Show detailed line-by-line analysis for a specific file.
Args:
detail_file (str): Path to the file to show details for (relative or
absolute)
file_results (dict): Dictionary mapping file paths to analysis results
srcdir (str): Root directory of the source tree
Returns:
True on success, False on error
"""
detail_path = os.path.realpath(detail_file)
if detail_path not in file_results:
# Try relative to source root
detail_path = os.path.realpath(os.path.join(srcdir, detail_file))
if detail_path in file_results:
result = file_results[detail_path]
rel_path = os.path.relpath(detail_path, srcdir)
print_heading(f'DETAIL FOR: {rel_path}', width=70)
print(f'Total lines: {result.total_lines:6}')
pct_active = percent(result.active_lines, result.total_lines)
pct_inactive = percent(result.inactive_lines, result.total_lines)
print(f'Active lines: {result.active_lines:6} ({pct_active:.1f}%)')
print(f'Inactive lines: {result.inactive_lines:6} ' +
f'({pct_inactive:.1f}%)')
print()
# Show the file with status annotations
with open(detail_path, 'r', encoding='utf-8', errors='ignore') as f:
lines = f.readlines()
col = terminal.Color()
for line_num, line in enumerate(lines, 1):
status = result.line_status.get(line_num, 'unknown')
marker = '-' if status == 'inactive' else ' '
prefix = f'{marker} {line_num:4} | '
code = line.rstrip()
if status == 'active':
# Normal color for active code
print(prefix + code)
else:
# Non-bright cyan for inactive code
print(prefix + col.build(terminal.Color.CYAN, code,
bright=False))
return True
# File not found - caller handles errors
return False
def show_file_summary(file_results, srcdir):
"""Show per-file summary of line analysis.
Args:
file_results (dict): Dictionary mapping file paths to analysis results
srcdir (str): Root directory of the source tree
Returns:
bool: True on success
"""
print_heading('PER-FILE SUMMARY', width=90)
print(f"{'File':<50} {'Total':>8} {'Active':>8} "
f"{'Inactive':>8} {'%Active':>8}")
print('-' * 90)
for source_file in sorted(file_results.keys()):
result = file_results[source_file]
rel_path = os.path.relpath(source_file, srcdir)
if len(rel_path) > 47:
rel_path = '...' + rel_path[-44:]
pct_active = percent(result.active_lines, result.total_lines)
print(f'{rel_path:<50} {result.total_lines:>8} '
f'{result.active_lines:>8} {result.inactive_lines:>8} '
f'{pct_active:>7.1f}%')
return True
def list_unused_files(skipped_sources, srcdir):
"""List unused source files.
Args:
skipped_sources (set of str): Set of unused source file paths (relative
to srcdir)
srcdir (str): Root directory of the source tree
Returns:
bool: True on success
"""
print(f'Unused source files ({len(skipped_sources)}):')
for source_file in sorted(skipped_sources):
try:
rel_path = os.path.relpath(source_file, srcdir)
except ValueError:
rel_path = source_file
print(f' {rel_path}')
return True
def list_used_files(used_sources, srcdir):
"""List used source files.
Args:
used_sources (set of str): Set of used source file paths (relative
to srcdir)
srcdir (str): Root directory of the source tree
Returns:
bool: True on success
"""
print(f'Used source files ({len(used_sources)}):')
for source_file in sorted(used_sources):
try:
rel_path = os.path.relpath(source_file, srcdir)
except ValueError:
rel_path = source_file
print(f' {rel_path}')
return True
def copy_used_files(used_sources, srcdir, dest_dir):
"""Copy used source files to a destination directory, preserving structure.
Args:
used_sources (set): Set of used source file paths (relative to srcdir)
srcdir (str): Root directory of the source tree
dest_dir (str): Destination directory for the copy
Returns:
True on success, False if errors occurred
"""
if os.path.exists(dest_dir):
tout.error(f'Destination directory already exists: {dest_dir}')
return False
tout.progress(f'Copying {len(used_sources)} used source files to ' +
f'{dest_dir}')
copied_count = 0
error_count = 0
for source_file in sorted(used_sources):
src_path = os.path.join(srcdir, source_file)
dest_path = os.path.join(dest_dir, source_file)
try:
# Create parent directory if needed
dest_parent = os.path.dirname(dest_path)
os.makedirs(dest_parent, exist_ok=True)
# Copy the file
shutil.copy2(src_path, dest_path)
copied_count += 1
except IOError as e:
error_count += 1
tout.error(f'Error copying {source_file}: {e}')
tout.progress(f'Copied {copied_count} files to {dest_dir}')
if error_count:
tout.error(f'Failed to copy {error_count} files')
return False
return True
def collect_dir_stats(all_sources, used_sources, file_results, srcdir,
by_subdirs, show_files):
"""Collect statistics organized by directory.
Args:
all_sources (set): Set of all source file paths
used_sources (set): Set of used source file paths
file_results (dict): Optional dict mapping file paths to line
analysis results (or None)
srcdir (str): Root directory of the source tree
by_subdirs (bool): If True, use full subdirectory paths;
otherwise top-level only
show_files (bool): If True, collect individual file info within
each directory
Returns:
dict: Directory statistics keyed by directory path
"""
dir_stats = defaultdict(DirStats)
for source_file in all_sources:
rel_path = os.path.relpath(source_file, srcdir)
if by_subdirs:
# Use the full directory path (not including the filename)
dir_path = os.path.dirname(rel_path)
if not dir_path:
dir_path = '.'
else:
# Use only the top-level directory
dir_path = (rel_path.split(os.sep)[0] if os.sep in rel_path
else rel_path)
line_count = count_lines(source_file)
dir_stats[dir_path].total += 1
dir_stats[dir_path].lines_total += line_count
if source_file in used_sources:
dir_stats[dir_path].used += 1
# Use active line count if line-level analysis was performed
# Normalize path to match file_results keys (absolute paths)
abs_source = os.path.realpath(source_file)
# Try to find the file in file_results
result = None
if file_results:
if abs_source in file_results:
result = file_results[abs_source]
elif source_file in file_results:
result = file_results[source_file]
if result:
active_lines = result.active_lines
inactive_lines = result.inactive_lines
dir_stats[dir_path].lines_used += active_lines
# Store file info for --show-files (exclude .h files)
if show_files and not rel_path.endswith('.h'):
dir_stats[dir_path].files.append({
'path': rel_path,
'total': line_count,
'active': active_lines,
'inactive': inactive_lines
})
else:
# File not found in results - count all lines
tout.debug(f'File not in results (using full count): '
f'{rel_path}')
dir_stats[dir_path].lines_used += line_count
if show_files and not rel_path.endswith('.h'):
dir_stats[dir_path].files.append({
'path': rel_path,
'total': line_count,
'active': line_count,
'inactive': 0
})
else:
dir_stats[dir_path].unused += 1
return dir_stats
def print_dir_stats(dir_stats, file_results, by_subdirs, show_files,
show_empty, use_kloc=False):
"""Print directory statistics table.
Args:
dir_stats (dict): Directory statistics keyed by directory path
file_results (dict): Optional dict mapping file paths to line analysis
results (or None)
by_subdirs (bool): If True, show full subdirectory breakdown; otherwise
top-level only
show_files (bool): If True, show individual files within directories
show_empty (bool): If True, show directories with 0 lines used
use_kloc (bool): If True, show line counts in kLOC; otherwise show lines
"""
# Sort alphabetically by directory name
sorted_dirs = sorted(dir_stats.items(), key=lambda x: x[0])
for dir_path, stats in sorted_dirs:
# Skip subdirectories with 0 lines used unless --show-zero-lines is set
if by_subdirs and not show_empty and stats.lines_used == 0:
continue
pct_used = percent(stats.used, stats.total)
pct_code = percent(stats.lines_used, stats.lines_total)
# Truncate long paths
display_path = dir_path
if len(display_path) > 37:
display_path = '...' + display_path[-34:]
# Format line counts based on use_kloc flag
if use_kloc:
lines_total_str = f'{klocs(stats.lines_total):>8}'
lines_used_str = f'{klocs(stats.lines_used):>7}'
else:
lines_total_str = f'{stats.lines_total:>8}'
lines_used_str = f'{stats.lines_used:>7}'
print(f'{display_path:<40} {stats.total:>7} {stats.used:>7} '
f'{pct_used:>6.0f} {pct_code:>6.0f} '
f'{lines_total_str} {lines_used_str}')
# Show individual files if requested
if show_files and stats.files:
# Sort files alphabetically by filename
sorted_files = sorted(stats.files, key=lambda x: os.path.basename(x['path']))
for info in sorted_files:
# Skip files with 0 active lines unless show_empty is set
if not show_empty and info['active'] == 0:
continue
filename = os.path.basename(info['path'])
if len(filename) > 35:
filename = filename[:32] + '...'
if file_results:
# Show line-level details
pct_active = percent(info['active'], info['total'])
# Format line counts based on use_kloc flag
if use_kloc:
total_str = f'{klocs(info["total"]):>8}'
active_str = f'{klocs(info["active"]):>7}'
else:
total_str = f'{info["total"]:>8}'
active_str = f'{info["active"]:>7}'
# Align with directory format: skip Files/Used columns,
# show %code, then lines column, active in Used column
print(f" {filename:<38} {'':>7} {'':>7} {'':>6} "
f"{pct_active:>6.0f} {total_str} {active_str}")
else:
# Show file-level only
print(f" {filename:<38} {info['total']:>7} lines")
# Add blank line after file list
print()
def show_dir_breakdown(all_sources, used_sources, file_results, srcdir,
by_subdirs, show_files, show_empty, use_kloc=False):
"""Show breakdown by directory (top-level or subdirectories).
Args:
all_sources (set): Set of all source file paths
used_sources (set): Set of used source file paths
file_results (dict): Optional dict mapping file paths to line analysis
results (or None)
srcdir (str): Root directory of the source tree
by_subdirs (bool): If True, show full subdirectory breakdown; otherwise
top-level only
show_files (bool): If True, show individual files within each directory
show_empty (bool): If True, show directories with 0 lines used
use_kloc (bool): If True, show line counts in kLOC; otherwise show lines
Returns:
bool: True on success
"""
# Width of the main table (Directory + Total + Used columns)
table_width = 87
print_heading('BREAKDOWN BY TOP-LEVEL DIRECTORY' if by_subdirs else '',
width=table_width)
# Column header changes based on use_kloc
lines_header = 'kLOC' if use_kloc else 'Lines'
print(f"{'Directory':<40} {'Files':>7} {'Used':>7} {'%Used':>6} " +
f"{'%Code':>6} {lines_header:>8} {'Used':>7}")
print('-' * table_width)
# Collect directory statistics
dir_stats = collect_dir_stats(all_sources, used_sources, file_results,
srcdir, by_subdirs, show_files)
# Print directory statistics
print_dir_stats(dir_stats, file_results, by_subdirs, show_files, show_empty,
use_kloc)
print('-' * table_width)
total_lines_all = sum(count_lines(f) for f in all_sources)
# Calculate used lines: if we have file_results, use active_lines from there
# Otherwise, count all lines in used files
if file_results:
total_lines_used = sum(r.active_lines for r in file_results.values())
else:
total_lines_used = sum(count_lines(f) for f in used_sources)
pct_files = percent(len(used_sources), len(all_sources))
pct_code = percent(total_lines_used, total_lines_all)
# Format totals based on use_kloc flag
if use_kloc:
total_str = f'{klocs(total_lines_all):>8}'
used_str = f'{klocs(total_lines_used):>7}'
else:
total_str = f'{total_lines_all:>8}'
used_str = f'{total_lines_used:>7}'
print(f"{'TOTAL':<40} {len(all_sources):>7} {len(used_sources):>7} "
f"{pct_files:>6.0f} {pct_code:>6.0f} "
f"{total_str} {used_str}")
print_heading('', width=table_width)
print()
return True
def generate_html_breakdown(all_sources, used_sources, file_results, srcdir,
by_subdirs, show_files, show_empty, use_kloc,
html_file, board=None, analysis_method=None):
"""Generate HTML output with colored directory breakdown.
Args:
all_sources (set): Set of all source file paths
used_sources (set): Set of used source file paths
file_results (dict): Optional dict mapping file paths to line analysis
results (or None)
srcdir (str): Root directory of the source tree
by_subdirs (bool): If True, show full subdirectory breakdown
show_files (bool): If True, show individual files within directories
show_empty (bool): If True, show directories with 0 lines used
use_kloc (bool): If True, show line counts in kLOC
html_file (str): Path to output HTML file
board (str): Board name (optional)
analysis_method (str): Analysis method used ('unifdef', 'lsp', or 'dwarf')
Returns:
bool: True on success
"""
# Get git information
import subprocess
import datetime
try:
# Get short commit hash
git_hash = subprocess.check_output(
['git', 'rev-parse', '--short', 'HEAD'],
cwd=srcdir, stderr=subprocess.DEVNULL, text=True
).strip()
except (subprocess.CalledProcessError, FileNotFoundError):
git_hash = 'unknown'
try:
# Get commit date
git_date = subprocess.check_output(
['git', 'log', '-1', '--format=%cd', '--date=short'],
cwd=srcdir, stderr=subprocess.DEVNULL, text=True
).strip()
except (subprocess.CalledProcessError, FileNotFoundError):
git_date = datetime.date.today().strftime('%Y-%m-%d')
# Collect directory statistics
dir_stats = collect_dir_stats(all_sources, used_sources, file_results,
srcdir, by_subdirs, show_files)
# Calculate totals
total_lines_all = sum(count_lines(f) for f in all_sources)
if file_results:
total_lines_used = sum(r.active_lines for r in file_results.values())
else:
total_lines_used = sum(count_lines(f) for f in used_sources)
# Generate HTML
lines_header = 'kLOC' if use_kloc else 'Lines'
board_name = board if board else 'unknown'
html = f'''<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Code Analysis Report</title>
<style>
body {{
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 20px;
background-color: #f5f5f5;
}}
h1 {{
color: #333;
border-bottom: 3px solid #4CAF50;
padding-bottom: 10px;
margin-bottom: 10px;
}}
.build-info {{
background-color: #e8f5e9;
padding: 10px 15px;
margin: 10px 0 20px 0;
border-radius: 5px;
border-left: 4px solid #4CAF50;
font-size: 0.95em;
}}
.build-info span {{
margin-right: 20px;
color: #555;
}}
.build-info .label {{
font-weight: bold;
color: #333;
}}
.summary {{
background-color: #fff;
padding: 15px;
margin: 20px 0;
border-radius: 5px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}}
.summary-stat {{
display: inline-block;
margin: 10px 20px 10px 0;
}}
.summary-stat .label {{
font-weight: bold;
color: #666;
}}
.summary-stat .value {{
font-size: 1.2em;
color: #4CAF50;
}}
table {{
border-collapse: collapse;
width: 100%;
background-color: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin: 20px 0;
}}
th {{
background-color: #4CAF50;
color: white;
padding: 12px;
text-align: right;
position: sticky;
top: 0;
}}
th:first-child {{
text-align: left;
}}
td {{
padding: 10px 12px;
border-bottom: 1px solid #ddd;
text-align: right;
}}
td:first-child {{
text-align: left;
font-weight: 500;
}}
tr.directory {{
background-color: #f9f9f9;
cursor: pointer;
}}
tr.directory:hover {{
background-color: #e8f5e9;
}}
tr.directory td:first-child::before {{
content: '';
color: #4CAF50;
font-size: 0.8em;
margin-right: 5px;
}}
tr.directory.collapsed td:first-child::before {{
content: '';
}}
tr.directory.hidden {{
display: none;
}}
tr.file {{
background-color: #ffffff;
font-size: 0.9em;
}}
tr.file.hidden {{
display: none;
}}
tr.file td:first-child {{
padding-left: 40px;
font-weight: normal;
color: #555;
}}
tr.file:hover {{
background-color: #f5f5f5;
}}
.pct-high {{
color: #4CAF50;
font-weight: bold;
}}
.pct-med {{
color: #FF9800;
font-weight: bold;
}}
.pct-low {{
color: #f44336;
font-weight: bold;
}}
.spacer {{
height: 10px;
}}
tr.total {{
background-color: #e8f5e9;
font-weight: bold;
border-top: 2px solid #4CAF50;
}}
</style>
</head>
<body>
<h1>Code Analysis Report</h1>
<div class="build-info">
<span><span class="label">Board:</span> {board_name}</span>
<span><span class="label">Commit:</span> {git_hash}</span>
<span><span class="label">Date:</span> {git_date}</span>
<span><span class="label">Analysis:</span> {analysis_method or 'unknown'}</span>
</div>
<div class="summary">
<div class="summary-stat">
<span class="label">Total Files:</span>
<span class="value">{len(all_sources)}</span>
</div>
<div class="summary-stat">
<span class="label">Used Files:</span>
<span class="value">{len(used_sources)}</span>
</div>
<div class="summary-stat">
<span class="label">Usage:</span>
<span class="value">{percent(len(used_sources), len(all_sources)):.0f}%</span>
</div>
<div class="summary-stat">
<span class="label">Total Lines:</span>
<span class="value">{total_lines_all:,}</span>
</div>
<div class="summary-stat">
<span class="label">Used Lines:</span>
<span class="value">{total_lines_used:,}</span>
</div>
<div class="summary-stat">
<span class="label">Code Usage:</span>
<span class="value">{percent(total_lines_used, total_lines_all):.0f}%</span>
</div>
</div>
<table>
<thead>
<tr>
<th>Directory</th>
<th>Files</th>
<th>Used</th>
<th>%Used</th>
<th>%Code</th>
<th>{lines_header}</th>
<th>Used</th>
</tr>
</thead>
<tbody>
'''
# Build hierarchical structure - only show top-level directories initially
# Group all directories by their top-level component
top_level_groups = {}
sorted_dirs = sorted(dir_stats.items(), key=lambda x: x[0])
for dir_path, stats in sorted_dirs:
# Skip directories with 0 lines used unless show_empty is set
if not show_empty and stats.lines_used == 0:
continue
# Get top-level directory name
parts = dir_path.split('/')
top_level = parts[0]
if top_level not in top_level_groups:
top_level_groups[top_level] = []
top_level_groups[top_level].append((dir_path, stats))
# Generate HTML for hierarchical structure
dir_counter = [0] # Use list to allow modification in nested function
def render_directory(dir_path, stats, parent_id=None, indent_level=0):
"""Render a directory row and its children."""
nonlocal html
dir_id = f'dir-{dir_counter[0]}'
dir_counter[0] += 1
pct_used = percent(stats.used, stats.total)
pct_code = percent(stats.lines_used, stats.lines_total)
pct_code_class = 'pct-high' if pct_code >= 75 else ('pct-med' if pct_code >= 50 else 'pct-low')
if use_kloc:
lines_total_str = f'{klocs(stats.lines_total)}'
lines_used_str = f'{klocs(stats.lines_used)}'
else:
lines_total_str = f'{stats.lines_total:,}'
lines_used_str = f'{stats.lines_used:,}'
# Add indentation to directory name
indent = '&nbsp;&nbsp;' * indent_level
display_name = f'{indent}{dir_path}' if indent_level > 0 else dir_path
# Start collapsed
collapsed_class = ' collapsed'
hidden_class = ' hidden' if parent_id else ''
parent_attr = f' data-parent-dir="{parent_id}"' if parent_id else ''
html += f''' <tr class="directory{collapsed_class}{hidden_class}" data-dir-id="{dir_id}"{parent_attr}>
<td>{display_name}</td>
<td>{stats.total}</td>
<td>{stats.used}</td>
<td>{pct_used:.0f}</td>
<td class="{pct_code_class}">{pct_code:.0f}</td>
<td>{lines_total_str}</td>
<td>{lines_used_str}</td>
</tr>
'''
return dir_id
# Render top-level directories and their hierarchies
for top_level in sorted(top_level_groups.keys()):
subdirs_list = top_level_groups[top_level]
# Aggregate stats for top-level directory
from collections import namedtuple
DirStats = namedtuple('DirStats', ['total', 'used', 'unused', 'lines_total', 'lines_used', 'files'])
total_files = sum(s.total for _, s in subdirs_list)
used_files = sum(s.used for _, s in subdirs_list)
total_lines = sum(s.lines_total for _, s in subdirs_list)
used_lines = sum(s.lines_used for _, s in subdirs_list)
top_stats = DirStats(total=total_files, used=used_files, unused=0,
lines_total=total_lines, lines_used=used_lines, files=[])
# Render top-level directory with aggregated stats
top_dir_id = render_directory(top_level, top_stats, None, 0)
# Render all subdirectories under this top-level directory
for subdir_path, subdir_stats in sorted(subdirs_list):
subdir_id = render_directory(subdir_path, subdir_stats, top_dir_id, 1)
# Render files for this subdirectory
if show_files and subdir_stats.files:
sorted_files = sorted(subdir_stats.files,
key=lambda x: os.path.basename(x['path']))
for info in sorted_files:
if not show_empty and info['active'] == 0:
continue
filename = os.path.basename(info['path'])
if file_results:
pct_active = percent(info['active'], info['total'])
pct_active_class = ('pct-high' if pct_active >= 75
else ('pct-med' if pct_active >= 50 else 'pct-low'))
if use_kloc:
total_str = f'{klocs(info["total"])}'
active_str = f'{klocs(info["active"])}'
else:
total_str = f'{info["total"]:,}'
active_str = f'{info["active"]:,}'
html += f''' <tr class="file hidden" data-parent-dir="{subdir_id}">
<td>&nbsp;&nbsp;&nbsp;&nbsp;{filename}</td>
<td></td>
<td></td>
<td></td>
<td class="{pct_active_class}">{pct_active:.0f}</td>
<td>{total_str}</td>
<td>{active_str}</td>
</tr>
'''
# Add total row
pct_files = percent(len(used_sources), len(all_sources))
pct_code_total = percent(total_lines_used, total_lines_all)
if use_kloc:
total_str = f'{klocs(total_lines_all)}'
used_str = f'{klocs(total_lines_used)}'
else:
total_str = f'{total_lines_all:,}'
used_str = f'{total_lines_used:,}'
html += f''' <tr class="total">
<td>TOTAL</td>
<td>{len(all_sources)}</td>
<td>{len(used_sources)}</td>
<td>{pct_files:.0f}</td>
<td>{pct_code_total:.0f}</td>
<td>{total_str}</td>
<td>{used_str}</td>
</tr>
</tbody>
</table>
<script>
// Toggle file and subdirectory visibility when clicking on directory rows
document.addEventListener('DOMContentLoaded', function() {{
const dirRows = document.querySelectorAll('tr.directory');
dirRows.forEach(function(dirRow) {{
dirRow.addEventListener('click', function() {{
const dirId = this.getAttribute('data-dir-id');
const childRows = document.querySelectorAll('[data-parent-dir="' + dirId + '"]');
// Toggle collapsed class on directory
this.classList.toggle('collapsed');
// Toggle hidden class on child rows (both files and subdirectories)
childRows.forEach(function(childRow) {{
childRow.classList.toggle('hidden');
}});
}});
}});
}});
</script>
</body>
</html>
'''
# Write HTML to file
try:
with open(html_file, 'w', encoding='utf-8') as f:
f.write(html)
tout.info(f'HTML report written to: {html_file}')
return True
except IOError as e:
tout.error(f'Failed to write HTML file: {e}')
return False
def show_statistics(all_sources, used_sources, skipped_sources, file_results,
srcdir, top_n):
"""Show overall statistics about source file usage.
Args:
all_sources (set of str): Set of all source file paths
used_sources (set of str): Set of used source file paths
skipped_sources (set of str): Set of unused source file paths
file_results (dict): Optional dict mapping file paths to line analysis
results
srcdir (str): Root directory of the source tree
top_n (int): Number of top files with most inactive code to show
Returns:
bool: True on success
"""
# Calculate line counts - use file_results (DWARF/unifdef) if available
if file_results:
# Use active lines from analysis results
used_lines = sum(r.active_lines for r in file_results.values())
else:
# Fall back to counting all lines in used files
used_lines = sum(count_lines(f) for f in used_sources)
unused_lines = sum(count_lines(f) for f in skipped_sources)
total_lines = used_lines + unused_lines
print_heading('FILE-LEVEL STATISTICS', width=70)
print(f'Total source files: {len(all_sources):6}')
used_pct = percent(len(used_sources), len(all_sources))
print(f'Used source files: {len(used_sources):6} ({used_pct:.1f}%)')
unused_pct = percent(len(skipped_sources), len(all_sources))
print(f'Unused source files: {len(skipped_sources):6} ' +
f'({unused_pct:.1f}%)')
print()
print(f'Total lines of code: {total_lines:6}')
used_lines_pct = percent(used_lines, total_lines)
print(f'Used lines of code: {used_lines:6} ({used_lines_pct:.1f}%)')
unused_lines_pct = percent(unused_lines, total_lines)
print(f'Unused lines of code: {unused_lines:6} ' +
f'({unused_lines_pct:.1f}%)')
print_heading('', width=70)
# If line-level analysis was performed, show those stats too
if file_results:
print()
total_lines_analysed = sum(r.total_lines for r in file_results.values())
active_lines = sum(r.active_lines for r in file_results.values())
inactive_lines = sum(r.inactive_lines for r in file_results.values())
print_heading('LINE-LEVEL STATISTICS (within compiled files)', width=70)
print(f'Files analysed: {len(file_results):6}')
print(f'Total lines in used files:{total_lines_analysed:6}')
active_pct = percent(active_lines, total_lines_analysed)
print(f'Active lines: {active_lines:6} ' +
f'({active_pct:.1f}%)')
inactive_pct = percent(inactive_lines, total_lines_analysed)
print(f'Inactive lines: {inactive_lines:6} ' +
f'({inactive_pct:.1f}%)')
print_heading('', width=70)
print()
# Show top files with most inactive code
files_by_inactive = sorted(
file_results.items(),
key=lambda x: x[1].inactive_lines,
reverse=True
)
print(f'TOP {top_n} FILES WITH MOST INACTIVE CODE:')
print('-' * 70)
for source_file, result in files_by_inactive[:top_n]:
rel_path = os.path.relpath(source_file, srcdir)
pct_inactive = percent(result.inactive_lines, result.total_lines)
print(f' {result.inactive_lines:5} inactive lines ' +
f'({pct_inactive:4.1f}%) - {rel_path}')
return True