#!/usr/bin/env python3 # file: scripts/audit_rust_const_blocks.py # version: 1 """Audit top-level Rust const blocks for visibility, names 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 _CONST = re.compile(r"^\s*(?:(pub|pub\(crate\))\s+)?const\s+([A-Za-z_][A-Za-z0-9_]*)\s*:") 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 _end_of_const(lines: list[str], start: int) -> int: """Find the terminating semicolon outside brackets and initializer braces.""" brackets: list[str] = [] closing = {"}": "{", ")": "(", "]": "["} for index in range(start, len(lines)): for char in lines[index]: if char in "{([": brackets.append(char) elif char in "})]" and brackets and brackets[-1] == closing[char]: brackets.pop() elif char == ";" and not brackets: return index return start def audit_file(path: pathlib.Path) -> list[str]: """Compare adjacent top-level const declarations in homogeneous blocks.""" text = path.read_text(encoding="utf-8") lines = text.splitlines() masked, _ = mask_source(text) code_lines = masked.splitlines() depths = line_depths(masked) errors: list[str] = [] previous: tuple[int, int, int, str] | None = None # end, line, visibility rank, name index = 0 while index < len(lines): code = code_lines[index] if depths[index] != 0: index += 1 continue match = _CONST.match(code) if match is not None: visibility, name = match.groups() rank = {"pub": 0, "pub(crate)": 1, None: 2}[visibility] if previous is not None: end, _, last_rank, last_name = previous if rank < last_rank: errors.append(f"{path}:{index + 1}: const visibility must be pub, pub(crate), private") if rank == last_rank and _natural_key(name) < _natural_key(last_name): errors.append(f"{path}:{index + 1}: const names out of alphabetical order: {last_name}, {name}") between = lines[end + 1:index] if all(not candidate.strip() for candidate in between): expected = 0 if rank == last_rank else 1 if len(between) != expected: errors.append(f"{path}:{index + 1}: expected {expected} blank line(s) between const declarations") end = _end_of_const(code_lines, index) previous = (end, index + 1, rank, name) index = end + 1 continue # Comments, attributes and whitespace attach to the surrounding block. if code.strip() and not lines[index].lstrip().startswith(("//", "/*", "*", "#[")): previous = None index += 1 return errors def main() -> int: """Run the read-only top-level const-block audit.""" 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 const-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 const-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 const-block audit: {len(errors)} error(s) across {len(files)} file(s)", file=sys.stderr) return 1 print(f"Rust const-block audit: clean ({len(files)} file(s))") return 0 if __name__ == "__main__": raise SystemExit(main())