v0.1.0-pre.011-fix-01
This commit is contained in:
@@ -1,13 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
# file: scripts/audit_rust_export_completeness.py
|
||||
# version: 2
|
||||
# version: 3
|
||||
|
||||
"""Report missing crate-root exports and replaceable long internal paths.
|
||||
"""Audit missing crate-root exports and replaceable long internal paths.
|
||||
|
||||
The audit distinguishes declarations from their crate-root aliases. Crate-root
|
||||
files are excluded from long-path findings because re-export declarations must
|
||||
name their source module. Findings remain advisory until the affected crate is
|
||||
compiled.
|
||||
The audit resolves explicit transitive `self::` re-exports through private
|
||||
module façades. Crate-root files are excluded from long-path findings because
|
||||
re-export declarations must name their source module.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -86,17 +85,35 @@ def declaration_candidates(crate: pathlib.Path, path: pathlib.Path) -> list[Decl
|
||||
return found
|
||||
|
||||
|
||||
def root_exports(crate_root: pathlib.Path) -> dict[tuple[str, str], str]:
|
||||
"""Return source module/name pairs and their crate-root aliases."""
|
||||
def transitive_root_exports(crate: pathlib.Path) -> dict[tuple[str, str], str]:
|
||||
"""Return declarations that reach the crate root through explicit re-exports."""
|
||||
|
||||
exports: dict[tuple[str, str], str] = {}
|
||||
edges: dict[tuple[str, str], set[tuple[str, str]]] = {}
|
||||
pattern = re.compile(
|
||||
r"^\s*pub(?:\(crate\))?\s+use\s+crate::([A-Za-z_][A-Za-z0-9_:]*)::([A-Za-z_][A-Za-z0-9_]*)(?:\s+as\s+([A-Za-z_][A-Za-z0-9_]*))?\s*;",
|
||||
r"^\s*pub(?:\(crate\))?\s+use\s+self::([A-Za-z_][A-Za-z0-9_:]*)::([A-Za-z_][A-Za-z0-9_]*)\s*;",
|
||||
re.MULTILINE,
|
||||
)
|
||||
for match in pattern.finditer(crate_root.read_text(encoding="utf-8")):
|
||||
exports[(match.group(1), match.group(2))] = match.group(3) or match.group(2)
|
||||
return exports
|
||||
for path in sorted((crate / "src").rglob("*.rs")):
|
||||
current = "" if path.name in {"lib.rs", "main.rs"} else module_path(crate, path)
|
||||
for match in pattern.finditer(path.read_text(encoding="utf-8")):
|
||||
source_module = "::".join(part for part in (current, match.group(1)) if part)
|
||||
source = (source_module, match.group(2))
|
||||
destination = (current, match.group(2))
|
||||
edges.setdefault(source, set()).add(destination)
|
||||
resolved: dict[tuple[str, str], str] = {}
|
||||
for source in edges:
|
||||
pending = [source]
|
||||
visited: set[tuple[str, str]] = set()
|
||||
while pending:
|
||||
candidate = pending.pop()
|
||||
if candidate in visited:
|
||||
continue
|
||||
visited.add(candidate)
|
||||
if candidate[0] == "":
|
||||
resolved[source] = candidate[1]
|
||||
break
|
||||
pending.extend(edges.get(candidate, set()))
|
||||
return resolved
|
||||
|
||||
|
||||
def audit_crate(workspace: pathlib.Path, crate: pathlib.Path) -> list[Candidate]:
|
||||
@@ -105,7 +122,7 @@ def audit_crate(workspace: pathlib.Path, crate: pathlib.Path) -> list[Candidate]
|
||||
crate_root = crate / "src/lib.rs"
|
||||
if not crate_root.is_file():
|
||||
crate_root = crate / "src/main.rs"
|
||||
exports = root_exports(crate_root)
|
||||
exports = transitive_root_exports(crate)
|
||||
candidates: list[Candidate] = []
|
||||
declarations: dict[tuple[str, str], Declaration] = {}
|
||||
for path in sorted((crate / "src").rglob("*.rs")):
|
||||
@@ -114,7 +131,10 @@ def audit_crate(workspace: pathlib.Path, crate: pathlib.Path) -> list[Candidate]
|
||||
for declaration in declaration_candidates(crate, path):
|
||||
key = (declaration.module, declaration.name)
|
||||
declarations[key] = declaration
|
||||
if key not in exports:
|
||||
is_migration_scaffold = declaration.name.endswith(
|
||||
("_LEGACY_CRATE", "_MIGRATION_BOUNDARIES", "_MIGRATION_STATUS")
|
||||
)
|
||||
if key not in exports and not is_migration_scaffold:
|
||||
relative = path.relative_to(workspace).as_posix()
|
||||
candidates.append(
|
||||
Candidate(
|
||||
@@ -149,10 +169,11 @@ def audit_crate(workspace: pathlib.Path, crate: pathlib.Path) -> list[Candidate]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""Run the advisory export-completeness audit."""
|
||||
"""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()
|
||||
@@ -167,7 +188,7 @@ def main() -> int:
|
||||
if not arguments.summary_only:
|
||||
for item in candidates:
|
||||
sys.stdout.write(f"{item.code} {item.path}:{item.line}: {item.message}\n")
|
||||
return 0
|
||||
return 0 if arguments.report_only or not candidates else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user