Files
benilla-octo/scripts/pass-span-lint.py
T
SamandClaude Opus 5.5 7c17921601 comments: the generators, the tools and the reference table headers
Docstrings keep their usage and exit codes; table headers keep the
column contract and the client addresses and lose the dates, the
correction history and every path into another repository. A
generated header changes in its generator's template, so each of the
four generators still reproduces its output byte for byte, and no data
row changed in any of the seven tables.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 19:32:13 +07:00

74 lines
3.0 KiB
Python
Executable File

#!/usr/bin/env python3
"""Refuse a second open `pass_span` on the same render pass (a wgpu validation error on Vulkan).
bevy's `DiagnosticsRecorder::pass_span` is a timestamp pair plus a pipeline-statistics query, and
wgpu allows one statistics query active at a time: a second `pass_span` on a pass whose span is
still open is a validation error on backends with `PIPELINE_STATISTICS_QUERY`, which in wgpu 27 is
Vulkan alone (Metal lacks it; wgpu-hal's DX12 arm has the line commented out). On Metal and DX12
the nesting is a silent no-op, so no macOS build, test or run sees it.
The shape it refuses, per function: `let g = <recorder>.pass_span(&mut P, …)` followed by another
`.pass_span(&mut P, …)` before `g.end(&mut P)`. Timestamp-only spans (`time_span`) nest fine and
are not looked at. Lexical, per file, reset at every `fn`: a span never outlives its function.
Usage: scripts/pass-span-lint.py [path …] (default: every crates/**/*.rs)
Exit: 0 clean, 1 findings.
"""
from __future__ import annotations
import pathlib
import re
import sys
OPEN = re.compile(r"\blet\s+(?:mut\s+)?(\w+)\s*=\s*[\w.]+\.pass_span\(\s*&mut\s+(\w+)\s*,")
ANY_OPEN = re.compile(r"\.pass_span\(\s*&mut\s+(\w+)\s*,")
END = re.compile(r"\b(\w+)\.end\(\s*&mut\s+(\w+)\s*\)")
FN = re.compile(r"^\s*(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?(?:unsafe\s+)?fn\s+\w+")
def lint(path: pathlib.Path) -> list[str]:
findings: list[str] = []
open_spans: list[tuple[str, str, int]] = [] # (guard, pass, line)
for lineno, raw in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
line = raw.split("//", 1)[0]
if FN.match(line):
open_spans.clear()
m = OPEN.search(line)
if m:
guard, pass_name = m.group(1), m.group(2)
else:
m = ANY_OPEN.search(line)
guard, pass_name = (None, m.group(1)) if m else (None, None)
if pass_name is not None:
for outer_guard, outer_pass, outer_line in open_spans:
if outer_pass == pass_name:
findings.append(
f"{path}:{lineno}: a second pass_span on `{pass_name}` while "
f"`{outer_guard}` (line {outer_line}) is still open — one "
f"pipeline-statistics query at a time (2258, B390)"
)
if guard is not None:
open_spans.append((guard, pass_name, lineno))
e = END.search(line)
if e:
open_spans = [s for s in open_spans if s[0] != e.group(1)]
return findings
def main() -> int:
args = [pathlib.Path(a) for a in sys.argv[1:]] or [pathlib.Path("crates")]
files: list[pathlib.Path] = []
for a in args:
files.extend(sorted(a.rglob("*.rs")) if a.is_dir() else [a])
findings = [f for p in files for f in lint(p)]
for f in findings:
print(f)
print(f"pass-span-lint: {'clean' if not findings else f'{len(findings)} finding(s)'} — "
f"{len(files)} file(s)")
return 1 if findings else 0
if __name__ == "__main__":
sys.exit(main())