Files
improvise/scripts/check-comment-runs.py
T
Edward Langley cd094b6dfb fix(kb): the dj1l cases discriminate, and a replaced rule cannot silently become an add
Five findings from the second review round, and the first is this PR's own
standard applied to its own P1.

**The two fixes improvise-dj1l names had no discriminating test.** Reverting
`has_key` to unification, or dropping `canon_key` from the duplicate gate, left
kb-test at 27 passed. Case 11 was carried by `check_total` refusing with a
DIFFERENT message, and case 10 by the other half of the fix. Both now assert
which refusal fired (`said()`), plus two cases aimed at each half: a rm key with
a variable in a non-final position must be refused as ABSENT rather than
matching everything, and a keyed FACT whose key holds a variable must ADD
without excluding the siblings it would unify with. Each proved by reverting
exactly one fix and watching exactly that case fail.

**Keying a rule on its whole clause made `replace` on an edited rule silently
become `add`** -- both arms live, the stale one still firing, and `check_total`
cannot see it because `NO + NF` is correct arithmetic for an add. Reachable on
the 15 `kb_error/1` arms and the `kb_text/2` rules, so editing the check
improvise-2f97 adds is the first thing to hit it. `check_rule_replace` refuses
and names the route (rm, then add); the negative arm -- replace of a rule
present verbatim -- is tested, or the refusal would just be "replace never
works on rules".

**The `decision(` arm was dead code.** `cited_name` took the first field, and
the repo spells `decision(<ticket>, <name>)`, whose ticket id fails
`name_shaped/1` on the hyphen -- so that marker could never fire for a real
citation. A second `cited_name` clause takes the second field. All three
markers are now exercised.

Minor, from the same round: `_JS_TEST` no longer accepts `test.only`, which
disables every other test in its file, with a case asserting a heading cannot
be satisfied by one; and `node-test-test.sh`'s last case no longer duplicates
an earlier one.

Filed rather than fixed: improvise-w8ih, extracting `web/assemble.sh`'s gate so
its own absence is visible.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-20 17:01:33 -07:00

853 lines
28 KiB
Python
Executable File

#!/usr/bin/env python3
"""Fail if any .rs file carries more than two consecutive comment lines.
The rule (user, 2026-09-20): reasoning lives in context/ prose, not in code.
A comment run is at most two lines, optionally naming a context/ file for the
rest. See context/code/ for where the prose went, and improvise-9c8c for why.
Counts `///`, `//!` and `//` alike. Attributes are not comments and are
ignored.
A ``` fence SUSPENDS the count rather than ending the run: the fenced body is
exempt (a doctest is a running test, not prose) but the prose on either side
belongs to ONE run and is summed. Two lines above a fence and two below is a
run of four, which is over the limit -- and that is deliberate, because a
reader meets it as one uninterrupted comment block. See --list-doctests.
Note rustdoc executes a bare ``` or ```rust fence and does NOT execute
```text, ```ignore or ```no_run. Only a runnable fence earns the exemption;
the rest is prose wearing a fence and the cap applies to it.
"""
import argparse
import os
import re
import sys
LIMIT = 2
_RAW_STR = re.compile(r'(?:b|c)?r(#*)"')
_CHAR_LIT = re.compile(r"'(?:\\.|[^'\\])'")
_DOC_ATTR = re.compile(r"#!?\[\s*doc\s*=")
def scan(lines):
"""Classify each line, respecting Rust strings and NESTED block comments.
Returns per line: has_code, has_comment, the `//` body, the CODE-ONLY text
(no string or comment content), whether comment content leads the line, and
whether the line began inside a block comment. See context/code/tooling.md.
"""
out = []
in_block = 0
in_raw = None
in_str = False
for raw in lines:
i, n = 0, len(raw)
started_in_block = in_block > 0
has_code = False
has_comment = started_in_block
body = None
code = []
leads = None
while i < n:
if in_block:
has_comment = True
leads = True if leads is None else leads
if raw.startswith("/*", i):
in_block += 1
i += 2
elif raw.startswith("*/", i):
in_block -= 1
i += 2
else:
i += 1
continue
if in_raw is not None:
closer = '"' + "#" * in_raw
if raw.startswith(closer, i):
in_raw = None
i += len(closer)
else:
i += 1
has_code = True
leads = False if leads is None else leads
continue
if in_str:
has_code = True
leads = False if leads is None else leads
if raw[i] == "\\":
i += 2
continue
if raw[i] == '"':
in_str = False
i += 1
continue
if raw[i].isspace():
i += 1
continue
if raw.startswith("//", i):
has_comment = True
leads = True if leads is None else leads
if not has_code:
body = raw[i:]
break
if raw.startswith("/*", i):
has_comment = True
leads = True if leads is None else leads
in_block = 1
i += 2
continue
m = _RAW_STR.match(raw, i)
if m:
in_raw = len(m.group(1))
has_code = True
leads = False if leads is None else leads
code.append('"')
i = m.end()
continue
m = _CHAR_LIT.match(raw, i)
if m:
has_code = True
leads = False if leads is None else leads
i = m.end()
continue
if raw[i] == '"':
in_str = True
has_code = True
leads = False if leads is None else leads
i += 1
continue
has_code = True
leads = False if leads is None else leads
code.append(raw[i])
i += 1
out.append((has_code, has_comment, body, "".join(code), bool(leads), started_in_block))
return out
def _kind(body):
if body is None:
return "//"
if body.startswith("//!"):
return "//!"
return "///" if body.startswith("///") else "//"
def comment_runs(lines):
"""Yield (start_line_number, length) for each run, skipping runnable doctests.
Every question here is asked of the SCANNER, never of the raw line: an
attribute's bracket depth is counted over code only, because `#[cfg(feature
= "[")]` otherwise exempted the whole file. See context/code/tooling.md.
"""
scanned = scan(lines)
run_start, run_len, run_kind = None, 0, None
in_fence = fence_exempt = False
attr_depth = 0
attr_inner = False
inner_pending = False
def add(n, kind):
nonlocal run_start, run_len, run_kind
if run_start is None:
run_start, run_len, run_kind = n, 0, kind
run_len += 1
for n, (has_code, has_comment, body, code, leads, in_block_at_start) in enumerate(scanned, 1):
# A line that CLOSES a block comment carries comment text even when code
# follows it; a line that opens one after code does not.
is_comment = has_comment and (not has_code or (in_block_at_start and leads))
code_s = code.strip()
if attr_depth > 0:
attr_depth += code.count("[") - code.count("]")
if is_comment:
add(n, _kind(body))
if attr_depth <= 0:
attr_depth = 0
inner_pending = inner_pending or attr_inner
continue
if not is_comment and (code_s.startswith("#[") or code_s.startswith("#![")):
# `#[doc = "..."]` and `#![doc = "..."]` ARE comments; `#[doc(hidden)]`
# is not, which an unanchored `#[doc` prefix could not tell apart.
if _DOC_ATTR.match(code_s):
add(n, "//!" if code_s.startswith("#![") else "///")
continue
inner = code_s.startswith("#![")
depth = code.count("[") - code.count("]")
if depth > 0:
attr_depth, attr_inner = depth, inner
continue
inner_pending = inner_pending or inner
continue
if is_comment:
kind = _kind(body)
# An inner attribute ends a run only where the DOC KIND changes: a
# crate doc and an item doc are two blocks, `//!` either side is one.
if inner_pending:
inner_pending = False
if run_kind is not None and kind != run_kind and run_start is not None and run_len > 0:
yield run_start, run_len
run_start, run_len, run_kind = None, 0, None
doc = kind in ("///", "//!")
text = body.lstrip("/!").strip() if body is not None else ""
if doc and text.startswith("```"):
if in_fence:
exempt_here = fence_exempt
in_fence = fence_exempt = False
else:
info = text[3:].strip().lower()
in_fence = True
fence_exempt = info in ("", "rust") or info.startswith("rust,")
exempt_here = fence_exempt
if exempt_here:
continue
add(n, kind)
continue
if doc and in_fence and fence_exempt:
continue
add(n, kind)
else:
in_fence = fence_exempt = False
inner_pending = False
if run_start is not None and run_len > 0:
yield run_start, run_len
run_start, run_len, run_kind = None, 0, None
if run_start is not None and run_len > 0:
yield run_start, run_len
def rust_files(roots):
for root in roots:
for dirpath, dirnames, filenames in os.walk(root):
dirnames[:] = [d for d in dirnames if d != "target"]
for f in filenames:
if f.endswith(".rs"):
yield os.path.join(dirpath, f)
_TEST_ATTRS = ("test", "tokio::test", "async_std::test", "wasm_bindgen_test")
def _is_test_attribute(line):
"""True only for an attribute that actually marks a test.
Matched by exact path, never by substring: `#[cfg(test)]` contains "test"
and marks a compilation condition, not a test.
"""
body = line.strip()
if not body.startswith("#["):
return False
body = body[2:].rstrip()
body = body[:-1] if body.endswith("]") else body
return body.strip().strip(":") in _TEST_ATTRS
EVIDENCE_DIR = os.path.join("context", "test-evidence")
EVIDENCE_CITE = "test-evidence"
JS_ROOTS = [os.path.join("harness", "tests"), os.path.join("web", "tests")]
_RUST_NAME = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
_QUOTED_NAME = re.compile(r'^"(.+)"$')
# Only a RUNNABLE LEAF test. `.skip`, `.todo`, `.describe` and `.only` are all
# excluded: the first three name no executed assertion, and `.only` disables
# every OTHER test in its file, so a heading satisfied by one is a heading
# satisfied by a file that stopped running (review, PR #69).
_JS_TEST = re.compile(
r"""(?:^|[^\w.])(?:test|it)\s*\(\s*(['"`])(.+?)\1"""
)
def evidence_claims(text):
"""Yield (kind, name, lineno) for every heading in an evidence file.
`### ` and `#### ` alike, because the browser entries have to use `####`
and were invisible to a check that collected only `### ` (improvise-5ray).
A heading is a CLAIM when it is shaped like a test name -- a bare
identifier (Rust) or a quoted string (JS). Anything else is prose about
the section: `Helper - x`, `Note - y`, a file's own scope. That shape rule
is the whole discrimination, so see context/code/tooling.md before loosening
it: a heading that is neither shape names nothing and is checked by nobody.
"""
for n, raw in enumerate(text.split("\n"), 1):
s = raw.strip()
if not (s.startswith("### ") or s.startswith("#### ")):
continue
body = s.lstrip("#").strip().strip("`").strip()
m = _QUOTED_NAME.match(body)
if m:
yield "js", m.group(1), n
elif _RUST_NAME.match(body):
yield "rust", body, n
def rust_tests(text):
"""(tests defined here, tests whose doc comment cites the evidence dir).
A heading must name a TEST, not merely a function. Accepting any `fn` let a
`###` naming a fixture or a helper pass, so an evidence file could report
itself satisfied while naming nothing that runs (found on PR #39).
"""
defined, citing = set(), set()
annotated = False
doc = []
# A `proptest!` block accepts its bodies as tests -- but only INSIDE the
# block. A bare flag was never cleared, so the first proptest! in a file
# made every later fn, helpers included, count as a test: a silent
# success in the gate itself (found by review on PR #40). Track the
# block's braces and leave when they balance.
proptest_depth = None
for raw in text.split("\n"):
s = raw.strip()
if proptest_depth is not None:
proptest_depth += raw.count("{") - raw.count("}")
if proptest_depth <= 0:
proptest_depth = None
elif s.startswith("proptest!"):
proptest_depth = raw.count("{") - raw.count("}")
if proptest_depth <= 0:
proptest_depth = None if "{" in raw else 0
in_proptest = proptest_depth is not None
if s.startswith("//"):
doc.append(s)
continue
# ANCHOR the attribute. `"test" in s` also matched `#[cfg(test)]`,
# so every cfg-gated fixture cleared a gate written to reject
# exactly those -- the third instance of the unanchored-substring
# class in this campaign, after the bd audit's exclusion list.
if s.startswith("#[") and _is_test_attribute(s):
annotated = True
continue
if s.startswith("fn ") or s.startswith("pub fn ") or s.startswith("async fn "):
if annotated or in_proptest:
name = s.split("fn ", 1)[1].split("(")[0].split("<")[0].strip()
defined.add(name)
if any(EVIDENCE_CITE in d for d in doc):
citing.add(name)
annotated = False
doc = []
elif s and not s.startswith("#["):
annotated = False
doc = []
return defined, citing
def js_tests(text):
"""(tests defined here, tests whose preceding comment cites the evidence dir).
The JS half exists because `defined` held only Rust `fn` names, so a
`####` naming a Playwright or node test could never have resolved even if
the heading had been collected -- the convention that kept the checker
quiet was the same one that made it blind (improvise-5ray).
"""
defined, citing = set(), set()
doc = []
for raw in text.split("\n"):
s = raw.strip()
m = _JS_TEST.search(raw)
if m:
defined.add(m.group(2))
if any(EVIDENCE_CITE in d for d in doc):
citing.add(m.group(2))
doc = []
continue
if s.startswith("//") or s.startswith("*") or s.startswith("/*"):
doc.append(s)
elif s:
doc = []
return defined, citing
def js_files(roots):
for root in roots:
for dirpath, dirnames, filenames in os.walk(root):
dirnames[:] = [d for d in dirnames if d != "node_modules"]
for f in filenames:
if f.endswith(".spec.js") or f.endswith(".test.js"):
yield os.path.join(dirpath, f)
def evidence_verdict(claims, defined, citing):
"""(orphans, unstated) -- the two directions, keyed by ("rust"|"js", name).
`orphans` is evidence naming a test nobody can find. `unstated` is the
reverse obligation (improvise-lyyc): a test that POINTS AT the evidence
file must appear in it, or a reader following the pointer arrives nowhere.
Only that narrower obligation, because most tests have no evidence and
should not be forced to. Note what neither direction can see: an entry can
exist and still describe its subject backwards, which is discipline rather
than a gate.
"""
orphans = {k: v for k, v in claims.items() if k[1] not in defined[k[0]]}
unstated = {}
for kind, names in citing.items():
for name in names:
if (kind, name) not in claims:
unstated.setdefault((kind, name), []).extend(sorted(names[name]))
return orphans, unstated
def check_evidence(roots):
"""Both directions over context/test-evidence/. See evidence_verdict."""
if not os.path.isdir(EVIDENCE_DIR):
print("no context/test-evidence/ yet: nothing to check")
return 0
claims = {}
for dirpath, _, filenames in os.walk(EVIDENCE_DIR):
for f in sorted(filenames):
if not f.endswith(".md"):
continue
path = os.path.join(dirpath, f)
text = open(path, encoding="utf8", errors="replace").read()
for kind, name, n in evidence_claims(text):
claims.setdefault((kind, name), []).append(f"{path}:{n}")
defined = {"rust": set(), "js": set()}
citing = {"rust": {}, "js": {}}
for path in rust_files(roots):
d, c = rust_tests(open(path, encoding="utf8", errors="replace").read())
defined["rust"] |= d
for name in c:
citing["rust"].setdefault(name, []).append(path)
for path in js_files(JS_ROOTS):
d, c = js_tests(open(path, encoding="utf8", errors="replace").read())
defined["js"] |= d
for name in c:
citing["js"].setdefault(name, []).append(path)
orphans, unstated = evidence_verdict(claims, defined, citing)
if not orphans and not unstated:
print(f"test-evidence: {len(claims)} entries, all name a test that exists; "
f"{sum(len(v) for v in citing.values())} tests cite the file and all have one")
return 0
if orphans:
print(f"{len(orphans)} test-evidence entries name no test:\n", file=sys.stderr)
for (kind, name), where in sorted(orphans.items()):
print(f" [{kind}] {name} ({', '.join(where)})", file=sys.stderr)
if unstated:
print(f"\n{len(unstated)} tests cite {EVIDENCE_DIR} and have no entry there:\n",
file=sys.stderr)
for (kind, name), where in sorted(unstated.items()):
print(f" [{kind}] {name} ({', '.join(where)})", file=sys.stderr)
return 1
SELF_TEST_CASES = [
("a plain run of three", 3, """
// one
// two
// three
fn f() {}
"""),
("a block comment of six", 6, """
/* one
two
three
four
five
*/
fn f() {}
"""),
("a rustdoc block comment of six", 6, """
/** one
* two
* three
* four
* five
*/
fn f() {}
"""),
("an unclosed fence must not exempt the rest of the file", 6, """
/// opens a fence and never closes it
/// ```
/// let x = 1;
fn a() {}
/// one
/// two
/// three
/// four
/// five
/// six
fn b() {}
"""),
("an attribute must not split a run", 4, """
/// one
/// two
#[inline]
/// three
/// four
fn c() {}
"""),
("a non-runnable fence is prose and counts", 4, """
/// one
/// ```text
/// two
/// ```
fn d() {}
"""),
("a fence in a PLAIN // comment earns no exemption", 7, """
// ```
// one
// two
// three
// four
// five
// ```
fn f() {}
"""),
("a block comment opened after code on the same line", 5, """
fn f() {} /* one
two
three
four
five
*/
"""),
("nested block comments do not close early", 7, """
/* /* */
one
two
three
four
five
*/
fn f() {}
"""),
("a multi-line attribute does not split a run", 4, """
/// one
/// two
#[cfg(all(
unix,
target_pointer_width = "64"
))]
/// three
/// four
fn f() {}
"""),
("an attribute inside a fence does not reopen it", 5, """
/// one
/// ```
/// let x = 1;
#[inline]
/// ```
/// hidden A
/// hidden B
/// hidden C
/// hidden D
fn g() {}
"""),
("#[doc = ...] is the one attribute that is a comment", 3, """
#[doc = "one"]
#[doc = "two"]
#[doc = "three"]
fn f() {}
"""),
("#![doc = ...] is the inner spelling of the same thing", 5, """
#![doc = "one"]
#![doc = "two"]
#![doc = "three"]
#![doc = "four"]
#![doc = "five"]
fn f() {}
"""),
("an attribute holding a bracket in a string exempts nothing", 6, """
#[cfg(feature = "[")]
fn g() {}
// a
// b
// c
// d
// e
// f
fn f() {}
"""),
("an inner attribute between two //! blocks does not split them", 4, """
//! one
//! two
#![allow(clippy::needless_return)]
//! three
//! four
fn f() {}
"""),
("a block comment closing on a line with code still counts", 3, """
/* one
two
three */ fn f() {}
"""),
("a char literal holding a quote does not open a string", 3, """
pub const C: char = '"';
// one
// two
// three
fn f() {}
"""),
]
SELF_TEST_CLEAN = [
("two lines is the limit", """
// one
// two
fn f() {}
"""),
("a runnable doctest is exempt", """
/// one
/// ```
/// let x = 1;
/// assert_eq!(x, 1);
/// ```
/// two
fn f() {}
"""),
("a closed fence does not leak into a later comment", """
/// one
/// ```
/// let x = 1;
/// ```
fn a() {}
// two
// three
fn b() {}
"""),
("a block comment inside a raw string is not a comment", """
pub const S: &str = r#"x"y /* not a comment
one
two
three
four
"#;
fn a() {}
"""),
("// inside a raw string is not a comment", """
pub const S: &str = r#"x"y // not a comment
// nor this
// nor this
// nor this
"#;
fn a() {}
"""),
("a crate doc and an item doc are separated by the inner attribute", """
//! crate one
//! crate two
#![allow(clippy::needless_return)]
/// item one
/// item two
pub fn go() {}
"""),
("// inside a multi-line plain string is not a comment", """
pub const S: &str = "open \\
// one
// two
// three
close";
fn a() {}
"""),
("#[doc(hidden)] is an attribute, not a comment", """
/// one
/// two
#[doc(hidden)]
pub fn f() {}
"""),
("an attribute's brackets close, so later code still ends a run", """
/// one
/// two
#[cfg(all(
unix,
))]
fn f() {}
/// three
/// four
fn g() {}
"""),
]
# (name, evidence markdown, rust source, js source, orphans, unstated).
#
# The evidence checker had no self-test at all, which is how both halves of
# improvise-5ray and improvise-lyyc survived: the shape that silenced the
# checker was the shape it could not see. Each case below is an injection.
SELF_TEST_EVIDENCE = [
("a #### heading naming no test is an orphan",
'#### "a test that does not exist anywhere"\n',
"", "", ["a test that does not exist anywhere"], []),
("a #### heading naming a JS test that exists is satisfied",
'#### "a peer that cannot be reached is named"\n',
"", 'test("a peer that cannot be reached is named", async () => {});\n', [], []),
("a quoted heading can never be satisfied by a Rust fn of that name",
'#### "a_rust_name"\n',
"#[test]\nfn a_rust_name() {}\n", "", ["a_rust_name"], []),
("a prose #### heading claims nothing",
"#### Helper — `addressed`\n#### Note — the basis_check module\n"
"#### harness/tests/shell.spec.js — the file's own scope\n",
"", "", [], []),
("a ### naming a helper rather than a test is still an orphan",
"### a_helper\n", "fn a_helper() {}\n", "", ["a_helper"], []),
("a test that cites the evidence file and has no entry is caught",
"", "/// See `context/test-evidence/clients.md`.\n#[test]\nfn a_cited_test() {}\n",
"", [], ["a_cited_test"]),
("the same test with an entry is satisfied",
"### a_cited_test\n",
"/// See `context/test-evidence/clients.md`.\n#[test]\nfn a_cited_test() {}\n",
"", [], []),
("a NON-test fn citing the file is under no obligation",
"", "/// See `context/test-evidence/clients.md`.\nfn a_helper() {}\n", "", [], []),
("a citation belonging to the fn ABOVE does not attach to the next one",
"### cited\n",
"/// See `context/test-evidence/clients.md`.\n#[test]\nfn cited() {}\n"
"\n#[test]\nfn uncited() {}\n",
"", [], []),
("a heading cannot be satisfied by a skipped or todo test",
'#### "a skipped one"\n#### "a todo one"\n', "",
'test.skip("a skipped one", () => {});\ntest.todo("a todo one");\n',
["a skipped one", "a todo one"], []),
("a heading cannot be satisfied by a test.only, which disables its file",
'#### "the only one left running"\n', "",
'test.only("the only one left running", () => {});\n',
["the only one left running"], []),
("a heading cannot be satisfied by an empty suite's name",
'#### "a suite with nothing in it"\n', "",
'test.describe("a suite with nothing in it", () => {});\n',
["a suite with nothing in it"], []),
("a test nested in a suite still satisfies its heading",
'#### "a nested test"\n', "",
'test.describe("a group", () => {\n test("a nested test", () => {});\n});\n',
[], []),
("a JS test that cites the evidence file and has no entry is caught",
"", "",
'// See context/test-evidence/clients.md.\ntest("a cited spec", async () => {});\n',
[], ["a cited spec"]),
]
def _evidence_self_test():
failures = []
for name, md, rs, js, want_orphans, want_unstated in SELF_TEST_EVIDENCE:
claims = {}
for kind, cname, n in evidence_claims(md):
claims.setdefault((kind, cname), []).append(f"fixture:{n}")
rd, rc = rust_tests(rs)
jd, jc = js_tests(js)
defined = {"rust": rd, "js": jd}
citing = {"rust": {k: ["fixture.rs"] for k in rc},
"js": {k: ["fixture.js"] for k in jc}}
orphans, unstated = evidence_verdict(claims, defined, citing)
got_o = sorted(k[1] for k in orphans)
got_u = sorted(k[1] for k in unstated)
if got_o != sorted(want_orphans):
failures.append(f" ORPHANS: {name}: expected {sorted(want_orphans)}, saw {got_o}")
if got_u != sorted(want_unstated):
failures.append(f" UNSTATED: {name}: expected {sorted(want_unstated)}, saw {got_u}")
return failures
def self_test():
"""Check the checker against every bypass it has already been fixed for.
Ten silent successes have been found in this file, every one failing in the
direction that looks like passing. See context/code/tooling.md.
"""
failures = []
for name, expected, src in SELF_TEST_CASES:
runs = [n for n in comment_runs(src.split("\n")) if n[1] > LIMIT]
longest = max((n[1] for n in runs), default=0)
if longest != expected:
failures.append(f" MISSED: {name}: expected a run of {expected}, saw {longest}")
for name, src in SELF_TEST_CLEAN:
runs = [n for n in comment_runs(src.split("\n")) if n[1] > LIMIT]
if runs:
failures.append(f" FALSE POSITIVE: {name}: flagged {runs}")
failures += _evidence_self_test()
if failures:
print("checker self-test FAILED:", file=sys.stderr)
print("\n".join(failures), file=sys.stderr)
return 1
print(f"checker self-test: {len(SELF_TEST_CASES)} violations caught, "
f"{len(SELF_TEST_CLEAN)} legitimate shapes allowed, "
f"{len(SELF_TEST_EVIDENCE)} evidence verdicts")
return 0
def main():
ap = argparse.ArgumentParser()
ap.add_argument("roots", nargs="*",
default=["src", "crates", "tests", "examples", "harness"])
ap.add_argument("--limit", type=int, default=LIMIT)
ap.add_argument("--list-doctests", action="store_true",
help="list doc comments containing a ``` fence and exit")
ap.add_argument("--check-evidence", action="store_true",
help="every context/test-evidence heading must name a test that exists")
ap.add_argument("--self-test", action="store_true",
help="check the checker against the bypasses it has been fixed for")
args = ap.parse_args()
if args.self_test:
return self_test()
default_roots = ["src", "crates", "tests", "examples", "harness"]
requested = args.roots or default_roots
# A path that is not a directory must FAIL. Filtering it away made
# `check-comment-runs.py some/file.rs` walk nothing and print success --
# a filter that over-matches fails in the direction that looks like
# passing, which is the one direction a checker must not fail in.
roots = [r for r in requested if os.path.isdir(r)]
missing = [r for r in requested if not os.path.isdir(r)]
if missing and args.roots:
for r in missing:
kind = "not a directory" if os.path.exists(r) else "does not exist"
print(f"error: {r}: {kind} (pass directories, not files)", file=sys.stderr)
return 2
if not roots:
print("error: nothing to scan", file=sys.stderr)
return 2
if args.check_evidence:
return check_evidence(roots)
if args.list_doctests:
for path in sorted(rust_files(roots)):
for n, raw in enumerate(open(path, encoding="utf8", errors="replace"), 1):
s = raw.strip()
if s.startswith(("///", "//!")) and s.lstrip("/!").strip().startswith("```"):
print(f"{path}:{n}")
return 0
violations = []
for path in sorted(rust_files(roots)):
lines = open(path, encoding="utf8", errors="replace").read().split("\n")
for start, length in comment_runs(lines):
if length > args.limit:
violations.append((path, start, length))
if not violations:
print(f"comment runs: none longer than {args.limit} lines")
return 0
worst = sorted(violations, key=lambda v: -v[2])
print(f"{len(violations)} comment runs longer than {args.limit} lines:\n", file=sys.stderr)
for path, start, length in worst:
print(f" {path}:{start} {length} lines", file=sys.stderr)
return 1
if __name__ == "__main__":
sys.exit(main())