#!/usr/bin/env python3 # file: scripts/audit_rust_module_order.py # version: 1 """Audit top-level Rust module ordering with test modules last.""" from __future__ import annotations import argparse import pathlib import re import sys from _rust_audit_common import line_depths, mask_source, selected_files def _natural_key(value: str) -> tuple[tuple[int, object], ...]: parts = re.findall(r"[A-Za-z_]+|[0-9]+", value) return tuple((1, int(part)) if part.isdigit() else (0, part) for part in parts) def audit_file(path: pathlib.Path) -> list[str]: """Require production modules before test modules and alphabetic order within each group.""" text = path.read_text(encoding="utf-8") lines = text.splitlines() masked, _ = mask_source(text) depths = line_depths(masked) modules: list[tuple[int, bool, str]] = [] for number, line in enumerate(lines, 1): if depths[number - 1] != 0: continue match = re.match(r"^mod\s+([A-Za-z_][A-Za-z0-9_]*)\s*(?:;|\{)", line.strip()) if match is None: continue cursor = number - 2 is_test = False while cursor >= 0 and lines[cursor].strip().startswith("#["): if "cfg(test)" in lines[cursor].replace(" ", ""): is_test = True cursor -= 1 modules.append((number, is_test, match.group(1))) production = [(number, name) for number, is_test, name in modules if not is_test] tests = [(number, name) for number, is_test, name in modules if is_test] errors: list[str] = [] if production and tests and min(number for number, _ in tests) < max(number for number, _ in production): errors.append(f"{path}:{min(number for number, _ in tests)}: test modules must follow production modules") for group, label in ((production, "production"), (tests, "test")): names = [name for _, name in group] expected = sorted(names, key=_natural_key) if names != expected: for (number, name), wanted in zip(group, expected, strict=True): if name != wanted: errors.append(f"{path}:{number}: {label} module declarations are not alphabetically ordered") break return errors def main() -> int: """Run the read-only module-order audit on explicitly selected Rust files.""" parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("paths", nargs="+", help="Rust files or directories to inspect") args = parser.parse_args() files, errors = selected_files(args.paths) if errors or not files: for error in errors or ["no Rust files selected"]: print(f"Rust module-order audit: {error}", file=sys.stderr) return 2 for path in files: try: errors.extend(audit_file(path)) except (OSError, UnicodeError) as error: print(f"Rust module-order audit: cannot read {path}: {error}", file=sys.stderr) return 2 for error in errors: print(error, file=sys.stderr) if errors: print(f"Rust module-order audit: {len(errors)} error(s) across {len(files)} file(s)", file=sys.stderr) return 1 print(f"Rust module-order audit: clean ({len(files)} file(s))") return 0 if __name__ == "__main__": raise SystemExit(main())