110 lines
4.1 KiB
Python
110 lines
4.1 KiB
Python
#!/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())
|