0.0.5-alpha.8
This commit is contained in:
168
scripts/audit_rust_item_spacing.py
Executable file
168
scripts/audit_rust_item_spacing.py
Executable 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())
|
||||
87
scripts/audit_rust_module_order.py
Executable file
87
scripts/audit_rust_module_order.py
Executable 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())
|
||||
88
scripts/audit_rust_reexport_blocks.py
Executable file
88
scripts/audit_rust_reexport_blocks.py
Executable 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())
|
||||
74
scripts/audit_rust_trait_import_blocks.py
Executable file
74
scripts/audit_rust_trait_import_blocks.py
Executable 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())
|
||||
Reference in New Issue
Block a user