#!/usr/bin/env python3 # file: scripts/audit_rust_general_rules.py # version: 8 """Audit mechanically verifiable Rust rules shared by all Rust projects.""" from __future__ import annotations import argparse import dataclasses import pathlib import re import sys @dataclasses.dataclass(frozen=True) class Violation: """One mechanically detected general Rust rule violation.""" code: str path: str line: int message: str def rust_files(root: pathlib.Path) -> list[pathlib.Path]: """Return Rust sources outside generated build directories.""" return sorted( path for path in root.rglob("*.rs") if "target" not in path.parts and ".git" not in path.parts and "mnt" not in path.relative_to(root).parts and path.relative_to(root).parts[:2] != ("migration", "khadhroony-bot2-reference") and not any(part.startswith("pre035_") for part in path.relative_to(root).parts) ) def rustfmt_re_export_key(export_path: str) -> tuple[object, ...]: """Return the natural path order used by rustfmt for separate imports.""" path = export_path.removesuffix(";") segments = path.split("::") root_priority = 0 if segments[0] == "self" else 1 if segments[0] == "super" else 2 segment_keys: list[tuple[tuple[int, object], ...]] = [] for segment in segments: raw_tokens = re.findall(r"[A-Za-z]+|[0-9]+", segment) tokens: list[tuple[int, object]] = [] for index, token in enumerate(raw_tokens): if token.isdigit(): previous = raw_tokens[index - 1].lower() if index > 0 else "" priority = -1 if token == "2022" and previous == "token" else 1 tokens.append((priority, int(token))) else: tokens.append((0, token)) segment_keys.append(tuple(tokens)) return (root_priority, *segment_keys) def audit_file(root: pathlib.Path, path: pathlib.Path) -> list[Violation]: """Audit one Rust source against general rules.""" relative = path.relative_to(root).as_posix() text = path.read_text(encoding="utf-8") lines = text.splitlines() violations: list[Violation] = [] local_modules = { match.group(1) for line in lines if (match := re.fullmatch(r"\s*mod\s+([A-Za-z_][A-Za-z0-9_]*)\s*;", line)) is not None } expected_header = f"// file: {relative}" if not lines or lines[0] != expected_header: violations.append(Violation("RUST001", relative, 1, f"expected `{expected_header}`")) if len(lines) < 2 or re.fullmatch(r"// version: [1-9][0-9]*", lines[1]) is None: violations.append(Violation("RUST002", relative, 2, "missing positive file version")) if not text.endswith("\n") or text.endswith("\n\n"): violations.append(Violation("RUST003", relative, max(len(lines), 1), "file must end with exactly one newline")) for index, line in enumerate(lines, 1): stripped = line.strip() if stripped.startswith("pub mod "): violations.append( Violation( "RUST014", relative, index, "modules must remain private and APIs must use explicit re-exports", ) ) if re.match(r"^(?:pub(?:\(crate\))?\s+)?use\s+", stripped) is None: continue is_export = stripped.startswith("pub use ") or stripped.startswith("pub(crate) use ") justified_trait = "rust-rules: trait-import" in stripped justified_derive = "rust-rules: derive-import" in stripped if not is_export and not justified_trait and not justified_derive: violations.append(Violation("RUST010", relative, index, "ordinary use requires a trait or derive import justification")) if "::*" in stripped: violations.append(Violation("RUST012", relative, index, "glob imports are forbidden")) if "{" in stripped or "}" in stripped: code = "RUST011" if is_export else "RUST013" violations.append(Violation(code, relative, index, "grouped use declarations are forbidden")) internal_re_export = re.match( r"^pub(?:\(crate\))?\s+use\s+(?:crate::)?([A-Za-z_][A-Za-z0-9_]*)::", stripped, ) if is_export and ( re.match(r"^pub(?:\(crate\))?\s+use\s+crate::", stripped) or ( internal_re_export is not None and internal_re_export.group(1) in local_modules ) ): violations.append(Violation("RUST015", relative, index, "internal re-export must start with `self::`")) if is_export and re.search(r"\s+as\s+[A-Za-z_][A-Za-z0-9_]*\s*;", stripped): violations.append(Violation("RUST016", relative, index, "re-export aliases are forbidden")) previous_export_line: int | None = None previous_export_visibility: str | None = None previous_export_key: tuple[object, ...] | None = None blank_since_export = False crate_visibility_seen = False enforce_visibility_order = relative == "kb-lib/src/lib.rs" or relative.startswith( "kb-lib/src/executor" ) for index, line in enumerate(lines, 1): if not line.strip(): if previous_export_line is not None: blank_since_export = True continue if line.startswith("///"): continue if line.startswith("pub use ") or line.startswith("pub(crate) use "): visibility = "pub(crate)" if line.startswith("pub(crate) use ") else "pub" if enforce_visibility_order and visibility == "pub" and crate_visibility_seen: violations.append( Violation( "RUST024", relative, index, "`pub use` block must precede the `pub(crate) use` block", ) ) if visibility == "pub(crate)": crate_visibility_seen = True if ( enforce_visibility_order and previous_export_line is not None and previous_export_visibility != visibility and not blank_since_export ): violations.append( Violation( "RUST025", relative, index, "re-export blocks with different visibility require one empty line", ) ) if ( previous_export_line is not None and previous_export_visibility == visibility and blank_since_export ): violations.append( Violation( "RUST022", relative, index, "re-export block contains an empty line", ) ) export_path = line.split(" use ", 1)[1] export_key = rustfmt_re_export_key(export_path) if ( previous_export_line is not None and previous_export_visibility == visibility and not blank_since_export and previous_export_key is not None and export_key < previous_export_key ): violations.append( Violation( "RUST023", relative, index, "re-export block is not ordered alphabetically", ) ) previous_export_line = index previous_export_visibility = visibility previous_export_key = export_key blank_since_export = False continue previous_export_line = None previous_export_visibility = None previous_export_key = None blank_since_export = False if path.name in {"lib.rs", "main.rs"}: for attribute in ("#![warn(missing_docs)]", "#![deny(unreachable_pub)]", "#![forbid(unsafe_code)]"): if attribute not in lines[:20]: violations.append(Violation("RUST020", relative, 1, f"missing `{attribute}`")) for index, line in enumerate(lines, 1): if not (line.startswith("pub use ") or line.startswith("pub(crate) use ")): continue previous = lines[index - 2].strip() if index >= 2 else "" if not previous.startswith("///"): violations.append(Violation("RUST021", relative, index, "crate-root re-export requires adjacent rustdoc")) return violations def main() -> int: """Run the general Rust audit.""" parser = argparse.ArgumentParser() parser.add_argument("--root", default=".") parser.add_argument("--report-only", action="store_true") arguments = parser.parse_args() root = pathlib.Path(arguments.root).resolve() violations = [item for path in rust_files(root) for item in audit_file(root, path)] violations.sort(key=lambda item: (item.code, item.path, item.line, item.message)) if not violations: sys.stdout.write("General Rust rule audit: clean\n") return 0 sys.stdout.write(f"General Rust rule audit: {len(violations)} violation(s)\n") for item in violations: sys.stdout.write(f"{item.code} {item.path}:{item.line}: {item.message}\n") return 0 if arguments.report_only else 1 if __name__ == "__main__": raise SystemExit(main())