diff --git a/CHANGELOG.md b/CHANGELOG.md index 0be41a723..0ce6bd1ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,49 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli --- +## [Unreleased] + + +### Added — RSpec's `described_class` is the class its example group names, so a spec's calls pin to the class under test (#338) + +`described_class` is how RSpec spells the class under test. Inside `RSpec.describe Calc do … end`, +`described_class.m( 1 )` is `Calc.m( 1 )`. Its receiver is a bare identifier that no binding names, though, so every +such call declined. A spec reached nothing through it, and `tested=`, `--seams` and `--test-gate` read the class under +test as unreached by the spec written for it. + +The call now pins the way a written `Calc.m( 1 )` does (#267's constant-receiver arm). The rule is RSpec's own +(rspec-core 3.13, `Metadata::ExampleGroupHash#described_class`): a group's described class is its first description +argument, unless that is `nil` or a String, in which case it is the parent group's. So the innermost enclosing example +group with a constant first argument answers. An example group is `describe` / `context` (and `feature`, +`example_group` and the `x`- / `f`- spellings), called bare or on `RSpec`, with a block. A shared group +(`shared_examples`, `shared_examples_for`, `shared_context`) stops the walk with no answer, because its body runs in +whichever group includes it. + +Stated floors, each pinned by `test/rubydescribedclasscheck.sh`: +- **(a)** A chained receiver (`described_class.new.m`) is untouched: #267's one-hop bound. +- **(b)** A group with no constant (`describe "text"` at the top, `describe :sym`) names no class. +- **(c)** A `describe` on any other receiver (`Docs.describe Calc do`) is not an RSpec example group. +- **(d)** `subject`, the implicit `described_class.new`, is not modelled. +- **(e)** A redefined `described_class` declines. A **method** of that name (any `:described_class` symbol, as in + `let( :described_class )`, or `def described_class`) declines every site in the file. A **local** of that name (an + assignment, `||=`, a multiple-assignment target, or a block or method parameter) declines the sites Ruby reads as + that local: after the binding in its own scope and in the blocks nested inside it, never in a sibling block, and + never across a `def`. The name is matched as a whole word, so `my_described_class = x` redefines nothing. +- **(f)** A qualified describe is named by its final segment: `RSpec.describe Cask::Tab` reads as `Tab`. Where another + `Tab` defines the method too, the call splits between them. Where only the other `Tab` defines it, the call pins + there, unmarked. A written `Cask::Tab.m` behaves the same way. + +Measured with `--no-cache`, `--report` edge totals, `main` (3fcd515f) against this change: + +| Corpus | Edges | +| --- | --- | +| Rails app A | 26,649 → 26,755 (+106) | +| Rails app B | 20,807 → 21,149 (+342) | +| activerecord, activesupport, actionpack 8.1.3 `lib/`; this repo's `src/` | default map byte-identical | + +`kParserVer` → 125 (carried as 121 on the PR): `RawRef::recv` / `recvVar` change value for these call sites. +The record layout is unchanged, so `kCacheVersion` stays 25. + ## [0.6.5] — 2026-09-27 diff --git a/README.md b/README.md index bb3b7e45d..369dcdd73 100644 --- a/README.md +++ b/README.md @@ -1977,9 +1977,9 @@ wrong, and it has. These are the results that say so, all in-tree, all published ### In the tests
-650 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures +651 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures -`test/regression.sh` names **650 gate scripts** and is the authoritative list; +`test/regression.sh` names **651 gate scripts** and is the authoritative list; `python3 test/pargates.py . ./build/ripwire -j 6` runs the same set in parallel. On top of them sit the contracts that do not fit a unit test: two runs byte-identical, warm output identical to cold, output that pipes clean through `xmllint --noout`, a sanitizer build with `-fno-sanitize-recover=all`, and a diff --git a/docs/EVALS.md b/docs/EVALS.md index a1ef168dd..26938021f 100644 --- a/docs/EVALS.md +++ b/docs/EVALS.md @@ -21,7 +21,7 @@ section, and it is not an afterthought. | **Co-change / known-item evals** | `--eval`, `--eval-retrieval` (see `bench/ANSWERQUALITY.md`) | Whether the tool surfaces the other files a real historical commit touched; and known-item retrieval across four rankers. | | **Ensemble calibration harness** | `bench/ensemblecal/` | Whether `--ensemble`'s four evidence families are actually orthogonal, how often each fires, how stable each is across commits — and the preset ladder derived from that (§9). | | **Differential argv harness** | `test/argvdiffcheck.sh` | That a refactor changed *nothing observable*: two binaries, every argv vector, stdout + stderr + exit code byte-identical. | -| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 650 gate scripts plus the determinism, cache-transparency and golden contracts. | +| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 651 gate scripts plus the determinism, cache-transparency and golden contracts. | | **`--quality-delta`** | `src/quality.h` | Ten measured code-quality failure modes, reported only where a change made them worse. | ### The labeling protocol (why the held-out eval is allowed to disagree with the ranker) @@ -5866,7 +5866,7 @@ copy here would be exactly the dialect divergence that gate exists to catch. Com tags, wrap, stable-order defaults), seven individually invoked standalone gates (`g1freshcheck`, `skillscan`, `htmlexport`, `compresscheck`, `handoffcheck`, `releaseinstallcheck`, `taskroutecheck`), and a single loop -naming **650 gate scripts**, all of which exist on disk. +naming **651 gate scripts**, all of which exist on disk. `python3 test/pargates.py . ./build/ripwire -j 6` runs the same scripts in parallel so a full verification fits in one sitting. It does not modify `regression.sh`. @@ -6885,7 +6885,7 @@ Listed because the reason is more useful than the silence. shipped**. See `bench/locbench/anchorhop_calib.json`. The mention anchor's reproducible numbers are the ablations in §4. - **A single round gate-count.** Two in-tree numbers disagree (`test/pargates.py`'s docstring says - ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 650. The + ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 651. The loop is the authority; the stale docstrings are a known drift. Since 2026-09-10 the number is not written by hand anywhere: `docs/gatecount_build.py` derives it from the loop and rewrites every published site, `test/gatecountcheck.sh` fails if any of them drifts, and `test/manifestcheck.sh` diff --git a/present/deck5_ripwire_build.js b/present/deck5_ripwire_build.js index d32ba5cc7..cbe043f1f 100644 --- a/present/deck5_ripwire_build.js +++ b/present/deck5_ripwire_build.js @@ -1123,7 +1123,7 @@ function storyCards(s, { kick, head, stories, footText }){ kicker(s, "// how it stays true", AMBER); title(s, "Proven, not promised"); const cards = [ - ["650 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is gated against the runner's own loop"], // gatecount + ["651 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is gated against the runner's own loop"], // gatecount ["byte-identical, always", "two runs over the same tree produce the same bytes; warm equals cold. Enforced in CI, twice — Release AND a plain flavour, because NDEBUG once blinded a whole class of checks"], ["differential refactoring", "a refactor must prove it changed nothing observable: two binaries, hundreds of argv vectors, stdout + stderr + exit codes byte-identical"], ["held-out labels, authored blind", "eval labels were written by reading source before the ranker ever ran on them — so the eval is allowed to say the ranker is wrong. It has."], @@ -1147,7 +1147,7 @@ function storyCards(s, { kick, head, stories, footText }){ title(s, "Claims you can trust, because we publish what failed", { size: 32 }); card(s, MX, 1.72, 3.86, 1.72); - stat(s, "650", "gate scripts named by test/regression.sh — and the COUNT itself is gated against the runner's own loop, so it cannot go stale quietly", // gatecount + stat(s, "651", "gate scripts named by test/regression.sh — and the COUNT itself is gated against the runner's own loop, so it cannot go stale quietly", // gatecount MX+0.15, 1.86, 3.56, CYAN, { bsize: 42, bh: 0.66, lsize: 9.5 }); card(s, 4.68, 1.72, 3.86, 1.72, CARD2); stat(s, "8", "registered NEGATIVES — changes built, gated green, measured against a band written before the code, and reverted rather than tuned", @@ -1397,7 +1397,7 @@ function storyCards(s, { kick, head, stories, footText }){ ["183 long flags · 34 slides", "bash test/deckclaimcheck.sh"], ["every --flag named here exists", "bash test/deckcheck.sh"], ["74.7% fewer element bytes", "bash test/showcasecapturecheck.sh"], - ["650 gate scripts", "bash test/manifestcheck.sh"], // gatecount + ["651 gate scripts", "bash test/manifestcheck.sh"], // gatecount ["49 repos · 71 papers · 237 surveyed","bash test/readmedriftcheck.sh"], ["the ten moments, any row", "ripwire . --callers=SYM | wc -c"], ["the head-to-head table", "bench/headtohead/r4-2026-08-06/"], diff --git a/src/ingest_binds.h b/src/ingest_binds.h index f8e69ec17..ad7c987a5 100644 --- a/src/ingest_binds.h +++ b/src/ingest_binds.h @@ -87,6 +87,228 @@ struct RecvShape // through to the honest ladder. `Outer::run( 1 )` never arrives as a scope_resolution at all — // tree-sitter-ruby parses it as an ordinary (call) with a (constant) receiver, exactly like // `Outer.run( 1 )`, so both spellings narrow through the (constant) arm. test/rubyrecvnarrowcheck.sh. +// The FINAL constant segment a (constant) / (scope_resolution) names — `Calc` → `Calc`, `Outer::Engine` → `Engine` — +// or empty when the node is a (scope_resolution) whose `name:` is not a (constant). Callers have checked the kind. +inline std::string_view rubyFinalConstant( TSNode node, std::string_view src ) +{ + if( kindIs( ts_node_type( node ), "constant" ) ) + { + return pattern::nodeText( node, src ); + } + return fieldChildTextOfKind( node, NodeField::Name, "constant", src ); +} + +inline bool isRubyConstantNode( TSNode node ) noexcept +{ + const char* t = ts_node_type( node ); + return kindIs( t, "constant" ) || kindIs( t, "scope_resolution" ); +} + +// An RSpec EXAMPLE GROUP call: describe/context (and the feature/example_group and x-/f- spellings), called bare or +// on `RSpec`. 1 = a group, 2 = a SHARED group (shared_examples/_for, shared_context), 0 = neither. A shared group's +// body runs inside whichever group INCLUDES it, so its described_class is not its lexical parent's. +inline int rspecGroupKind( TSNode call, std::string_view src ) +{ + const TSNode recv = fieldChild( call, NodeField::Receiver ); + if( !ts_node_is_null( recv ) && !( kindIs( ts_node_type( recv ), "constant" ) && pattern::nodeText( recv, src ) == "RSpec" ) ) + { + return 0; + } + const TSNode method = fieldChild( call, NodeField::Method ); + if( ts_node_is_null( method ) ) + { + return 0; + } + static constexpr std::string_view kGroups[] = { "describe", "context", "feature", "example_group", + "xdescribe", "fdescribe", "xcontext", "fcontext" }; + static constexpr std::string_view kShared[] = { "shared_examples", "shared_examples_for", "shared_context" }; + const std::string_view m = pattern::nodeText( method, src ); + if( std::ranges::find( kGroups, m ) != std::end( kGroups ) ) + { + return 1; + } + return std::ranges::find( kShared, m ) != std::end( kShared ) ? 2 : 0; +} + +// One example group's FIRST description argument, as RSpec reads it: nullopt when it is nil or a String (the parent +// group's described class stands), otherwise the answer — a constant's final segment, or empty for anything else +// (`describe :sym`, a variable), which names no class this tool can resolve. +inline std::optional rspecGroupArgument( TSNode call, std::string_view src ) +{ + const TSNode args = fieldChild( call, NodeField::Arguments ); + const TSNode first = ts_node_is_null( args ) ? args : ts_node_named_child( args, 0 ); + if( ts_node_is_null( first ) || kindIs( ts_node_type( first ), "string" ) || kindIs( ts_node_type( first ), "nil" ) ) + { + return std::nullopt; + } + return isRubyConstantNode( first ) ? rubyFinalConstant( first, src ) : std::string_view {}; +} + +// True when the file defines a METHOD named described_class — any `:described_class` symbol (`let( :described_class )`, +// `subject( :described_class )`, `define_method( :described_class )`) or `def described_class` / `def self.described_class`. +// A method reaches every example in its group, so the file's sites decline (floor (e)). Conservative on purpose, but a +// whole word: `:described_class_name` and `my_described_class` are other names. +inline bool rubyDefinesDescribedClassMethod( std::string_view src ) noexcept +{ + static constexpr std::string_view kName = "described_class"; + for( std::size_t at = src.find( kName ); at != std::string_view::npos; at = src.find( kName, at + kName.size() ) ) + { + const std::size_t end = at + kName.size(); + if( end < src.size() && ( namesplit::isIdentChar( src[ end ] ) || src[ end ] == '?' || src[ end ] == '!' ) ) + { + continue; // `described_class_name`, `described_class?` — another name + } + const std::string_view before = src.substr( 0, at ); + if( before.ends_with( ':' ) || before.ends_with( "def " ) || before.ends_with( "def self." ) ) + { + return true; + } + } + return false; +} + +// How a Ruby node kind takes part in LOCAL scoping, for rubyDescribedClassIsLocal. A Closure (a block, a lambda) keeps +// its locals in but sees the enclosing ones; a Wall (a def, a class, a module) sees no outer local; the Binds* kinds +// bind a local — the `left:` child, the `name:` child (not a parameter's default value), or any child identifier. +enum class RubyLocalRole : std::uint8_t { Closure, Wall, BindsLeft, BindsName, BindsChildren }; +struct RubyLocalKind +{ + std::string_view kind; + RubyLocalRole role; +}; +inline constexpr RubyLocalKind kRubyLocalKinds[] = { + { "block", RubyLocalRole::Closure }, { "do_block", RubyLocalRole::Closure }, + { "lambda", RubyLocalRole::Closure }, { "method", RubyLocalRole::Wall }, + { "singleton_method", RubyLocalRole::Wall }, { "class", RubyLocalRole::Wall }, + { "singleton_class", RubyLocalRole::Wall }, { "module", RubyLocalRole::Wall }, + { "assignment", RubyLocalRole::BindsLeft }, { "operator_assignment", RubyLocalRole::BindsLeft }, + { "optional_parameter", RubyLocalRole::BindsName }, { "keyword_parameter", RubyLocalRole::BindsName }, + { "block_parameters", RubyLocalRole::BindsChildren }, { "method_parameters", RubyLocalRole::BindsChildren }, + { "lambda_parameters", RubyLocalRole::BindsChildren }, { "left_assignment_list", RubyLocalRole::BindsChildren }, + { "destructured_left_assignment", RubyLocalRole::BindsChildren }, { "rest_assignment", RubyLocalRole::BindsChildren }, + { "destructured_parameter", RubyLocalRole::BindsChildren }, { "splat_parameter", RubyLocalRole::BindsChildren }, + { "hash_splat_parameter", RubyLocalRole::BindsChildren }, { "block_parameter", RubyLocalRole::BindsChildren }, + { "exception_variable", RubyLocalRole::BindsChildren }, +}; + +inline std::optional rubyLocalRole( const char* t ) noexcept +{ + const auto it = std::ranges::find( kRubyLocalKinds, std::string_view( t ), &RubyLocalKind::kind ); + return it == std::end( kRubyLocalKinds ) ? std::nullopt : std::optional( it->role ); +} + +// True when `n`, whose kind plays `role`, itself BINDS the local described_class. +inline bool rubyNodeBindsDescribedClass( TSNode n, std::optional role, std::string_view src ) +{ + const auto named = [ & ]( TSNode c ) { return !ts_node_is_null( c ) && kindIs( ts_node_type( c ), "identifier" ) && pattern::nodeText( c, src ) == "described_class"; }; + if( role == RubyLocalRole::BindsLeft || role == RubyLocalRole::BindsName ) + { + return named( fieldChild( n, role == RubyLocalRole::BindsLeft ? NodeField::Left : NodeField::Name ) ); + } + if( role != RubyLocalRole::BindsChildren ) + { + return false; + } + const std::uint32_t cc = ts_node_named_child_count( n ); + for( std::uint32_t i = 0; i < cc; ++i ) + { + if( named( ts_node_named_child( n, i ) ) ) + { + return true; + } + } + return false; +} + +// True when a child of `n` that ENDS before byte `at` — an earlier statement, a block's or a def's parameters — binds +// described_class, searched without entering a nested block, lambda, def, class or module, whose locals never reach +// past it. `stack` is the caller's scratch, so a hostile nesting depth costs heap, not stack. +inline bool rubyEarlierChildBindsDescribedClass( TSNode n, std::uint32_t at, std::string_view src, std::vector& stack ) +{ + stack.clear(); + const std::uint32_t cc = ts_node_named_child_count( n ); + for( std::uint32_t i = 0; i < cc && ts_node_end_byte( ts_node_named_child( n, i ) ) <= at; ++i ) + { + stack.push_back( ts_node_named_child( n, i ) ); + } + while( !stack.empty() ) + { + const TSNode cur = stack.back(); + stack.pop_back(); + const std::optional role = rubyLocalRole( ts_node_type( cur ) ); + if( rubyNodeBindsDescribedClass( cur, role, src ) ) + { + return true; + } + if( role == RubyLocalRole::Closure || role == RubyLocalRole::Wall ) + { + continue; // a nested scope's locals never reach the site + } + const std::uint32_t kids = ts_node_named_child_count( cur ); + for( std::uint32_t i = 0; i < kids; ++i ) + { + stack.push_back( ts_node_named_child( cur, i ) ); + } + } + return false; +} + +// True when the (identifier) `site` reads a LOCAL named described_class, not RSpec's method — Ruby's own rule: a local +// is visible after its binding, in its own scope and in the blocks nested inside it. So the walk climbs from the site, +// searching each enclosing node's earlier children, and stops after the first def, class or module. An enclosing +// assignment counts too: in `described_class = described_class.m` the right side already reads the local. Floor (e). +inline bool rubyDescribedClassIsLocal( TSNode site, std::string_view src ) +{ + const std::uint32_t at = ts_node_start_byte( site ); + std::vector stack; + for( TSNode n = ts_node_parent( site ); !ts_node_is_null( n ); n = ts_node_parent( n ) ) + { + const std::optional role = rubyLocalRole( ts_node_type( n ) ); + if( rubyNodeBindsDescribedClass( n, role, src ) || rubyEarlierChildBindsDescribedClass( n, at, src, stack ) ) + { + return true; + } + if( role == RubyLocalRole::Wall ) + { + break; // a def, class or module: no outer local reaches in + } + } + return false; +} + +// RSpec's `described_class` — a bare (identifier) receiver no binding names — read as the constant it IS. RSpec's +// own rule (rspec-core 3.13, Metadata::ExampleGroupHash#described_class): a group's described class is its FIRST +// description argument unless that is nil or a String; otherwise it is the parent group's. So the walk climbs the +// enclosing calls whose BLOCK holds the site (the child it came from is a do_block/block), and the innermost example +// group with a constant first argument answers. A string or absent argument passes outward; any other argument +// (`describe :sym`, a variable) and a shared group stop the walk with no answer, and the site is left exactly as it +// was. test/rubydescribedclasscheck.sh. +inline std::string_view rspecDescribedClass( TSNode node, std::string_view src ) +{ + TSNode prev = node; + for( TSNode n = ts_node_parent( node ); !ts_node_is_null( n ); prev = n, n = ts_node_parent( n ) ) + { + const char* pt = ts_node_type( prev ); + if( !kindIs( ts_node_type( n ), "call" ) || !( kindIs( pt, "do_block" ) || kindIs( pt, "block" ) ) ) + { + continue; // not a call, or the site is in its receiver/arguments rather than its block + } + const int group = rspecGroupKind( n, src ); + if( group == 2 ) + { + return {}; // a shared group: its body runs in whichever group includes it + } + if( group == 1 ) + { + if( const std::optional arg = rspecGroupArgument( n, src ) ) + { + return *arg; + } + } + } + return {}; +} + // nullopt when the node is neither (classifyReceiver's shared arms decide it); otherwise the answer, which is empty for a // (scope_resolution) whose `name:` is not a (constant). inline std::optional classifyRubyReceiver( TSNode node, std::string_view src ) @@ -96,16 +318,21 @@ inline std::optional classifyRubyReceiver( TSNode node, std::string_v { return RecvShape { RecvKind::ThisObj, {}, {} }; // Ruby `self` — its own node kind, not an identifier } - if( !kindIs( rt, "constant" ) && !kindIs( rt, "scope_resolution" ) ) + if( kindIs( rt, "identifier" ) && pattern::nodeText( node, src ) == "described_class" ) { - return std::nullopt; + const bool redefined = rubyDefinesDescribedClassMethod( src ) || rubyDescribedClassIsLocal( node, src ); + const std::string_view cls = redefined ? std::string_view {} : rspecDescribedClass( node, src ); + if( cls.empty() ) + { + return std::nullopt; // redefined, or no constant-described group encloses it: the identifier arm answers as before + } + return RecvShape { RecvKind::NamedVar, std::string( cls ), {} }; // the group's constant — Rule 2c fuel } - const TSNode leaf = kindIs( rt, "constant" ) ? node : fieldChild( node, NodeField::Name ); - if( ts_node_is_null( leaf ) || !kindIs( ts_node_type( leaf ), "constant" ) ) + if( !isRubyConstantNode( node ) ) { - return RecvShape {}; + return std::nullopt; } - const std::string_view v = pattern::nodeText( leaf, src ); + const std::string_view v = rubyFinalConstant( node, src ); if( v.empty() ) { return RecvShape {}; diff --git a/src/ingest_cache.h b/src/ingest_cache.h index c5f828963..5b04bbb6b 100644 --- a/src/ingest_cache.h +++ b/src/ingest_cache.h @@ -279,7 +279,17 @@ constexpr std::uint32_t kCacheVersion = 25; // 25: #150 AND #157 (trai // (Py `pkg.mod`, TS `./x`, Rust `crate::a::b`/`mod:x`) — // a target FORMAT change → old caches must be rejected. // 4: Include gained a `bool isAngle` (quote/angle) field -constexpr std::uint32_t kParserVer = 124; // bump on any grammar/.scm/extraction change +constexpr std::uint32_t kParserVer = 125; // bump on any grammar/.scm/extraction change + // 125 = 2026-09-27 (#338, test/rubydescribedclasscheck.sh): RSpec's + // `described_class` receiver is classified as the constant + // the innermost constant-described example group names + // (ingest_binds.h::rspecDescribedClass), so RawRef::recv / + // recvVar change VALUE for those call sites — NamedVar + // "Calc" where they were NamedVar "described_class". Carried + // as 121 on the PR; renumbered 125 after #310 (121), #320 + // (122) and #220 (124), with 123 reserved for #325. No record + // layout change: kCacheVersion stays 25 (NOT 24); a Ruby cache + // written at 124 holds the old receiver and must re-parse. // 124 = 2026-09-26 (#220 part 2, test/depsprecisecheck.sh // P2-O): a TS/JS RE-EXPORT (`export … from './y'`) is an // Include like an import (ingest_relations.h diff --git a/src/ingest_relations.h b/src/ingest_relations.h index ccfa1cab6..9c9e665ac 100644 --- a/src/ingest_relations.h +++ b/src/ingest_relations.h @@ -1295,17 +1295,22 @@ inline std::string rubyAutoloadTarget( TSNode n, std::string_view src, bool& sym return std::string( txt ); } -// If the node's field child is a bare identifier, its text; else empty. The field-child-is-an-identifier- -// and-give-me-its-text shape recurs across grammars (elixirTarget, elixirDirectiveTarget, elixirAliasGroup, -// and the Ruby readers below all open with it). -inline std::string_view fieldIdentifierText( TSNode n, NodeField field, std::string_view src ) noexcept +// If the node's field child is of node kind `kind`, its text; else empty. The field-child-of-one-kind-and-give- +// me-its-text shape recurs across grammars: fieldIdentifierText below is its `identifier` case, and +// ingest_binds.h's rubyFinalConstant its `constant` case. The kind stays a literal, so kindIs keeps its +// compile-time length. +template< std::size_t N > +inline std::string_view fieldChildTextOfKind( TSNode n, NodeField field, const char ( &kind )[N], std::string_view src ) noexcept { const TSNode c = fieldChild( n, field ); - if( ts_node_is_null( c ) || !kindIs( ts_node_type( c ), "identifier" ) ) - { - return {}; - } - return nodeTextOf( c, src ); + return !ts_node_is_null( c ) && kindIs( ts_node_type( c ), kind ) ? nodeTextOf( c, src ) : std::string_view {}; +} + +// If the node's field child is a bare identifier, its text; else empty (elixirTarget, elixirDirectiveTarget, +// elixirAliasGroup, and the Ruby readers below all open with it). +inline std::string_view fieldIdentifierText( TSNode n, NodeField field, std::string_view src ) noexcept +{ + return fieldChildTextOfKind( n, field, "identifier", src ); } // The two Ruby named-directive verb sets. `attributes` (plural) has no runtime meaning in base Rails diff --git a/src/quality.h b/src/quality.h index 096862e99..9c77b6522 100644 --- a/src/quality.h +++ b/src/quality.h @@ -2113,7 +2113,9 @@ inline std::string cacheRootKeyHex( const std::string& root ) // not include this header; it relies on ingest.cpp including quality.h (line 13) before ingest_cache.h, and a reorder // that broke that fails the build on the undeclared name rather than passing. constexpr std::uint32_t kIngestCacheVersionMirror = 25; // MUST equal ingest.cpp's kCacheVersion (gated); 25 = #157 + #150 -constexpr std::uint32_t kIngestParserVerMirror = 124; // MUST equal ingest.cpp's kParserVer (gated) +constexpr std::uint32_t kIngestParserVerMirror = 125; // MUST equal ingest.cpp's kParserVer (gated) + // 125 = 2026-09-27 (#338, RSpec described_class, see kParserVer note; + // kIngestCacheVersionMirror stays 25) // 122 = 2026-09-25 (#320/#67, Astro frontmatter, see kParserVer note; // kIngestCacheVersionMirror stays 25) // 121 = 2026-09-24 (#310, Ruby attr DSL, see kParserVer note) diff --git a/test/qschemetrip.hash b/test/qschemetrip.hash index f75fced32..7f446db02 100644 --- a/test/qschemetrip.hash +++ b/test/qschemetrip.hash @@ -1 +1 @@ -c451a79f1cdb996e4263c79f5e30fe0486e0917efdeecd08c1cb375d1040c2cf +dc868b59c7eb9612588c98e631ef03799592213292ac9000a680b7b3ff5981ab diff --git a/test/qschemetripcheck.sh b/test/qschemetripcheck.sh index b28bf0a78..c2f3d78e7 100755 --- a/test/qschemetripcheck.sh +++ b/test/qschemetripcheck.sh @@ -34,6 +34,10 @@ 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-09-27, feat/ruby-described-class (#338) rebased onto upstream/main 3fcd515f: RE-DERIVED with UPDATE_GOLDEN=1 — +# kParserVer 124 -> 125 (RSpec's described_class receiver takes the group's constant; recv/recvVar change value, +# same layout; carried as 121 on the PR, 123 stays reserved for #325). kCacheVersion stays 25, kQSnapCacheScheme +# is untouched, so no cached Snapshot MEANING changes. # 2026-09-27, train 21 (#220 part 2 + builtin-bind merged): RE-DERIVED on the merged tree with UPDATE_GOLDEN=1 — the # hash is c451a79f1c…40c2cf, unchanged from #220's entry below: builtin-bind's kQSnapCacheScheme 14 -> 15 and its # declinedCallMayReach exemption move no hashed declaration, and #220's kParserVer 124 is already in the pin. diff --git a/test/regression.sh b/test/regression.sh index 93f4edea3..92fe71b3a 100755 --- a/test/regression.sh +++ b/test/regression.sh @@ -277,7 +277,7 @@ else RIPWIRE_BIN="$BIN" bash "$ROOT/test/codexdoctorcheck.sh" 2>&1 | sed 's/^/ | /' fi # retired: cacheexclkeycheck — the per-configuration auto-cache key it pinned is a registered NEGATIVE (docs/EVALS.md, "The auto-cache key ignores --exclude", RUN 2026-09-03: a 158K-file root with >= 12 gate configurations thrashed the 2 GiB sweep); the retry design keeps ONE superset blob per root and will bring its own gate -for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agentsurfacecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck astqueryregexcheck astrocheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck buildtypestampcheck builtinbindcheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachereservecheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck ceilingverdictcheck chacheck chaconecheck chainguardcheck chainidcheck childwalkscalecheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck cppbenchcheck cppoperatorcheck cppqualcheck cpptmplscopecheck crashsweepcheck crawlescapecheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck declinecheck declinedlistcheck decltodefcheck deeptailcheck defaultceilingcheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck diagnoticecheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck elixirnamearitycheck elixirsemanticcheck emitescapecheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck enumtablecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandbodyfirstcheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check extentcheck externalvetocheck fficheck fieldaffinitycheck fieldidcheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnptrcheck forautobodycheck forblowupcheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forhdrshapecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck forsectioncollapsecheck forwidencheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck gdscriptcheck genrecallcheck gitenvhermeticcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck hazardpatterncheck headbinstagecheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactimportcheck impactpartitioncheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javamethodrefcheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck jsxcallcheck knownitemcheck kotlincheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legendrefcheck legobundlecheck legocheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck listingpagingcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck macroreparsecheck manifestcheck mapdiffcheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstdiolinecapcheck mcpstrictschemacheck mcptoolprunecheck mcptranchecheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck noaliascheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck notesdegradecheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck osswitchcheck oswin32logiccheck outlinecheck overbudgetcommentcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck ppdeadrolescheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck preprocdeadscalecheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pymodulealiascheck pyshapecheck qackconcurrencycheck qackorigincheck qbaselineproducercheck qchurncheck qchurnmemocheck qddialscheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qsnapproducercheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recalltablecheck recalltotalcheck receiptpostcheck recentscopecheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexguardcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck rootspellingcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyargcheck rubyattrscheck rubyconstcheck rubymetricscheck rubyrecvcheck rubyrecvnarrowcheck rubyrequirecheck rubyscopecheck rubysettercheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck scroundtripcheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcheckcheck selfcontainedcheck shadowcheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sidecarsymlinkcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck situshapecheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skipclassifycheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck stdqualcheck strkerncheck structlayoutcheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck tempfilesymlinkcheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck traceasanlinearcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck worktreeleakcheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do +for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agentsurfacecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck astqueryregexcheck astrocheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck buildtypestampcheck builtinbindcheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachereservecheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck ceilingverdictcheck chacheck chaconecheck chainguardcheck chainidcheck childwalkscalecheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck cppbenchcheck cppoperatorcheck cppqualcheck cpptmplscopecheck crashsweepcheck crawlescapecheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck declinecheck declinedlistcheck decltodefcheck deeptailcheck defaultceilingcheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck diagnoticecheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck elixirnamearitycheck elixirsemanticcheck emitescapecheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck enumtablecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandbodyfirstcheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check extentcheck externalvetocheck fficheck fieldaffinitycheck fieldidcheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnptrcheck forautobodycheck forblowupcheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forhdrshapecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck forsectioncollapsecheck forwidencheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck gdscriptcheck genrecallcheck gitenvhermeticcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck hazardpatterncheck headbinstagecheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactimportcheck impactpartitioncheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javamethodrefcheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck jsxcallcheck knownitemcheck kotlincheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legendrefcheck legobundlecheck legocheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck listingpagingcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck macroreparsecheck manifestcheck mapdiffcheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstdiolinecapcheck mcpstrictschemacheck mcptoolprunecheck mcptranchecheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck noaliascheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck notesdegradecheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck osswitchcheck oswin32logiccheck outlinecheck overbudgetcommentcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck ppdeadrolescheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck preprocdeadscalecheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pymodulealiascheck pyshapecheck qackconcurrencycheck qackorigincheck qbaselineproducercheck qchurncheck qchurnmemocheck qddialscheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qsnapproducercheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recalltablecheck recalltotalcheck receiptpostcheck recentscopecheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexguardcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck rootspellingcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyargcheck rubyattrscheck rubyconstcheck rubydescribedclasscheck rubymetricscheck rubyrecvcheck rubyrecvnarrowcheck rubyrequirecheck rubyscopecheck rubysettercheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck scroundtripcheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcheckcheck selfcontainedcheck shadowcheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sidecarsymlinkcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck situshapecheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skipclassifycheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck stdqualcheck strkerncheck structlayoutcheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck tempfilesymlinkcheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck traceasanlinearcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck worktreeleakcheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do [ -f "$ROOT/test/$_g.sh" ] || continue if RIPWIRE_BIN="$BIN" bash "$ROOT/test/$_g.sh" >/dev/null 2>&1; then ok "absorb gate ($_g.sh)" diff --git a/test/rubydescribedclasscheck.sh b/test/rubydescribedclasscheck.sh new file mode 100755 index 000000000..fb70190d7 --- /dev/null +++ b/test/rubydescribedclasscheck.sh @@ -0,0 +1,546 @@ +#!/usr/bin/env bash +# rubydescribedclasscheck.sh — gate for RSpec's `described_class` as a CONSTANT receiver: inside +# `RSpec.describe Calc do … end`, `described_class.m1( 1 )` is `Calc.m1( 1 )`, so it pins to Calc::m1 through the +# same Rule 2c arm a written `Calc.m1( 1 )` takes (#267), instead of declining. +# +# Why it matters: a spec's calls are the test→code edges tested=, --seams and --test-gate read. `described_class` +# is how RSpec spells the class under test (902 uses in one measured Rails app's spec/, 4216 in another), and it is +# a bare (identifier) receiver no binding names, so every such call declined and the spec reached nothing. +# +# The rule is RSpec's own (rspec-core 3.13, Metadata::ExampleGroupHash#described_class): a group's described class +# is its FIRST description argument unless that is nil or a String; otherwise it is the parent group's. So the +# INNERMOST enclosing example group whose first argument is a constant wins, and a string-described group passes +# its parent's through. An example group is a call of describe/context (and their x/f and feature spellings) +# with no receiver or the receiver `RSpec`, that carries a block. +# +# Stated floors, pinned below so each stays a decision: +# (a) a CHAINED receiver (`described_class.new.m_chain`) is untouched — the same one-hop bound as #267's +# `Calc.new.scale`: the receiver is a call, not a constant. +# (b) a group with no constant (`RSpec.describe "no class"`, `RSpec.describe :sym`) names no class, so +# `described_class` there is left exactly as it was. +# (c) a `describe` call on any other receiver (`Docs.describe Calc do`) is not an RSpec example group. +# (d) `subject` — the implicit `described_class.new` — is NOT modeled in this round: an explicit `subject { … }` +# can be anything, and telling the two apart is its own round. +# (e) a REDEFINED `described_class` declines: the redefinition names what it means, and this rule does not read +# it, so it answers nothing rather than the group's constant. A METHOD of that name — any `:described_class` +# symbol (`let( :described_class ) { … }`) or `def described_class` — declines every site in the file, since a +# method reaches the whole group. A LOCAL of that name — an assignment (`=`, `||=`), a multiple-assignment +# target, a block or method parameter — declines the sites Ruby reads as that local: after the binding in its +# own scope and in the blocks nested inside it, never in a sibling block, and never across a `def`. The name +# is matched as a whole word: `my_described_class = x` and `:described_class_name` redefine nothing. +# (f) a QUALIFIED describe is named by its FINAL segment: `RSpec.describe Cask::Tab` reads as `Tab`, the same +# final-segment rule #267 gives a written `Cask::Tab.m`. So where another `Tab` defines the method too, the +# call splits between the two; where only the other `Tab` defines it (the described class inherits it), the +# call pins to that one, unmarked. Carrying the full path to the resolver is its own round. +# +# A SHARED group (`shared_examples`, `shared_examples_for`, `shared_context`) stops the walk with no answer: its body +# runs inside whichever group includes it, so its lexical parent's described class is not its own. +# +# Usage: test/rubydescribedclasscheck.sh | RIPWIRE_BIN=asan/ripwire test/rubydescribedclasscheck.sh +# Exits non-zero on any failure. Does NOT edit test/regression.sh. Self-contained via mktemp. +set -u +ROOT="$( cd "$( dirname "$0" )/.." && pwd )" +BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" +[ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" +fail=0 +ok(){ echo " PASS $1" || { fail=1; echo " FAIL could not write the PASS line for: $1"; }; return 0; } +no(){ echo " FAIL $1"; fail=1; } + +[ -x "$BIN" ] || { echo "no ripwire binary at $BIN — build first (cmake --build build -j)"; exit 2; } + +DIR="$( mktemp -d )"; trap 'rm -rf "$DIR"' EXIT +FIX="$DIR/fix"; mkdir -p "$FIX/lib" "$FIX/spec" + +# Every method name is defined on TWO classes, so an unpinned call is a split or a decline and a pinned one names +# exactly one class. Each case below uses its own name: a spec's calls all land in one owner. +cat > "$FIX/lib/calc.rb" <<'RUBY' +class Calc + def self.m_top( a ) + a + end + + def self.m_str( a ) + a + end + + def self.m_ctx( a ) + a + end + + def self.m_inner( a ) + a + end + + def self.m_none( a ) + a + end + + def self.m_sym( a ) + a + end + + def self.m_docs( a ) + a + end + + def self.m_nil( a ) + a + end + + def self.m_let( a ) + a + end + + def self.m_asgn( a ) + a + end + + def self.r_bparam( a ) + a + end + + def self.r_mparam( a ) + a + end + + def self.r_masgn( a ) + a + end + + def self.r_opasgn( a ) + a + end + + def self.m_sib( a ) + a + end + + def self.m_pre( a ) + a + end + + def self.m_wb1( a ) + a + end + + def self.m_wb2( a ) + a + end + + def self.m_shared( a ) + a + end + + def self.m_sctx( a ) + a + end + + def self.r_self( a ) + a + end + + def m_chain + 1 + end +end + +class Tally + def self.m_top( a ) + a + end + + def self.m_str( a ) + a + end + + def self.m_ctx( a ) + a + end + + def self.m_inner( a ) + a + end + + def self.m_none( a ) + a + end + + def self.m_sym( a ) + a + end + + def self.m_docs( a ) + a + end + + def self.m_nil( a ) + a + end + + def self.m_let( a ) + a + end + + def self.m_asgn( a ) + a + end + + def self.r_bparam( a ) + a + end + + def self.r_mparam( a ) + a + end + + def self.r_masgn( a ) + a + end + + def self.r_opasgn( a ) + a + end + + def self.m_sib( a ) + a + end + + def self.m_pre( a ) + a + end + + def self.m_wb1( a ) + a + end + + def self.m_wb2( a ) + a + end + + def self.m_shared( a ) + a + end + + def self.m_sctx( a ) + a + end + + def self.r_self( a ) + a + end + + def m_chain + 2 + end +end + +module Outer + class Engine + def self.e_run + 1 + end + end +end + +class Other + def self.e_run + 2 + end +end + +module Docs + def self.describe( what ) + yield + end +end +RUBY + +# floor (f): two classes whose FINAL segment matches, each in its own file so FILE:SYM names one definition +cat > "$FIX/lib/cask_tab.rb" <<'RUBY' +module Cask + class Tab + def self.t_both( a ) + a + end + end +end +RUBY + +cat > "$FIX/lib/tab.rb" <<'RUBY' +class Tab + def self.t_both( a ) + a + end + + def self.t_lone( a ) + a + end +end +RUBY + +cat > "$FIX/spec/calc_spec.rb" <<'RUBY' +RSpec.describe Calc do + it "pins the top-level group's class" do + described_class.m_top( 1 ) + end + + describe "a string group" do + it "passes its parent's class through" do + described_class.m_str( 1 ) + end + end + + context "a context" do + it "passes its parent's class through too" do + described_class.m_ctx( 1 ) + end + end + + describe nil do + it "passes its parent's class through, as a string does" do + described_class.m_nil( 1 ) + end + end + + describe Tally do + it "takes the innermost constant" do + described_class.m_inner( 1 ) + end + end + + it "leaves a chained receiver alone" do + described_class.new.m_chain + end +end +RUBY + +cat > "$FIX/spec/engine_spec.rb" <<'RUBY' +describe Outer::Engine do + it { described_class.e_run } +end +RUBY + +cat > "$FIX/spec/noclass_spec.rb" <<'RUBY' +RSpec.describe "no class" do + it { described_class.m_none( 1 ) } +end + +RSpec.describe :sym do + it { described_class.m_sym( 1 ) } +end +RUBY + +cat > "$FIX/spec/docs_spec.rb" <<'RUBY' +Docs.describe Calc do + described_class.m_docs( 1 ) +end +RUBY + +cat > "$FIX/spec/let_spec.rb" <<'RUBY' +RSpec.describe Calc do + let( :described_class ) { Tally } + it { described_class.m_let( 1 ) } +end +RUBY + +cat > "$FIX/spec/asgn_spec.rb" <<'RUBY' +RSpec.describe Calc do + it "calls through a local of the same name" do + described_class = Tally + described_class.m_asgn( 1 ) + end +end +RUBY + +cat > "$FIX/spec/local_spec.rb" <<'RUBY' +RSpec.describe Calc do + [Tally].each do |described_class| + it { described_class.r_bparam( 1 ) } + end + + def helper( described_class ) + described_class.r_mparam( 1 ) + end + + it { described_class, other = Tally, 1 + described_class.r_masgn( 1 ) } + + it do + described_class ||= Tally + described_class.r_opasgn( 1 ) + end + + it { described_class = described_class.r_self( 1 ) } +end +RUBY + +cat > "$FIX/spec/scope_spec.rb" <<'RUBY' +RSpec.describe Calc do + it "binds a local after one call" do + described_class.m_pre( 1 ) + described_class = Tally + end + + it "is a sibling of the example that binds it" do + described_class.m_sib( 1 ) + end +end +RUBY + +cat > "$FIX/spec/wordb1_spec.rb" <<'RUBY' +RSpec.describe Calc do + it do + my_described_class = Tally + described_class.m_wb1( 1 ) + end +end +RUBY + +cat > "$FIX/spec/wordb2_spec.rb" <<'RUBY' +RSpec.describe Calc do + it do + respond_to( :described_class_name ) + described_class.m_wb2( 1 ) + end +end +RUBY + +cat > "$FIX/spec/shared_spec.rb" <<'RUBY' +RSpec.describe Calc do + shared_examples "a shared group" do + it { described_class.m_shared( 1 ) } + end + + shared_context "a shared context" do + it { described_class.m_sctx( 1 ) } + end +end +RUBY + +cat > "$FIX/spec/qual_spec.rb" <<'RUBY' +RSpec.describe Cask::Tab do + it { described_class.t_both( 1 ) } + it { described_class.t_lone( 1 ) } +end +RUBY + +MAP="$DIR/map.xml" +"$BIN" "$FIX" --no-cache >"$MAP" 2>"$DIR/map.err" +if [ $? -eq 0 ]; then ok "default map exits 0"; else no "default map exited non-zero: $( cat "$DIR/map.err" )"; fi +[ -s "$DIR/map.err" ] && no "unexpected stderr: $( head -3 "$DIR/map.err" )" || ok "clean stderr" +command -v xmllint >/dev/null 2>&1 && { if xmllint --noout "$MAP"; then ok "xmllint --noout"; else no "xmllint failed"; fi; } + +# calls ROOT SYM SPEC — 0 when --callers=SYM lists SPEC's as a caller, 1 when it does not, and a FAIL (2) +# when the run itself failed: an absence arm must never read a crashed run as "not a caller". +calls(){ + if ! "$BIN" "$1" --no-cache --callers="$2" >"$DIR/c.out" 2>"$DIR/c.err" + then + no "--callers=$2 exited non-zero: $( head -3 "$DIR/c.err" )" + return 2 + fi + sed 's/>\n"$DIR/b.xml" 2>/dev/null +cmp -s "$MAP" "$DIR/b.xml" && ok "byte-identical across two --no-cache runs" \ + || no "output differs across runs" +if ! "$BIN" "$FIX" --cache="$DIR/c.bin" >"$DIR/cold.xml" 2>"$DIR/cold.err" +then + no "the cold cache run exited non-zero: $( head -3 "$DIR/cold.err" )" +fi +if ! "$BIN" "$FIX" --cache="$DIR/c.bin" >"$DIR/warm.xml" 2>"$DIR/warm.err" +then + no "the warm cache run exited non-zero: $( head -3 "$DIR/warm.err" )" +fi +cmp -s "$DIR/cold.xml" "$DIR/warm.xml" && ok "warm run == cold run" \ + || no "the warm cache disagrees with the cold run" + +echo "=== mutation: describe Tally instead of Calc → the pin must follow the group's constant ===" +MUT="$DIR/mut"; mkdir -p "$MUT/lib" "$MUT/spec"; cp "$FIX/lib/calc.rb" "$MUT/lib/calc.rb" +sed 's/^RSpec.describe Calc do$/RSpec.describe Tally do/' "$FIX/spec/calc_spec.rb" >"$MUT/spec/calc_spec.rb" +grep -q '^RSpec.describe Tally do$' "$MUT/spec/calc_spec.rb" || no "mutation did not apply" +calls "$MUT" "Tally::m_top" calc_spec.rb; mt=$? +calls "$MUT" "Calc::m_top" calc_spec.rb; mc=$? +if [ "$mt" -ne 2 ] && [ "$mc" -ne 2 ] +then + [ "$mt" -eq 0 ] && [ "$mc" -eq 1 ] && ok "mutation: described_class.m_top follows the group to Tally::m_top" \ + || no "mutation: with RSpec.describe Tally, Tally caller=$( [ "$mt" -eq 0 ] && echo yes || echo no ), Calc caller=$( [ "$mc" -eq 0 ] && echo yes || echo no )" +fi + +echo +[ "$fail" -eq 0 ] && echo "ALL PASS" || { echo "SOME CHECKS FAILED"; exit 1; }