From 2785f4c7a0ab973f6675e2dac33bca1acfb32927 Mon Sep 17 00:00:00 2001 From: Oleg XXL Date: Sun, 1 Nov 2026 22:56:43 +0200 Subject: [PATCH 01/11] test(quality): pin Rust test-attribute dead-code arms (#493) registermacrocheck.sh gains two arms for #[test] / #[tokio::test] functions, which the test harness calls by name and the call graph never sees. Arm 9 fails on main: six #[test] shapes are reported as dead-code. Its negative lookalikes must keep being flagged: an attribute inside a comment, #[testing], #[test_case], a dead fn placed directly after a #[test] item, and an ordinary dead fn. Arm 10 pins that a dangling #[test] at end of file does not break the scan. Refs #493 --- test/registermacrocheck.sh | 74 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/test/registermacrocheck.sh b/test/registermacrocheck.sh index d8787d1d5..1e2eae32b 100755 --- a/test/registermacrocheck.sh +++ b/test/registermacrocheck.sh @@ -358,5 +358,79 @@ printf '%s' "$DC8B_TAG" | grep -q 'config-warnings=' \ && { no "arm8: a genuinely-used register_macros= name still warned as inert"; printf '%s\n' "$DC8B_TAG"; } \ || ok "arm8: a genuinely-used config name draws no warning (negative control)" +# ── ARM 9: Rust test-harness registration, via --quality-delta (issue #493) ────────────────────────────── +# A #[test] (or #[tokio::test…]) function is called by the test harness, and no line in the tree names it, so the +# name-based call graph sees zero callers. The attribute sits ABOVE the item, outside the signature text the macro +# check reads, so it needed its own rule (quality.h rustTestAttributed). Positive shapes must be exempt; each +# negative lookalike must still be flagged: an attribute inside a comment, '#[testing]', '#[test_case]', a dead fn +# placed directly after a #[test] item, and an ordinary dead fn. +A9="$WORK/a9"; newrepo "$A9" +cat > "$A9/lib.rs" <<'EOF' +fn live_helper() -> u32 { 1 } +EOF +commit "$A9" "baseline: no tests yet" +cat > "$A9/lib.rs" <<'EOF' +fn live_helper() -> u32 { 1 } + +#[test] +fn plain_case() { assert_eq!(live_helper(), 1); } + +#[tokio::test(flavor = "current_thread")] +async fn flavored_case() { assert_eq!(live_helper(), 1); } + +#[inline] +#[test] +fn stacked_case() { assert_eq!(live_helper(), 1); } + +#[test] + +fn blank_line_case() { assert_eq!(live_helper(), 1); } + +/// A documented harness case: the doc line sits between the attribute and the item. +#[test] +fn doc_comment_case() { assert_eq!(live_helper(), 1); } + +#[testing] +fn testing_attribute_is_not_a_test() { } + +// #[test] +fn commented_attribute_is_not_a_test() { } + +#[test_case] +fn test_case_attribute_is_not_a_test() { } + +#[test] +fn test_is_here() { assert_eq!(live_helper(), 1); } +fn dead_after_test_attribute() { } + +fn dead_plain() { } +EOF +QD9="$( cd "$A9" && "$BIN" . --quality-delta --no-cache 2>/dev/null )" +for sym in plain_case flavored_case stacked_case blank_line_case doc_comment_case test_is_here; do + printf '%s' "$QD9" | grep -q "kind=\"dead-code\" sym=\"$sym\"" \ + && no "arm9: '$sym' is a #[test] function but was reported as dead-code" \ + || ok "arm9: '$sym' (#[test] shape) excluded from dead-code" +done +for sym in testing_attribute_is_not_a_test commented_attribute_is_not_a_test test_case_attribute_is_not_a_test dead_after_test_attribute dead_plain; do + printf '%s' "$QD9" | grep -q "kind=\"dead-code\" sym=\"$sym\"" \ + && ok "arm9: '$sym' (negative lookalike) still flagged" \ + || { no "arm9: '$sym' should still be flagged — the test-attribute exemption leaked"; printf '%s\n' "$QD9" | grep -oE 'sym="[^"]*"' | head -12; } +done +printf '%s' "$QD9" | grep -q 'kind="dead-code" sym="live_helper"' \ + && no "arm9: live_helper (called from tests) wrongly reported" \ + || ok "arm9: live_helper (called from the tests) not reported" +printf '%s' "$QD9" | grep -q 'register-macro-excluded="6"' \ + && ok "arm9: register-macro-excluded=\"6\" (the six #[test] functions)" \ + || { no "arm9: register-macro-excluded count wrong"; printf '%s\n' "$QD9" | grep -oE 'register-macro-excluded="[0-9]*"'; } + +# ── ARM 10: a test attribute with no item after it must not break the scan ───────────────────────────── +A10="$WORK/a10"; newrepo "$A10" +printf 'fn live() -> u32 { 1 }\n\n#[test]\n' > "$A10/lib.rs" +commit "$A10" "a dangling test attribute at end of file" +"$BIN" "$A10" --quality-delta --no-cache >/dev/null 2>&1; RC10=$? +# 0 = clean, 2 = a gating finding; anything else (a signal, an abort) is the crash this arm guards against. +if [ "$RC10" -eq 0 ] || [ "$RC10" -eq 2 ]; then ok "arm10: a dangling #[test] at end of file does not crash the scan (exit $RC10)" +else no "arm10: a dangling #[test] at end of file made ripwire exit $RC10"; fi + [ "$fail" -eq 0 ] && echo "ALL PASS" || echo "SOME FAILED" exit "$fail" From a2ecfba740baa785576f42c2f521868168bc4319 Mon Sep 17 00:00:00 2001 From: Oleg XXL Date: Mon, 2 Nov 2026 00:07:32 +0200 Subject: [PATCH 02/11] fix(quality): exempt Rust test-attribute functions from dead-code (#493) A #[test] or #[tokio::test] function is called by the test harness, which the name-based call graph never sees. --quality-delta reported every new test as a dead-code row, and the --dead-code verb repeated the same rule. The attribute stacks above the item, outside the signature text the built-in macro check reads, so it needs its own report-time rule. rustTestAttributed scans backwards from the item over blank lines, // comments and other attribute lines, and matches the whole attribute token: #[test], or #[tokio::test] with an optional argument list. #[testing], #[test_case] and a #[test] inside a comment do not match. Which languages carry this rule is a LangFacts column, attributeRegistersTests, set on the Rust row. The shared code reads the column rather than comparing against Lang::Rust, so the == Lang:: ratchet in langidentitycheck does not rise. Both verbs read one predicate (registersItself), so they cannot disagree about a symbol. The exemption counts toward register-macro-excluded=. The dead set changed for Rust test code, so the cached quality snapshot changed meaning. kQSnapCacheScheme moves 24 -> 25 (quality.h v25 note), as the qschemetripcheck tripwire requires. The hash re-pin is its own commit. forEachSymbolBody keeps its three-argument visitor for every existing caller. The whole-file variant forEachSymbolFile is the one registeredMacroSymbolIds uses. Floor, stated: a multi-line attribute, or a block comment between the attribute and the item, ends the scan. The item then stays a dead-code candidate, a miss in the reporting direction and never a false exemption. Refs #493 --- changelog.d/rust-test-harness-dead-code.md | 3 + src/langfacts.h | 6 +- src/quality.h | 100 +++++++++++++++++++-- src/verbs_quality.h | 2 +- 4 files changed, 104 insertions(+), 7 deletions(-) create mode 100644 changelog.d/rust-test-harness-dead-code.md diff --git a/changelog.d/rust-test-harness-dead-code.md b/changelog.d/rust-test-harness-dead-code.md new file mode 100644 index 000000000..0a01e6d2c --- /dev/null +++ b/changelog.d/rust-test-harness-dead-code.md @@ -0,0 +1,3 @@ +### Fixed — Rust `#[test]` functions no longer read as dead code (#493) + +A `#[test]` or `#[tokio::test]` function is run by the test harness, not called by name, so the call graph has no caller for it. `--quality-delta` reported every new test as a `dead-code` row. Those functions are now exempt the way `TEST` and `TEST_CASE` already are, and they count toward `register-macro-excluded=`. An attribute that only looks like the test attribute (`#[testing]`, `#[test_case]`, a `#[test]` inside a comment) does not exempt the function below it, and an ordinary uncalled function is still reported. diff --git a/src/langfacts.h b/src/langfacts.h index 407a5b709..f4fcabe81 100644 --- a/src/langfacts.h +++ b/src/langfacts.h @@ -301,6 +301,10 @@ struct LangFacts /// Whether this language's files can carry a file→file edge in this build (FileEdges above). FileEdges fileEdges = FileEdges::Unmeasured; + + /// Rust: a `#[test]` or `#[tokio::test]` attribute registers the function with the test harness, which calls it by name + /// without any call in the tree (issue #493). quality.h's registersItself reads this column; nothing else does. + bool attributeRegistersTests = false; }; /// java.lang.Object's public methods an interface may redeclare abstract (LangFacts::samIgnoredMembers). @@ -320,7 +324,7 @@ inline constexpr std::array kLangFacts = { { { .lang = Lang::Go, .structuralTyping = true, .writtenModule = WrittenModuleRule::PackageDir, .memberNeverReachesFreeFunction = true, .moduleCallReachesFunctionsOnly = true, .signatureRows = true }, { .lang = Lang::Rust, .contractSignaturesUnindexed = true, .traitImplBlocks = true, .returnTypedDispatchLet = true, .constructorMethod = "new", .memberSeparator = "::", .baseImportEvidence = BaseImportEvidence::RustUse, - .instanceReceiver = "self", .memberNeverReachesFreeFunction = true, .fileEdges = FileEdges::Resolved }, + .instanceReceiver = "self", .memberNeverReachesFreeFunction = true, .fileEdges = FileEdges::Resolved, .attributeRegistersTests = true }, { .lang = Lang::Swift, .writtenModule = WrittenModuleRule::DirsOnly, .instanceReceiver = "self", .implicitReceiver = true, .memberNeverReachesFreeFunction = true, .classNameReceiver = true }, { .lang = Lang::ObjC, .noNestedNamedFunctions = true, .transitiveIncludes = true, .staticLinkage = true, .fileEdges = FileEdges::Resolved }, diff --git a/src/quality.h b/src/quality.h index 9995774aa..fd4f2e41e 100644 --- a/src/quality.h +++ b/src/quality.h @@ -1221,8 +1221,10 @@ struct DefectSiteAt // three call sites — which is precisely the finding that kind exists to catch, on the run that was adding // it. Two copies of "read each file once and hand me each body" is two places for the degrade rules to // drift, and those rules are the honesty contract here, not an implementation detail. +// visit( NodeId, const Symbol&, body, fileBytes ): the body slice plus the whole file, for a rule that reads text OUTSIDE +// the slice (forEachSymbolBody below is the three-argument spelling every other caller uses). template -inline void forEachSymbolBody( const IngestResult& ing, Fn&& visit ) +inline void forEachSymbolFile( const IngestResult& ing, Fn&& visit ) { // per-file def ids with a real body (see constructCountsBySym above on `symbols[i].id == i`). const SymbolsByFile byFile = symbolsByFileInIdOrder( ing, []( const Symbol& s ) { return s.endByte > s.sigStartByte; } ); @@ -1244,11 +1246,20 @@ inline void forEachSymbolBody( const IngestResult& ing, Fn&& visit ) { continue; } - visit( i, s, std::string_view( bytes.data() + s.sigStartByte, s.endByte - s.sigStartByte ) ); + visit( i, s, std::string_view( bytes.data() + s.sigStartByte, s.endByte - s.sigStartByte ), std::string_view( bytes ) ); } } } +template +inline void forEachSymbolBody( const IngestResult& ing, Fn&& visit ) +{ + forEachSymbolFile( ing, [ & ]( NodeId i, const Symbol& s, std::string_view body, std::string_view ) + { + visit( i, s, body ); + } ); +} + // Direct Python class members get an owner: the enclosing class. A def nested inside a method gets a host: the // nearest enclosing method, whose self/cls it reads as a free variable. Module-level functions have neither. struct PythonScopes @@ -1660,6 +1671,81 @@ inline std::vector trailingDefaultByNode( const IngestResult& ing } +// ---- Rust test-harness registration (issue #493) --------------------------------------------------------------- +// `#[test]` and `#[tokio::test]` (optionally with an argument list) register a function with the test harness, +// which calls it by name without any call in the tree. They are attributes, not macro calls, so the signature- +// text check above never sees them: the attribute stacks ABOVE the item, outside [sigStartByte, endByte). +// isRustTestAttribute answers whether the text between "#[" and "]" is one of the two spellings. +inline bool isRustTestAttribute( std::string_view attr ) noexcept +{ + if( attr == "test" ) + { + return true; + } + constexpr std::string_view tokio = "tokio::test"; + if( attr.substr( 0, tokio.size() ) == tokio ) + { + return attr.size() == tokio.size() || attr[ tokio.size() ] == '('; + } + return false; +} + +// rustTestAttributed reads backwards from the item's signature start over blank lines, `//` comments and other +// attribute lines (`#[inline]`, `#[allow(...)]`), and answers true when one of them is a test attribute. Whole-token +// compare: `#[testing]` and a `#[test]` inside a comment do not count. +// FLOOR, stated: a multi-line attribute, or a block comment between attribute and item, ends the scan and the item +// stays a dead-code candidate. That is a miss in the reporting direction, never a false exemption. +inline bool rustTestAttributed( std::string_view file, std::size_t sigStart ) noexcept +{ + std::size_t end = std::min( sigStart, file.size() ); // everything before `end` is still unread + for( ;; ) + { + while( end > 0 && std::isspace( static_cast( file[ end - 1 ] ) ) ) + { + --end; + } + if( end == 0 ) + { + return false; + } + const std::size_t nl = file.rfind( '\n', end - 1 ); + const std::size_t from = nl == std::string_view::npos ? 0 : nl + 1; + std::string_view line = file.substr( from, end - from ); + while( !line.empty() && std::isspace( static_cast( line.front() ) ) ) + { + line.remove_prefix( 1 ); + } + if( line.substr( 0, 2 ) == "//" ) + { + end = from; // a comment or doc line: keep reading upward + continue; + } + const std::size_t open = line.rfind( "#[" ); + if( open == std::string_view::npos || line.back() != ']' ) + { + return false; // any other code ends the attribute block + } + if( isRustTestAttribute( line.substr( open + 2, line.size() - open - 3 ) ) ) + { + return true; + } + end = from; // another attribute on the way up: keep reading + } +} + +// The exemption a definition gets for registering itself with a runner the call graph cannot see: its own signature +// begins with a registered macro call (the list above), or, for Rust only, a test attribute stacks above it. Both +// --dead-code and --quality-delta read this one predicate, so the two verbs cannot disagree about one symbol. +inline bool registersItself( const Symbol& s, std::string_view file, std::string_view sigRegion, + const std::vector& names ) noexcept +{ + if( startsWithRegisteredMacro( sigRegion, names ) ) + { + return true; + } + return langFacts( s.lang ).attributeRegistersTests && rustTestAttributed( file, s.sigStartByte ); +} + // P2.2 — every symbol in THIS tree whose own signature text is a registered-macro call (built ONCE per // computeSnapshot/computeDelta run, exactly like topLevelCallees above), reading each file's bytes once via // forEachSymbolBody — whose per-symbol `body` view already starts at sigStartByte, which is precisely where @@ -1674,9 +1760,9 @@ inline std::vector registeredMacroSymbolIds( const IngestResult& ing, co { return ids; } - forEachSymbolBody( ing, [ & ]( NodeId i, const Symbol&, std::string_view body ) + forEachSymbolFile( ing, [ & ]( NodeId i, const Symbol& s, std::string_view body, std::string_view file ) { - if( startsWithRegisteredMacro( body, names ) ) + if( registersItself( s, file, body, names ) ) { ids.push_back( i ); } @@ -3787,7 +3873,11 @@ inline void evictOldHeadSnapCaches( const std::string& dir, const std::string& r // The producer identity already keeps this build's blobs apart; bumped 22 -> 23 so the history above stays complete. // v24 (2026-10-25, train 36) — #228's v23 was the PR's placeholder; renumbered above every live value (main / train 35 / every // other ref 22, #437 23). No further shape change. -constexpr std::uint32_t kQSnapCacheScheme = 24; +// v25 (2026-11-02, #493: Rust test-attribute functions) — no blob shape change; the dead SET's inputs moved. registeredMacroSymbolIds, +// which isDeadCandidate's register-macro exemption reads, now also holds a Rust definition that a #[test] or #[tokio::test] attribute +// stacks above (registersItself). The harness calls such a function, so it is no dead candidate. Bumped 24 -> 25 so the history above +// stays complete. Lane note: v25 is this PR's number; a second scheme bump from another open PR renumbers on the merge. +constexpr std::uint32_t kQSnapCacheScheme = 25; constexpr char kQSnapMagic[4] = { 'Q', 'S', 'N', 'P' }; // The qsnap EXCLUDES-config key folds the qsnap SCHEME (independent of the ingest cache's kHeadSnapCacheScheme) diff --git a/src/verbs_quality.h b/src/verbs_quality.h index 1af6c0abd..76cc74d40 100644 --- a/src/verbs_quality.h +++ b/src/verbs_quality.h @@ -2933,7 +2933,7 @@ std::optional runQualityViews( const MainDispatch& d ) { return false; } - return quality::startsWithRegisteredMacro( std::string_view( src ).substr( symbol.sigStartByte ), registerMacroNames ); + return quality::registersItself( symbol, src, std::string_view( src ).substr( symbol.sigStartByte ), registerMacroNames ); }; std::size_t registerMacroExcluded = 0; // P2.2: disclosed count — see the header comment below std::size_t runnerRootExcluded = 0; // 0.6.6 D4: disclosed count (runner-root-excluded=, absent at 0) From c6e148b80bbf4fb3fd6df4bb1579cfcd52e34e92 Mon Sep 17 00:00:00 2001 From: Oleg XXL Date: Mon, 2 Nov 2026 00:07:32 +0200 Subject: [PATCH 03/11] chore(qschemetrip): re-pin the snapshot-meaning hash for the #493 change registeredMacroSymbolIds now reads registersItself, and kQSnapCacheScheme moved 24 -> 25 in the fix commit. The pin is re-derived with UPDATE_GOLDEN=1, and the re-pin log in qschemetripcheck.sh records what moved and why. Refs #493 --- test/qschemetrip.hash | 2 +- test/qschemetripcheck.sh | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/test/qschemetrip.hash b/test/qschemetrip.hash index 4c1024133..b7c102596 100644 --- a/test/qschemetrip.hash +++ b/test/qschemetrip.hash @@ -1 +1 @@ -b4dc5584d11c8acf92612c87b1d3756422d1baf3d714f46cb668e1c5eb525cdc +caf2d40157e1f51bda6dece96dc6a2da10717d16fd0deabe1897e599dbcf9821 diff --git a/test/qschemetripcheck.sh b/test/qschemetripcheck.sh index f3934fab1..bc42d560b 100755 --- a/test/qschemetripcheck.sh +++ b/test/qschemetripcheck.sh @@ -34,6 +34,9 @@ SRC="$ROOT/src/quality.h" ING="$ROOT/src/ingest_cache.h" # extraction-identity constants moved here (2026-08-29 ingest.cpp section split); the hashed CONCAT label keeps its historical spelling so the pin holds PIN="$ROOT/test/qschemetrip.hash" # RE-PIN LOG (the pin is a bare hash, so its justification has to live here). +# 2026-11-02, PR fix/rust-test-harness-dead-code (#493): RE-PINNED ONCE with UPDATE_GOLDEN=1. Watched text: registeredMacroSymbolIds +# now reads registersItself (a Rust #[test] / #[tokio::test] attribute above a definition exempts it), and kQSnapCacheScheme 24 -> 25 +# (quality.h's v25 note). kParserVer and kCacheVersion stay: no extraction change, no record shape change. # 2026-10-31, train 43b (lanes registrar, string-import, self-hook, decl-row merged onto main 65210e86): RE-DERIVED ONCE with UPDATE_GOLDEN=1 on the # composed tree (main's pin and the registrar / string-import branch pins conflicted). Watched text: kParserVer 271 -> 283 (rich 284; above the # lane placeholders up to 281, rich 282), quality.h's kIngestParserVerMirror with it. kCacheVersion stays 48 and kQSnapCacheScheme stays 24: From b1efc836eb827d49a47b56a5996025d22343f192 Mon Sep 17 00:00:00 2001 From: Oleg XXL Date: Mon, 2 Nov 2026 10:44:08 +0200 Subject: [PATCH 04/11] fix(quality): read a test attribute through its trailing // comment #[test] // case was not recognised: rustTestAttributed required the line to end in ']', so the trailing comment ended the attribute scan and the test fn stayed a dead-code candidate. rustCodeOfAttributeLine drops a trailing // outside a double-quoted string, so a // inside an attribute argument is kept. Adds registermacrocheck arm 11: trailing comment on #[test] and #[tokio::test], a // inside a string argument, and negative controls (#[testing] // note and a dead fn stay flagged). Review comment on PR #499. --- changelog.d/rust-test-harness-dead-code.md | 2 +- src/quality.h | 43 ++++++++++++++++++++++ test/registermacrocheck.sh | 36 ++++++++++++++++++ 3 files changed, 80 insertions(+), 1 deletion(-) diff --git a/changelog.d/rust-test-harness-dead-code.md b/changelog.d/rust-test-harness-dead-code.md index 0a01e6d2c..50f685b4b 100644 --- a/changelog.d/rust-test-harness-dead-code.md +++ b/changelog.d/rust-test-harness-dead-code.md @@ -1,3 +1,3 @@ ### Fixed — Rust `#[test]` functions no longer read as dead code (#493) -A `#[test]` or `#[tokio::test]` function is run by the test harness, not called by name, so the call graph has no caller for it. `--quality-delta` reported every new test as a `dead-code` row. Those functions are now exempt the way `TEST` and `TEST_CASE` already are, and they count toward `register-macro-excluded=`. An attribute that only looks like the test attribute (`#[testing]`, `#[test_case]`, a `#[test]` inside a comment) does not exempt the function below it, and an ordinary uncalled function is still reported. +A `#[test]` or `#[tokio::test]` function is run by the test harness, not called by name, so the call graph has no caller for it. `--quality-delta` reported every new test as a `dead-code` row. Those functions are now exempt the way `TEST` and `TEST_CASE` already are, and they count toward `register-macro-excluded=`. An attribute that only looks like the test attribute (`#[testing]`, `#[test_case]`, a `#[test]` inside a comment) does not exempt the function below it, and an ordinary uncalled function is still reported. A trailing `//` comment after the test attribute (`#[test] // case`) does not hide the attribute either. diff --git a/src/quality.h b/src/quality.h index fd4f2e41e..a409531b2 100644 --- a/src/quality.h +++ b/src/quality.h @@ -1690,6 +1690,43 @@ inline bool isRustTestAttribute( std::string_view attr ) noexcept return false; } +// rustCodeOfAttributeLine returns an attribute line without a trailing `//` comment. Rust reads that comment as +// whitespace, so `#[test] // case` is still `#[test]`. A `//` inside a double-quoted string is attribute text: +// `#[tokio::test(flavor = "http://x")]` keeps its argument whether or not a comment follows it. +inline std::string_view rustCodeOfAttributeLine( std::string_view line ) noexcept +{ + bool inString = false; + for( std::size_t i = 0; i < line.size(); ++i ) + { + const char c = line[ i ]; + if( inString ) + { + if( c == '\\' ) + { + ++i; + } + else if( c == '"' ) + { + inString = false; + } + } + else if( c == '"' ) + { + inString = true; + } + else if( c == '/' && i + 1 < line.size() && line[ i + 1 ] == '/' ) + { + line = line.substr( 0, i ); + break; + } + } + while( !line.empty() && std::isspace( static_cast( line.back() ) ) ) + { + line.remove_suffix( 1 ); + } + return line; +} + // rustTestAttributed reads backwards from the item's signature start over blank lines, `//` comments and other // attribute lines (`#[inline]`, `#[allow(...)]`), and answers true when one of them is a test attribute. Whole-token // compare: `#[testing]` and a `#[test]` inside a comment do not count. @@ -1720,6 +1757,12 @@ inline bool rustTestAttributed( std::string_view file, std::size_t sigStart ) no end = from; // a comment or doc line: keep reading upward continue; } + line = rustCodeOfAttributeLine( line ); + if( line.empty() ) + { + end = from; + continue; + } const std::size_t open = line.rfind( "#[" ); if( open == std::string_view::npos || line.back() != ']' ) { diff --git a/test/registermacrocheck.sh b/test/registermacrocheck.sh index 1e2eae32b..7c50b39a1 100755 --- a/test/registermacrocheck.sh +++ b/test/registermacrocheck.sh @@ -432,5 +432,41 @@ commit "$A10" "a dangling test attribute at end of file" if [ "$RC10" -eq 0 ] || [ "$RC10" -eq 2 ]; then ok "arm10: a dangling #[test] at end of file does not crash the scan (exit $RC10)" else no "arm10: a dangling #[test] at end of file made ripwire exit $RC10"; fi +# ── ARM 11: a trailing // comment after a test attribute (issue #499 review) ─────────────────────────────── +# Rust reads `#[test] // case` as `#[test]` followed by a comment, so the harness still registers the fn and the +# exemption must hold. A `//` inside an attribute's string argument is text, not a comment: the +# `#[tokio::test(flavor = "http://x")]` shape stays exempt with no trailing comment at all. A trailing comment on a +# NON-test attribute changes nothing: `#[testing] // note` stays flagged, and so does an ordinary dead fn. +A11="$WORK/a11"; newrepo "$A11" +printf 'fn live_helper() -> u32 { 1 }\n' > "$A11/lib.rs" +commit "$A11" "baseline for the trailing-comment arms" +cat >> "$A11/lib.rs" <<'EOF' + +#[test] // regression case +fn trailing_comment_case() { assert_eq!(live_helper(), 1); } + +#[tokio::test(flavor = "current_thread")] // harness case +fn flavored_trailing_case() { assert_eq!(live_helper(), 1); } + +#[tokio::test(flavor = "http://x")] +fn slashes_in_string_case() { assert_eq!(live_helper(), 1); } + +#[testing] // not a test attribute +fn testing_attr_with_comment() { } + +fn dead_plain() { } +EOF +QD11="$( cd "$A11" && "$BIN" . --quality-delta --no-cache 2>/dev/null )" +for sym in trailing_comment_case flavored_trailing_case slashes_in_string_case; do + printf '%s' "$QD11" | grep -q "kind=\"dead-code\" sym=\"$sym\"" \ + && no "arm11: '$sym' is a #[test] function but was reported as dead-code" \ + || ok "arm11: '$sym' (#[test] shape with trailing comment or // in its string) excluded from dead-code" +done +for sym in testing_attr_with_comment dead_plain; do + printf '%s' "$QD11" | grep -q "kind=\"dead-code\" sym=\"$sym\"" \ + && ok "arm11: '$sym' (negative lookalike) still flagged" \ + || no "arm11: '$sym' should still be flagged — the trailing comment leaked into the exemption" +done + [ "$fail" -eq 0 ] && echo "ALL PASS" || echo "SOME FAILED" exit "$fail" From d591bd5d89dc258f18cacdc2181229fb172fb204 Mon Sep 17 00:00:00 2001 From: Oleg XXL Date: Mon, 2 Nov 2026 11:34:47 +0200 Subject: [PATCH 05/11] docs(quality): name the open #494 bump v27 in the scheme history The #494 PR takes kQSnapCacheScheme 27, so the two open bumps (v26 here, v27 there) stay distinct. Re-pinned test/qschemetrip.hash. --- src/quality.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/quality.h b/src/quality.h index 4977279b9..87a5962f9 100644 --- a/src/quality.h +++ b/src/quality.h @@ -3920,7 +3920,7 @@ inline void evictOldHeadSnapCaches( const std::string& dir, const std::string& r // v26 (2026-11-02, #493 / #499: Rust test-attribute functions) — no blob shape change; the dead SET's inputs moved. // registeredMacroSymbolIds, which isDeadCandidate's register-macro exemption reads, now also holds a Rust definition that a // #[test] or #[tokio::test] attribute stacks above (registersItself). The harness calls such a function, so it is no dead -// candidate. Renumbered above main's v25 (land-472); whichever of the two open scheme bumps merges second renumbers again. +// candidate. Renumbered above main's v25 (land-472); The open #494 bump takes v27, so the two never share a value. constexpr std::uint32_t kQSnapCacheScheme = 26; constexpr char kQSnapMagic[4] = { 'Q', 'S', 'N', 'P' }; From ee3c07aa833a4f22923649a0e9fd09604c642c95 Mon Sep 17 00:00:00 2001 From: Oleg XXL Date: Mon, 2 Nov 2026 15:18:40 +0200 Subject: [PATCH 06/11] docs(quality): the register-macro-excluded legend covers Rust test attributes (#499 review) The count now includes Rust #[test] and #[tokio::test] functions, so the legend says so. The registermacrocheck fixture puts the doc comment between the attribute and the item, as its own comment says it does. Gates: registermacrocheck, docscommandscheck, legend*, compactlegendcheck, panellegendcheck all pass. --- src/verbs_quality.h | 2 +- test/registermacrocheck.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/verbs_quality.h b/src/verbs_quality.h index 76cc74d40..f30e8508c 100644 --- a/src/verbs_quality.h +++ b/src/verbs_quality.h @@ -895,7 +895,7 @@ inline constexpr const char* kQdLegendCore = "whose target no longer applies. " "api-new-surface= COUNTS the new PUBLIC symbols (never gates, not in regressions=, printed even at zero). " "register-macro-excluded= is a FLOOR, not a finding: symbols this run excluded from the dead-code kind " - "because their own definition is a registered self-registering test/benchmark macro call. Never gates, " + "because their own definition is a registered self-registering test/benchmark macro call, or a Rust #[test] or #[tokio::test] function the test harness calls. Never gates, " "never counted in regressions=, printed even at zero (zero means none excluded, not that the check did " "not run). " // P3 (L7): next= on gating rows, defined where the reader meets it diff --git a/test/registermacrocheck.sh b/test/registermacrocheck.sh index 7c50b39a1..c70b587d9 100755 --- a/test/registermacrocheck.sh +++ b/test/registermacrocheck.sh @@ -386,8 +386,8 @@ fn stacked_case() { assert_eq!(live_helper(), 1); } fn blank_line_case() { assert_eq!(live_helper(), 1); } -/// A documented harness case: the doc line sits between the attribute and the item. #[test] +/// A documented harness case: the doc line sits between the attribute and the item. fn doc_comment_case() { assert_eq!(live_helper(), 1); } #[testing] From 64caf671b474f39ea73ec750cc5f6d37e1e0147f Mon Sep 17 00:00:00 2001 From: Oleg XXL Date: Tue, 3 Nov 2026 14:55:46 +0200 Subject: [PATCH 07/11] fix(quality): count a Rust test attribute only at the start of an attribute line rustTestAttributed treated any line that held `#[` and ended in `]` as an attribute line. So `const A: u32 = 1; #[test]` exempted the function written below it, and that function was hidden from --dead-code and --quality-delta. A line now counts as an attribute line only when, after its leading whitespace and any trailing `//` comment, it starts with `#[`. Any other code ends the scan. The four Rust test-attribute helpers join the scheme tripwire manifest, and test/qschemetrip.hash is re-pinned. The re-pin log entry now reads kQSnapCacheScheme 25 -> 26 (v26). The scheme stays at 26: this PR has not shipped v26, and the producer identity (a content hash of src/) already keeps a blob written before this change from being served to this build. Raised in the maintainer review of #499. --- src/quality.h | 12 +++++++++--- test/qschemetrip.hash | 2 +- test/qschemetripcheck.sh | 8 +++++--- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/quality.h b/src/quality.h index 775ea39d7..2d6fdbecb 100644 --- a/src/quality.h +++ b/src/quality.h @@ -1727,7 +1727,9 @@ inline std::string_view rustCodeOfAttributeLine( std::string_view line ) noexcep // rustTestAttributed reads backwards from the item's signature start over blank lines, `//` comments and other // attribute lines (`#[inline]`, `#[allow(...)]`), and answers true when one of them is a test attribute. Whole-token -// compare: `#[testing]` and a `#[test]` inside a comment do not count. +// compare: `#[testing]` and a `#[test]` inside a comment do not count. An attribute line STARTS with `#[` once its leading +// whitespace is gone: a `#[test]` written after code on the same line (`const A: u32 = 1; #[test]`) is not an attribute +// of the item below it, so it ends the scan like any other code (issue #499 review). // FLOOR, stated: a multi-line attribute, or a block comment between attribute and item, ends the scan and the item // stays a dead-code candidate. That is a miss in the reporting direction, never a false exemption. inline bool rustTestAttributed( std::string_view file, std::size_t sigStart ) noexcept @@ -1761,11 +1763,11 @@ inline bool rustTestAttributed( std::string_view file, std::size_t sigStart ) no end = from; continue; } - const std::size_t open = line.rfind( "#[" ); - if( open == std::string_view::npos || line.back() != ']' ) + if( line.substr( 0, 2 ) != "#[" || line.back() != ']' ) { return false; // any other code ends the attribute block } + const std::size_t open = line.rfind( "#[" ); if( isRustTestAttribute( line.substr( open + 2, line.size() - open - 3 ) ) ) { return true; @@ -3802,6 +3804,10 @@ inline void evictOldHeadSnapCaches( const std::string& dir, const std::string& r // constexpr array, not a function, so this manifest mechanism // cannot hash IT — a future edit that only changes the built-in // NAME LIST (not these functions' text) will not trip this gate. +// registersItself (quality.h) — the self-registration exemption --dead-code and --quality-delta share (a registered macro call, or a Rust test attribute above) +// rustTestAttributed (quality.h) — the backward scan over blank, comment and attribute lines that finds a #[test] or #[tokio::test] above a Rust definition +// isRustTestAttribute (quality.h) — whole-token check that an attribute's text is `test` or `tokio::test`, with or without an argument list +// rustCodeOfAttributeLine (quality.h) — an attribute line without its trailing // comment (a // inside a double-quoted string does not start one) // defectSitesOf (quality.h) — the defect-shape site identity (anchor + facet + normalized text) // the baseline stores. NOTE (disclosed limitation): the scanners it // calls live in defectshape.h, outside this manifest, so a change diff --git a/test/qschemetrip.hash b/test/qschemetrip.hash index 3fc0b2208..4845837dc 100644 --- a/test/qschemetrip.hash +++ b/test/qschemetrip.hash @@ -1 +1 @@ -ab16162e16ee6d0bdb2cd00423f2cb644c66f35c6d58bc5c7c729faed0fdd45f +32fa1194f79e61290d97c44337de3434fcd770e40ab6813a0f273a8f8e0a0b62 diff --git a/test/qschemetripcheck.sh b/test/qschemetripcheck.sh index 3a7da1613..8c3578a24 100755 --- a/test/qschemetripcheck.sh +++ b/test/qschemetripcheck.sh @@ -34,9 +34,11 @@ SRC="$ROOT/src/quality.h" ING="$ROOT/src/ingest_cache.h" # extraction-identity constants moved here (2026-08-29 ingest.cpp section split); the hashed CONCAT label keeps its historical spelling so the pin holds PIN="$ROOT/test/qschemetrip.hash" # RE-PIN LOG (the pin is a bare hash, so its justification has to live here). -# 2026-11-02, PR fix/rust-test-harness-dead-code (#493): RE-PINNED ONCE with UPDATE_GOLDEN=1. Watched text: registeredMacroSymbolIds -# now reads registersItself (a Rust #[test] / #[tokio::test] attribute above a definition exempts it), and kQSnapCacheScheme 24 -> 25 -# (quality.h's v25 note). kParserVer and kCacheVersion stay: no extraction change, no record shape change. +# 2026-11-02, PR fix/rust-test-harness-dead-code (#493, maintainer review of #499): RE-PINNED with UPDATE_GOLDEN=1 after merging main +# (main holds kQSnapCacheScheme 25). Watched text: registeredMacroSymbolIds now reads registersItself (a Rust #[test] / #[tokio::test] +# attribute above a definition exempts it); rustTestAttributed reads an attribute line only when it starts with `#[`; the manifest +# now lists registersItself, rustTestAttributed, isRustTestAttribute and rustCodeOfAttributeLine. kQSnapCacheScheme 25 -> 26 (v26) +# (quality.h's v26 note). kParserVer and kCacheVersion stay: no extraction change, no record shape change. # 2026-11-02, lane rule3-member-guard fix 4 (base main 00a16550; PLACEHOLDER, the train renumbers): RE-PINNED ONCE with UPDATE_GOLDEN=1. # Watched text: kParserVer 283 -> 285 (rich 286; 284 is 283's rich tag): a Rust path call whose qualifier has more than one segment keeps # it whole in Reference::fieldName; quality.h's kIngestParserVerMirror with it. kCacheVersion stays 48 and kQSnapCacheScheme stays 24: From 2d04bf0923d1d6d5baaa33050c761c284e7ee9f4 Mon Sep 17 00:00:00 2001 From: Oleg XXL Date: Tue, 3 Nov 2026 14:55:52 +0200 Subject: [PATCH 08/11] test(quality): arm 9 covers the false exemption and the same-line attribute Arm 9 gains the repro as a negative: `const A: u32 = 1; #[test]` followed by a dead fn. A trailing comment that only mentions the attribute (`static X: u32 = 1; // used to be #[test]`) followed by a dead fn is the negative control. The positives gain a one-line `#[test] fn` and a #[test] indented inside `#[cfg(test)] mod tests { }`. The register-macro-excluded assertion moves to 8. The #[test_case] lookalike becomes #[tested]. test_case is a real test registration that this change does not support, so the gate no longer calls it "not a test". The changelog fragment drops #[test_case] from its list of lookalikes, lists #[tested], and names the same-line case. Red-first: against the pre-change binary, arm 9 fails `not_a_test_after_code` (wrongly exempted) and the count (9, not 8). Raised in the maintainer review of #499. --- changelog.d/rust-test-harness-dead-code.md | 2 +- test/registermacrocheck.sh | 31 ++++++++++++++++------ 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/changelog.d/rust-test-harness-dead-code.md b/changelog.d/rust-test-harness-dead-code.md index 50f685b4b..35ec1a69e 100644 --- a/changelog.d/rust-test-harness-dead-code.md +++ b/changelog.d/rust-test-harness-dead-code.md @@ -1,3 +1,3 @@ ### Fixed — Rust `#[test]` functions no longer read as dead code (#493) -A `#[test]` or `#[tokio::test]` function is run by the test harness, not called by name, so the call graph has no caller for it. `--quality-delta` reported every new test as a `dead-code` row. Those functions are now exempt the way `TEST` and `TEST_CASE` already are, and they count toward `register-macro-excluded=`. An attribute that only looks like the test attribute (`#[testing]`, `#[test_case]`, a `#[test]` inside a comment) does not exempt the function below it, and an ordinary uncalled function is still reported. A trailing `//` comment after the test attribute (`#[test] // case`) does not hide the attribute either. +A `#[test]` or `#[tokio::test]` function is run by the test harness, not called by name, so the call graph has no caller for it. `--quality-delta` reported every new test as a `dead-code` row. Those functions are now exempt the way `TEST` and `TEST_CASE` already are, and they count toward `register-macro-excluded=`. An attribute that only looks like the test attribute (`#[testing]`, `#[tested]`, a `#[test]` inside a comment, a `#[test]` written after code on the same line as in `const A: u32 = 1; #[test]`) does not exempt the function below it, and an ordinary uncalled function is still reported. A trailing `//` comment after the test attribute (`#[test] // case`) does not hide the attribute either. diff --git a/test/registermacrocheck.sh b/test/registermacrocheck.sh index c70b587d9..50c4d0cca 100755 --- a/test/registermacrocheck.sh +++ b/test/registermacrocheck.sh @@ -362,8 +362,10 @@ printf '%s' "$DC8B_TAG" | grep -q 'config-warnings=' \ # A #[test] (or #[tokio::test…]) function is called by the test harness, and no line in the tree names it, so the # name-based call graph sees zero callers. The attribute sits ABOVE the item, outside the signature text the macro # check reads, so it needed its own rule (quality.h rustTestAttributed). Positive shapes must be exempt; each -# negative lookalike must still be flagged: an attribute inside a comment, '#[testing]', '#[test_case]', a dead fn -# placed directly after a #[test] item, and an ordinary dead fn. +# negative lookalike must still be flagged: an attribute inside a comment, '#[testing]', '#[tested]', a test attribute +# written after code on the same line (`const A: u32 = 1; #[test]`), a `#[test]` that only sits in a trailing // comment +# (`static X: u32 = 1; // used to be #[test]`), a dead fn placed directly after a #[test] item, and an ordinary dead fn. +# A one-line `#[test] fn ...` and a #[test] indented inside `#[cfg(test)] mod tests { }` are positives. A9="$WORK/a9"; newrepo "$A9" cat > "$A9/lib.rs" <<'EOF' fn live_helper() -> u32 { 1 } @@ -390,14 +392,27 @@ fn blank_line_case() { assert_eq!(live_helper(), 1); } /// A documented harness case: the doc line sits between the attribute and the item. fn doc_comment_case() { assert_eq!(live_helper(), 1); } +#[test] fn one_line_case() { assert_eq!(live_helper(), 1); } + +#[cfg(test)] mod tests { + #[test] + fn indented_in_test_mod() { assert_eq!(live_helper(), 1); } +} + #[testing] fn testing_attribute_is_not_a_test() { } // #[test] fn commented_attribute_is_not_a_test() { } -#[test_case] -fn test_case_attribute_is_not_a_test() { } +#[tested] +fn tested_attribute_is_not_a_test() { } + +const A: u32 = 1; #[test] +fn not_a_test_after_code() { } + +static X: u32 = 1; // used to be #[test] +fn really_dead() { } #[test] fn test_is_here() { assert_eq!(live_helper(), 1); } @@ -406,12 +421,12 @@ fn dead_after_test_attribute() { } fn dead_plain() { } EOF QD9="$( cd "$A9" && "$BIN" . --quality-delta --no-cache 2>/dev/null )" -for sym in plain_case flavored_case stacked_case blank_line_case doc_comment_case test_is_here; do +for sym in plain_case flavored_case stacked_case blank_line_case doc_comment_case one_line_case indented_in_test_mod test_is_here; do printf '%s' "$QD9" | grep -q "kind=\"dead-code\" sym=\"$sym\"" \ && no "arm9: '$sym' is a #[test] function but was reported as dead-code" \ || ok "arm9: '$sym' (#[test] shape) excluded from dead-code" done -for sym in testing_attribute_is_not_a_test commented_attribute_is_not_a_test test_case_attribute_is_not_a_test dead_after_test_attribute dead_plain; do +for sym in testing_attribute_is_not_a_test commented_attribute_is_not_a_test tested_attribute_is_not_a_test not_a_test_after_code really_dead dead_after_test_attribute dead_plain; do printf '%s' "$QD9" | grep -q "kind=\"dead-code\" sym=\"$sym\"" \ && ok "arm9: '$sym' (negative lookalike) still flagged" \ || { no "arm9: '$sym' should still be flagged — the test-attribute exemption leaked"; printf '%s\n' "$QD9" | grep -oE 'sym="[^"]*"' | head -12; } @@ -419,8 +434,8 @@ done printf '%s' "$QD9" | grep -q 'kind="dead-code" sym="live_helper"' \ && no "arm9: live_helper (called from tests) wrongly reported" \ || ok "arm9: live_helper (called from the tests) not reported" -printf '%s' "$QD9" | grep -q 'register-macro-excluded="6"' \ - && ok "arm9: register-macro-excluded=\"6\" (the six #[test] functions)" \ +printf '%s' "$QD9" | grep -q 'register-macro-excluded="8"' \ + && ok "arm9: register-macro-excluded=\"8\" (the eight #[test] functions)" \ || { no "arm9: register-macro-excluded count wrong"; printf '%s\n' "$QD9" | grep -oE 'register-macro-excluded="[0-9]*"'; } # ── ARM 10: a test attribute with no item after it must not break the scan ───────────────────────────── From 8ff0cae9f339c3a20ad8943dac6322f7fa4cd0b2 Mon Sep 17 00:00:00 2001 From: Oleg XXL Date: Tue, 3 Nov 2026 14:56:00 +0200 Subject: [PATCH 09/11] docs(legend): register-macro-excluded= also names Rust test attributes The register-macro-excluded= rows in the dead-code and quality-delta compact legends (src/compactlegend.h), the kQdRegisterMacroLegend constant, and the dead-code header comment (src/verbs_quality.h) now say the count includes Rust #[test] and #[tokio::test] functions. The quality-delta row says "skipped" where it said "kept out of dead-code": the longer wording with "Rust test attributes" put the compact quality-delta legend 16 B over its 940 B budget (compactlegendcheck), and "Rust #[test]" with "skipped" measures 934 B. Raised in the maintainer review of #499. --- src/compactlegend.h | 4 ++-- src/verbs_quality.h | 10 ++++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/compactlegend.h b/src/compactlegend.h index 7eeec2eb2..a022773b6 100644 --- a/src/compactlegend.h +++ b/src/compactlegend.h @@ -829,7 +829,7 @@ inline constexpr CompactCompletenessTerm kCompactAttributeReadings[] = { "sig", "s sig=: the join symbol's declaration signature; dropped first when a max_tokens ceiling trims", true, "s", MapHeaderRead::No, {}, "connect" }, // dead-code: src/verbs_quality.h (the root emit) { "evidence", "evidence=: the rule every row met, internal linkage and no caller in the index; verify before deleting", false, "dead-code", MapHeaderRead::No, {}, "dead-code" }, - { "register-macro-excluded", "register-macro-excluded=N: symbols skipped as self-registering test/bench macros (TEST, BENCHMARK...); a floor", false, "dead-code", MapHeaderRead::No, {}, "dead-code" }, + { "register-macro-excluded", "register-macro-excluded=N: symbols skipped as self-registering test/bench macros or Rust #[test] (TEST, BENCHMARK...); a floor", false, "dead-code", MapHeaderRead::No, {}, "dead-code" }, // 0.6.6 D4: present-only — absent at 0, so a tree with no Python runner root is byte-identical { "runner-root-excluded", "runner-root-excluded=N: Python defs skipped as pytest/unittest test-runner roots; a floor", true, "dead-code", MapHeaderRead::No, {}, "dead-code" }, // 0.6.6 D4 review: counted apart — a decorator MAY register the def; a wrapper (@property, @lru_cache) registers nothing @@ -931,7 +931,7 @@ inline constexpr CompactCompletenessTerm kCompactAttributeReadings[] = { "stale_next", "stale_next=: the call printing the sa rows and the full header", false, "quality-delta", MapHeaderRead::No, {}, "quality-delta" }, { "preexisting-worse", "preexisting-worse=N: regressions on symbols that existed at baseline; they gate when major", false, "quality-delta", MapHeaderRead::No, {}, "quality-delta" }, { "new-symbol", "new-symbol=N: findings on NEW code; new-symbol rows never gate, except defect-shape format-arity", false, "quality-delta", MapHeaderRead::No, {}, "quality-delta" }, - { "register-macro-excluded", "register-macro-excluded=N: symbols kept out of dead-code as self-registering test/bench macros; a floor", false, "quality-delta", MapHeaderRead::No, {}, "quality-delta" }, + { "register-macro-excluded", "register-macro-excluded=N: symbols skipped as self-registering test/bench macros or Rust #[test]; a floor", false, "quality-delta", MapHeaderRead::No, {}, "quality-delta" }, { "declined-call-excluded", "declined-call-excluded=N: symbols kept out of dead-code only because a declined call may mean them; a floor", false, "quality-delta", MapHeaderRead::No, {}, "quality-delta" }, { "api-new-surface", "api-new-surface=N: new PUBLIC symbols; a count, never gates, not in regressions=", false, "quality-delta", MapHeaderRead::No, {}, "quality-delta" }, { "renames", "renames=/rename_window_commits=: git rename pairs read over that many commits, to re-file baseline and acks", false, "quality-delta", MapHeaderRead::No, {}, "quality-delta" }, // also defines rename_window_commits= diff --git a/src/verbs_quality.h b/src/verbs_quality.h index e8dfdcf62..2393b4fb3 100644 --- a/src/verbs_quality.h +++ b/src/verbs_quality.h @@ -911,8 +911,9 @@ inline constexpr const char* kQdLegendCore = // excluded something, because on a repo with no such macro the roster explains an empty set. inline constexpr const char* kQdRegisterMacroLegend = "The registered families are doctest/Catch2 TEST_CASE, GoogleTest TEST/TEST_F/TEST_P, Google Benchmark " - "BENCHMARK, plus any name a .ripwire_config register_macros= line adds; each registers itself through a " - "static initializer the call graph cannot see, so zero in-edges on one is not evidence of anything. "; + "BENCHMARK, plus any name a .ripwire_config register_macros= line adds, and Rust #[test] / #[tokio::test] " + "functions; each registers itself through a static initializer or the test harness, which the call graph cannot " + "see, so zero in-edges on one is not evidence of anything. "; // F-13 (audit 2026-09-02) — printed only when .ripwire_config itself has something wrong with it, which // used to be a silent no-op: a typo'd key was skipped with no trace, and a register_macros= name that @@ -3122,8 +3123,9 @@ std::optional runQualityViews( const MainDispatch& d ) "(test/x/src/y.cpp included); anchor with ./ (filter=\"./src\") to pin the root-level directory only. " "register-macro-excluded= counts symbols excluded because their OWN definition is a registered " "self-registering test/benchmark macro call (doctest/Catch2 TEST_CASE family, GoogleTest TEST/TEST_F/TEST_P, " - "Google Benchmark BENCHMARK family, plus any name a .ripwire_config register_macros= line adds): such a " - "symbol registers itself through a static initializer the call graph cannot see, so zero in-edges on it is " + "Google Benchmark BENCHMARK family, plus any name a .ripwire_config register_macros= line adds), or a Rust " + "#[test] or #[tokio::test] function: such a symbol registers itself through a static initializer or the test " + "harness, which the call graph cannot see, so zero in-edges on it is " "not evidence of anything — never a finding, never gating, absent nothing (0 is printed, not omitted). " "config-warnings= counts two DISCLOSED .ripwire_config problems, each also written to stderr — an " "unrecognized key, and a register_macros= name matching no indexed symbol — never gating, present " From 270548d74673e01de32a3ef11da5d1450c073980 Mon Sep 17 00:00:00 2001 From: Oleg XXL Date: Tue, 3 Nov 2026 15:18:05 +0200 Subject: [PATCH 10/11] docs(legend): name the macro examples beside the macros in the dead-code row The register-macro-excluded= dead-code legend row now reads "...self-registering test/bench macros (TEST, BENCHMARK...) or Rust #[test]", so the examples sit next to the macros they name and the Rust attribute is the trailing clause. The wording is the same length (126 B) and the quality-delta row is unchanged. Follow-up to the maintainer review of #499. --- src/compactlegend.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compactlegend.h b/src/compactlegend.h index a022773b6..ca48c7942 100644 --- a/src/compactlegend.h +++ b/src/compactlegend.h @@ -829,7 +829,7 @@ inline constexpr CompactCompletenessTerm kCompactAttributeReadings[] = { "sig", "s sig=: the join symbol's declaration signature; dropped first when a max_tokens ceiling trims", true, "s", MapHeaderRead::No, {}, "connect" }, // dead-code: src/verbs_quality.h (the root emit) { "evidence", "evidence=: the rule every row met, internal linkage and no caller in the index; verify before deleting", false, "dead-code", MapHeaderRead::No, {}, "dead-code" }, - { "register-macro-excluded", "register-macro-excluded=N: symbols skipped as self-registering test/bench macros or Rust #[test] (TEST, BENCHMARK...); a floor", false, "dead-code", MapHeaderRead::No, {}, "dead-code" }, + { "register-macro-excluded", "register-macro-excluded=N: symbols skipped as self-registering test/bench macros (TEST, BENCHMARK...) or Rust #[test]; a floor", false, "dead-code", MapHeaderRead::No, {}, "dead-code" }, // 0.6.6 D4: present-only — absent at 0, so a tree with no Python runner root is byte-identical { "runner-root-excluded", "runner-root-excluded=N: Python defs skipped as pytest/unittest test-runner roots; a floor", true, "dead-code", MapHeaderRead::No, {}, "dead-code" }, // 0.6.6 D4 review: counted apart — a decorator MAY register the def; a wrapper (@property, @lru_cache) registers nothing From d09c126994011f85821e64dec8af708e56858920 Mon Sep 17 00:00:00 2001 From: Oleg XXL Date: Tue, 3 Nov 2026 15:25:38 +0200 Subject: [PATCH 11/11] docs(captures): re-record the legend pair from the built binary (#499 follow-up) The newest capture's --legend-dict line, and the two places that quote that exact pair (docs/COMMANDS.md, changelog.d/versions.md), now carry the live dictionary of the rebuilt binary. The capture's legend-dict pair was dictv=02b8cec3f8563e22 entries=1014, and the live pair is dictv=2451f0e77d06e369 entries=1014. The changed dead-code legend row is the cause. No other byte of the capture moved. Done by hand, with the same grep, sed and replacement steps as scripts/merge_main.sh --regen-after-build, because this merge (0772277f) carries no REGEN AFTER BUILD mark. docs/docs_commands_build.py --bin build/ripwire and its --check both pass. The older capture (2026-10-31) keeps its own pair. Follow-up to the maintainer review of #499. --- changelog.d/versions.md | 2 +- docs/COMMANDS.md | 2 +- docs/captures/COMMANDS_showcase_2026-11-02.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/changelog.d/versions.md b/changelog.d/versions.md index e5bc390d5..204c88c46 100644 --- a/changelog.d/versions.md +++ b/changelog.d/versions.md @@ -9,6 +9,6 @@ smart-pointer alias carries its pointee, a member whose written type is a templa branches (257–262), so a cache written before them would replay the older rows. 271 sits above every live branch and pull-request number (the string/literal records carry 263, 265 and 267 on theirs). `kCacheVersion` 46 → 48: a re-export specifier is a new binding kind appended to the cache record (its branch carried 47). A cache written by 0.6.9 is re-parsed -once. The quality-snapshot scheme moves 24 → 25 (#472: `isTestPath` reads more shell test names, which moves the dead set's test-script exemption; no snapshot record changes shape, and a v24 blob is rebuilt once). The session legend dictionary is `dictv=02b8cec3f8563e22 entries=1014`. +once. The quality-snapshot scheme moves 24 → 25 (#472: `isTestPath` reads more shell test names, which moves the dead set's test-script exemption; no snapshot record changes shape, and a v24 blob is rebuilt once). The session legend dictionary is `dictv=2451f0e77d06e369 entries=1014`. The history-walk cache's `kQChurnCacheScheme` moves 3 → 4 (#433: a blob written by a pre-fix binary holds the old line-frame reader's stream, which for the repositories the framing fix targets is the corrupted one, and no key component can tell that blob from a fresh read; every repository's history is walked once more after the upgrade, and on trees with ordinary names the walk produces the same stream, so the cost is one cold walk and no changed answer). diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index 72e51a2eb..5f6bc51de 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -4624,7 +4624,7 @@ _The session legend dictionary the MCP server serves as ripwire://legend-dict/fu ``` $ ./build/ripwire . --legend-dict -ripwire legend dictionary ripwire.dict/v1 dictv=02b8cec3f8563e22 entries=1014 +ripwire legend dictionary ripwire.dict/v1 dictv=2451f0e77d06e369 entries=1014 : the answer's rows come first; its root keeps only task= changed= from= to=, and this LAST child carries every other root attribute unchanged (schema= included); legend="ref": a definition is sent once per session (this dictionary's core, or the first answer that ne … [line truncated: 83 more bytes on this line] schema=ripwire.KEY/v1: the line ripwire.KEY/v1 below reads the answer's rows window: shown= total= capped= has_more= next_offset= offset= limit= page a list (capped=1 cut; next_offset= pastes as offset=) diff --git a/docs/captures/COMMANDS_showcase_2026-11-02.md b/docs/captures/COMMANDS_showcase_2026-11-02.md index 0ff661d68..496f52e14 100644 --- a/docs/captures/COMMANDS_showcase_2026-11-02.md +++ b/docs/captures/COMMANDS_showcase_2026-11-02.md @@ -6052,7 +6052,7 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir *The session legend dictionary the MCP server serves as ripwire://legend-dict/full — one definition per line, headed by its dictv= version; no corpus needed. =roster lists the completeness attributes it defines.* ````` -ripwire legend dictionary ripwire.dict/v1 dictv=02b8cec3f8563e22 entries=1014 +ripwire legend dictionary ripwire.dict/v1 dictv=2451f0e77d06e369 entries=1014 : the answer's rows come first; its root keeps only task= changed= from= to=, and this LAST child carries every other root attribute unchanged (schema= included); legend="ref": a definition is sent once per session (this dictionary's core, or the first answer that ne … [line truncated: 83 more bytes on this line] schema=ripwire.KEY/v1: the line ripwire.KEY/v1 below reads the answer's rows window: shown= total= capped= has_more= next_offset= offset= limit= page a list (capped=1 cut; next_offset= pastes as offset=)