0.1.0-pre.004
This commit is contained in:
102
scripts/audit_rust_general_rules.py
Executable file
102
scripts/audit_rust_general_rules.py
Executable file
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env python3
|
||||
# file: scripts/audit_rust_general_rules.py
|
||||
# version: 3
|
||||
|
||||
"""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 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] = []
|
||||
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 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"))
|
||||
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())
|
||||
Reference in New Issue
Block a user