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,144 @@
# file: scripts/_rust_audit_common.py
# version: 1
"""Small shared lexical and file-selection helpers for read-only Rust audits."""
from __future__ import annotations
import pathlib
_IGNORED = frozenset({".git", ".venv", "__pycache__", "target", "builds"})
def selected_files(raw_paths: list[str]) -> tuple[list[pathlib.Path], list[str]]:
"""Select explicit Rust files and directories, without following generated trees."""
files: set[pathlib.Path] = set()
errors: list[str] = []
for raw in raw_paths:
path = pathlib.Path(raw)
if path.is_dir():
files.update(
candidate
for candidate in path.rglob("*.rs")
if candidate.is_file() and not (set(candidate.parts) & _IGNORED)
)
elif path.is_file() and path.suffix == ".rs":
files.add(path)
else:
errors.append(f"unsupported or missing Rust path: {path}")
return sorted(files), errors
def mask_source(text: str) -> tuple[str, set[int]]:
"""Mask comments and literals, preserving offsets and structural punctuation.
Return line numbers whose first character is inside a multiline literal, so
an empty line in a raw string cannot be mistaken for an empty code line.
This is a lexical helper, not a full Rust parser or a compiler substitute.
"""
output = list(text)
protected: set[int] = set()
index = 0
line = 1
state = "code"
block_depth = 0
raw_hashes = 0
while index < len(text):
char = text[index]
nxt = text[index + 1] if index + 1 < len(text) else ""
if char == "\n":
line += 1
if state in {"string", "raw_string"}:
protected.add(line)
if state == "line_comment":
state = "code"
index += 1
continue
if state == "code":
if char == "/" and nxt in {"/", "*"}:
output[index:index + 2] = [" ", " "]
state = "line_comment" if nxt == "/" else "block_comment"
block_depth = 1 if nxt == "*" else 0
index += 2
continue
if char == '"':
output[index] = " "
state = "string"
index += 1
continue
if char == "'":
# Lifetimes have no nearby closing quote; char literals do.
probe = index + 1
escaped = False
found = False
while probe < min(len(text), index + 8) and text[probe] != "\n":
if text[probe] == "'" and not escaped:
found = True
break
escaped = text[probe] == "\\" and not escaped
probe += 1
if found:
output[index] = " "
state = "char"
index += 1
continue
# Also covers r#"..."#, br#"..."# and cr#"..."#.
if char == "r" and (index == 0 or text[index - 1] not in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"):
probe = index + 1
while probe < len(text) and text[probe] == "#":
probe += 1
if probe < len(text) and text[probe] == '"':
output[index:probe + 1] = [" "] * (probe + 1 - index)
raw_hashes = probe - index - 1
state = "raw_string"
index = probe + 1
continue
index += 1
continue
if state == "line_comment":
output[index] = " "
elif state == "block_comment":
output[index] = " "
if char == "/" and nxt == "*":
output[index + 1] = " "
block_depth += 1
index += 2
continue
if char == "*" and nxt == "/":
output[index + 1] = " "
block_depth -= 1
if block_depth == 0:
state = "code"
index += 2
continue
elif state in {"string", "char"}:
output[index] = " "
if char == "\\" and index + 1 < len(text):
if text[index + 1] != "\n":
output[index + 1] = " "
index += 2
continue
if char == ('"' if state == "string" else "'"):
state = "code"
elif state == "raw_string":
output[index] = " "
if char == '"' and text.startswith("#" * raw_hashes, index + 1):
output[index + 1:index + 1 + raw_hashes] = [" "] * raw_hashes
index += 1 + raw_hashes
state = "code"
continue
index += 1
return "".join(output), protected
def line_depths(masked: str) -> list[int]:
"""Return brace depth at the start of each line, excluding literals."""
depths: list[int] = []
depth = 0
for line in masked.splitlines():
depths.append(depth)
depth = max(0, depth + line.count("{") - line.count("}"))
return depths

View File

@@ -0,0 +1,83 @@
#!/usr/bin/env python3
# file: scripts/audit_rust_blank_lines.py
# version: 1
"""Audit blank-line spacing in explicitly selected Rust sources."""
from __future__ import annotations
import argparse
import pathlib
import re
import sys
from _rust_audit_common import line_depths, mask_source, selected_files
_BODY = re.compile(r"^\s*(?:(?:pub(?:\([^)]*\))?|async|const|unsafe|extern|default)\s+)*(fn|struct|enum)\b")
def audit_file(path: pathlib.Path) -> list[str]:
"""Check successive empty lines and empty function/struct/enum body lines."""
text = path.read_text(encoding="utf-8")
lines = text.splitlines()
masked, protected = mask_source(text)
masked_lines = masked.splitlines()
depths = line_depths(masked)
errors: list[str] = []
previous_blank = False
pending: tuple[str, int] | None = None
active: tuple[str, int] | None = None
for number, (original, code, depth) in enumerate(zip(lines, masked_lines, depths, strict=True), 1):
blank = not original.strip() and number not in protected
if blank and previous_blank:
errors.append(f"{path}:{number}: consecutive blank Rust source lines")
previous_blank = blank
if active is not None and depth <= active[1]:
active = None
if active is not None and blank:
errors.append(f"{path}:{number}: blank line inside {active[0]} body")
if active is None and pending is None:
match = _BODY.match(code)
if match is not None:
pending = (match.group(1), depth)
if pending is not None:
if "{" in code:
active = pending
pending = None
elif ";" in code:
pending = None
return errors
def main() -> int:
"""Run the read-only 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 blank-line 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 blank-line audit: cannot read {path}: {error}", file=sys.stderr)
return 2
for error in errors:
print(error, file=sys.stderr)
if errors:
print(f"Rust blank-line audit: {len(errors)} error(s) across {len(files)} file(s)", file=sys.stderr)
return 1
print(f"Rust blank-line audit: clean ({len(files)} file(s))")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,510 @@
#!/usr/bin/env python3
# file: scripts/audit_rust_export_completeness.py
# version: 1
"""Audit Rust crate-root façade completeness and canonical same-crate references."""
from __future__ import annotations
import argparse
import dataclasses
import pathlib
import re
import sys
import tomllib
from _rust_audit_common import line_depths, mask_source
_IGNORED_DIRECTORY_NAMES = frozenset({
".git",
".venv",
"__pycache__",
"build",
"builds",
"dist",
"node_modules",
"target",
})
@dataclasses.dataclass(frozen=True)
class Candidate:
"""One façade or canonical-reference violation candidate."""
code: str
path: str
line: int
message: str
@dataclasses.dataclass(frozen=True)
class Declaration:
"""One module-level public or crate-public declaration."""
module: str
name: str
visibility: str
kind: str
path: pathlib.Path
line: int
column: int
@dataclasses.dataclass(frozen=True)
class Target:
"""One Rust crate target and the module files reachable from its root."""
package: pathlib.Path
root: pathlib.Path
modules: tuple[tuple[pathlib.Path, str], ...]
@dataclasses.dataclass(frozen=True)
class CanonicalItem:
"""One visible item with its canonical crate-root name."""
declaration: Declaration
canonical_name: str
def _is_ignored(path: pathlib.Path, root: pathlib.Path) -> bool:
"""Return whether a path belongs to a generated or local-only tree."""
try:
relative = path.relative_to(root)
except ValueError:
return True
return bool(set(relative.parts) & _IGNORED_DIRECTORY_NAMES)
def crate_roots(root: pathlib.Path) -> list[pathlib.Path]:
"""Discover Rust package roots recursively below one project root."""
crates: set[pathlib.Path] = set()
for manifest in root.rglob("Cargo.toml"):
if _is_ignored(manifest, root):
continue
crate = manifest.parent
if (crate / "src/lib.rs").is_file() or (crate / "src/main.rs").is_file() or _explicit_target_paths(crate):
crates.add(crate)
return sorted(crates)
def _explicit_target_paths(crate: pathlib.Path) -> tuple[pathlib.Path, ...]:
"""Return existing explicit lib/bin target paths declared in Cargo.toml."""
manifest = crate / "Cargo.toml"
if not manifest.is_file():
return ()
try:
data = tomllib.loads(manifest.read_text(encoding="utf-8"))
except (OSError, tomllib.TOMLDecodeError):
return ()
paths: set[pathlib.Path] = set()
lib = data.get("lib")
if isinstance(lib, dict) and isinstance(lib.get("path"), str):
candidate = crate / lib["path"]
if candidate.is_file():
paths.add(candidate)
bins = data.get("bin", [])
if isinstance(bins, list):
for entry in bins:
if isinstance(entry, dict) and isinstance(entry.get("path"), str):
candidate = crate / entry["path"]
if candidate.is_file():
paths.add(candidate)
return tuple(sorted(paths))
def target_roots(crate: pathlib.Path) -> list[pathlib.Path]:
"""Return standard and explicitly configured Rust crate target roots."""
roots: set[pathlib.Path] = set(_explicit_target_paths(crate))
manifest = crate / "Cargo.toml"
package: dict[str, object] = {}
if manifest.is_file():
try:
parsed = tomllib.loads(manifest.read_text(encoding="utf-8"))
raw_package = parsed.get("package", {})
if isinstance(raw_package, dict):
package = raw_package
except (OSError, tomllib.TOMLDecodeError):
package = {}
library = crate / "src/lib.rs"
if library.is_file():
roots.add(library)
if package.get("autobins", True) is not False:
main = crate / "src/main.rs"
if main.is_file():
roots.add(main)
bin_dir = crate / "src/bin"
if bin_dir.is_dir():
roots.update(path for path in bin_dir.glob("*.rs") if path.is_file())
roots.update(path for path in bin_dir.glob("*/main.rs") if path.is_file())
return sorted(roots)
def _standard_child_module_path(current: pathlib.Path, target_root: pathlib.Path, name: str) -> pathlib.Path | None:
"""Resolve a standard `mod name;` file relative to one source module."""
if current == target_root or current.name == "mod.rs":
base = current.parent
else:
base = current.parent / current.stem
direct = base / f"{name}.rs"
nested = base / name / "mod.rs"
if direct.is_file():
return direct
if nested.is_file():
return nested
return None
def _module_children(path: pathlib.Path, target_root: pathlib.Path) -> list[tuple[pathlib.Path, str]]:
"""Return top-level external module declarations from one Rust source file."""
text = path.read_text(encoding="utf-8")
lines = text.splitlines()
masked, _ = mask_source(text)
masked_lines = masked.splitlines()
depths = line_depths(masked)
module_pattern = re.compile(r"^\s*(?:(?:pub|pub\([^)]*\))\s+)?mod\s+([A-Za-z_][A-Za-z0-9_]*)\s*;")
path_pattern = re.compile(r'^\s*#\[path\s*=\s*"([^"]+)"\]\s*$')
found: list[tuple[pathlib.Path, str]] = []
for index, masked_line in enumerate(masked_lines):
if index >= len(depths) or depths[index] != 0:
continue
match = module_pattern.match(masked_line)
if match is None:
continue
module_name = match.group(1)
explicit: pathlib.Path | None = None
probe = index - 1
while probe >= 0 and lines[probe].lstrip().startswith("#["):
path_match = path_pattern.match(lines[probe])
if path_match is not None:
candidate = (path.parent / path_match.group(1)).resolve()
if candidate.is_file():
explicit = candidate
break
probe -= 1
child = explicit or _standard_child_module_path(path, target_root, module_name)
if child is not None:
found.append((child.resolve(), module_name))
return found
def target_modules(crate: pathlib.Path, target_root: pathlib.Path) -> dict[pathlib.Path, str]:
"""Return module files reachable from one target root and their logical paths."""
root = target_root.resolve()
modules: dict[pathlib.Path, str] = {root: ""}
pending: list[pathlib.Path] = [root]
while pending:
current = pending.pop()
parent_module = modules[current]
for child, name in _module_children(current, root):
if _is_ignored(child, crate) or child in modules:
continue
module = f"{parent_module}::{name}" if parent_module else name
modules[child] = module
pending.append(child)
return modules
def targets(crate: pathlib.Path) -> list[Target]:
"""Return independently compiled crate targets for one Cargo package."""
found: list[Target] = []
for root in target_roots(crate):
modules = target_modules(crate, root)
found.append(Target(crate, root.resolve(), tuple(sorted(modules.items(), key=lambda item: item[0].as_posix()))))
return found
def declaration_candidates(path: pathlib.Path, module: str) -> list[Declaration]:
"""Return top-level public or crate-public declarations from one module file."""
text = path.read_text(encoding="utf-8")
lines = text.splitlines()
masked, _ = mask_source(text)
depths = line_depths(masked)
pattern = re.compile(
r"^\s*(pub(?:\(crate\))?)\s+"
r"(?:(?:async|unsafe|const)\s+)*"
r"(const|static|type|struct|enum|trait|union|fn)\s+"
r"([A-Za-z_][A-Za-z0-9_]*)"
)
found: list[Declaration] = []
for line_number, line in enumerate(lines, 1):
if line_number - 1 >= len(depths) or depths[line_number - 1] != 0:
continue
match = pattern.match(line)
if match is None:
continue
found.append(
Declaration(
module=module,
name=match.group(3),
visibility=match.group(1),
kind=match.group(2),
path=path,
line=line_number,
column=match.start(3),
)
)
return found
def _parse_export_item(module: str, item: str) -> tuple[tuple[str, str], str] | None:
"""Parse one simple explicit item from a crate-root re-export."""
stripped = item.strip()
if not stripped or stripped == "*" or "{" in stripped or "}" in stripped:
return None
alias_match = re.fullmatch(
r"([A-Za-z_][A-Za-z0-9_]*)(?:\s+as\s+([A-Za-z_][A-Za-z0-9_]*))?",
stripped,
)
if alias_match is None:
return None
name = alias_match.group(1)
alias = alias_match.group(2) or name
return (module, name), alias
def root_exports(root_source: pathlib.Path) -> dict[tuple[str, str], str]:
"""Return explicit crate-root re-exports keyed by source module and symbol."""
exports: dict[tuple[str, str], str] = {}
text = root_source.read_text(encoding="utf-8")
pattern = re.compile(
r"^\s*pub(?:\(crate\))?\s+use\s+(?:self|crate)::(.*?);",
re.MULTILINE | re.DOTALL,
)
for match in pattern.finditer(text):
body = " ".join(part.strip() for part in match.group(1).splitlines())
if "{" in body and body.endswith("}"):
prefix, group = body.split("{", 1)
module = prefix.rstrip().rstrip(":")
for item in group[:-1].split(","):
parsed = _parse_export_item(module, item)
if parsed is not None:
key, alias = parsed
exports[key] = alias
continue
if "::" not in body:
continue
module, item = body.rsplit("::", 1)
parsed = _parse_export_item(module, item)
if parsed is not None:
key, alias = parsed
exports[key] = alias
return exports
def _reexport_lines(root_source: pathlib.Path) -> set[int]:
"""Return crate-root line numbers occupied by explicit re-export statements."""
text = root_source.read_text(encoding="utf-8")
pattern = re.compile(
r"^\s*pub(?:\(crate\))?\s+use\s+(?:self|crate)::.*?;",
re.MULTILINE | re.DOTALL,
)
lines: set[int] = set()
for match in pattern.finditer(text):
start = text.count("\n", 0, match.start()) + 1
end = text.count("\n", 0, match.end()) + 1
lines.update(range(start, end + 1))
return lines
def _target_declarations(target: Target) -> list[Declaration]:
"""Return visible module-level declarations for one crate target."""
return [
declaration
for path, module in target.modules
for declaration in declaration_candidates(path, module)
]
def _external_usage(target: Target, declaration: Declaration) -> bool:
"""Return whether a crate-public item is clearly referenced from another module file."""
if not declaration.module:
return True
direct = f"crate::{declaration.module}::{declaration.name}"
root_direct = f"crate::{declaration.name}"
super_direct = f"super::{declaration.name}"
for path, _ in target.modules:
if path.resolve() == declaration.path.resolve():
continue
masked, _ = mask_source(path.read_text(encoding="utf-8"))
if direct in masked or root_direct in masked or super_direct in masked:
return True
return False
def _declaration_lookup(declarations: list[Declaration]) -> dict[tuple[pathlib.Path, int, str], Declaration]:
"""Index declaration-name occurrences that must not be treated as references."""
return {(item.path.resolve(), item.line, item.name): item for item in declarations}
def _canonical_reference_candidates(
workspace: pathlib.Path,
target: Target,
declarations: list[Declaration],
exports: dict[tuple[str, str], str],
) -> list[Candidate]:
"""Return non-canonical references to items that have a crate-root identity."""
items: list[CanonicalItem] = []
for declaration in declarations:
if declaration.path.resolve() == target.root and not declaration.module:
items.append(CanonicalItem(declaration, declaration.name))
continue
alias = exports.get((declaration.module, declaration.name))
if alias is not None:
items.append(CanonicalItem(declaration, alias))
by_token: dict[str, list[CanonicalItem]] = {}
for item in items:
by_token.setdefault(item.declaration.name, []).append(item)
if item.canonical_name != item.declaration.name:
by_token.setdefault(item.canonical_name, []).append(item)
if not by_token:
return []
token_pattern = re.compile(r"\b(?:" + "|".join(re.escape(name) for name in sorted(by_token, key=len, reverse=True)) + r")\b")
declaration_lookup = _declaration_lookup(declarations)
root_reexport_lines = _reexport_lines(target.root)
internal_roots = {segment for _, module in target.modules if module for segment in module.split("::")}
candidates: list[Candidate] = []
for path, _ in target.modules:
relative = path.relative_to(workspace).as_posix()
masked, _ = mask_source(path.read_text(encoding="utf-8"))
for line_number, line in enumerate(masked.splitlines(), 1):
for match in token_pattern.finditer(line):
token = match.group(0)
for item in by_token[token]:
declaration = item.declaration
if declaration.kind == "fn" and re.match(r"\s*(?:\(|::<)", line[match.end():]) is None:
continue
prefix = line[:match.start()]
if re.search(r"(?:const|static|type|struct|enum|trait|union|fn)\s+$", prefix) is not None:
continue
if (path.resolve(), line_number, token) in declaration_lookup and token == declaration.name:
continue
if path.resolve() == target.root and line_number in root_reexport_lines:
continue
if token == item.canonical_name and re.search(r"\bcrate::\s*$", prefix) is not None:
continue
if match.start() > 0 and line[match.start() - 1] == ".":
continue
qualified = re.search(
r"([A-Za-z_][A-Za-z0-9_]*(?:::[A-Za-z_][A-Za-z0-9_]*)*)::\s*$",
prefix,
)
if qualified is not None:
first = qualified.group(1).split("::", 1)[0]
if first not in internal_roots and first not in {"crate", "self", "super"}:
continue
candidates.append(
Candidate(
code="RUST-IMPORT-201",
path=relative,
line=line_number,
message=(
f"non-canonical same-crate reference `{token}` for shared "
f"{declaration.visibility} `{declaration.name}`; use `crate::{item.canonical_name}`"
),
)
)
return candidates
def audit_target(workspace: pathlib.Path, target: Target) -> list[Candidate]:
"""Audit one independently compiled Rust crate target."""
declarations = _target_declarations(target)
exports = root_exports(target.root)
candidates: list[Candidate] = []
for declaration in declarations:
if declaration.path.resolve() == target.root and not declaration.module:
continue
required = declaration.visibility == "pub" or _external_usage(target, declaration)
if required and (declaration.module, declaration.name) not in exports:
reason = "public item" if declaration.visibility == "pub" else "crate-public item used outside its module"
candidates.append(
Candidate(
code="RUST-API-201",
path=declaration.path.relative_to(workspace).as_posix(),
line=declaration.line,
message=f"{reason} `{declaration.name}` requires an explicit crate-root re-export",
)
)
candidates.extend(_canonical_reference_candidates(workspace, target, declarations, exports))
return candidates
def audit_crate(workspace: pathlib.Path, crate: pathlib.Path) -> list[Candidate]:
"""Audit every independently compiled target in one Cargo package."""
return [candidate for target in targets(crate) for candidate in audit_target(workspace, target)]
def audit_workspace(root: pathlib.Path) -> tuple[list[pathlib.Path], list[Candidate]]:
"""Return discovered packages and all façade candidates below one project root."""
crates = crate_roots(root)
candidates = list({candidate for crate in crates for candidate in audit_crate(root, crate)})
candidates.sort(key=lambda candidate: (candidate.code, candidate.path, candidate.line, candidate.message))
return crates, candidates
def main() -> int:
"""Run the export-completeness audit."""
parser = argparse.ArgumentParser()
parser.add_argument("--root", default=".")
parser.add_argument("--report-only", action="store_true")
parser.add_argument("--summary-only", action="store_true")
arguments = parser.parse_args()
workspace = pathlib.Path(arguments.root).resolve()
if not workspace.is_dir():
sys.stderr.write(f"Rust export completeness audit: invalid root: {workspace}\n")
return 2
crates, candidates = audit_workspace(workspace)
if not crates:
sys.stderr.write(f"Rust export completeness audit: no Rust package found below {workspace}\n")
return 2
counts: dict[str, int] = {}
for candidate in candidates:
counts[candidate.code] = counts.get(candidate.code, 0) + 1
sys.stdout.write(f"Rust export completeness audit: {len(candidates)} candidate(s) across {len(crates)} package(s)\n")
for code in sorted(counts):
sys.stdout.write(f"{code}: {counts[code]}\n")
if not arguments.summary_only:
for candidate in candidates:
sys.stdout.write(f"{candidate.code} {candidate.path}:{candidate.line}: {candidate.message}\n")
return 0 if arguments.report_only or not candidates else 1
if __name__ == "__main__":
raise SystemExit(main())

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())

View File

@@ -0,0 +1,87 @@
#!/usr/bin/env python3
# file: scripts/audit_rust_module_order.py
# version: 1
"""Audit top-level Rust module ordering with test modules last."""
from __future__ import annotations
import argparse
import pathlib
import re
import sys
from _rust_audit_common import line_depths, mask_source, selected_files
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 audit_file(path: pathlib.Path) -> list[str]:
"""Require production modules before test modules and alphabetic order within each group."""
text = path.read_text(encoding="utf-8")
lines = text.splitlines()
masked, _ = mask_source(text)
depths = line_depths(masked)
modules: list[tuple[int, bool, str]] = []
for number, line in enumerate(lines, 1):
if depths[number - 1] != 0:
continue
match = re.match(r"^mod\s+([A-Za-z_][A-Za-z0-9_]*)\s*(?:;|\{)", line.strip())
if match is None:
continue
cursor = number - 2
is_test = False
while cursor >= 0 and lines[cursor].strip().startswith("#["):
if "cfg(test)" in lines[cursor].replace(" ", ""):
is_test = True
cursor -= 1
modules.append((number, is_test, match.group(1)))
production = [(number, name) for number, is_test, name in modules if not is_test]
tests = [(number, name) for number, is_test, name in modules if is_test]
errors: list[str] = []
if production and tests and min(number for number, _ in tests) < max(number for number, _ in production):
errors.append(f"{path}:{min(number for number, _ in tests)}: test modules must follow production modules")
for group, label in ((production, "production"), (tests, "test")):
names = [name for _, name in group]
expected = sorted(names, key=_natural_key)
if names != expected:
for (number, name), wanted in zip(group, expected, strict=True):
if name != wanted:
errors.append(f"{path}:{number}: {label} module declarations are not alphabetically ordered")
break
return errors
def main() -> int:
"""Run the read-only module-order 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 module-order 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 module-order audit: cannot read {path}: {error}", file=sys.stderr)
return 2
for error in errors:
print(error, file=sys.stderr)
if errors:
print(f"Rust module-order audit: {len(errors)} error(s) across {len(files)} file(s)", file=sys.stderr)
return 1
print(f"Rust module-order audit: clean ({len(files)} file(s))")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,88 @@
#!/usr/bin/env python3
# file: scripts/audit_rust_reexport_blocks.py
# version: 1
"""Audit crate-root Rust re-export block visibility 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
_EXPORT = re.compile(r"^(pub(?:\(crate\))?)\s+use\s+")
def audit_file(path: pathlib.Path) -> list[str]:
"""Require public re-exports before crate-public re-exports and compact blocks."""
text = path.read_text(encoding="utf-8")
lines = text.splitlines()
masked, _ = mask_source(text)
depths = line_depths(masked)
exports: list[tuple[int, str]] = []
for number, line in enumerate(lines, 1):
if depths[number - 1] != 0:
continue
match = _EXPORT.match(line.strip())
if match is None:
continue
visibility = "pub(crate)" if match.group(1) == "pub(crate)" else "pub"
exports.append((number, visibility))
errors: list[str] = []
crate_seen = False
previous_by_visibility: dict[str, int] = {}
for number, visibility in exports:
if visibility == "pub(crate)":
crate_seen = True
elif crate_seen:
errors.append(f"{path}:{number}: pub use block must precede pub(crate) use block")
previous = previous_by_visibility.get(visibility)
if previous is not None:
between = lines[previous:number - 1]
if any(not candidate.strip() for candidate in between):
errors.append(f"{path}:{number}: {visibility} use block contains an empty line")
previous_by_visibility[visibility] = number
public = [number for number, visibility in exports if visibility == "pub"]
crate_public = [number for number, visibility in exports if visibility == "pub(crate)"]
if public and crate_public:
transition = lines[public[-1]:crate_public[0] - 1]
blank_count = sum(1 for candidate in transition if not candidate.strip())
if blank_count != 1:
errors.append(f"{path}:{crate_public[0]}: pub use and pub(crate) use blocks require exactly one blank line; found {blank_count}")
return errors
def main() -> int:
"""Run the read-only re-export-block audit on explicitly selected Rust files."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("paths", nargs="+", help="Rust crate-root 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 re-export-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 re-export-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 re-export-block audit: {len(errors)} error(s) across {len(files)} file(s)", file=sys.stderr)
return 1
print(f"Rust re-export-block audit: clean ({len(files)} file(s))")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,74 @@
#!/usr/bin/env python3
# file: scripts/audit_rust_trait_import_blocks.py
# version: 1
"""Audit placement and contiguity of module-level Rust trait-import blocks."""
from __future__ import annotations
import argparse
import pathlib
import re
import sys
from _rust_audit_common import line_depths, mask_source, selected_files
_DECLARATION = re.compile(r"^(?:pub(?:\(crate\))?\s+)?(?:async\s+|const\s+|unsafe\s+)*(?:const|static|type|struct|enum|trait|union|fn|impl)\b")
_USE = re.compile(r"^use\s+")
def audit_file(path: pathlib.Path) -> list[str]:
"""Require ordinary module-level uses before declarations and in one compact block."""
text = path.read_text(encoding="utf-8")
lines = text.splitlines()
masked, _ = mask_source(text)
depths = line_depths(masked)
seen_declaration = False
uses: list[int] = []
errors: list[str] = []
for number, line in enumerate(lines, 1):
if depths[number - 1] != 0:
continue
stripped = line.strip()
if _DECLARATION.match(stripped):
seen_declaration = True
if _USE.match(stripped):
uses.append(number)
if seen_declaration:
errors.append(f"{path}:{number}: module-level trait import must precede Rust declarations")
for previous, current in zip(uses, uses[1:]):
between = lines[previous:current - 1]
if any(not candidate.strip() for candidate in between):
errors.append(f"{path}:{current}: module-level trait import block contains an empty line")
return errors
def main() -> int:
"""Run the read-only trait-import-block 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 trait-import-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 trait-import-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 trait-import-block audit: {len(errors)} error(s) across {len(files)} file(s)", file=sys.stderr)
return 1
print(f"Rust trait-import-block audit: clean ({len(files)} file(s))")
return 0
if __name__ == "__main__":
raise SystemExit(main())