v0.2.5-pre.010.fix.002

This commit is contained in:
2026-08-20 11:53:55 +02:00
parent c0b131bf6f
commit 08c8046262
21 changed files with 467 additions and 37 deletions

44
scripts/audit_rust_export_completeness.py Executable file → Normal file
View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3
# file: scripts/audit_rust_export_completeness.py
# version: 1
# version: 3
"""Audit crate-root export completeness and canonical same-crate paths."""
@@ -124,6 +124,33 @@ def external_usage(crate: pathlib.Path, declaration: Declaration, test_parents:
return False
def crate_root_symbols(crate: pathlib.Path) -> set[str]:
"""Return names that can resolve directly after `crate::`."""
crate_root = crate / "src/lib.rs"
if not crate_root.is_file():
crate_root = crate / "src/main.rs"
symbols: set[str] = set()
if crate_root.is_file():
text = crate_root.read_text(encoding="utf-8")
for match in re.finditer(r"^\s*mod\s+([A-Za-z_][A-Za-z0-9_]*)\s*(?:;|\{)", text, re.MULTILINE):
symbols.add(match.group(1))
for match in re.finditer(r"^\s*(?:(?:pub|pub\(crate\))\s+)?(?:(?:async|const|unsafe)\s+)*(?:const|static|type|struct|enum|trait|union|fn)\s+([A-Za-z_][A-Za-z0-9_]*)", text, re.MULTILINE):
symbols.add(match.group(1))
for match in re.finditer(r"^\s*pub(?:\(crate\))?\s+use\s+[^;]*::([A-Za-z_][A-Za-z0-9_]*)\s*;", text, re.MULTILINE):
symbols.add(match.group(1))
for match in re.finditer(r"^\s*pub\s+extern\s+crate\s+([A-Za-z_][A-Za-z0-9_]*)\s*;", text, re.MULTILINE):
symbols.add(match.group(1))
for path in sorted((crate / "src").rglob("*.rs")):
text = path.read_text(encoding="utf-8")
pattern = re.compile(r"#\[macro_export\]\s*\n\s*macro_rules!\s+([A-Za-z_][A-Za-z0-9_]*)")
for match in pattern.finditer(text):
symbols.add(match.group(1))
return symbols
def audit_crate(workspace: pathlib.Path, crate: pathlib.Path) -> list[Candidate]:
"""Audit one crate for export completeness and canonical paths."""
@@ -156,6 +183,21 @@ def audit_crate(workspace: pathlib.Path, crate: pathlib.Path) -> list[Candidate]
alias = exports.get((match.group(1), match.group(2)))
if alias is not None:
candidates.append(Candidate("RUST-IMPORT-201", relative, idx, f"long internal path `crate::{match.group(1)}::{match.group(2)}` must use `crate::{alias}`"))
# Simple `crate::Item` references must resolve at the crate root.
root_symbols = crate_root_symbols(crate)
simple_root = re.compile(r"\bcrate::([A-Za-z_][A-Za-z0-9_]*)\b(?!::)")
for path in sorted(crate.rglob("*.rs")):
if "target" in path.parts:
continue
relative = path.relative_to(workspace).as_posix()
text = path.read_text(encoding="utf-8")
masked = mask_rust_source(text)
for idx, line in enumerate(masked.splitlines(), 1):
for match in simple_root.finditer(line):
symbol = match.group(1)
if symbol not in root_symbols:
candidates.append(Candidate("RUST-IMPORT-203", relative, idx, f"`crate::{symbol}` does not resolve to a declared/re-exported crate-root symbol"))
# `super::Item` is reserved to strictly private parent items in separated unit tests.
declarations_by_source: dict[pathlib.Path, dict[str, Declaration]] = {}
for declaration in declarations.values():