v0.2.5-pre.010.fix.002
This commit is contained in:
148
scripts/audit_rust_general_rules.py
Executable file → Normal file
148
scripts/audit_rust_general_rules.py
Executable file → Normal file
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
# file: scripts/audit_rust_general_rules.py
|
||||
# version: 1
|
||||
# version: 4
|
||||
|
||||
"""Audit mechanically verifiable Rust normalization rules used by KSP."""
|
||||
|
||||
@@ -222,6 +222,139 @@ def declaration_start(stripped: str, kind: str) -> bool:
|
||||
return re.match(rf"^{prefixes}{kind}\b", stripped) is not None
|
||||
|
||||
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class RustItem:
|
||||
"""One mechanically detected Rust item used for spacing checks."""
|
||||
|
||||
kind: str
|
||||
visibility: str
|
||||
start: int
|
||||
declaration: int
|
||||
end: int
|
||||
depth: int
|
||||
braced: bool
|
||||
|
||||
|
||||
def item_visibility(stripped: str) -> str:
|
||||
"""Return normalized visibility for one item declaration."""
|
||||
|
||||
if stripped.startswith("pub(crate) "):
|
||||
return "pub(crate)"
|
||||
if stripped.startswith("pub "):
|
||||
return "pub"
|
||||
return "private"
|
||||
|
||||
|
||||
def item_kind(stripped: str) -> str | None:
|
||||
"""Return the normalized declaration kind for spacing checks."""
|
||||
|
||||
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 item_leading_line(lines: list[str], declaration_index: int) -> int:
|
||||
"""Return the first rustdoc/attribute line attached to an item."""
|
||||
|
||||
cursor = declaration_index - 2
|
||||
while cursor >= 0:
|
||||
stripped = lines[cursor].strip()
|
||||
if stripped.startswith("///") or stripped.startswith("#["):
|
||||
cursor -= 1
|
||||
continue
|
||||
break
|
||||
return cursor + 2
|
||||
|
||||
|
||||
def item_end_line(masked_lines: list[str], depths: list[int], declaration_index: int, kind: str) -> tuple[int, bool]:
|
||||
"""Return the end line and whether the item owns a braced body."""
|
||||
|
||||
start_depth = depths[declaration_index - 1]
|
||||
if kind in {"const", "static", "type"}:
|
||||
for line_index in range(declaration_index - 1, len(masked_lines)):
|
||||
if ";" in masked_lines[line_index]:
|
||||
return line_index + 1, False
|
||||
return declaration_index, False
|
||||
body_started = False
|
||||
for line_index in range(declaration_index - 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_index, body_started
|
||||
|
||||
|
||||
def rust_items(lines: list[str], masked_lines: list[str], depths: list[int]) -> list[RustItem]:
|
||||
"""Return mechanically detected Rust items with source spans."""
|
||||
|
||||
result: list[RustItem] = []
|
||||
for idx, line in enumerate(lines, 1):
|
||||
stripped = line.strip()
|
||||
kind = item_kind(stripped)
|
||||
if kind is None:
|
||||
continue
|
||||
end, braced = item_end_line(masked_lines, depths, idx, kind)
|
||||
result.append(RustItem(kind, item_visibility(stripped), item_leading_line(lines, idx), idx, end, depths[idx - 1], braced))
|
||||
return result
|
||||
|
||||
|
||||
def item_parent(item: RustItem, items: list[RustItem]) -> RustItem | None:
|
||||
"""Return the smallest braced item that contains another item."""
|
||||
|
||||
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_item_spacing_and_nesting(relative: str, lines: list[str], masked_lines: list[str], depths: list[int]) -> list[Violation]:
|
||||
"""Enforce exact item separation and reject declarations nested in functions."""
|
||||
|
||||
violations: list[Violation] = []
|
||||
items = rust_items(lines, masked_lines, depths)
|
||||
for item in items:
|
||||
parent = item_parent(item, items)
|
||||
if parent is not None and parent.kind == "fn":
|
||||
body_depth = parent.depth + 1
|
||||
preceding = lines[parent.declaration:item.declaration - 1]
|
||||
preceding_depths = depths[parent.declaration:item.declaration - 1]
|
||||
returned = any(depth == body_depth and re.match(r"^\s*return\b.*;\s*$", line) is not None for line, depth in zip(preceding, preceding_depths, strict=True))
|
||||
if returned:
|
||||
violations.append(Violation("RUST-FMT-110", relative, item.declaration, f"item `{item.kind}` follows an unconditional function-level return; probable misplaced closing brace"))
|
||||
children: dict[tuple[int, int] | None, list[RustItem]] = {}
|
||||
for item in items:
|
||||
parent = item_parent(item, items)
|
||||
key = None if parent is None else (parent.declaration, parent.end)
|
||||
children.setdefault(key, []).append(item)
|
||||
for siblings in children.values():
|
||||
siblings.sort(key=lambda item: item.declaration)
|
||||
for previous, current in zip(siblings, siblings[1:]):
|
||||
# Do not infer spacing across unparsed syntax such as macro invocations.
|
||||
between = lines[previous.end:current.start - 1]
|
||||
if any(candidate.strip() for candidate in between):
|
||||
continue
|
||||
blank_count = sum(1 for candidate in between if not candidate.strip())
|
||||
same_homogeneous_block = previous.kind == current.kind and previous.kind in {"const", "static", "type"} and previous.visibility == current.visibility
|
||||
expected = 0 if same_homogeneous_block else 1
|
||||
if blank_count != expected:
|
||||
rule = "RUST-FMT-111" if expected == 1 else "RUST-FMT-112"
|
||||
expectation = "exactly one blank line between Rust items" if expected == 1 else "no blank line inside one homogeneous declaration block"
|
||||
violations.append(Violation(rule, relative, current.start, f"{expectation}; found {blank_count}"))
|
||||
return violations
|
||||
|
||||
|
||||
def audit_blank_lines_in_bodies(relative: str, lines: list[str], masked_lines: list[str], depths: list[int]) -> list[Violation]:
|
||||
"""Reject blank lines inside functions/methods and struct/enum bodies."""
|
||||
|
||||
@@ -277,9 +410,6 @@ def audit_top_level_const_blocks(relative: str, lines: list[str], depths: list[i
|
||||
name = match.group(2)
|
||||
if previous is not None:
|
||||
previous_line, previous_rank, previous_name = previous
|
||||
between = lines[previous_line:idx - 1]
|
||||
if any(not candidate.strip() for candidate in between):
|
||||
violations.append(Violation("RUST-FMT-102", relative, idx, "blank line inside one top-level const block"))
|
||||
if rank < previous_rank:
|
||||
violations.append(Violation("RUST-FMT-103", relative, idx, "const visibility order must be pub, pub(crate), then private"))
|
||||
if rank == previous_rank and natural_key(name) < natural_key(previous_name):
|
||||
@@ -404,6 +534,15 @@ def audit_file(root: pathlib.Path, path: pathlib.Path) -> list[Violation]:
|
||||
source_match = re.match(r"^pub(?:\(crate\))?\s+use\s+(?:self::)?([A-Za-z_][A-Za-z0-9_]*)::", stripped)
|
||||
if source_match is not None and source_match.group(1) in local_modules and not re.match(r"^pub(?:\(crate\))?\s+use\s+self::", stripped):
|
||||
violations.append(Violation("RUST-IMPORT-106", relative, idx, "crate-root internal re-export must start with self::"))
|
||||
public_exports = [item for item in exports if item[1] == "pub"]
|
||||
crate_exports = [item for item in exports if item[1] == "pub(crate)"]
|
||||
if public_exports and crate_exports:
|
||||
public_end = public_exports[-1][0]
|
||||
crate_start = crate_exports[0][0]
|
||||
transition = lines[public_end:crate_start - 1]
|
||||
blank_count = sum(1 for candidate in transition if not candidate.strip())
|
||||
if blank_count != 1:
|
||||
violations.append(Violation("RUST-FMT-113", relative, crate_start, f"pub use and pub(crate) use blocks require exactly one blank line; found {blank_count}"))
|
||||
crate_seen = False
|
||||
previous_by_visibility: dict[str, tuple[int, str]] = {}
|
||||
for idx, visibility, symbol in exports:
|
||||
@@ -422,6 +561,7 @@ def audit_file(root: pathlib.Path, path: pathlib.Path) -> list[Violation]:
|
||||
violations.append(Violation("RUST-FMT-107", relative, idx, f"{visibility} use block is not alphabetically ordered by exported symbol"))
|
||||
previous_by_visibility[visibility] = (idx, symbol)
|
||||
violations.extend(audit_blank_lines_in_bodies(relative, lines, masked_lines, depths))
|
||||
violations.extend(audit_item_spacing_and_nesting(relative, lines, masked_lines, depths))
|
||||
violations.extend(audit_module_order(relative, lines, depths))
|
||||
violations.extend(audit_top_level_const_blocks(relative, lines, depths))
|
||||
return violations
|
||||
|
||||
Reference in New Issue
Block a user