0.0.1-alpha.3

This commit is contained in:
2026-09-27 22:23:43 +02:00
parent 8cd187b700
commit 6b6bdcfc71
29 changed files with 1973 additions and 32 deletions

View File

@@ -0,0 +1,168 @@
#!/usr/bin/env python3
# file: scripts/audit_rust_item_spacing.py
# version: 1
"""Audit exact spacing between mechanically detectable Rust items."""
from __future__ import annotations
import argparse
import dataclasses
import pathlib
import re
import sys
from _rust_audit_common import line_depths, mask_source, selected_files
@dataclasses.dataclass(frozen=True)
class RustItem:
"""One mechanically detected Rust item and its source span."""
kind: str
visibility: str
start: int
declaration: int
end: int
depth: int
braced: bool
def _visibility(stripped: str) -> str:
if stripped.startswith("pub(crate) "):
return "pub(crate)"
if stripped.startswith("pub "):
return "pub"
return "private"
def _kind(stripped: str) -> str | None:
prefix = r"(?:(?:pub|pub\(crate\))\s+)?"
qualifiers = r"(?:(?:async|const|unsafe)\s+)*"
if re.match(rf"^{prefix}{qualifiers}fn\b", stripped):
return "fn"
for kind in ("struct", "enum", "union", "trait", "impl", "const", "static", "type"):
if re.match(rf"^{prefix}{qualifiers}{kind}\b", stripped):
return kind
return None
def _leading_line(lines: list[str], declaration: int) -> int:
cursor = declaration - 2
while cursor >= 0:
stripped = lines[cursor].strip()
if stripped.startswith("///") or stripped.startswith("#["):
cursor -= 1
continue
break
return cursor + 2
def _end_line(masked_lines: list[str], depths: list[int], declaration: int, kind: str) -> tuple[int, bool]:
start_depth = depths[declaration - 1]
if kind in {"const", "static", "type"}:
brackets: list[str] = []
closing = {"}": "{", ")": "(", "]": "["}
for line_index in range(declaration - 1, len(masked_lines)):
for char in masked_lines[line_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 line_index + 1, False
return declaration, False
body_started = False
for line_index in range(declaration - 1, len(masked_lines)):
masked_line = masked_lines[line_index]
if not body_started and ";" in masked_line and "{" not in masked_line:
return line_index + 1, False
if "{" in masked_line:
body_started = True
if body_started:
end_depth = depths[line_index] + masked_line.count("{") - masked_line.count("}")
if end_depth == start_depth:
return line_index + 1, True
return declaration, body_started
def _items(lines: list[str], masked_lines: list[str], depths: list[int]) -> list[RustItem]:
result: list[RustItem] = []
for number, line in enumerate(lines, 1):
stripped = line.strip()
kind = _kind(stripped)
if kind is None:
continue
end, braced = _end_line(masked_lines, depths, number, kind)
result.append(RustItem(kind, _visibility(stripped), _leading_line(lines, number), number, end, depths[number - 1], braced))
return result
def _parent(item: RustItem, items: list[RustItem]) -> RustItem | None:
parents = [candidate for candidate in items if candidate.braced and candidate.declaration < item.declaration <= candidate.end and candidate.depth < item.depth]
if not parents:
return None
return max(parents, key=lambda candidate: candidate.depth)
def audit_file(path: pathlib.Path) -> list[str]:
"""Require exact separation between adjacent items in one Rust scope."""
text = path.read_text(encoding="utf-8")
lines = text.splitlines()
masked, _ = mask_source(text)
masked_lines = masked.splitlines()
depths = line_depths(masked)
items = _items(lines, masked_lines, depths)
siblings: dict[tuple[int, int] | None, list[RustItem]] = {}
for item in items:
parent = _parent(item, items)
key = None if parent is None else (parent.declaration, parent.end)
siblings.setdefault(key, []).append(item)
errors: list[str] = []
for group in siblings.values():
group.sort(key=lambda item: item.declaration)
for previous, current in zip(group, group[1:]):
between = lines[previous.end:current.start - 1]
# Unknown syntax or a macro between items owns its own layout; do not infer across it.
if any(candidate.strip() for candidate in between):
continue
blank_count = sum(1 for candidate in between if not candidate.strip())
compact = previous.kind == current.kind and previous.kind in {"const", "static", "type"} and previous.visibility == current.visibility
if compact:
# Compact declaration blocks have a dedicated owner when a project selects one.
continue
if blank_count != 1:
errors.append(f"{path}:{current.start}: expected exactly one blank line between Rust items; found {blank_count}")
return errors
def main() -> int:
"""Run the read-only item-spacing audit on explicitly selected Rust files."""
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 item-spacing 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 item-spacing audit: cannot read {path}: {error}", file=sys.stderr)
return 2
for error in errors:
print(error, file=sys.stderr)
if errors:
print(f"Rust item-spacing audit: {len(errors)} error(s) across {len(files)} file(s)", file=sys.stderr)
return 1
print(f"Rust item-spacing audit: clean ({len(files)} file(s))")
return 0
if __name__ == "__main__":
raise SystemExit(main())