0.0.3-alpha.3
This commit is contained in:
144
scripts/_rust_audit_common.py
Normal file
144
scripts/_rust_audit_common.py
Normal file
@@ -0,0 +1,144 @@
|
||||
# file: scripts/_rust_audit_common.py
|
||||
# version: 1
|
||||
|
||||
"""Small shared lexical and file-selection helpers for read-only Rust audits."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pathlib
|
||||
|
||||
_IGNORED = frozenset({".git", ".venv", "__pycache__", "target", "builds"})
|
||||
|
||||
|
||||
def selected_files(raw_paths: list[str]) -> tuple[list[pathlib.Path], list[str]]:
|
||||
"""Select explicit Rust files and directories, without following generated trees."""
|
||||
|
||||
files: set[pathlib.Path] = set()
|
||||
errors: list[str] = []
|
||||
for raw in raw_paths:
|
||||
path = pathlib.Path(raw)
|
||||
if path.is_dir():
|
||||
files.update(
|
||||
candidate
|
||||
for candidate in path.rglob("*.rs")
|
||||
if candidate.is_file() and not (set(candidate.parts) & _IGNORED)
|
||||
)
|
||||
elif path.is_file() and path.suffix == ".rs":
|
||||
files.add(path)
|
||||
else:
|
||||
errors.append(f"unsupported or missing Rust path: {path}")
|
||||
return sorted(files), errors
|
||||
|
||||
|
||||
def mask_source(text: str) -> tuple[str, set[int]]:
|
||||
"""Mask comments and literals, preserving offsets and structural punctuation.
|
||||
|
||||
Return line numbers whose first character is inside a multiline literal, so
|
||||
an empty line in a raw string cannot be mistaken for an empty code line.
|
||||
This is a lexical helper, not a full Rust parser or a compiler substitute.
|
||||
"""
|
||||
|
||||
output = list(text)
|
||||
protected: set[int] = set()
|
||||
index = 0
|
||||
line = 1
|
||||
state = "code"
|
||||
block_depth = 0
|
||||
raw_hashes = 0
|
||||
while index < len(text):
|
||||
char = text[index]
|
||||
nxt = text[index + 1] if index + 1 < len(text) else ""
|
||||
if char == "\n":
|
||||
line += 1
|
||||
if state in {"string", "raw_string"}:
|
||||
protected.add(line)
|
||||
if state == "line_comment":
|
||||
state = "code"
|
||||
index += 1
|
||||
continue
|
||||
if state == "code":
|
||||
if char == "/" and nxt in {"/", "*"}:
|
||||
output[index:index + 2] = [" ", " "]
|
||||
state = "line_comment" if nxt == "/" else "block_comment"
|
||||
block_depth = 1 if nxt == "*" else 0
|
||||
index += 2
|
||||
continue
|
||||
if char == '"':
|
||||
output[index] = " "
|
||||
state = "string"
|
||||
index += 1
|
||||
continue
|
||||
if char == "'":
|
||||
# Lifetimes have no nearby closing quote; char literals do.
|
||||
probe = index + 1
|
||||
escaped = False
|
||||
found = False
|
||||
while probe < min(len(text), index + 8) and text[probe] != "\n":
|
||||
if text[probe] == "'" and not escaped:
|
||||
found = True
|
||||
break
|
||||
escaped = text[probe] == "\\" and not escaped
|
||||
probe += 1
|
||||
if found:
|
||||
output[index] = " "
|
||||
state = "char"
|
||||
index += 1
|
||||
continue
|
||||
# Also covers r#"..."#, br#"..."# and cr#"..."#.
|
||||
if char == "r" and (index == 0 or text[index - 1] not in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"):
|
||||
probe = index + 1
|
||||
while probe < len(text) and text[probe] == "#":
|
||||
probe += 1
|
||||
if probe < len(text) and text[probe] == '"':
|
||||
output[index:probe + 1] = [" "] * (probe + 1 - index)
|
||||
raw_hashes = probe - index - 1
|
||||
state = "raw_string"
|
||||
index = probe + 1
|
||||
continue
|
||||
index += 1
|
||||
continue
|
||||
if state == "line_comment":
|
||||
output[index] = " "
|
||||
elif state == "block_comment":
|
||||
output[index] = " "
|
||||
if char == "/" and nxt == "*":
|
||||
output[index + 1] = " "
|
||||
block_depth += 1
|
||||
index += 2
|
||||
continue
|
||||
if char == "*" and nxt == "/":
|
||||
output[index + 1] = " "
|
||||
block_depth -= 1
|
||||
if block_depth == 0:
|
||||
state = "code"
|
||||
index += 2
|
||||
continue
|
||||
elif state in {"string", "char"}:
|
||||
output[index] = " "
|
||||
if char == "\\" and index + 1 < len(text):
|
||||
if text[index + 1] != "\n":
|
||||
output[index + 1] = " "
|
||||
index += 2
|
||||
continue
|
||||
if char == ('"' if state == "string" else "'"):
|
||||
state = "code"
|
||||
elif state == "raw_string":
|
||||
output[index] = " "
|
||||
if char == '"' and text.startswith("#" * raw_hashes, index + 1):
|
||||
output[index + 1:index + 1 + raw_hashes] = [" "] * raw_hashes
|
||||
index += 1 + raw_hashes
|
||||
state = "code"
|
||||
continue
|
||||
index += 1
|
||||
return "".join(output), protected
|
||||
|
||||
|
||||
def line_depths(masked: str) -> list[int]:
|
||||
"""Return brace depth at the start of each line, excluding literals."""
|
||||
|
||||
depths: list[int] = []
|
||||
depth = 0
|
||||
for line in masked.splitlines():
|
||||
depths.append(depth)
|
||||
depth = max(0, depth + line.count("{") - line.count("}"))
|
||||
return depths
|
||||
83
scripts/audit_rust_blank_lines.py
Normal file
83
scripts/audit_rust_blank_lines.py
Normal file
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env python3
|
||||
# file: scripts/audit_rust_blank_lines.py
|
||||
# version: 1
|
||||
|
||||
"""Audit blank-line spacing in explicitly selected Rust sources."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
|
||||
from _rust_audit_common import line_depths, mask_source, selected_files
|
||||
|
||||
_BODY = re.compile(r"^\s*(?:(?:pub(?:\([^)]*\))?|async|const|unsafe|extern|default)\s+)*(fn|struct|enum)\b")
|
||||
|
||||
|
||||
def audit_file(path: pathlib.Path) -> list[str]:
|
||||
"""Check successive empty lines and empty function/struct/enum body lines."""
|
||||
|
||||
text = path.read_text(encoding="utf-8")
|
||||
lines = text.splitlines()
|
||||
masked, protected = mask_source(text)
|
||||
masked_lines = masked.splitlines()
|
||||
depths = line_depths(masked)
|
||||
errors: list[str] = []
|
||||
previous_blank = False
|
||||
pending: tuple[str, int] | None = None
|
||||
active: tuple[str, int] | None = None
|
||||
|
||||
for number, (original, code, depth) in enumerate(zip(lines, masked_lines, depths, strict=True), 1):
|
||||
blank = not original.strip() and number not in protected
|
||||
if blank and previous_blank:
|
||||
errors.append(f"{path}:{number}: consecutive blank Rust source lines")
|
||||
previous_blank = blank
|
||||
|
||||
if active is not None and depth <= active[1]:
|
||||
active = None
|
||||
if active is not None and blank:
|
||||
errors.append(f"{path}:{number}: blank line inside {active[0]} body")
|
||||
|
||||
if active is None and pending is None:
|
||||
match = _BODY.match(code)
|
||||
if match is not None:
|
||||
pending = (match.group(1), depth)
|
||||
if pending is not None:
|
||||
if "{" in code:
|
||||
active = pending
|
||||
pending = None
|
||||
elif ";" in code:
|
||||
pending = None
|
||||
return errors
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""Run the read-only 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 blank-line 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 blank-line audit: cannot read {path}: {error}", file=sys.stderr)
|
||||
return 2
|
||||
for error in errors:
|
||||
print(error, file=sys.stderr)
|
||||
if errors:
|
||||
print(f"Rust blank-line audit: {len(errors)} error(s) across {len(files)} file(s)", file=sys.stderr)
|
||||
return 1
|
||||
print(f"Rust blank-line audit: clean ({len(files)} file(s))")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
109
scripts/audit_rust_const_blocks.py
Normal file
109
scripts/audit_rust_const_blocks.py
Normal file
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env python3
|
||||
# file: scripts/audit_rust_const_blocks.py
|
||||
# version: 1
|
||||
|
||||
"""Audit top-level Rust const blocks for visibility, names 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
|
||||
|
||||
_CONST = re.compile(r"^\s*(?:(pub|pub\(crate\))\s+)?const\s+([A-Za-z_][A-Za-z0-9_]*)\s*:")
|
||||
|
||||
|
||||
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 _end_of_const(lines: list[str], start: int) -> int:
|
||||
"""Find the terminating semicolon outside brackets and initializer braces."""
|
||||
|
||||
brackets: list[str] = []
|
||||
closing = {"}": "{", ")": "(", "]": "["}
|
||||
for index in range(start, len(lines)):
|
||||
for char in lines[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 index
|
||||
return start
|
||||
|
||||
|
||||
def audit_file(path: pathlib.Path) -> list[str]:
|
||||
"""Compare adjacent top-level const declarations in homogeneous blocks."""
|
||||
|
||||
text = path.read_text(encoding="utf-8")
|
||||
lines = text.splitlines()
|
||||
masked, _ = mask_source(text)
|
||||
code_lines = masked.splitlines()
|
||||
depths = line_depths(masked)
|
||||
errors: list[str] = []
|
||||
previous: tuple[int, int, int, str] | None = None # end, line, visibility rank, name
|
||||
index = 0
|
||||
while index < len(lines):
|
||||
code = code_lines[index]
|
||||
if depths[index] != 0:
|
||||
index += 1
|
||||
continue
|
||||
match = _CONST.match(code)
|
||||
if match is not None:
|
||||
visibility, name = match.groups()
|
||||
rank = {"pub": 0, "pub(crate)": 1, None: 2}[visibility]
|
||||
if previous is not None:
|
||||
end, _, last_rank, last_name = previous
|
||||
if rank < last_rank:
|
||||
errors.append(f"{path}:{index + 1}: const visibility must be pub, pub(crate), private")
|
||||
if rank == last_rank and _natural_key(name) < _natural_key(last_name):
|
||||
errors.append(f"{path}:{index + 1}: const names out of alphabetical order: {last_name}, {name}")
|
||||
between = lines[end + 1:index]
|
||||
if all(not candidate.strip() for candidate in between):
|
||||
expected = 0 if rank == last_rank else 1
|
||||
if len(between) != expected:
|
||||
errors.append(f"{path}:{index + 1}: expected {expected} blank line(s) between const declarations")
|
||||
end = _end_of_const(code_lines, index)
|
||||
previous = (end, index + 1, rank, name)
|
||||
index = end + 1
|
||||
continue
|
||||
# Comments, attributes and whitespace attach to the surrounding block.
|
||||
if code.strip() and not lines[index].lstrip().startswith(("//", "/*", "*", "#[")):
|
||||
previous = None
|
||||
index += 1
|
||||
return errors
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""Run the read-only top-level const-block audit."""
|
||||
|
||||
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 const-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 const-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 const-block audit: {len(errors)} error(s) across {len(files)} file(s)", file=sys.stderr)
|
||||
return 1
|
||||
print(f"Rust const-block audit: clean ({len(files)} file(s))")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user