#!/usr/bin/env python3 # file: scripts/audit_rust_reexport_blocks.py # version: 1 """Audit crate-root Rust re-export block visibility and spacing.""" from __future__ import annotations import argparse import pathlib import re import sys from _rust_audit_common import line_depths, mask_source, selected_files _EXPORT = re.compile(r"^(pub(?:\(crate\))?)\s+use\s+") def audit_file(path: pathlib.Path) -> list[str]: """Require public re-exports before crate-public re-exports and compact blocks.""" text = path.read_text(encoding="utf-8") lines = text.splitlines() masked, _ = mask_source(text) depths = line_depths(masked) exports: list[tuple[int, str]] = [] for number, line in enumerate(lines, 1): if depths[number - 1] != 0: continue match = _EXPORT.match(line.strip()) if match is None: continue visibility = "pub(crate)" if match.group(1) == "pub(crate)" else "pub" exports.append((number, visibility)) errors: list[str] = [] crate_seen = False previous_by_visibility: dict[str, int] = {} for number, visibility in exports: if visibility == "pub(crate)": crate_seen = True elif crate_seen: errors.append(f"{path}:{number}: pub use block must precede pub(crate) use block") previous = previous_by_visibility.get(visibility) if previous is not None: between = lines[previous:number - 1] if any(not candidate.strip() for candidate in between): errors.append(f"{path}:{number}: {visibility} use block contains an empty line") previous_by_visibility[visibility] = number public = [number for number, visibility in exports if visibility == "pub"] crate_public = [number for number, visibility in exports if visibility == "pub(crate)"] if public and crate_public: transition = lines[public[-1]:crate_public[0] - 1] blank_count = sum(1 for candidate in transition if not candidate.strip()) if blank_count != 1: errors.append(f"{path}:{crate_public[0]}: pub use and pub(crate) use blocks require exactly one blank line; found {blank_count}") return errors def main() -> int: """Run the read-only re-export-block audit on explicitly selected Rust files.""" parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("paths", nargs="+", help="Rust crate-root 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 re-export-block 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 re-export-block audit: cannot read {path}: {error}", file=sys.stderr) return 2 for error in errors: print(error, file=sys.stderr) if errors: print(f"Rust re-export-block audit: {len(errors)} error(s) across {len(files)} file(s)", file=sys.stderr) return 1 print(f"Rust re-export-block audit: clean ({len(files)} file(s))") return 0 if __name__ == "__main__": raise SystemExit(main())