Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog.d/rust-test-harness-dead-code.md
Original file line number Diff line number Diff line change
@@ -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]`, `#[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.
2 changes: 1 addition & 1 deletion changelog.d/versions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
2 changes: 1 addition & 1 deletion docs/COMMANDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<about legend="ref" dict= dictv=>: 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=)
Expand Down
2 changes: 1 addition & 1 deletion docs/captures/COMMANDS_showcase_2026-11-02.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<about legend="ref" dict= dictv=>: 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=)
Expand Down
4 changes: 2 additions & 2 deletions src/compactlegend.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 <dead-code> 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 (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
Expand Down Expand Up @@ -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=
Expand Down
6 changes: 5 additions & 1 deletion src/langfacts.h
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,10 @@ struct LangFacts
/// TypeScript, JavaScript, Rust, Swift, Kotlin: a bare `{` assigned or returned inside a function body is an object literal (a
/// call's options or a value), a block or a closure, never a table; in Python (a dict), C, C++, Java, Go it can hold data (forliteral.h).
bool localBraceNotData = false;

/// 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).
Expand All @@ -338,7 +342,7 @@ inline constexpr std::array<LangFacts, kLangCount> kLangFacts = { {
{ .lang = Lang::Go, .structuralTyping = true, .writtenModule = WrittenModuleRule::PackageDir,
.memberNeverReachesFreeFunction = true, .moduleCallReachesFunctionsOnly = true, .signatureRows = true },
{ .lang = Lang::Rust, .contractSignaturesUnindexed = true, .traitImplBlocks = true, .rule3NameProof = Rule3NameProof::RustUse, .returnTypedDispatchLet = true, .constructorMethod = "new", .memberSeparator = "::", .baseImportEvidence = BaseImportEvidence::RustUse,
.instanceReceiver = "self", .memberNeverReachesFreeFunction = true, .fileEdges = FileEdges::Resolved, .localBraceNotData = true },
.instanceReceiver = "self", .memberNeverReachesFreeFunction = true, .fileEdges = FileEdges::Resolved, .localBraceNotData = true, .attributeRegistersTests = true },
{ .lang = Lang::Swift, .writtenModule = WrittenModuleRule::DirsOnly, .instanceReceiver = "self",
.implicitReceiver = true, .memberNeverReachesFreeFunction = true, .classNameReceiver = true, .localBraceNotData = true },
{ .lang = Lang::ObjC, .noNestedNamedFunctions = true, .transitiveIncludes = true, .staticLinkage = true, .fileEdges = FileEdges::Resolved },
Expand Down
149 changes: 144 additions & 5 deletions src/quality.h
Original file line number Diff line number Diff line change
Expand Up @@ -1219,8 +1219,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<class Fn>
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; } );
Expand All @@ -1242,11 +1244,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<class Fn>
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
Expand Down Expand Up @@ -1658,6 +1669,126 @@ inline std::vector<std::uint8_t> 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;
}

// 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<unsigned char>( 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. 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
{
std::size_t end = std::min( sigStart, file.size() ); // everything before `end` is still unread
for( ;; )
{
while( end > 0 && std::isspace( static_cast<unsigned char>( 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<unsigned char>( line.front() ) ) )
{
line.remove_prefix( 1 );
}
if( line.substr( 0, 2 ) == "//" )
{
end = from; // a comment or doc line: keep reading upward
continue;
}
line = rustCodeOfAttributeLine( line );
if( line.empty() )
{
end = from;
continue;
}
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;
}
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<std::string>& 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
Expand All @@ -1672,9 +1803,9 @@ inline std::vector<NodeId> 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 );
}
Expand Down Expand Up @@ -3673,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
Expand Down Expand Up @@ -3800,7 +3935,11 @@ inline void evictOldHeadSnapCaches( const std::string& dir, const std::string& r
// v25 (2026-11-01, land-472, #466 / #472) — filter.h's isTestPath reads the hyphen test names `test-*.sh`, `*-test.sh` (and the
// other shell/batch suffixes) and `.e2e-spec.` / `.perf-spec.` as tests, and isTestScriptPath calls it, so a shell test script the
// dead-set exemption did not cover before is covered now. A v24 blob holds the old dead set. Renumbered above every live value (24).
constexpr std::uint32_t kQSnapCacheScheme = 25;
// 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); 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' };

// The qsnap EXCLUDES-config key folds the qsnap SCHEME (independent of the ingest cache's kHeadSnapCacheScheme)
Expand Down
Loading