#!/usr/bin/env python3 """ Scan repository for TODO/FIXME comments and generate a Markdown report. Usage: python tools/generate_todo_report.py [--exclude-dir dir1,dir2] [--no-default-excludes] Produces: docs/TODO_FIXME_REPORT.md """ import argparse import os import re import sys from pathlib import Path SCRIPT_DIR = Path(__file__).resolve().parent ROOT = None DEFAULT_EXCLUDED_DIRS = { '.git', 'build', 'qt', 'toolchain', 'ptinstaller', 'docs', 'tools', 'gamemode', 'libpng', 'zlib', 'quazip', 'node_modules', 'tomlplusplus', 'json', 'libqrencode', 'extra-cmake-modules', '.gitignore', '.github', 'cmark', 'flatpak', 'bzip2', 'ci', } PATTERN = re.compile(r"\b(TODO|FIXME)\b", re.IGNORECASE) def is_text_file(path: Path) -> bool: try: with open(path, 'rb') as f: chunk = f.read(4096) if b"\0" in chunk: return False except Exception: return False return True def classify(text: str) -> str: t = text.lower() hints_complex = [ 'design', 'refactor', 'race', 'concurrent', 'abort', 'retry', 'performance', 'memory', 'security', 'validate', 'schema', 'generic', 'nuke', 'inefficient', 'algorithm', 'thread', 'blocking', 'async', 'validate', 'jwt', 'schema', 'hack' ] hints_trivial = ['typo', 'wrap', 'link', 'format', 'docs', 'documentation', 'spell', 'grammar', 'rename', 'cleanup', 'whitespace'] if any(h in t for h in hints_trivial): return 'trivial' if any(h in t for h in hints_complex): return 'complex' # Context-sensitive keyword matching for implementable items implementable_keywords = ['should', 'maybe', 'add', 'implement', 'todo:', 'need', 'missing', 'want', 'consider', 'allow', 'support', 'fixme', 'refactor', 'update', 'remove', 'bug'] if any(k in t for k in implementable_keywords): return 'implementable' return 'unknown' def scan(root: Path, excluded_dirs): results = [] for p in root.rglob('*'): # Skip if any parent directory is in the excluded list if any(part in excluded_dirs for part in p.parts): continue if p.is_dir(): continue if p.suffix in {'.patch', '.diff'}: continue if not is_text_file(p): continue try: text = p.read_text(encoding='utf-8') except Exception: try: text = p.read_text(encoding='latin-1') except Exception: continue lines = text.splitlines() for i, line in enumerate(lines, start=1): if PATTERN.search(line): before = '\n'.join(lines[max(0, i-3):i-1]) after = '\n'.join(lines[i:i+2]) whole = '\n'.join(lines[max(0, i-3):min(len(lines), i+2)]) tag = PATTERN.search(line).group(1) cls = classify(line + ' ' + whole) results.append({ 'path': str(p.relative_to(root)), 'line': i, 'tag': tag, 'text': line.strip(), 'context': whole, 'classification': cls, }) return results def generate_md(results, out_path: Path): out_path.parent.mkdir(parents=True, exist_ok=True) by_class = {} for r in results: by_class.setdefault(r['classification'], []).append(r) with open(out_path, 'w', encoding='utf-8') as f: f.write('# TODO/FIXME Report\n\n') f.write('Generated by `tools/generate_todo_report.py`.\n\n') f.write('Summary:\n\n') for k in sorted(by_class.keys()): f.write(f'- **{k}**: {len(by_class[k])} items\n') f.write('\n---\n\n') for k in sorted(by_class.keys()): f.write(f'## {k.capitalize()} ({len(by_class[k])})\n\n') for item in by_class[k]: f.write(f'- **{item["tag"]}** in `{item["path"]}`:{item["line"]} — {item["text"]}\n') f.write('```\n') f.write(item['context'] + '\n') f.write('```\n\n') f.write('\n---\n\n') f.write('Notes:\n') f.write('- `trivial`: likely simple fixes (typos, docs).\n') f.write('- `implementable`: small code changes may resolve.\n') f.write('- `complex`: needs design review or larger refactor.\n') f.write('- `unknown`: manual review recommended.\n') print(f'Wrote report to {out_path}') def parse_excludes(exclude_args): excludes = set() for item in exclude_args or []: for part in item.split(','): part = part.strip() if part: excludes.add(part) return excludes def main(): parser = argparse.ArgumentParser(description="Generate TODO/FIXME report for the repository.") parser.add_argument( "--exclude-dir", action="append", default=[], help="Directory name(s) to exclude. Repeat or provide comma-separated values.", ) parser.add_argument( "--no-default-excludes", action="store_true", help="Do not use the built-in excluded directory list.", ) parser.add_argument( "--root", default=None, help="Root directory to scan (defaults to $PWD).", ) parser.add_argument( "--output", default=None, help="Output markdown path (defaults to /docs/TODO_FIXME_REPORT.md).", ) args = parser.parse_args() # Use PWD to match user's shell location (avoids resolving symlinks to Trash/cloud storage) root_path_str = args.root or os.getenv('PWD') or os.getcwd() root = Path(root_path_str) out_path = Path(args.output) if args.output else root / "docs" / "TODO_FIXME_REPORT.md" excludes = set() if not args.no_default_excludes: excludes.update(DEFAULT_EXCLUDED_DIRS) excludes.update(parse_excludes(args.exclude_dir)) results = scan(root, excludes) generate_md(results, out_path) if __name__ == '__main__': main()