llm-quant/scripts/todo_report.py

134 lines
3.8 KiB
Python
Executable File

#!/usr/bin/env python3
"""Generate a consolidated report of TODO/FIXME markers in the repository."""
from __future__ import annotations
import argparse
import json
import os
import re
import sys
from pathlib import Path
from typing import Iterable, List, Dict, Any
DEFAULT_PATTERNS = (
r"\bTODO\b",
r"\bFIXME\b",
r"\bHACK\b",
r"\bXXX\b",
)
EXCLUDE_DIRS = {
".git",
".mypy_cache",
".pytest_cache",
"__pycache__",
"node_modules",
"build",
"dist",
".venv",
}
def iter_source_files(root: Path, extensions: Iterable[str] | None = None) -> Iterable[Path]:
extensions = set(ext.lower() for ext in (extensions or []))
for path in root.rglob("*"):
if path.is_dir():
if path.name in EXCLUDE_DIRS:
# Prevent descending into excluded directories
dirs = list(path.iterdir())
for child in dirs:
if child.is_dir():
iter_source_files(child, extensions)
continue
continue
if extensions and path.suffix.lower() not in extensions:
continue
yield path
def scan_file(path: Path, patterns: List[re.Pattern[str]]) -> List[Dict[str, Any]]:
issues: List[Dict[str, Any]] = []
try:
text = path.read_text(encoding="utf-8")
except (UnicodeDecodeError, OSError):
return issues
for idx, line in enumerate(text.splitlines(), start=1):
for pattern in patterns:
match = pattern.search(line)
if match:
issues.append(
{
"file": str(path),
"line": idx,
"tag": match.group(0),
"text": line.strip(),
}
)
break
return issues
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("path", nargs="?", default=".", help="Root directory to scan.")
parser.add_argument(
"--format",
choices={"table", "json"},
default="table",
help="Output format (default: table).",
)
parser.add_argument(
"--ext",
action="append",
default=None,
help="Restrict scan to specific file extensions (e.g., --ext .py --ext .md).",
)
parser.add_argument(
"--pattern",
action="append",
default=None,
help="Additional regex pattern to match (case-insensitive).",
)
args = parser.parse_args()
root = Path(args.path).resolve()
if not root.exists():
print(f"Path not found: {root}", file=sys.stderr)
return 1
patterns = [re.compile(pat, re.IGNORECASE) for pat in DEFAULT_PATTERNS]
if args.pattern:
patterns.extend(re.compile(pat, re.IGNORECASE) for pat in args.pattern)
issues: List[Dict[str, Any]] = []
for file_path in iter_source_files(root, args.ext):
issues.extend(scan_file(file_path, patterns))
issues.sort(key=lambda item: (item["file"], item["line"]))
if args.format == "json":
json.dump(issues, sys.stdout, indent=2, ensure_ascii=False)
print()
return 0
# Default table output
if not issues:
print("No TODO/FIXME markers found.")
return 0
width_file = max(len(item["file"]) for item in issues)
width_tag = max(len(item["tag"]) for item in issues)
header = f"{'File'.ljust(width_file)} {'Line':>5} {'Tag'.ljust(width_tag)} Text"
print(header)
print("-" * len(header))
for item in issues:
file_display = item["file"].ljust(width_file)
tag_display = item["tag"].ljust(width_tag)
print(f"{file_display} {item['line']:>5} {tag_display} {item['text']}")
return 0
if __name__ == "__main__":
raise SystemExit(main())