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

148
scripts/audit_rust_general_rules.py Executable file → Normal file
View 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