Accept only the braced test-item shapes this crate uses

Lead fix, folded in. The previous repair accepted any `#[cfg(test)]` item whose
first line ended in `{`, which is the same latch in another spelling: a
semicolon-terminated `static` or `const` can open a block initializer there and
close with `};`, so the exemption ran past it into the next function. Verified
by reverting the condition — the scanner returned no offender at all for a
`std::fs::write` in the function following a `LazyLock` initializer.

Only `mod` and `impl` are accepted as braced shapes, being the two the crate
actually uses. Everything else fails the guard by name rather than being
bounded by a brace that may not be the item's own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XKzM69CHmBuDcA3qN1jFdh
This commit is contained in:
Levi Neuwirth 2026-07-29 20:30:41 -04:00
parent c0df18f15f
commit ef73028887
2 changed files with 42 additions and 11 deletions

View File

@ -315,9 +315,12 @@ const FORBIDDEN: &[&str] = &[
/// ```
///
/// the first column-zero `}` is *`shipping_code`'s*, so every line of it was
/// skipped. A semicolon-terminated item therefore exempts only itself, and any
/// third shape — including an item header rustfmt has split across lines — is a
/// scanner error rather than a guess.
/// skipped. A semicolon-terminated item therefore exempts only itself. Of the
/// braced shapes, this crate currently needs only `mod` and `impl`, so those are
/// the only two accepted: merely ending a line in `{` is not enough, because a
/// semicolon-terminated `static` or `const` can begin a block initializer there
/// and close with `};`. Any other shape — including an item header rustfmt has
/// split across lines — is a scanner error rather than a guess.
fn scan_for_unfunnelled_calls(text: &str) -> Result<Vec<(usize, String)>, String> {
#[derive(PartialEq)]
enum Exempt {
@ -361,7 +364,9 @@ fn scan_for_unfunnelled_calls(text: &str) -> Result<Vec<(usize, String)>, String
index + 1
));
};
if item.ends_with('{') {
let item = item.trim_start();
let recognized_braced_item = item.starts_with("mod ") || item.starts_with("impl ");
if item.ends_with('{') && recognized_braced_item {
exempt = Exempt::UntilUnindentedBrace;
index = head + 1;
continue;
@ -493,6 +498,29 @@ fn ship() {
"a `#[cfg(test)] use ...;` must not exempt the function that follows it"
);
// A semicolon-terminated item can *start* with a line ending in `{`. It is
// not a braced item: the closure belongs to the initializer, and the item
// closes with `});`. Treating the first `{` as the item's delimiter latches
// the exemption through `ship`, exactly like the single-line `use` defect
// above. `static` is not one of the two braced shapes this crate needs, so
// the scanner refuses to guess where it ends.
let block_initializer = "\
#[cfg(test)]
static HOOK: LazyLock<()> = LazyLock::new(|| {
setup();
});
fn ship() {
std::fs::write(p, b\"\");
}
";
let reason = scan_for_unfunnelled_calls(block_initializer)
.expect_err("a block initializer must not be mistaken for a braced test item");
assert!(
reason.contains("cannot bound") && reason.contains("static HOOK"),
"the scanner must name the unsupported item rather than latch its exemption: {reason}"
);
// Stacked attributes and doc comments between the trigger and the item.
let stacked = "\
#[cfg(test)]

View File

@ -1284,13 +1284,16 @@ and the failure is the signal that the documented assumption changed. §3.1 reco
the "directory holding only `LOCK` is empty" special case that startup state 1 depends on.
**The funnel guard's exemption could still latch, and the fix is a scanner that refuses to guess.**
Bounding a `#[cfg(test)]` exemption at the next column-zero `}` is right for a braced item and wrong
for every other shape: after `#[cfg(test)] use crate::test_support;` the first such brace belongs to
the *next* function, so all of it was skipped. The scanner now reads the shape of the attributed
item — braced items are exempt to their closing brace, semicolon-terminated items exempt only
themselves, and any third shape (including an item header rustfmt split across lines) **fails the
guard** rather than being assumed safe. A shape the scanner cannot bound is not a shape it may
treat as harmless.
Bounding a `#[cfg(test)]` exemption at the next column-zero `}` is right for a recognized braced
item and wrong for every other shape: after `#[cfg(test)] use crate::test_support;` the first such
brace belongs to the *next* function, so all of it was skipped. A first repair still accepted
anything whose first item line ended in `{`; that was the same latch in another spelling, because a
semicolon-terminated `static` or `const` can begin a block initializer there and close with `};`.
The scanner now accepts only the two braced top-level shapes the crate actually uses (`mod` and
`impl`), exempts a one-line semicolon-terminated item only for that line, and **fails the guard** on
every other shape (including a block initializer or an item header rustfmt split across lines)
rather than assuming it is safe. A shape the scanner cannot bound is not a shape it may treat as
harmless.
It is also now a function over `&str` with synthetic tests, which is the load-bearing half. Mutating
real sources only ever probes the shapes those sources happen to contain: no file in the crate has a