mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-09 03:17:54 +00:00
17 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
77360e1043
|
fix(scope-resolution): make interface dispatch generic-instantiation aware (#2912) (#2939)
Some checks are pending
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
* fix(scope-resolution): make interface dispatch generic-instantiation aware (#2912) Interface-dispatch fan-out walked the subtype closure with generic arguments erased, so `IValidator<string>` and `IValidator<int>` — one declaration, one subtype list — were indistinguishable and a call through the first reached `IntValidator.Check(int)`, a target no runtime dispatch can produce. The arguments were already in the capture, unread: every language anchors `@reference.inherits` on the whole base node while `@reference.name` keeps the erased base. `ReferenceSite.typeArguments` is therefore derived generically in `scope-extractor.ts` from the anchor's own spelling — no per-language query changed — covering C#, Java, TypeScript, Kotlin, Go (`Base[int]` embedding), Python (`Base[User]`) and Swift; Rust and Dart anchor on the bare name and get nothing, which reads as "unknown". `preEmitInheritanceEdges` is the only code that pairs a heritage site with a resolved (subtype, supertype), so it records the instantiation there and hands it to the dispatch pass. The closure is then walked carrying a substitution, as a type checker would: `Wrapper<T> : IValidator<T>` binds T to the receiver's argument and stays reachable from every instantiation, while its own subtypes are matched against that binding. An incompatible hop is skipped without being marked seen, so a type reachable by a second, compatible path still gets its edge, and without descending, since its subtypes inherit the mismatch. Pruning happens only on positive evidence that two instantiations differ. Unknown arguments on either side, an arity that does not line up, an unresolved qualified spelling of the same simple name, or an argument that might be a type variable the language never captured all keep the target. Telling an uncaptured type VARIABLE from a concrete type is the crux: `typeParameters` is absent both for a non-generic declaration and for every declaration in a language whose query omits `@declaration.type-parameters`, so the pass reads the evidence in front of it — one run resolves one language, so a single generic declaration anywhere in it proves the captures record parameters. A language recording neither arguments nor parameters keeps exactly its pre-#2912 fan-out. Type arguments are compared as resolved declarations rather than spellings, so `Models.User` and an imported `User` are one type; the new optional `ScopeResolver.normalizeTypeArgument` hook canonicalizes a language's predefined aliases, implemented for C# (`string` ≡ `String`) where mixing the spellings would otherwise delete a real implementor. Fan-out cap, skipped-target reporting, overload selection and non-generic closure behaviour are unchanged. SCHEMA_BUMP 60 -> 64: the heritage arguments are a parse-time capture, so a warm cache would replay pre-fix sites and leave the filter silently inert on unchanged files (61/62/63 are claimed by open PRs). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01StNKYi7Qxv5DnSURZuFBef * fix(scope-resolution): close the two generic-dispatch gaps (#2912) The first commit left two shapes on the pre-#2912 fan-out. Both are now covered, and the second one turned out to need a route the pipeline did not have at all. **Folded receivers (Cases 0 and 3b).** `this._validator.Check(x)` is typed by the compound fold, and the fold answers with a CLASS — which is exactly what loses the instantiation, since `IValidator<string>` and `IValidator<int>` fold to one declaration. The fold now reports the SPELLING it typed each receiver position from, through a pure side channel (`recordReceiverType`) added to the one helper every declared-type route already shares plus the two return-type routes; resolution is unchanged whether or not a caller passes it. The reader keeps the last report and uses it only when it names the class the fold returned, so an intermediate position cannot lend its arguments to another class. This covers the dependency-injection shape the issue is really about — a field-held generic interface — and multi-hop chains, where it is the last hop's spelling that types the receiver. **Rust and Dart heritage.** Neither recorded arguments, for two different reasons, so both routes exist now: - Rust's `@reference.inherits` anchor is the trait identifier INSIDE a `generic_type`. Widening the anchor would move the site's range, and that range is part of every inheritance edge's id, so the arguments arrive through a new `@reference.type-arguments` sub-tag instead. - Dart's `implements` / `with` never become reference sites at all: they travel as heritage MARKERS and their edges are emitted by the language hook. The arguments ride the marker payload as an optional fourth field (dropped, not encoded, when the spelling contains the marker delimiter), and `ScopeResolver.emitHeritageEdges` now receives the same sink `preEmitInheritanceEdges` writes to, so whichever pass emits an edge records that edge's instantiation. Dart also gained the `@declaration.type-parameters` capture, without which its own type VARIABLES are indistinguishable from concrete arguments and `class Box<T> implements Validator<T>` would be pruned from every instantiation. Note this makes Rust and Dart record their instantiations; it does not make them fan out. Interface dispatch still fires only for a receiver whose folded type is an `Interface` symbol, so a Rust `Trait` or a Dart abstract `Class` receiver has no secondary targets to filter. Widening that gate emits new edges for several languages and belongs to its own issue. **Two matcher rules the wider coverage exposed.** A WILDCARD names a set of types rather than one — `Repo<? extends User>` holds a `Repo<User>`, and Kotlin's `Repo<*>` / `Repo<out User>` say the same — so a position with one on either side is unknown; nullable spellings trip the same test, which costs a little precision in the safe direction. And insignificant whitespace inside a nested spelling (`Map<string, User>` vs `Map<string,User>`) is no longer a difference. One expectation changed in the #2833 field-receiver matrix: a `Repo<Repo<User>>` receiver no longer reaches `UserRepo implements Repo<User>`. That edge is precisely the false positive this issue is about, and the primary edge to the interface's own declaration — which is what the matrix row exists to prove — is untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01StNKYi7Qxv5DnSURZuFBef * refactor(scope-resolution): apply the quality pass to the #2912 change Three cleanups, no behaviour change. **One balanced-list scanner, not two.** `erasedTypeApplication` and `typeApplicationArguments` each carried a copy of the same fiddly scan — one bracket list, balanced, closing on the last character, non-empty — differing only in what they did with the result. Both now call `balancedTailList`; the rule that rejects `User[][]` and `Repo<User>?` lives in one place instead of being free to drift between two. **The receiver's arguments are parsed after the gates, not before them.** `emitInterfaceDispatchFor` takes the receiver's declared SPELLING and parses it itself, once the owner is known to be an Interface with subtypes. Every one of the five cases calls it unconditionally and the overwhelming majority of receivers are concrete classes that return at the first line, so the parse was running per resolved receiver site to be discarded immediately. Case 4 and Case 6 now hand over the string they already hold, and the folded-receiver helper returns the recorded spelling rather than parsing it. **One question gates the whole instantiation apparatus.** Inside the closure walk, the graph-id lookups now hang off "is the supertype's instantiation known?" — false for every non-generic receiver and for every language that captures no heritage arguments, which is what makes those walks cost exactly what they cost before #2912. Also lifted the argument-route choice in `pass5CollectReferences` out of a nested ternary into a named `heritageTypeArguments`, where the reason the explicit sub-tag wins over the anchor text can be stated once. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01StNKYi7Qxv5DnSURZuFBef * test(scope-resolution): cover generic interface dispatch in Kotlin and Go (#2912) Extends the #2912 dispatch coverage past C#/Java/TypeScript. No production code changes — the derivation is language-agnostic by construction (`heritageTypeArguments` reads the heritage anchor's own spelling), so the question was only which languages actually reach the filter. Kotlin rides the shared heritage pre-pass; Go reaches the same filter from the other side, matching implementors structurally while the receiver's `Validator[string]` spelling carries the instantiation. Both are confirmed to prune the mismatched implementor. Each language gets a NON-GENERIC control asserting the fan-out still reaches every implementor. Without it the `not.toContain` assertion passes just as well when a language emits no dispatch edge at all — which is what Dart, Python and Rust were measured doing for this receiver shape, generic or not. They are deliberately not asserted on here: a "filtered correctly" test over a path that never fans out measures nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Px638Zyqa9CJMUU7DsJoB * test(bench): re-baseline the Rust and Dart capture fingerprints for #2912 The Rust trait-impl and Dart heritage capture changes this branch makes are additive TEXT on existing matches — each carries the instantiation the clause was written with — so they drift the scope-capture digest without adding or removing a match. The baselines were never re-measured when those captures landed, which left `measure.mjs --check` red on this branch independently of the merge. Re-measured rather than hand-edited. Rust's capture_groups_fp (3556) and fixture_count (202) are unchanged across the move, which is the evidence that this is digest drift and not a capture-set regression. The other 13 languages are byte-identical; 15/15 pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Px638Zyqa9CJMUU7DsJoB * refactor(scope-resolution): quality pass over the #2912 change Cleanup only — no behavior change. Findings from a four-angle review (reuse, simplification, efficiency, altitude), applied where they were verified. Reuse / duplication: * `stripTrailingCallSuffix` was a second copy of `matchingOpenParen`'s backward balanced-paren scan. Both now live in `template-arguments.ts` beside `balancedTailList`, for the reason that helper was shared in the first place: two copies of a scan this fiddly are free to disagree. * The two call-return arms of the compound fold repeated the same four-part expression character for character; they share `classOfReturnType` now, the return-type twin of `classOfDeclaredType`, which keeps the "look up by rawName, report the erased application" pairing in one place. * `pipeline/run.ts` implemented first-writer-wins twice — once in the pre-pass and once in the provider sink. One store, one sink, one rule; the pass keeps its `Set<string>` return and the callable-flow-only arm stops building an empty map to satisfy a widened return shape. Simplification: * `subtypeParametersComplete` dropped a disjunct that could never decide: every `subDef` reaching it comes out of the same loop that sets `languageCapturesTypeParameters`, from exactly those defs. * The heritage-argument lookup asked "is the supertype's instantiation known?" three times; `superGraphId` now gates the block once. * `TypeArgumentResolver` and `HeritageInstantiationResult` un-exported — no consumer outside their module. Efficiency (all on the per-site dispatch walk): * `resolveSupertypeArgument` captures only the site, so it is built once per site instead of once per subtype visited; the subtype's scope id is looked up once per subtype instead of once per argument position. * `erasedTypeApplication` no longer runs on every fold hop through a call — the spelling is built only once the lookup has found a class, since it is discarded otherwise. * `normalize`+`compact` computed once per side rather than twice. * Regex literals and the identity `normalize` fallback hoisted to module scope. * C# `System.` prefix stripped with `startsWith`/`slice` instead of a regex. Verified: tsc clean, build clean, 1994 scope-resolution unit tests, 171 generic-dispatch + generic-field-receiver integration tests, 15/15 capture bench fingerprints unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Px638Zyqa9CJMUU7DsJoB * style: apply Prettier to the two files the quality pass reformatted Whitespace only — `quality / format` (npx prettier --check .) was red. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Px638Zyqa9CJMUU7DsJoB * fix(scope-resolution): close the generic-dispatch review findings (#2912) Addresses the gitnexus-check review on #2939. A repeated type variable was rebound rather than unified: `class C<T> : Pair<T, T>` accepted a `Pair<string, int>` receiver, with `T = int` silently replacing `T = string` and the bogus substitution carried to the next hop. It now unifies, and prunes only on the same positive evidence the concrete path demands — an undecidable repeat keeps the target with no binding. A type PARAMETER of the declaration enclosing either side is now recognised and never compared. `subtypeParametersComplete` is evidence about the SUBTYPE's parameter list and says nothing about a `T` written at the call site, so `void Run<T>(IValidator<T> v) { v.Check(x); }` pruned every implementor: unbounded, `T` grounds to nothing; bounded, it grounds to its BOUND. Both read as a difference of type. That is the missing-edge failure this filter is built to avoid, and it is the common dependency-injection shape in C#, Java and Kotlin. Making that recognition reliable is why generic METHODS now capture `@declaration.type-parameters` in C#, Java and Kotlin — TypeScript already did, which is why its generic functions never had the defect. The capture feeds the existing `bindsTypeParameter` guard, so a method-level `T` also stops resolving to a same-named class in every other lookup. C# alias normalization additionally strips the `global::` qualifier, which `import-decomposer` already unwraps elsewhere: `global::System.String` read as unequal to `string` and pruned a live implementor. The C# captures golden fixture is regenerated for the new capture; the extractor reads `@declaration.type-parameters` generically, so no reader changed. SCHEMA_BUMP 64 already covers these capture changes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Px638Zyqa9CJMUU7DsJoB * fix(scope-resolution): close the two remaining gitnexus-check findings (#2912) `balancedTailList` counted ONE bracket family, so a crossed pair slipped through: scanning `Foo<Bar]>` it never sees the `]`, reaches the final `>` at depth zero, and reports `Bar]` as a balanced argument list — which `typeApplicationArguments` then splits and `erasedTypeApplication` rebuilds a spelling from. It now tracks a stack of expected closers, so every closer must match the opener it actually closes and a crossed pair declines to `undefined`, the "unknown" both callers already fail open on. Well-formed mixed nesting (`List<Dict[a, b]>`) is unaffected. C# `normalizeTypeArgument` stripped `System.` from every qualified spelling, so `System.Custom` answered `Custom` and compared equal to an unrelated `Custom` elsewhere in the workspace. The strip is now earned: a keyword answers from the alias table first, and the qualifier is dropped only when what remains IS a predefined type. `System.Custom` is returned as written and goes to the identity comparison instead — the step that can actually tell two declarations apart. `global::System.String` still meets `string`. Both are pinned by unit tests, including the well-formed mixed nesting and the `global::`-qualified ordinary type that must keep its qualifier. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Px638Zyqa9CJMUU7DsJoB * docs(csharp): record why a shadowed `String` keeps its implementor (#2912) Answers a review finding rather than changing behavior. A workspace may declare its own type named `String`, shadowing the BCL simple name, and the alias table then reads `IValidator<String>` as the `string` instantiation and keeps that implementor. That is the SAFE direction, not an oversight: pruning instead would rest on the belief that two spellings differ, which is the missing-edge failure `generic-instantiation.ts` exists to avoid. Resolving rather than normalizing cannot settle it either — the identity comparison needs a `definitionId` from both sides, and a built-in name carries none, so "built-in versus workspace-declared implies different" would be a new prune with no positive evidence behind it. The cost is one surplus edge for that pair, which is exactly the pre-#2912 fan-out and no worse. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Px638Zyqa9CJMUU7DsJoB * refactor(scope-resolution): pair the receiver spelling with the class structurally (#2912) The fan-out needs the spelling a receiver position was typed from, because the class the fold returns has lost the generic arguments. That was carried by a PASS-LEVEL mutable holder, written by every declared-type lookup anywhere in the fold and read back through a def-id coincidence check, with the holder cleared by hand before each call site. Three things were load-bearing and none were enforced: * the reset had to be remembered at every call site. It was not: the Case 3b retry (`rawName` then `rawName + '()'`) reset once, BEFORE the first attempt, so a spelling reported by the attempt that failed could be attributed to the one that succeeded. * the holder outlived every resolution, so a site that resolved through a route reporting nothing could read the previous site's spelling if the def ids happened to line up. * the pairing itself was inferred from "whichever lookup reported last", not from the fold's own bookkeeping — losing branches (an MRO walk that moved on, a step later folded past) report too. `foldReceiverChain` already had the answer and threw it away: its final `FoldState` holds `def` and `declaredType` produced by the SAME step. It now reports that pairing last, so the structural route is the one that stands. `resolveCompoundReceiverTyped` returns `{def, declaredSpelling}` and owns a sink created and read within the single call, which is what removes the reset discipline — a local cannot be forgotten, and each of the two retry attempts carries its own. The def-id guard stays as the check that a report names the class actually returned. Behavior is unchanged: 1975 scope-resolution unit tests, 177 generic-dispatch and generic-field-receiver integration tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Px638Zyqa9CJMUU7DsJoB --------- Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
997fc05b83
|
fix(resolution): resolve calls through a generic-typed field receiver in every language (#2833) (#2855)
* test(resolution): pin generic-typed field receivers across languages (#2833) A field whose declared type carries a type argument (`repo: Repo<User>`) emits zero CALLS edges — not a truncated chain, not an edge to the interface declaration, nothing. This adds the cross-language matrix that measures it, modelled on the #2807 inferred-field matrix: every language runs the same two calls, one through a generic-typed field and one through a non-generic control field, and each language is compared against its OWN control row rather than an absolute edge count. Measured state, pinned here as `known-gap` so the file is green on main and flipping a row is a visible edit: affected TypeScript, C#, C++, Python unaffected Java, Kotlin, Go, Rust, Swift, Dart The unaffected six erase type arguments at interpret time (Java's `stripGeneric`, F41 #1928; Swift likewise). TypeScript, C# and Python instead run a container ALLOW-LIST that returns the type ARGUMENT, so a user-defined `Repo<User>` survives verbatim into a lookup that binds nothing. The `ts-local-vs-field` case is the bug in one file: `viaLocal` and `viaParam` both resolve for the identical type, and only `viaField` loses every edge — a bare name reaches Case 4 and its generic-aware lookup, a dotted field receiver does not. Negative controls pin what erasure must NOT do: an unbounded type parameter denotes no declaration, and a C++ explicit specialization is a different class from its primary template. The `Box2<T>` row pins a PRE-EXISTING false edge (a workspace class named `T`) so it cannot later be mistaken for fallout from this work. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KtNfG6EPn738Y51AYs7wDp * refactor(resolution): move resolveClassBindingForName to the shared walkers (#2833) Pure relocation, no behaviour change: the generic-aware class lookup moves from `passes/receiver-bound-calls.ts` to `scope/walkers.ts`, beside the bare `findClassBindingInScope` it wraps. Its two existing callers — `classifyReceiverOrigin` and Case 4 — import it from the new home and are otherwise untouched. The move is required rather than cosmetic: `receiver-bound-calls.ts` already imports from `compound-receiver.ts`, so having the compound receiver call into the pass would close an import cycle. `walkers.ts` is the shared floor both already depend on. Verified behaviour-neutral: the #2833 matrix is 44/44 identical before and after, across all fifteen fixtures. detect_changes attributes `resolveInheritanceBaseInScope`, `resolveQualifiedInheritanceBase` and `EMPTY_BINDINGS` to this commit; those are line-shift artifacts of inserting a function above them, and their bodies are byte-identical. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KtNfG6EPn738Y51AYs7wDp * fix(resolution): type generic field receivers through the generic-aware lookup (#2833) A field receiver is spelled `this.repo` — dotted — so it types through the receiver-chain fold and the text cascade, both of which reach `findClassBindingInScope`. That function has no notion of type arguments, so a field declared `Repo<User>` resolved to nothing and the call site emitted NO edge at all: not the interface declaration, not the implementation fan-out, nothing. A local or parameter of the identical type is a bare name, reaches Case 4 and its generic-aware `resolveClassBindingForName`, and resolved fine. The bug was the asymmetry, not the generics. Three receiver-typing lookups now call the generic-aware helper instead: `typeOfMemberOnClass`'s primary and module-hoist branches, and the cascade's bare-identifier type-binding read. Every other one of the 38 `findClassBindingInScope` call sites is untouched — its own docstring records that widening it globally suppresses the `?? otherResolver(...)` fallbacks two dozen callers rely on, which would retarget inheritance edges, and impact rates it CRITICAL with 12 direct dependents. Order matters and is preserved: the helper tries the exact name, then an arity- and token-exact match against `def.templateArguments`, and only then falls back to the base name. Erasing first would collapse a C++ explicit specialization onto its primary template — `Vec<bool>` really is a different class. A bare type parameter carries no type arguments, so it never enters the generic branch and cannot be erased into a class that happens to share its name. Measured: TypeScript and C# generic-typed fields now emit exactly what their non-generic control rows emit, primary plus interface-dispatch fan-out. Java, Kotlin, Go, Rust, Swift and Dart are byte-identical. Both type-parameter negative controls are unchanged. C++ and Python are still open and stay pinned as known-gaps — they fail for different reasons and get their own commits. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KtNfG6EPn738Y51AYs7wDp * fix(cpp,python): bind generic-typed member fields so their calls resolve (#2833) Completes #2833 for the two languages the shared resolution change could not reach. Each failed for its own reason, and both were found by measurement rather than assumed. C++ — a CAPTURE gap, not a resolution one. All three `field_declaration` type-binding rules required `type: (type_identifier)`, so a member declared `Repo<User> repo;` is a `template_type` and matched none of them: the field got no type binding at all, and every call through it lost its edge in both the bare and `this->` spellings. A LOCAL of the identical type resolved the whole time, because the local declaration rules gained their `template_type` variant long ago. Three mirrored rules close it, one per declarator shape (plain, pointer, reference). Written as separate patterns rather than one alternation: a node-type alternation in a field position is a tree-sitter 0.21 hazard this repo has been bitten by before. Python — the bracket spelling never entered the generic branch. Its `stripGeneric` is a container allow-list over `[...]` that returns the type ARGUMENT (`list[User]` to `User`), so a user-defined `Repo[User]` matched nothing and survived verbatim, and the shared lookup's generic branch is gated on `<`. It now reduces a subscripted type neither allow-list claims to its base name — the same rule Java and Swift already apply to `<...>`. Deliberately the LAST resort: a container must reach its own rule first, or `list[User]` would type the receiver as the container and retarget every call in a for-loop chain. The as-written spelling survives on `TypeRef.declaredSpelling`, which is what the fold's index step reads. Both are parse-time and land in the cached ParsedFile, so SCHEMA_BUMP goes 45 -> 46 with its pin test. Verified free against origin/main; the ledger in that file records three prior EXACT clashes, so re-check again immediately before merge. The matrix now covers the spellings real code writes, all measured: a nullable generic, a bounded wildcard, a raw type, a nested generic and a multi-argument one. None needed work beyond the shared lookup, which is the evidence that base-name erasure is the right primitive. The C++ specialization control now asserts what it was written for: `Vec<bool>.save` and `Vec.save` are DIFFERENT target ids, so the arity/token match still wins over erasure. scope-capture is byte-identical for cpp and c, so no rebaseline — the bench corpus contains no generic-typed member field, which is worth its own coverage issue. Two pre-existing gaps were measured and are deliberately NOT fixed here, because in both cases the language's own non-generic CONTROL row fails identically: C++ `this->field.m()` emits nothing, and JavaScript/PHP docblock-declared field types bind nothing at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KtNfG6EPn738Y51AYs7wDp * fix(python): do not reduce containers or typing special forms to a base name (#2833) Review finding on this branch's own Python change, caught by probing the interpreter directly rather than by reading it. The base-name reduction was reached by FALLTHROUGH: "neither container rule matched" was treated as "not a container". It is not, and two measured shapes proved it: dict[str, list[User]] -> dict (was: the annotation, intact) Dict[str, Repo[User]] -> Dict Callable[[int], User] -> Callable Literal["a"] -> Literal Union[A, B] -> Union tuple[int, ...] -> tuple The dict rule's value group cannot span a nested `]`, so a nested value declines and falls through — and the dict rule's own comment says that shape is deliberately "left for a downstream strip pass". Collapsing it to `dict` destroyed the value type instead. The typing SPECIAL FORMS are worse: `Callable`, `Literal`, `Annotated` and `Union` are not classes, and reducing them to a bare name binds any workspace class that happens to share it — a fabricated edge, which is strictly worse than the missing edge #2833 set out to fix, and those names are ordinary enough for a real codebase to declare. Reduction is now guarded by an explicit deny set covering the containers the two allow-lists already own and the typing special forms. Everything named there keeps its as-written text and resolves exactly as it did before #2833. `arr[0]` also reduces to `arr` in isolation, but that is unreachable and is now documented as such: every Python `@type-binding.type` capture is a `(type)`, `(identifier)`, `(attribute)` or `(dotted_name)` node, so a subscripted VALUE expression never reaches the interpreter. Pinned by a new unit test that asserts all four groups — user generic reduces, container reduces to its ELEMENT, declined container shape stays intact, special form untouched. Reverting the deny set fails three of its five cases. Also corrects `resolveClassBindingForName`'s docstring, which this branch had made false: it claimed only `classifyReceiverOrigin` passes the decoration stripper, while the three receiver-typing lookups in compound-receiver.ts now pass it too. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KtNfG6EPn738Y51AYs7wDp * fix(resolution): rank base-name candidates lexically and refuse arg-pinned defs (#2833) Review of #2855 found that this PR turned a MISSING C++ edge into a CONFIDENTLY WRONG one — the direction this subsystem calls unrecoverable. `resolveClassBindingForName` ended with an unguarded base-name fallback that returned the first same-named class the scope chain reached. A C++ primary template carries `templateArguments === undefined`, so it can never satisfy the exact-args branch, and every non-specialized instantiation fell through to that fallback. Measured through the real pipeline: with the primary forward-declared and the specialization defined first, `Vec<int> vi; vi.save()` emitted `Vec<bool>::save`. Declaring the primary first gave the correct target — selection was SOURCE-ORDER DEPENDENT. Two more triggers behaved the same way: a partial specialization (`Vec<int*>` against `Vec<T*>`), and lexical shadowing between a global `Box<bool>` and a namespaced `N::Box<bool>`. Two changes, neither of which is any of the three remediations the review proposed — each was rejected on measured evidence: - Exact-argument matching is now LEXICAL-FIRST. Candidates come from the scope chain, and the workspace-wide qualified-name bucket is consulted only when the chain produced no exact match, so cross-file specializations still bind. - The base-name route refuses a definition that pinned its own template arguments: if the fallback's answer carries `templateArguments`, the visible candidates are re-decided with those removed — exactly one, or decline. Why not the filed options. "If specializations exist and none matches exactly, return undefined" deletes a green committed row (`neg-cpp-specialization/runInt` legitimately resolves to the primary). "Resolve all defs for the base name, return only on exactly one" deletes a working edge for C# `partial class Repo<T>` split across files — two unspecialized defs under one name is legitimate, and `QualifiedNameIndex`'s own docstring names that case. Preferring the primary alone fixes nothing about shadowing, which is a ranking bug. The guard is expressed as `carriesOwnTemplateArguments`, not as "specialization", so shared pipeline code still names no language (AGENTS.md R6). It can only fire where a declared name carries concrete arguments — measured `undefined` for `class Repo<T>` in TypeScript and C# and for a C++ primary template — so the blast radius is bounded to C++-style specializations. Partial-specialization SELECTION is deliberately not implemented: choosing `Vec<T*>` for `Vec<int*>` needs template-argument deduction, which is a semantics expansion and cannot live in language-neutral shared code. The source-order dependence is what is fixed; the answer is now deterministically the primary. Also in this commit: dropped an unreachable `?? []` (QualifiedNameIndex returns a frozen empty array on miss by contract) whose comment was wrong on both clauses; made the docstring true about argument ERASURE being what widens what binds, rather than only the decoration stripper; and corrected a stale pointer that still placed `resolveClassBindingForName` in `receiver-bound-calls`. `findClassBindingInScope` itself is untouched — 38 call sites, CRITICAL. Verified: matrix 56/56, cpp.test.ts 334, unit scope-resolution 1505. Mutation proof: reverting this file fails the three trigger cases and passes the non-regression cases; restoring it passes all five. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KtNfG6EPn738Y51AYs7wDp * fix(python): close the deny-set drift axis by case-folding, not by vigilance (#2833) Review of #2855 found `NOT_A_USER_GENERIC` was a closed list over an open universe: four review lanes each escaped it with a DIFFERENT set of names. `Deque` was the sharpest — its lowercase twin `deque` was already listed, so the omission was an internal inconsistency rather than a judgement call, and with a workspace `class Deque` present `self.dq: Deque[User]` fabricated a `Deque.appendleft` edge. The structural cause is PEP 585: nearly every container has two spellings differing only in case (`deque`/`typing.Deque`, `frozenset`/`FrozenSet`). Exact matching forced every pair to be listed twice, so any half-pair was a silent escape. The deny lookup is now CASE-FOLDED, which closes that axis by construction — `Deque` becomes impossible rather than remembered. `SINGLE_ARG_CONTAINERS` and `MAPPING_CONTAINERS` are now the single source of truth: they build the two container regexes (verified byte-identical `.source` and `.flags`, so zero behaviour change) and feed the property test. The deny set is re-scoped to a closed, auditable universe — the documented Python stdlib type-system surface — and grew 39 -> 65 concepts: the `collections.abc` views, `contextlib` managers, `re.Pattern`/`Match`, the `IO` family, ordinary-named stdlib generics (`Queue`, `Task`, `Future`, `PathLike`), the remaining typing special forms, and the generic machinery (`Generic`, `Protocol`, `TypeVar`...). Third-party generics (`Mapped`, `QuerySet`, `Model`) are deliberately NOT added and are pinned as a decision: that universe is open, enumerating it only chases the last escape, and declining `Model` would cost real edges in the many projects that declare one. The review's suggested property test — derive the names from the `single`/`dict` regex sources — would NOT have caught `Deque`: `deque` appears in neither regex, only in the deny set. Both properties are implemented, since they catch different drift. The unit test was also TAUTOLOGICAL: it asserted members OF the deny set, so it structurally could not detect an omission. It now asserts case-fold closure and PEP 585 alias coverage, and the capture fixture drops its `as unknown as` cast for the fully-typed helper pattern the sibling `java-interpret.test.ts` already uses. Still at interpret time, so no further SCHEMA_BUMP (already 45 -> 46). Proving the base is a class the FILE can see — the real fix for the remaining exposure, since `findClassBindingInScope` binds any name with exactly one workspace def regardless of scope or imports — is a follow-up, not reachable from this file. Mutation proof: restoring HEAD's deny-set contents and exact-match lookup fails four assertions including the `Deque` pair, with the pre-existing guard rows still passing; restoring gives 125/125. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KtNfG6EPn738Y51AYs7wDp * fix(cpp): capture qualified generic member fields, and make the bench gate see them (#2833) Review of #2855 found that the three `field_declaration` rules this PR added only matched a DIRECT `template_type`, so the common real-world spelling still bound nothing: `std::vector<Item> items;`, `ns::Repo<User> r;` and `std::unique_ptr<Repo> p;` parse as a `qualified_identifier` WRAPPING a `template_type`. "C++ fixed" was overstated. Six new patterns — three declarator shapes (plain, pointer, reference) by two qualifier depths — written as separate patterns rather than one alternation, keeping the tree-sitter 0.21 field-position discipline the existing rules follow. The design choice was measured, not assumed. Codex suggested preserving the full qualified spelling and normalizing `::`; preserving resolves NOTHING, because `findClassBindingInScope`'s dotted-tail fallback splits on `.` while C++ writes `::`, and `ns::Repo` is not an index key either (C++ emits no `@declaration.qualified_name`). Measured: `ns::Repo<User>` resolves to nothing, `ns.Repo<User>` resolves to `Repo`. Since a tree-sitter capture is a NODE and not synthesized text, the only lever is which node to capture — so `@type-binding.type` goes on the INNER `template_type`, dropping the qualifier and landing on the same single-match-or-decline path the bare spelling already takes. Qualifier depth 3+ (`a:🅱️:c::Repo<User>`) remains uncaptured. Stated as a limit and pinned by a test row, not claimed as fixed. The bench blindness the review identified is also closed. The `scope-capture` C++ corpus contained ZERO template-typed member fields — confirmed a fourth way by applying six demonstrably behaviour-changing patterns and getting a byte-identical fingerprint. The corpus now carries generic and qualified-generic members, and the gate is load bearing for the first time: three states that all hashed to 856d02f3 before now differ (pre-#2833 0e7cbda7, +this PR's 3 rules de07d8b5, +these 6 rules bd47c82d). Rebaselined for cpp only; c is unchanged. Histogram diff: only 5 tags move with the fields, each by exactly +40 (20 entities x 2), and every `@reference.*` count is unchanged. Over-match is preserved: 20 shapes still produce no field capture, including the 8 original method/pointer/reference/function-pointer/ using/typedef/friend/operator forms plus their `std::`- and `a:🅱️:`-qualified variants. Not fixed here, deliberately: NON-generic qualified fields (`ns::Address addr;`, `std::string name;`) still capture nothing. Closing that needs six more patterns and would newly bind every `std::string`/`std::mutex` member repo-wide, changing edges far outside #2833. Separate issue. The template-template-parameter hazard the review filed against these rules is NOT capture-side: a tree-sitter query has no scope knowledge, so it cannot know `Map` is bound by the enclosing `template <...>` header, and the PRE-EXISTING `type: (type_identifier)` rule already captures a bare `T item;` and erases it the same way. It is handled by the lexical ranking in `walkers.ts` in this series. Mutation proof: reverting this file fails 9 of 32 assertions (all eight qualified spellings return no capture) while every over-match negative still passes; restoring gives ALL PASS. Bench `--check` passes for all 15 languages. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KtNfG6EPn738Y51AYs7wDp * test(resolution): pin specialization order, shadowing and the untested spellings (#2833) Grows the generic-field matrix 56 -> 114 tests, closing every coverage gap the #2855 review named and turning the fix-agents' scratch evidence into permanent rows. The rows that discriminate against the resolver fix (they fail if `walkers.ts` is reverted): - C++ specialization must not depend on DECLARATION ORDER: the forward-declared-primary/specialization-first arrangement must land on the primary, same as the mirror arrangement. Plus a cross-case property asserting the two independently built fixtures agree. - Partial specialization is deterministic in both orders. The note says explicitly that selecting `Vec<T*>` would need argument deduction and that flipping this row later is a deliberate expansion, not a regression fix. - Lexical shadowing: the namespace-local `N::Box<bool>` wins for a field inside `N`, and the global specialization wins at global scope. The NON-REGRESSION rows are load-bearing — they are why two of the three proposed remediations were rejected: cross-file C++ specialization binding, and C# `partial class Repo<T>` split across two files with the field in a third (two legitimate unspecialized defs under one name). Coverage the review found missing: C++ pointer and reference generic fields (two of this PR's three original rules had ZERO coverage); all six qualified patterns plus the depth-3 boundary pinned as empty; TS/C# multi-arg container collision; an anti-vacuity sibling for `neg-bounded-type-parameter`; Swift/Dart rows restructured so the ANNOTATION is the only possible source (the old rows gave the field an initializer of the same generic type and could not tell which resolved); and cross-file, inheritance/MRO, import-alias, static-member and the TypeScript module-hoist branch. Six things were measured and pinned AS MEASURED rather than asserted as wishes, each flagged in its row note: a static/class-level member emits nothing for generic AND non-generic alike (a static gap, not a generics one); a cross-file C++ primary template does not bind while the cross-file specialization does; `std::unique_ptr<Payload>` types to `unique_ptr` rather than `Payload` (smart-pointer transparency is not applied on the qualified path); two same-named C++ specializations in one file collapse to one node id; and the container-name collision (`Map<string, User>` binding a workspace `class Map`) is recorded as INTENDED, since the annotation does name that class. The `new Set(...)` dedup was kept rather than narrowed: a per-case surplus-edge sweep measured ZERO duplicate edges anywhere in this file, Swift included, so the quirk that justified a blanket dedup does not reproduce. The sweep now pins zero surplus per case, so a real double-emit fails instead of being absorbed. The file is deliberately NOT split: four assertions compare cases against each other, cost is linear in cases, and the 1,800,000 ms `beforeAll` is kept because the same run measured 271-428 s depending on host load — a tighter bound converts contention into a red suite. The reasoning is recorded in the file header. Also corrects the SCHEMA_BUMP pin-test title, which still said (#2766). Mutation proof: reverting `walkers.ts` fails exactly the five order and shadowing assertions and passes the other 109; restoring gives 114/114. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KtNfG6EPn738Y51AYs7wDp * feat(resolution): capture declared type parameters so a type variable is not a class (#2833) Three review findings were blocked on one missing fact. `templateArguments` records the arguments a declaration was written AGAINST (`struct Vec<bool>`); nothing recorded the parameter list a declaration DECLARES (`template <class T>`, `class Box<T extends Repo>`). So the resolver could not tell a type variable from a class, and: - `class Box2<T> { t: T }` beside a workspace `class T` emitted a FALSE edge `run2 -> T.foo`. `T` carries no type arguments, so it never entered the generic branch — the plain lookup simply bound a same-named class. The lexical grounding added elsewhere in this series cannot help, because `export class T` IS lexically bound. - `class Box<T extends Repo> { t: T }` resolved to nothing: no recorded bound to resolve through. - A full specialization `template<> struct Vec<T*>` and a partial `template<class T> struct Vec<T*>` were byte-identical (`['T*']`). `SymbolDefinition.typeParameters` now records `{ name, bound? }` in declaration order (substitution is positional). `bound` is kept verbatim and un-split, so `Repo & Closeable` stays whole; ABSENT means UNKNOWN, never "unbounded", which is what keeps unconverted languages behaving exactly as before. Transport is the raw parameter-list node via `@declaration.type-parameters`, read by a language-neutral parser that recognizes TOKENS, not languages: `extends`/`:` introduce a bound, the name is the trailing identifier, so `class T`, `typename T`, `in T`, `out T`, `reified T` and `class... Ts` are one rule. Populated for TypeScript, C++, Java, Kotlin, C# and Rust. JavaScript, C, COBOL, PHP and Ruby have no declared type parameters to capture; Go and Python spell them with SQUARE brackets, which this parser deliberately rejects as ambiguous against subscript and array spellings (Go already has a working main-thread sidecar in this series); Dart and Swift are straightforward follow-ups. Two latent hazards found and closed on the way: - The new capture was not in `KNOWN_SUB_TAGS`, so it could out-span its own declaration and become the anchor — silently DROPPING the whole class def. - A templated C++ struct matches both the standalone and `template_declaration` patterns, minting two defs under one id, and only one twin could see the parameter list. `buildDefIndex` is first-write-wins, so MATCH ORDER decided whether `Vec` remembered `T`. A narrow duplicate-declaration backfill gives both twins the list. Also fixed by its own test: a Rust lifetime `'a` parsed as a parameter named `a`, which would have shadowed a real class. Parse-time output lands in the cached ParsedFile, so SCHEMA_BUMP goes 46 -> 47. Re-checked against origin/main at write time: main is on 45; 46 was taken by this same branch, and a warm cache stamped 46 carries ParsedFiles with no `typeParameters` at all. The csharp and rust capture goldens were regenerated with the tests' own documented `UPDATE_GOLDEN=1`; only digests moved, no captureGroups. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KtNfG6EPn738Y51AYs7wDp * fix(resolution): ground erased base names, and stop a class name from being enough (#2833) The review's central risk was that this PR converts MISSING edges into CONFIDENTLY WRONG ones. Base-name erasure (`Repo<User>` -> `Repo`, `Repo[User]` -> `Repo`) bound through a workspace-wide qualified-name fallback that consults NO scope, NO import and NO module — it bound any name with exactly one workspace def. That is why a Python `Mapped[User]` could bind an unrelated `class Mapped`, and why the language deny lists were papering over an open universe. `resolveErasedBaseName` now admits an erased base on one of four grounds, strongest first: the scope chain binds it; the declaration is in the SAME FILE; the index proves the name is a template family; or the file binds no cross-file class at all, so its silence is no evidence. The last ground fails toward permissive on purpose — every way it can be wrong costs a wrong edge that already existed, never a working one. Two measurements drove that design and refuted the simpler rule. A C++ `#include` materializes NO binding whatever, and C# resolves cross-namespace without `using` through the index — so a pure "require lexical grounding" rule would have deleted every cross-file C++ generic member. Both are now pinned. Python erases at CAPTURE time, so by resolution there is no `<` and the grounded route was never entered. `erasedTypeApplication` rebuilds the application from `TypeRef.declaredSpelling` — strictly: the raw name must be the base and the argument list the whole balanced remainder, so `User[]`, `vector<Item>` and `Repo<User>?` decline and behave exactly as before. Closing it took finding FOUR emitters, not one. Three were in Case 4; the fourth was `emitReferencesViaLookup` re-emitting the refused edge from the pre-resolved reference index, which needed the site marked handled with a recorded `receiver-unresolved`. A fifth lived in the text cascade: a declined fold falls THROUGH by design, and the cascade held its own ungrounded copy of the member-typing lookup. This file typed a receiver from a `TypeRef` in five places and the PR had wired three; all five now go through one `classOfDeclaredType`. Also here, from the same review: - Type parameters no longer bind a same-named class (uses the new `typeParameters`), and a BOUNDED parameter resolves through its bound. - A cross-file C++ PRIMARY template now binds: a ranking bug, not a capture one — the index fallback needs exactly one candidate and `Vec` held two, so removing the argument-pinned declaration leaves one. - `this->field.m()` resolved to nothing for generic AND non-generic alike. A language that declares `this` IS the enclosing class (`resolveThisViaEnclosingClass`) synthesizes no `this` typeBinding, so a chain whose BASE is `this` could never seed its head. Reading the provider flag keeps the rule language-free. - Class-level (static) member receivers emit nothing in TypeScript and Kotlin — for the non-generic control too. Case 6 types them from the DEF side (`isStatic` + `declaredType` on the field node), which needs no capture change; the target lookup stays the ordinary instance walk, so a static field HOLDING an instance still binds an instance method and a genuine static call is untouched. Partial-specialization SELECTION is deliberately not implemented: it needs argument deduction against a parameter list, and full C++ partial ordering is a real algorithm with no measured driving case. The discriminator now exists if someone wants it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KtNfG6EPn738Y51AYs7wDp * fix(cpp,js,php,go): close the remaining per-language generic-field gaps (#2833) Four language gaps the review measured, each with a different cause. **C++ qualified member fields.** `std::vector<Item> items;`, `ns::Repo<User> r;` and `ns::Address addr;` captured NOTHING: every field rule required the type node to BE a `type_identifier` or `template_type`, and a qualified member type is neither — tree-sitter wraps both in a `qualified_identifier`. Three depth-agnostic rules (one per declarator shape) now match the outer node, which also REMOVES the depth boundary rather than raising it: depths 1-4 capture, generic and non-generic alike. Preserving the qualifier resolves nothing — measured: `ns::Repo<User>` binds neither way, because the dotted-tail fallback splits on `.` while C++ writes `::`, and `ns::Repo` is not an index key. Since a capture is a NODE and not synthesized text, the qualifier is dropped in `interpret.ts` by a top-level-only `::` split, so `std::vector<std::string>` reduces to `vector<std::string>`, not `string`. Measured cost of the non-generic half, which was the reason to hesitate: field captures go 8 -> 32 across the C++ bench corpus, but the resolution-level census over those 13 repos is 32 CALLS edges before and 32 after, BYTE-IDENTICAL. It fabricates only where a workspace class shares a std name (`class string` beside `std::string name;`), which is the same accepted policy the already-landed qualified-generic rules carry, pinned in the matrix as intended. **JavaScript `@type {Repo<User>}` and PHP `@var Repo<User>`.** Neither bound a field type — and neither did the NON-generic control, so this was a docblock gap rather than a generics one. PHP needed TWO captures, not one: with only the type binding, `$this->repo->save()` resolved until a second class declared `save` and then went unresolved, because narrowing a same-named method needs the receiver's member owned. Generics do NOT come free in PHP — `normalizePhpType('Repo<User>')` returns `'User'` by the container-element convention, so passing the raw spelling through would have emitted `User::save`; type arguments are erased at capture instead. In JavaScript they DO come free, verified byte-identical to the TypeScript control. Both decline what they cannot prove: arrays, `list<User>`, unions, `Promise`/`Array` wrappers (via an exported predicate rather than a copied name list), statics, and any property that already has a native type. **Go generic interfaces.** `UserRepo` genuinely DOES implement `Repo[User]` — the spec says a generic type must be instantiated, that instantiation substitutes type arguments and yields a new non-generic type, and that a type implements an interface when it is in its type set. So the old behaviour was a FALSE NEGATIVE and the matrix note calling it "already correct" was wrong. Satisfaction is now checked against POSITIONALLY SUBSTITUTED method sets, so `Repo[Order]` does not match a `Save(x User)` implementor — substitution, not erasure. #2829's exact method-set model is untouched: pointer receivers still follow MS(*T), unexported names stay package-scoped, the declaration's own method set is still checked first, and the harvest is gated so a repo with no generic interface never runs it. `go.test.ts` is unchanged at 296 passing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KtNfG6EPn738Y51AYs7wDp * test(resolution): pin every fix from the review, 114 -> 155 rows (#2833) Eight rows in this matrix pinned gaps that the fixes in this series close, so each asserted the opposite of the new truth. All eight are flipped, and the prose describing them as open gaps is corrected. Nine new cases cover the fixes that would otherwise have shipped unpinned. Flipped, each measured: the type-parameter FALSE edge (`run2`) is gone; a bounded parameter now resolves through its bound with fan-out; the cross-file C++ primary binds; the C++ qualifier depth boundary is removed rather than raised; Go gains its two structural implementors and JOINS the paired sweep, which had quietly excluded it — that exclusion was the taxonomy admitting a bug; and both static-member rows resolve. Added: JS `@type` and PHP `@var` docblock fields with three PHP declines; a Kotlin `companion object` receiver (given an INTERFACE control so the paired sweep can check it, which `ts-reach-shapes` cannot — its two sides are not count-comparable); the Python third-party grounding refusal plus the ground that still ADMITS, so an empty row can never be read as "erased names never resolve"; the four mirrors that would break if grounding were tightened (same-file and imported Python, a C++ `#include`, C# cross-namespace without `using`); C++ qualified non-generic fields including the fabrication policy and its absence case; `this->field.m()` for generic and non-generic with bare controls; and a Go negative proving substitution is positional, not erasure. Three shapes are pinned AS MEASURED with notes saying they are deliberate limits so nobody "fixes" them by accident: C++ partial-specialization selection is deterministically the primary (real selection needs argument deduction); `std::unique_ptr<T>` types to the pointer, not the pointee (`.` and `->` are indistinguishable to the resolver, so transparency would trade a recoverable miss for a confident wrong edge); and two same-named C++ specializations in one file collapse to one node id, which is why the shadowing fixture uses two files. One row pins a REMAINING wrong edge rather than hiding it: `m.inner.ping()` on a `Mapped[User]` head still binds the unrelated workspace class, while the one-segment-shallower `m.save(u)` correctly declines. The obvious one-line guard was written and MEASURED not to close it, so the surviving route is elsewhere and wants its own diagnosis — a broader refusal would change chain-head resolution for every language without pinning the shape it is meant to fix. `bench/scope-capture` is rebaselined for the six languages whose captures moved, regenerated from a fresh measurement rather than pasted; `--check` passes for all 15. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KtNfG6EPn738Y51AYs7wDp * perf(resolution): remove three measured hot-path regressions this series added (#2833) A quality pass over the #2833 series found three performance defects it had introduced, all measured, plus dead code and stale docs from six agents having appended to the same files across four rounds. No behaviour change: the resolver suite is identical before and after, and every scope-capture fingerprint is byte-identical. **An accidental quadratic in Go instantiation harvesting.** `collectGoInstantiations` calls `record()` for every type binding and every declared, return and parameter type in every Go file, and the `includes('[')` gate does not filter Go's most common types — `map[string]string`, `[]map[string]*v1.Pod` and `map[string]map[string]int` all produce a `map` candidate. Each false base then failed a full scope-chain walk and fell through to a LINEAR SCAN OF EVERY INTERFACE IN THE PROGRAM, with no dedupe on the spelling, so the same `map[string]string` written 10,000 times paid 10,000 scans. Now a qualified-name index built in `buildDetectionIndexes` (one probe, ambiguity semantics preserved exactly) plus a per-scope base memo: 8,000 interfaces / 80,000 spellings: 6,662 ms -> 104 ms (64x) `resolveEmbeddedInterface` held a byte-identical copy of that scan and now shares the helper. `GoInstantiation` was a single-field wrapper and collapses to the array it wrapped; its two parallel maps fold into one whose inner key IS the dedupe. `candidateStructIdsFor` was rebuilt per instantiation although every substituted method set has the same key set — hoisted, and materialized, because one branch returned a live iterator that would have yielded nothing on a second pass. **`scanForCrossFileClass` asked a name-keyed question that needs no name key.** It answered "does this file bind any cross-file class" by probing every accessible namespace once PER NAME. It now iterates the channels directly, taking whichever side is smaller so a large namespace table cannot reintroduce the product. Predicate and early exit preserved: 5,000 module names x 1,000 namespaces: 159.0 ms -> 1.2 ms (132x) **A duplicated scope walk on every generic receiver.** `resolveClassBindingForName` computed the lexical candidate list, then `resolveErasedBaseName` recomputed the identical `findAllBindingsInScope`. Computed once and passed: receiver at depth 8: 5,617 ns -> 3,091 ns (-45%) **A whole extra AST traversal per JavaScript and PHP file.** The docblock synthesis passes each added a full tree walk to find one node kind — the ninth in the JS emitter, the third in PHP. `node.namedChildren` materializes a wrapper array across the N-API boundary for every node, so one added pass cost 1.9x what parsing the entire file costs. Folded into the existing walks as one more node kind; capture output is byte-identical and every fingerprint is unchanged. Total emit time per file drops 4-7%. Hygiene, all verified stale rather than assumed: - `receiverOriginOpts` passed `resolveThisViaEnclosingClass`, which `classifyReceiverOrigin` never reads — the "both hooks" comment above it is true again. - The `stripDecoration` docstring's caller roll-call claimed the only edge-emitting caller "emits no edge and can only change a diagnostic label". Case 6 passes it and does emit edges. Replaced the roll-call with the rule; six rounds each appending a name to a list is how it went wrong. - A Python comment described the resolution-time grounding as a follow-up that "this parse-time pass cannot do" — it landed in this same branch and is pinned by `py-erased-grounding`. - `classOfDeclaredType` took a `scopeId` all five callers derived from the `TypeRef` they also passed. Dropped, so "these five are the same call" is enforced rather than asserted. - Three exports with no consumer outside their own file. - PHP had three copies of one preceding-comment sibling walk and two regexes for one tag, so a fix to either reader of `@var` would land on one and not the other — the symptom being a field typed differently from its own foreach element type. One walk, one regex. Tests: the new matrix leaked a fixture repo per case; it now carries the sibling suite's `cleanupTempDirSync` and the Windows EBUSY reasoning that goes with it. `PAIRED` was a second hand-maintained list and 19 of 41 cases had silently fallen out of it — it is derived from the cases now, with a new assertion that each case is either swept as a pair or carries a written reason it is not. That recovered one genuine omission (`php-typed-property`). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KtNfG6EPn738Y51AYs7wDp * test(bench): rebaseline receiver-resolution for the #2833 this-> fix The `Receiver-resolution drop guards` CI step failed on this branch: shapeArm.cpp.fieldReceiverCall: "INVISIBLE-GAP" -> "RESOLVES" shapeArm.cpp.decoratedFieldType: "INVISIBLE-GAP" -> "RESOLVES" Both are the intended improvement. The guard is exact-match by design — the drop count cannot move without a deliberate rebaseline, and the rebaseline path demands the movement be explained — so this records the two shape flips and leaves the call-drop count arm untouched. BASELINE.md still claimed `this->repo.save()` and `this->repo->save()` were INVISIBLE-GAP. That is now false: the `resolveThisViaEnclosingClass` head seed added in this PR resolves both. Also notes what the control established — this was never a generics gap, since the non-generic control failed identically before the fix. * docs(parse-cache): narrow the SCHEMA_BUMP ledger to what the bump delivers The ledger claimed a warm cache would make "the whole fix ... a silent no-op on every incremental analyze". That overstates the constant. The bump invalidates the PARSE half; whether the re-parsed captures reach the graph is gated separately and does not move: - `isIncremental` (core/run-analyze.ts) tests `!options.force`, an existing meta, `!schemaFingerprintMismatch(...)`, feature parity, non-empty `fileHashes` and a git repo. SCHEMA_BUMP is in none of them. - the incremental branch writes back only `hashDiff.toWrite` and logs the rest as "unchanged file rows preserved". - SCHEMA_FINGERPRINT hashes node/relation DDL, untouched here, so it is byte-identical and moves nothing either. So an incremental analyze re-parses an unchanged file correctly but keeps its existing rows; the new edges land on the next full rebuild. That is the pre-existing contract for every capture change, not a regression in this PR — but the comment should not promise more than it delivers. Comment only; no behavior change. SCHEMA_BUMP stays 48. --------- Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
911151e230
|
fix(resolution): resolve Go pointer-receiver calls, and report the program boundary instead of hedging (#2766) (#2782)
Some checks are pending
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
|
||
|
|
27ab37c432
|
feat(resolution): type receiver chains from AST structure across all 14 languages (#2708) + epistemic lower-bound (#2744) (#2747) | ||
|
|
9c24e3459e
|
fix(rust): qualify items by their enclosing mod chain so same-named ones stay distinct (#2742) (#2745)
Some checks are pending
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
* fix(rust): let the qualified-call filter see inline modules The negative filter added in #2741 builds its set of known module names from FILE PATHS, so an inline `mod x { … }` — which appears in no path — was absent from it. Every module-qualified call into an inline module was therefore rejected before any candidate channel ran, which is a hole in that optimisation rather than in the resolution logic it guards. The per-pass index now unions the file-derived names with inline module names taken from the scope model: a `mod` declaration binds a `Namespace` def locally in the declaring scope, and that binding is the only place an inline module's name exists. Collected in the same walk that already builds the module → scope map, so it costs no extra pass. Found while fixing #2742, where a correctly resolved call into `mod inner { … }` still could not reach its target. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5nWP4BFeBSjbQ3uqAjtxV * fix(scope-resolution): try the namespace-prefixed node key before the bare one `resolveDefGraphId` looked up the plain `qualifiedName` key first and only retried with the `namespacePrefix`-qualified key afterwards. For the defs that carry a prefix the qualified name is a bare TAIL, so the plain key happily matched a same-named item at a different namespace depth in the same file and returned it before the more specific retry was ever reached. The namespace-prefixed key is strictly the more specific of the two, so it is now tried first. Where no such node exists the lookup falls through to exactly the previous order, which keeps the #1982 behaviour this retry was added for. Without this, a call into `mod inner { fn dispatch }` resolved to the correct definition and then mapped it onto the crate-root `fn dispatch` node — the self-loop #2742 describes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5nWP4BFeBSjbQ3uqAjtxV * fix(rust): qualify items by their enclosing mod chain so same-named ones stay distinct (#2742) Node identity is `<label>:<file>:<qualifiedName>` and carried no module path, so an inline `mod inner { fn dispatch }` and a crate-root `fn dispatch` in the same file collapsed onto `Function:<file>:dispatch`, first-wins. Resolution already picked the right definition — the target simply was not representable, so a correct resolution still rendered as a self-loop and `impact` reported the real callee as unreached. The mechanism already existed: `qualifyRustImplTargetByModScope` has walked `mod_item` ancestors for impl targets since #1982. Generalised to `qualifyByEnclosingModScope` and applied to free items, so `mod inner { fn dispatch }` becomes `Function:<file>:inner.dispatch`. Keyed purely on the `mod_item` node type, exactly as the impl qualifier already was, so it is a no-op for every language whose grammar has no such node. Two constraints found by tests rather than by reading, both now encoded: - The helper normalised `::` to `.` unconditionally. With no enclosing `mod` that rewrote a top-level `impl a::Inner` from `a::Inner` to `a.Inner` and moved its node id away from the one the HAS_METHOD owner edge emits, breaking the #1975 scoped-impl ownership. It now returns raw text untouched when there are no mod segments, which also makes the change strictly additive for every id that has no enclosing module. - Qualification is scoped to items with no enclosing class/impl. A method already carries its owner's name, and that owner's id is mod-scoped by the impl qualifier, so qualifying the method again breaks the same byte-for-byte agreement. Same-named methods on same-named types in sibling modules therefore still collapse — a narrower residual than the free-item case fixed here, and one belonging to the owner edge rather than to this path. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5nWP4BFeBSjbQ3uqAjtxV * fix(storage): bump schema versions for the mod-qualified Rust node ids (#2742) `INCREMENTAL_SCHEMA_VERSION` 24 -> 25 and `SCHEMA_BUMP` 31 -> 32. Node ids change for every Rust item inside any `mod` block, and `#[cfg(test)] mod tests` makes that close to every Rust repository. A pre-v25 index therefore holds ids an incremental top-up cannot reconcile — the old nodes would simply be stranded — so the reuse gate has to force a full re-analyze. The qualified name is computed in the parse worker, so a warm parse cache would likewise replay the old unqualified ids and keep the collapse. This branch originally claimed v24; #2708 took that number and merged first, so it is renumbered to v25 here. That is exactly the collision the v29 note in parse-cache.ts warns about, and re-checking against origin/main at rebase time rather than at branch time is what caught it. #2708 did not touch `SCHEMA_BUMP`, so 32 is free. The version-pin test moves with the bump by design, including the new pre-v25 row in the reuse-gate table. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5nWP4BFeBSjbQ3uqAjtxV * fix(rust): stop mod-qualifying container ids while their owner edges stay bare (#2745 review) #2742 re-keyed Rust node ids by the enclosing `mod` chain. The mint moved; the owner-edge anchor did not. `findEnclosingClassInfo` mints a member's owner id from the container's BARE `nameNode.text` and only follows a qualified shape when the provider sets `classExtractor.qualifiedNodeId`, which Rust does not. So every `struct` / `trait` / `enum` / `impl` declared directly inside a `mod` got a node id that none of its member edges pointed at. Five lines of idiomatic Rust were enough: pub mod engine { pub struct Config { pub retries: usize } } NODE Struct:src/lib.rs:engine.Config DANGLING HAS_PROPERTY Struct:src/lib.rs:Config -> Property:src/lib.rs:Config.retries The rows are discarded by the IGNORE_ERRORS COPY retry, so the struct silently lost every field. A trait impl inside a `mod` additionally dropped its METHOD_IMPLEMENTS edge outright. The same gap put `impl a::Inner` inside a `mod` back on the #1975 rake that `qualifyByEnclosingModScope`'s own docblock warns about. The impl-target branch deliberately fires only for an UNSCOPED `type_identifier`; the new gate had no such restriction and picked up the scoped targets that branch had just excluded, minting `Impl:<file>:outer.a.Inner` against an anchor still reading `Impl:<file>🅰️:Inner`. The member side was already excluded via `!enclosingClassInfo`. This adds the owner side, gated on `MEMBER_OWNER_NODE_TYPES` — derived from `CLASS_CONTAINER_TYPES`, which is already the single source of "this node type owns member edges" and already carries an INVARIANT note binding it to `CONTAINER_TYPE_TO_LABEL`. A language adding a container therefore cannot gain a mismatched id shape here without also failing that invariant. Keyed purely on tree-sitter node types, so no language name enters shared ingestion. `union_item` is listed too: its fields are captured as Property but it is not a recognized owner, so they carry no HAS_PROPERTY edge and cannot dangle — it is here so a union's id keeps the same shape as the struct beside it. Containers still collapse across sibling modules, exactly as before this fix. That residual belongs to the owner edge, and is not worked around here. Regression tests use the UNFILTERED `findDanglingEdges(result)`. Every other dangling assertion in `rust.test.ts` passes `['HAS_METHOD']`, which is precisely why the HAS_PROPERTY breakage shipped with a green suite. They assert the NODE id rather than only the edge's anchor, because the anchor was already bare while the bug was live — an edge-only assertion passes in both builds. All four fail when the new gate clause alone is reverted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019GvVxWt1ShhEP8CiMj3b6Z * fix(rust): resolve modules nested inside an inline mod (#2745 review) The #2730 self-loop survived one `mod` deeper: pub mod outer { pub mod tools { pub fn dispatch() {} } pub fn dispatch() { tools::dispatch(); } } CALLS outer.dispatch -> outer.dispatch <- the #2730 symptom NODE outer.tools.dispatch <- correct target, unlinked Two gates were blind to a nested inline module, so the hook refused and the shared lexical tier bound the call to the enclosing same-name `dispatch`: `knownModuleNames` was collected by walking `moduleScopeByFile`, which maps a file to its ROOT `Module` scope only. A `mod` nested inside an inline `mod` binds in the parent module's scope, so the walk saw depth-1 inline modules and missed every nested one — `tools` never entered the set and the negative filter rejected the qualifier before any candidate ran. `declaresSubmodule` had the same root-only assumption, so even with the name known the candidate `outer::tools` was never yielded. Both now read the def index. Names come from every `Namespace` def; inline module PATHS are derived from the members' `namespacePrefix` rather than from the `mod` defs, because a `mod` def carries no nesting information of its own — inside `mod outer { mod tools { … } }` the inner def is `qualifiedName: 'tools'` with NO `namespacePrefix`, while every def within it is stamped `outer.tools`. A `Namespace` scope also owns its OWN def rather than its children's, so the scope tree cannot answer this either: the `mod outer` scope lists `outer`, never `tools`. Restricted to non-empty prefixes, so this stays a DECLARATION check. Including file-derived modules would let an undeclared or `cfg`-gated file on disk outrank a real `use` binding — the regression #2741's review already fixed once. File-backed submodules therefore keep going through the binding check. A module with no defs at all is absent from the set, which is harmless: it has no member for a qualified call to resolve to. Cost is one pass over an already-resident def index, memoized per resolution pass on the existing WeakMap — the same order of work as the binding walk it replaces, and it subsumes it. `isLocalNamespaceBinding` was going to single-source the duplicated "locally declared submodule" predicate the review flagged; deriving paths from members removed the second copy outright instead. Regression fixture covers depth 2 and depth 3, so the fix is depth-agnostic rather than depth-2 special-cased. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019GvVxWt1ShhEP8CiMj3b6Z * fix(rust): let an imported type outrank a same-named module (#2745 review) Widening the negative filter to inline `mod` names let a type-qualified call through whenever a module happened to share the type's name. The crate-root-relative candidate then captured it: // src/lib.rs pub mod Buffer { pub fn with_capacity() -> usize { 111 } } // src/b.rs use crate::c::Buffer; // the real target lives in c.rs pub fn call() -> usize { Buffer::with_capacity() } base: (no CALLS edge — unresolved) PR: CALLS b::call -> Function:src/lib.rs:Buffer.with_capacity <- fabricated `ids.ts` states the doctrine this broke: a missing edge is the correct failure direction for a graph whose consumers include `impact`; a fabricated caller is not. The base produced the missing edge and the PR produced the fabricated one. That third candidate is the loosest of the three — a guess at a crate-root-relative path the caller never wrote, kept for 2015-edition style. In Rust 2018 a bare first segment resolves in the CALLER's module, so a local binding for that segment settles the question: it is now skipped when the head names anything non-module in the caller's own module. Candidates 1 and 2 are untouched, and they run first, so the legitimate `use crate::tools;` path is unaffected. The binding lookup goes through `lookupBindingsAt`. A first attempt read `Scope.bindings` directly and the guard never fired: a `use` binding is finalize OUTPUT and absent from the scope's own local table, which is exactly the imported-type case being guarded. Contract I8 in `contract/scope-resolver.ts` requires that channel anyway. The regression test asserts the forbidden TARGET rather than an empty edge set, and separately asserts the module member still exists as a node — otherwise the test would pass just as well if the call went unresolved for some unrelated reason, or if the module node disappeared entirely. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019GvVxWt1ShhEP8CiMj3b6Z * fix(scope-resolution): try the namespace-prefixed name on the TAGGED keys too (#2745 review) `resolveDefGraphId` gained a namespace-prefixed retry for the plain qualified key in this PR, but the five tagged keys above it — template constraints, parameter types, parameter shape, arity, template arguments — kept composing from the bare `qualifiedName`. For a namespace- or `mod`-qualified def those keys are simply dead: `node-lookup.ts` registers them under the QUALIFIED name (`inner.dispatch#0`) while this side built `dispatch#0`. The keys exist to separate overloads, so a mod-scoped overload set was relying on whichever later key happened to catch it. Verified as a miss rather than a mis-hit before changing anything — an end-to-end run with a crate-root decoy of the same name and arity binds correctly — so this is hygiene, not a live bug. Worth doing while the code is open rather than leaving five keys dead and the behaviour dependent on fallback order. Both name forms now go through one `lookupTagged` helper, most specific first, so a sixth tagged key cannot be added with the bare form only. That also removes the five hand-repeated `qualifiedKey(...)` / `nodeLookup.get(...)` pairs. Also pins the C++ `EXTENDS` retarget this PR's reorder produces. `cpp-two-phase-dependent-base-cross-ns-deep` declares a global `Inner` decoy alongside `ns:🅰️🅱️:Inner`; the base's `qualifiedName` is a bare `Inner` with the path on `namespacePrefix`, so only the prefixed key separates them, and only if it runs first. The improvement was riding unasserted in a Rust-scoped PR. The captures golden covers every `rust-*` fixture, so the three fixtures added by this review series drift it; regenerated with UPDATE_GOLDEN=1. Verified: 785 tests across cpp / csharp / rust resolvers and the callable-id-lockstep unit test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019GvVxWt1ShhEP8CiMj3b6Z * fix(rust): keep a mod declared inside a fn from hoisting above the callable (#2745 review) `fn wrapper() { mod helper { fn dispatch } }` minted `Function:<file>:helper.wrapper.dispatch@2:8` — the mod segment composed OUTSIDE the enclosing-callable prefix, inverting the real nesting. Nothing dangled: the `@line:col` suffix already makes a function-local callable's id unique, which is also why the mod segment adds no identity in this position. The path simply read as a lie about the source. It is now skipped rather than reordered — interleaving two qualifier passes to fix the order would be real machinery for a shape whose ids are already unique. Also folds in the three documentation and structure findings from the same review: - The 4-clause gate is extracted to a named `qualifiesByEnclosingModScope`, matching the two conditions directly above it in the same function, which were already named consts. - `qualifyByEnclosingModScope`'s docblock documented only the impl-target contract even though the generalized name has had a second, looser caller since #2742. It now states both, and says which gate belongs to which — that gap is what let the #1975 scoped-impl regression through in the first place. - The "cheap rejection BEFORE any index work" comment was no longer true: `passIndexFor` walks the def index on its first call in a pass. Corrected rather than left to mislead the next reader into thinking the filter is free. What it still buys — skipping the per-site candidate search, the part that scales with the workspace — is stated instead. Verified: 279 tests across the Rust resolver suite and the Rust scope-resolution unit tests. Captures golden regenerated for the extended fixture. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019GvVxWt1ShhEP8CiMj3b6Z * test(storage): move main's SCHEMA_BUMP pin to 32 for the mod-qualified ids (#2745 review) `#2736` added a pin asserting `SCHEMA_BUMP === 31` on main, which arrived on this branch through the merge of main while `0062a5c2` had already bumped the constant to 32. Neither side conflicted textually — the pin and the constant live in different files — so the merge was clean and the test failed instead. That is the pin working as designed: it exists so a bump cannot ride along unnoticed, and this is the fifth time a SCHEMA_BUMP collision has been caught by a guard rather than by review. Updated to 32 with the reason recorded inline. `INCREMENTAL_SCHEMA_VERSION` needs no second bump: 25 was introduced by this unmerged branch, so no released index carries it, and its own pin in `call-summary-schema-version.test.ts` is already consistent. Verified: 119 tests across the parse-cache, schema-version, incremental-orchestration and the two identity suites that arrived with the merge. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019GvVxWt1ShhEP8CiMj3b6Z * test(bench): rebaseline the Rust capture fingerprint for the three new fixtures (#2745 review) CI caught what I missed: [scope-capture --check] FAIL: rust: capture fingerprint drift (got 05acbaca..., expected 90fda086...) fixture_count 202 `bench/scope-capture` fingerprints the whole `rust-*` fixture corpus, so the three fixtures added by this review series drift it. I rebaselined the `rust-captures-golden` snapshot and stopped there — a new fixture is a call site of BOTH, and updating only one is how this reached CI red. This is the same class as PR #2743's headline finding, from the other direction: an id-shape change makes every synthetic corpus a call site, and the author fixed the unit-test fixture and missed the bench. Here it is a fixture-count change rather than an id-shape change, and the review that flagged the #2743 lead as "REFUTED, bench/ has no Rust node-id corpus" was right about node ids and wrong about the corpus fingerprint. Noted for the next author in the baseline entry itself. Verified as pure corpus growth rather than a capture-logic shift: removing ONLY the three new fixture directories and re-running reproduces the prior fingerprint exactly (196 fixtures, capture_groups_fp 3432), and restoring them gives the new one (202, 3556). `emitRustScopeCaptures` is untouched by this series. Scaling 1.022 local / 1.057 CI, well inside the 1.5 budget. `bench/python-scope` globs `python-*` only and is unaffected; no other bench walks the Rust corpus. `--check` now PASSes for all 15 languages. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019GvVxWt1ShhEP8CiMj3b6Z --------- Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
df06529950
|
fix(rust): resolve module-qualified calls against the module tree (#2730) (#2741)
* fix(rust): resolve module-qualified calls against the module tree (#2730) A Rust call written with a path (`tools::dispatch(..)`) was captured with only its tail identifier, making it indistinguishable from a bare `dispatch(..)`. The scope-chain walk then resolved the bare name lexically and bound it to whatever `dispatch` was nearest — which, for the common wrapper idiom fn dispatch(..) -> ToolOutcome { tools::dispatch(..) } is the wrapper itself. The graph gained a self-loop, the real cross-module edge never existed, and `impact` reported the callee as unreached: the issue's repository showed its central tool dispatcher as `risk: LOW` with 0 affected processes and both "callers" being `#[cfg(test)]` functions, while still labelling the result `epistemic: "exact"`. Resolve paths the way rustc does, over the module tree rather than the filesystem: - `mod_item` now emits `@declaration.namespace`, so a Rust module is a named definition rather than an anonymous scope region. This mirrors the existing C++ `namespace_definition` capture and lets the shared `tagNamespacePrefixes` pass stamp members with their enclosing module path — that pass needed no changes to start working for Rust. - `module-path.ts` reconstructs the other half of the tree: crate roots are directories holding `main.rs`/`lib.rs`, and a file's module path is its location below that root. A definition's module is its file's module plus any enclosing `mod` blocks. - `crate::`, `self::` and `super::` are prefix transforms on the calling module, not reasons to stop resolving. - The final path segment is looked up as a member of the resolved module, including members it only re-exports. A `pub use` creates no binding on the re-exporting module's own scope, so re-exports are followed through that module's import edges. Resolution runs ahead of the implicit-`this` and scope-chain tiers, so an explicit path outranks a lexical shadow, and returns undefined on an unknown module, a missing member or a tie — leaving the existing chain untouched. The new `ScopeResolver.resolveQualifiedFreeCall` hook is optional and unset for every other language, so this is additive. Fixes the reported case (direct callers 2 -> 3, impacted 2 -> 6, the Agent module now visible) plus multi-segment paths, `super::` paths and `pub use` facades, each of which previously produced a wrong edge. Known limitation, pre-existing and unchanged by this commit: an inline `mod inner { fn dispatch }` and a crate-root `fn dispatch` in the same file collapse to one graph node, because node identity is `<file>:<qualifiedName>` and does not carry the module path. That is a separate defect requiring module-path-qualified node ids and an incremental-schema migration. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5nWP4BFeBSjbQ3uqAjtxV * test(rust): rebaseline the scope-capture fingerprint for the module-tree captures `mod_item` now emits `@declaration.namespace` and scoped call sites carry `@reference.qualified-name`. Both are additive, so every bench fixture holding a `mod` block or a `Foo::bar()` call gains capture groups, and the corpus grew by the three `rust-2730-*` fixtures. Only the Rust fingerprint moves. The other 14 languages are byte-identical, which is the intended blast radius for a language-local capture change. Scaling stays linear at 1.043, well inside the 1.5 budget. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5nWP4BFeBSjbQ3uqAjtxV * fix(rust): carry crate identity in qualified module paths (#2741 review H1) A module was identified by its path segments below a crate root, so `crates/alpha/src/tools.rs` and `crates/beta/src/tools.rs` were the same module. A cargo workspace routinely gives several members the same internal module name — `util`, `error`, `config`, `types` are near-universal — and that made qualified resolution do one of two wrong things: - where only one member defined the called name, the call bound ACROSS crates; - where both defined it, the lookup saw two candidates, refused, and handed the site back to the lexical walk that emits the same-name self-loop. The fix for #2730 therefore switched itself off in exactly the workspace layouts it was written for, and #2730's own reported reproduction repository is multi-crate. A module is now `{ crateRoot, segments }` and `sameModule` compares both. Rust has no implicit cross-crate paths — reaching another crate requires naming it — so two modules in different crates are never the same module. Anchored paths (`crate::`, `self::`, `super::`) resolve inside the caller's own crate and inherit its root. Covered by a two-member workspace fixture where both crates define `tools::dispatch` behind a same-name wrapper, plus unit tests for the path arithmetic itself, including the branches no fixture reaches (a file under no crate root, a `super::` chain walking above the crate root). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5nWP4BFeBSjbQ3uqAjtxV * fix(rust): count only module members when resolving a qualified call (#2741 review H3) Module membership was inferred from the file path alone, so any callable in the right file counted as a member of the module. A `fn` nested inside another `fn` has the same `filePath`, the same bare `qualifiedName` and no owner, making it indistinguishable from a module-level item: pub fn dispatch() -> usize { 3 } // the real member pub fn wrapper() -> usize { fn dispatch() -> usize { 99 } // counted as a second member dispatch() } Two candidates tie, the lookup refuses, and the call falls back to the lexical walk that emits the same-name self-loop — so an unrelated local helper anywhere in a module silently reinstated #2730 for every qualified call into it. The scope model already draws the line exactly: a module-level item is bound with `origin: 'local'` in its module's own scope, a function-local item binds in the enclosing Block, and an `impl`/trait method binds in the Class scope. Membership is now that binding lookup rather than a path comparison. Inline-`mod` members bind in their Namespace scope rather than the file's Module scope, and reaching it would mean walking every child scope — faulting them back in from disk on the out-of-core path. They keep being identified by the `namespacePrefix` the shared tagging pass stamps on them, which a file-module member never carries. The documented residual is a `fn` nested inside a `fn` inside an inline `mod`, which inherits that prefix; that is strictly smaller than before and costs a refusal, never a wrong edge. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5nWP4BFeBSjbQ3uqAjtxV * fix(rust): require a use-binding to name a module, not a type (#2741 review H2) Import resolution deliberately strips a trailing symbol segment when probing for a file — "the last segment might be a symbol (function, struct, etc.), not a module. Strip it and try again" (import-resolvers/rust.ts). So `use crate::client::ClientBuilder;` also resolves to `client/mod.rs`. The qualified-call resolver took that at face value and treated the imported TYPE as the module `client`. Rust impl methods carry a bare `qualifiedName`, so `ClientBuilder::new()` was then looked up among `client`'s module members and bound to an unrelated module-level `new` — turning an unresolved site into a false edge, which the module's own contract calls the worse outcome. A binding now has to name the module it resolved to. The edge's `targetExportedName` is the tail of the written path, so comparing it against the resolved module's own tail separates the cases exactly: use crate::tools; tail `tools` module ['tools'] accept use crate:🅰️:b as tools; tail `b` module ['a','b'] accept use crate::tools::{self, Ctx}; tail `tools` module ['tools'] accept use crate::client::ClientBuilder; tail `ClientBuilder` module ['client'] reject Covered by a fixture where `client/mod.rs` deliberately holds both `impl ClientBuilder { fn new }` and a module-level `fn new`, so a regression re-binds to the wrong one, plus a control asserting a genuine `client::new()` module qualifier still resolves. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5nWP4BFeBSjbQ3uqAjtxV * fix(rust): give src/bin targets their own crate root (#2741 review) Cargo auto-discovers a binary target for every `src/bin/<name>.rs`. Each is a separate crate with its own `crate::` root, and its submodules live under `src/bin/<name>/`. Only `main.rs` and `lib.rs` established a crate root, so those entry files were folded into the surrounding library and given the invented module path `bin::<name>`. That made `crate::helper()` inside a binary resolve into the LIBRARY's `helper` — and unlike the other findings in this review, this one downgraded an edge the lexical walk had previously resolved correctly, so it made existing output worse rather than merely failing to improve it. `src/bin/<name>.rs` is now its own crate root (as is the `src/bin/<name>/main.rs` directory form), so a binary's modules and the library's modules of the same name are no longer the same module. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5nWP4BFeBSjbQ3uqAjtxV * fix(rust): only try a submodule candidate the caller actually declares (#2741 review) The first candidate module was `callerModule ++ qualifier`, yielded before the `use` channel and never checked against anything. That let file layout outrank a real import: with `use crate::b;` in `src/a/mod.rs` and an undeclared — or `cfg`-gated — `src/a/b.rs` present on disk, `b::f()` bound to the sibling file, where rustc resolves it to `crate::b`. A `mod` declaration, inline or file-backed, emits a `Namespace` def bound locally in the declaring scope, so the candidate is now gated on that binding rather than assumed. When the caller does not declare the submodule the candidate is skipped and the `use` and crate-root channels still run, so this only removes guesses. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5nWP4BFeBSjbQ3uqAjtxV * fix(rust): follow only real re-exports, and refuse on an ambiguous one (#2741 review) Two problems in the re-export channel. A private `use` was followed as though it re-exported. `use crate::tools::helper;` makes `helper` visible INSIDE the module; it does not put it on the module's public surface, so `facade::helper()` does not compile. Only `pub use` does, and finalize already distinguishes them — `reexport` for `pub use`, `named` for a private one. The `alias` kind is now accepted alongside `reexport`, because `pub use x::y as name` is a re-export that was previously ignored entirely. The lookup also took the first matching edge in file-iteration order, which is parse-pool order. Two `cfg`-exclusive facades re-exporting the same name are indistinguishable at this layer, so picking one baked a coin flip into the graph. It now refuses on a genuine tie, consistent with how member lookup already behaves. The pre-existing limitation that only FILE modules are reachable — a `pub use` inside an inline `mod facade { … }` has no `moduleScopeByFile` entry — is now stated in the code. Reaching those would mean walking every child scope and faulting the scope tree back in from disk, which is the cost that index exists to avoid; a miss falls through to the unchanged chain rather than guessing. The regression test deliberately makes the re-exported name globally ambiguous. Without that, the pre-existing unique-global free-call fallback resolves the call on its own and the assertion passes whatever this channel does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5nWP4BFeBSjbQ3uqAjtxV * perf(rust): stop type-qualified calls paying for module resolution (#2741 review) The capture carrying `rawQualifiedName` matches every `scoped_identifier` callee, so this hook was reached by `Vec::new()`, `String::from()`, `Self::method()` and every other type-qualified call — the overwhelming majority of `::` calls in real Rust, none of which name a module. Each one ran the full candidate search before returning undefined, and every candidate that missed then walked all of `workspaceIndex.moduleScopeByFile`. Total cost grew as `qualified-call-sites x files`; two independent measurements put per-site cost at 0.117 -> 0.428 ms across 301 -> 1201 files, i.e. linear in workspace size. Two changes: - The module index now carries a flat set of every module segment name in the workspace, and a qualifier whose head matches none of them is rejected before any candidate work. Measured at 0.02 us per rejected call and flat in file count (500 -> 8000 files), against a previously linear per-site cost. - Module scopes are indexed by module identity once per pass rather than rediscovered by scanning every file per candidate. On the out-of-core scope index that scan was worse than CPU: `moduleScopeByFile` fetches through `scopeTree.getScope`, so a full sweep could fault every module scope back in from disk — the pattern `workspace-index.ts` added `exportedCallableByName` to avoid. Given the #2649 and #1871 history this mattered before merge. The captures golden is regenerated for the fixture files added earlier in this series; `emitRustScopeCaptures` itself is unchanged by this commit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5nWP4BFeBSjbQ3uqAjtxV * fix(storage): bump schema versions so the #2730 fix reaches existing indexes Neither invalidation constant was bumped, so the fix did not reach the users who reported the bug. `INCREMENTAL_SCHEMA_VERSION` 22 -> 23. The incremental write set only covers CHANGED files, so a top-up against a pre-v23 index keeps the wrong self-loop — and keeps reporting the callee as unreached — for every unchanged Rust file. The constant's own doc block states this rule, and the precedent is exact: v11 is the same file (`rust/query.ts`) gaining a capture that changes CALLS edges, with the same "force a full re-analyze" contract, and v12 is a second Rust instance. `SCHEMA_BUMP` 30 -> 31. `@declaration.namespace` and `@reference.qualified-name` are parse-time captures, so a warm parse cache replays the old capture set verbatim: `rawQualifiedName` comes back undefined and no Namespace def exists to hang a module prefix on, turning the entire resolution tier into a no-op on unchanged files. `PARSE_CACHE_VERSION` folds in the package version, so a tagged release would have invalidated eventually — but source, dev and CI builds at the same version would not, and the v29 note already warns that relying on someone else's bump is how a change ships with no invalidation at all. Re-checked against origin/main at commit time, as that note instructs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5nWP4BFeBSjbQ3uqAjtxV * fix(scope-resolution): let a language opt out of the already-namespaced guard (#2741 review) `tagNamespacePrefixes` skips a def whose `qualifiedName` already equals, or is prefixed by, its enclosing namespace path. That is right for C++ and C#, where the qualified name genuinely carries the namespace. Rust qualified names never do, so the guard fired on a coincidence: in `mod a { pub fn a() }` the member's name equals its module's name, the prefix was skipped, and `moduleOfDef` then reported the member as belonging to the PARENT module. `crate:🅰️:a()` refused, and the def became indistinguishable from a crate-root `fn a` for the module matcher. The guard is now conditional on a `qualifiedNamesCarryNamespace` option that defaults to the existing behaviour, and Rust opts out. The shared pass stays language-neutral — the decision lives with the provider that knows what its own qualified names contain. C++ and C# resolver suites pass unchanged alongside the Rust ones (600 tests). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5nWP4BFeBSjbQ3uqAjtxV * fix(rust): refuse a leading :: path instead of reading it as relative (#2741 review) A leading `::` anchors at the extern prelude: `::tools::dispatch()` names the CRATE `tools`, not a module of the current one. The path split filtered the empty leading segment away, which silently reinterpreted the path as relative and let it resolve against a local module that happens to share the name. Extern crates are outside the workspace module tree, so the qualified tier now refuses and leaves the site to the unchanged chain. The regression test asserts the tier does not bind into the local `tools` module, rather than asserting no edge at all: the lexical tier still resolves the bare tail on its own, and that behaviour is not what this change governs. Asserting an empty edge list would have been testing a different tier. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5nWP4BFeBSjbQ3uqAjtxV * refactor(rust): reuse the canonical callable predicate and drop dead re-exports (#2741 review) `CALLABLE_TYPES` was a local copy of the set behind `isOverloadableCallable` in `utils/callable-labels.ts`. Two copies of the same set drift: extending the canonical one with a new callable kind would silently leave qualified calls of that kind unresolved here, with nothing to catch it. Use the shared predicate. The trailing `export { moduleOfFile, moduleOfDef }` and `export type { ScopeResolutionIndexes }` were commented as being "for the resolver's unit tests". No test imports them: the only importer of this module anywhere in src or test is `rust/scope-resolver.ts`, which takes just `resolveRustQualifiedFreeCall`. Both functions are already exported from `module-path.ts` (where the new unit tests take them from), and `ScopeResolutionIndexes` is canonically exported from `model/scope-resolution-indexes.ts`. Removed rather than left as surface that implies a contract it does not have. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5nWP4BFeBSjbQ3uqAjtxV * test(rust): rebaseline the scope-capture fingerprint with the correct prior hash The rebaseline note added with the original fix cited `Prior 655aed01…`, which was two rebaselines stale — it predates both #2604 and #2714. The true pre-PR value on the base commit is `7f1240b3…`. CI could not catch it: the gate compares the live fingerprint against the stored one and never reads the prose, so the audit chain these notes exist to provide was broken with nothing to flag it. The note now carries the correct prior value, and the fingerprint is regenerated for the fixtures this review series added. Scaling 1.061, well inside the 1.5 budget; fixture_count 196; the other 14 languages remain byte-identical. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5nWP4BFeBSjbQ3uqAjtxV * test: move the schema-version pin to 23 `call-summary-schema-version.test.ts` asserts the exact value of `INCREMENTAL_SCHEMA_VERSION` and enumerates which stamped versions the incremental reuse gate accepts. It moves with every bump by design — that pin is what stops an id- or edge-changing commit shipping without invalidation. Updated for the bump to 23, with the pre-v23 case added to the reuse-gate table: a v22 index predates Rust module-qualified call resolution, so every unchanged Rust file would keep the same-name self-loop and keep reporting the real callee as unreached. Caught by CI rather than locally, because the earlier sweeps in this series covered `test/integration/resolvers/` and `test/unit/scope-resolution/` only — the pin lives outside both. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5nWP4BFeBSjbQ3uqAjtxV --------- Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
e307286d52
|
fix(scope-resolution): a named receiver's member never resolves lexically, + two #2695 follow-ups (#2714)
Some checks are pending
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
* fix(scope-resolution): a named receiver's member never resolves lexically (#2699) `lookupCore` Step 1 walked the lexical scope chain for every lookup, including explicit-receiver property reads. So `options.baseUrl` could bind to an unrelated function-local `const baseUrl` in the same file, and `config.extractVisibility(node)` to the enclosing class's own method. This is the residual half of the defect JS/TS block scopes narrowed in #2695. Blocks moved nested-block locals off the chain of a reference outside the block, which removed 114 false edges; a local declared directly in the function body stayed on it, and no amount of extra scopes reaches that case. Fixed at the cause instead: `recv.name` names a member of whatever `recv` denotes, so a binding of the bare tail name in an enclosing scope is never the right answer. Steps 2 and 3 (receiver type / owner members) are the legitimate routes. `this` and `self` are EXEMPT, and that exemption was measured, not assumed. Skipping Step 1 for every explicit receiver removed 711 edges on a 762-file corpus — but 2 of those were genuine: `self.srcIx` and `self.streamedAt(...)` after `const self = this`, reaching their own class's members through the class-body scope. For a self-receiver the members and the lexical chain legitimately overlap; for a named receiver they never do. Exempting the self names keeps both true edges and still removes 709 false ones, adding none. The removals were classified by reading source at the site, not by pattern- matching ids — an "is the target a member of the source's owner?" heuristic labelled 43 of them plausible and every one I then read was false: language = config.language; -> the class's own `language` dirMap.get(...) / exactMap.get(...) -> a sibling object-literal `get` return config.extractVisibility(n); -> the class's own method (self-edge) writer.close(); -> GraphEmitSink.close Residual, deliberately kept: a `this.x` read can still bind lexically to a same-named local. That is the price of the two true self-alias edges above. `INCREMENTAL_SCHEMA_VERSION` 19 -> 20: a v19 index holds these false CALLS/ACCESSES on every unchanged file and would keep serving them through the reuse gate. Test confirmed discriminating: it fails with the guard reverted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0184RmD24KFJidYqpM7v3XjR * fix(typescript,javascript): a generator expression binding is a Function node (#2693) `const g = function* () {}` matched none of the closure-binding definition rules — they covered `arrow_function` and `function_expression` only — so the binding emitted a `Const` node. `buildGraphTargetIndex` admits callable nodes only, so `g()` resolved to nothing. Same defect shape as the `var` case #2693 already fixed: a different grammar node for the same construct, and the resulting graph node was not callable. Adds the four variable-binding shapes in both languages: `const`/`let` and `var`, each plain and exported. Purely additive — no existing pattern is reordered or rewritten, because the #2687 pre-scan dedup is order-dependent and collapsing the value/callable pair depends on which match wins. Deliberately NOT covered, and the query comment says so: a generator in an object-literal pair or a HOC wrapper still falls through anonymous. Those are rarer, and each additional pattern is another chance to disturb the dedup. `SCHEMA_BUMP` 26 -> 27: definition captures are parse-time, so a warm parse cache would replay the old ones verbatim — `--force` does not clear it. Two tests confirmed discriminating (they fail with the patterns reverted), plus a guard that the already-working generator DECLARATION form is unaffected, since it shares the emit path these were inserted beside. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0184RmD24KFJidYqpM7v3XjR * fix(ingestion): keep caller attribution in lockstep with definition ids (#2699) The definition phase appends `localIdentity` to a nested callable's own name segment (`run.save@3:2`); `findEnclosingFunctionId` did not, so the two phases derived different ids for the same callable. The failure mode is silent — the caller id names a node that does not exist, so the edge is dropped rather than reported — which is why the parse-worker docblock calls this pair a lockstep guarantee and asks that both phases derive the prefix from one place. The condition is now byte-identical to the definition phase's (`nestedPrefix !== undefined`), so the two cannot diverge again. Scope of the claim, stated plainly: no reproducing case was found, and this changes nothing measurable on a 762-file TypeScript corpus. TS/JS resolve callers through `resolveCallerGraphId` in the graph bridge, not this path; `findEnclosingFunctionId` serves the `callExtractor` languages, and the corpus does not exercise a nested callable there. The review that raised it (P3) observed zero dangling edges, and "zero dangling" is also what silently dropped edges look like — so this closes a documented contract rather than a demonstrated bug, and carries no test of its own. Rides the `SCHEMA_BUMP` 26 -> 27 in the preceding commit: caller attribution runs in the worker, so a warm parse cache would replay the old ids. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0184RmD24KFJidYqpM7v3XjR * docs(test): correct the block-scope header that this PR made false (#2699) Review finding (MEDIUM). The file header still described `lookupCore` Step 1 as walking the lexical chain for EVERY lookup, and called the function-body-local case "unchanged and still mis-resolves ... pre-existing and tracked separately". Commit |
||
|
|
170805647c
|
fix(rust): keep duplicate type names ambiguous in range binding (#2514) (#2652)
* fix(rust): latch duplicate type-name ambiguity in range binding (#2514) The range-binding prepass tracked cross-file return and field types in two maps and used map presence itself as the ambiguity flag: the second definition of a name deleted it, but a third definition found it absent and re-inserted the last-scanned file's type. Odd duplicate counts (3, 5, ...) therefore resolved a genuinely ambiguous name to whichever file was scanned last, while even counts stayed ambiguous. Latch ambiguity in a dedicated Set per registry (ambiguousReturnTypes, ambiguousFieldTypes): once a name has two or more workspace definitions it never resolves again, regardless of duplicate count or file order. Adds integration coverage for two/three-duplicate functions and structs, permuted file order, and a unique-name over-suppression guard. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(rust): bump INCREMENTAL_SCHEMA_VERSION to 12 for the #2514 range-binding fix The duplicate-name ambiguity latch changes which cross-file Rust CALLS edges the range-binding prepass emits. The incremental writeback persists only changed-file nodes, so an incremental top-up against a pre-v12 index would keep the old spurious edges on every unchanged Rust file. Bump the schema version to force a one-time full re-analyze, matching the v7/v11 contract for edge-affecting resolver changes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(rust): resolve import-disambiguated duplicate types in for-loops & destructuring Follow-up to the #2514 ambiguity latch. When several modules define the same function/struct name and a call site disambiguates it with a `use` import (including aliases and `use x::*` globs), range-binding now resolves the for-loop element type and the destructured field type to that specific imported definition, instead of leaving it unresolved. The bare-name return/field maps are (correctly) ambiguous for duplicates, but the call site's import pins a definition. range-binding records the full, untruncated return/field type per defining file, and resolveImportedDef() resolves a name to the single in-scope definition, mirroring Rust name resolution: - tier 1: explicit `use`/re-export imports and local defs (lookupBindingsAt); these shadow globs, so if any exist we decide within them alone; - tier 2: glob imports, consulted only when tier 1 is empty; a `wildcard-expanded` ImportEdge names the target module, so we resolve only when exactly one glob-target file actually defines the name. Two or more visible definitions stay unresolved, preserving the #2514 latch. normalizeRustReturnType is untouched (its Vec<T> -> Vec truncation is load-bearing for receiver resolution), so the full generic is read from the per-file map instead. Covered by integration tests: explicit / aliased / single-glob imports resolve to the imported definition; two globs that both export the name stay ambiguous; a local definition shadows a glob; no-import duplicates stay unresolved (#2514). INCREMENTAL_SCHEMA_VERSION stays at 12 (bumped by the #2514 commit in this PR); its note now also covers these added resolution edges. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * perf(rust): parse each file once in range-binding when the workspace fits a budget populateRustRangeBindings makes two passes over every file and, because the shared treeCache is empty in the analyze flow, re-parsed each file in both — a workspace of N files paid 2N parses. It now parses each file once and reuses the tree across both passes via an in-function store, gated by a source-byte budget: workspaces up to 16 MiB of Rust source (essentially every real repo) reuse trees; larger ones fall back to per-pass re-parsing so peak RSS stays bounded on huge repos (the memory-sensitive case keeps its current profile). Also collapses the parse+timeout boilerplate that was copy-pasted in both loops into one getOrParseTree helper, and adds a PROF-gated `rangeBind=` segment to the scope-resolution profiler for phase-level observability. Measured on a 500-file synthetic Rust workspace (PROF_SCOPE_RESOLUTION=1): the range-binding phase drops ~370ms -> ~320ms (~14%), parses 1000 -> 500. Behavior is unchanged (199 rust + range-binding-order + parse-timeout tests green); repos above the budget are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(rust): update schema-version gate to v12; regenerate golden + bench baseline for new fixtures CI surfaced three deterministic-artifact failures, all from this PR's own additions: - call-summary-schema-version.test.ts hardcoded INCREMENTAL_SCHEMA_VERSION === 11 (the #2604 window); #2514 bumped it to 12. Update the gate and extend the reuse-gate version history so a v11 stamp now forces a full re-analyze. - rust-captures-golden expected-captures.json drifted (130 -> 174 entries) because the new rust-import-* / rust-dup-* fixtures joined the rust-* corpus. Regenerated (UPDATE_GOLDEN=1): additions only, no existing captures changed — emitRustScopeCaptures is untouched. - bench/scope-capture/baselines.json rust fingerprint drifted for the same reason. Rebaselined with a provenance note; scaling 1.06 < 1.5 budget. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude <claude@anthropic.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
881c6bccc7 |
test(rust): regenerate captures golden snapshot for function_signature_item
Expected drift from the query.ts change: abstract trait methods now emit a scope + declaration capture, shifting captureGroups/digest for every rust-* fixture containing a trait with a required (bodyless) method. |
||
|
|
ed8ab1c246
|
fix(scope-resolution): resolve callable reference flows (#2437) (#2522)
Some checks are pending
CodeQL / Analyze (python) (push) Waiting to run
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
* docs(plans): add provider-hook value-refs plan (#2437) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(plans): deepen #2437 plan to USES + property-dispatch design Design revised after prior-art research (Kythe ref vs ref/call, Joern METHOD_REF, Feldthaus field-based call graphs, CodeQL impliedReceiverStep): registration sites emit reference-class USES, invocation is recovered by a field-based property-dispatch pass synthesizing CALLS at member-call sites. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(scope-resolution): model provider-hook value references (#2437) Functions referenced as object-literal property values (provider hooks like emitScopeCaptures: emitCppScopeCaptures) previously produced no edge at all, so impact/context reported a false-safe 0 upstream dependents. Two coordinated halves, per prior art (Kythe ref vs ref/call, Joern METHOD_REF, Feldthaus ICSE'13 field-based call graphs, CodeQL impliedReceiverStep): - Registration -> USES: new ReferenceKind 'value-ref'; TS/JS queries capture pair values and shorthand properties (with @reference.property-key); emitted as a reference-class USES edge, reason 'scope-resolution: value-ref'. Resolution is callable-gated so plain values emit nothing. - Dispatch -> CALLS: new shared pass emitPropertyDispatchCalls synthesizes CALLS (reason 'property-dispatch', confidence 0.7, per-key fan-out cap 32 calibrated on this repo's 16-provider hook tables) from member-call sites to every function registered under the same property key. Deviation from plan: the pass owns value-ref resolution entirely via the post-finalize findCallableBindingInScope walker — the shared registries only see pre-finalize local bindings, so imported hooks (the c-cpp.ts case) were unresolvable through lookupForSite; Reference.propertyKey passthrough dropped as unnecessary. SCHEMA_BUMP 13 -> 14: ParsedFile gains value-ref sites + propertyKey. Verified end-to-end: impact(emitCppScopeCaptures, upstream) now reports 8 impacted / HIGH with extractParsedFile (true dispatch caller) at d=1 via property-dispatch and the c-cpp.ts registration via USES. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(scope-resolution): cover value-ref registration and property dispatch (#2437) Integration: same-file/cross-file/aliased/shorthand registrations emit USES; non-callable and destructuring values emit nothing; dispatch sites gain property-dispatch CALLS (incl. JS twins and per-language partitioning); fan-out-capped keys are dropped entirely; factory-call values unchanged. Unit: capture-shape pins for @reference.value-ref + @reference.property-key. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(scope-resolution): surface dropped property-dispatch keys in stats (#2437) Review finding: skippedKeys was returned but discarded — a hook table larger than the fan-out cap silently reopened the #2437 gap for those keys. Log dropped keys and fold value-ref USES + dispatch CALLS into referenceEdgesEmitted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(plans): add callable reference-flow implementation plan * fix(scope-resolution): close property-dispatch review gaps * feat(scope-resolution): add callable flow facts * feat(scope-resolution): resolve callable value flow * feat(scope-resolution): resolve callable references across providers * fix: harden callable reference flow resolution * fix(scope-resolution): preserve callable binding semantics * docs(plans): add pr-2522-review-fixes plan Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(storage): bump INCREMENTAL_SCHEMA_VERSION for callable-value-flow edges Callable-value-flow CALLS/USES edges (#2437) can connect two files whose content did not change, but the incremental write set only covers changed files — a top-up against a pre-v7 index would silently omit the new edges for every unchanged file pair, indefinitely. Force the one-time full re-analyze (review finding 1, #2522). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(storage): sanitize callable-flow sites per-site at load, log drops The load-time validator rejected the WHOLE ParsedFile when one site was malformed or over-bound, with no logging — and C++ legitimately emits empty-string parameterTypes entries ('' = unknown, the ReferenceSite.argumentTypes convention) for cv-only/ERROR-recovered types, so real repos fell into a permanent, silent warm-cache-miss reparse loop through the #1983-sensitive main-thread path (review finding 7, #2522). Now: '' entries are valid in type arrays; a malformed/over-bound site drops only itself (counted, warned once per load); only non-array garbage — evidence the serialization itself is untrustworthy — rejects the file. Deviation from plan §6 wording: validator-side tolerance replaces emit-side clamps — smaller diff, same asymmetry closed at the single chokepoint. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(scope-resolution): keep declarations in the union for reassigned callable cells The binding-lookup suppression for fact-constrained cells was wholesale: reassigning a declared function through its own name (greet = other; greet()) deferred the call to the solver, which then refused the lexical lookup that resolves the declaration — an unresolvable RHS yielded zero CALLS for a call that resolved pre-flow (review finding 8, #2522). Suppression now applies only to cells bound by FORMAL facts — its actual purpose (a parameter whose grammar emits no declaration binding must not adopt a same-named outer function). Copy/alias/store/load destinations keep their declaration as an inclusion seed (Andersen-style union). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(scope-resolution): count forfeited deferred sites in the budget-bailout warning On work-budget exhaustion the deferred invoke sites end the run with zero CALLS — free-call fallback and reference emission already skipped them — but the warning said 'ordinary graph emission remains untouched', which is false for exactly those sites. The warning context now carries the unresolved deferred-site count and the comment states the real cost (review finding: budget-bailout honesty, #2522). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(scope-resolution): surface dropped property-dispatch keys in stats and warn payload The over-cap warning carried only a count; the dropped key NAMES were discarded and RunScopeResolutionStats had no field, so the PR-body claim 'includes them in resolver statistics' was unimplemented (review finding, #2522; reviewer ask on the fan-out cap). The warn payload now names up to 20 dropped keys and the stats carry propertyDispatchSkippedKeys. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(scope-resolution): drop producer-less ownerQualifiedName from formal sites No capture emitter anywhere produces @callable-flow.owner-qualified-name — the solver branch consuming it was unreachable in production, yet the field was typed, parsed, validated, and unit-tested with hand-built input (review finding 16, #2522; YAGNI). Re-add with a real producer if C++ qualified member declarators ever need it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(scope-resolution): drop dead callable-flow knobs CallableFlowPassingMode 'callable-object' had no producer and no consumer distinguishing it, and CallableFlowCaptureOptions.extractCallArguments had no language providing it (unlike its live sibling extractCallCallee) — review finding 17, #2522 (YAGNI). The invocation-kind 'callable-object' is a different, live concept and stays. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ingestion): bind subscripted callable cells to the container, not the index terminalIdentifier iterates children in reverse, so tbl[i] = handler seeded the INDEX variable's cell (polluting a same-named formal) and tbl[i](7) looked up the callee under i in a different scope — no join, no CALLS edge for the classic function-pointer-array dispatch (review finding 12, #2522). Subscript nodes now recurse into their container field only, in both bindingIdentifier and terminalIdentifier, across the fielded grammars (C/C++/JS/TS/Python/Go/Java). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ingestion): make cross-function file-scope callable bindings resolvable Two stacked gaps killed the canonical C callback-registration pattern (fp assigned in init(), called in run()) — the exact #2437 false-safe this PR exists to fix (review finding H1, #2522): 1. isVisibleValueBinding only consulted assignment regions and formals, so a call in a function OTHER than the assigning one emitted no invoke fact. A declared callable-typed binding is now a value binding wherever its declaration is visible (visibleCallableSignature). 2. The C scope query had no @declaration.variable pattern for function- pointer declarators — void (*fp)(int); created no scope-tree binding, so the seed (init) and invoke (run) cells canonicalized to different keys and never joined. Both bare and initialized forms now bind. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(c): detect variadic parameters via the named variadic_parameter node tree-sitter-c materializes '...' as a named variadic_parameter node; the anonymous-token checks never matched, so variadic function-pointer signatures were emitted with a wrong fixed arity and no '...' sentinel (review finding, #2522). C++ is unaffected ('...' stays an anonymous token there); the token checks remain for such grammars. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ingestion): emit invoke facts for field-stored callable member calls The C ops-vtable pattern (o->run = handler; o->run(1)) captured the store but never the call — the member path in emitCallFacts bailed for languages without protocol methods, and the value-binding index recorded the member store under the OBJECT's name ('o'), not the member's ('run') (review finding 11/M3, #2522). Member destinations now also record their terminal member name, and a member call whose name-cell has a visible store emits an indirect invoke — gated on the store so plain accessor calls (map.get) stay inert. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(cpp): disambiguate (obj->*ptr)() ERROR recovery by token order tree-sitter-cpp groups the recovered '->*' two ways depending on error-recovery cost (identifier lengths): [identifier, ERROR '->*m'] or [ERROR 'obj->*', identifier]. The recovery assumed the first shape, so the second silently swapped receiver/member and dropped the call site — the committed test passed only by name luck (review finding H2, #2522). The identifier's position relative to '->*' inside the ERROR now decides roles. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(cpp): class members are never file-local in hasFileLocalCallableLinkage The name-keyed file-local set is populated from every static declaration, so an in-class 'static void make();' (external linkage — in-class static means no-instance) and any member sharing a name with a static free function were over-marked, refusing legitimate cross-file declaration/definition joins (review finding 13/M2, #2522). Method and Constructor defs now bypass the name-set, per the hook's own linkage-only contract. Deviation from plan step 13: the regression is a unit-level contract pin rather than an end-to-end join test — C++ merges out-of-line member definitions onto the member node by qualified identity, so the graph shape cannot discriminate the join refusal for members. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(cpp): classify parameter passing mode from the declarator chain only A whole-subtree scan for reference_declarator inverted copy vs alias: void reg(void (*cb)(int& out)) marked the by-value pointer cb as 'reference' because of the NESTED parameter's int&, making the solver back-propagate formal targets into every caller's argument cell — alias semantics for a copy (review finding 14/M5, #2522). The chain walk never descends into nested parameter lists; a reference anywhere ON the chain (int& x, void (*&cb)(int)) still aliases. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ruby): bare identifiers are calls, not callable references Ruby parses a receiver-less zero-arg method call identically to a variable read, so 'action = process' — which CALLS process and stores its return — seeded action with the callable and minted a wrong CALLS edge from any dispatch through it, confirmed end-to-end (review finding 15/HIGH, #2522). New provider knob bareNamesAreCalls: a bare name that is not a provably local value binding and not an explicit reference form (method(:x), lambda/proc) emits no flow fact, on both the assignment and argument paths. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(go): pair multi-value := positionally instead of cross-wiring The shared field fallback took the FIRST LHS identifier and the LAST RHS identifier of Go's expression_list pair, cross-wiring 'a, b := f, g' and synthesizing a garbage comma-joined qualified name — the real relationships were silently dropped (review finding 16, #2522). extractAssignment may now return multiple pairs; Go pairs list entries positionally and emits nothing for a length mismatch (multi-return call RHS). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(java): drop get/test from callableProtocolMethods 'get' and 'test' collide with ubiquitous non-functional-interface APIs (Map/List/Optional/Future.get), so every ordinary container access emitted a spurious callable-object invoke fact — high-volume misleading graph facts with a cross-wiring risk on receiver-name reuse (review finding 17, #2522). Supplier.get/Predicate.test dispatch is deliberately traded away until the check can gate on the receiver's declared type. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(rust): pin the qualified-name no-degrade guard as a hard invariant Rust's scoped_identifier callable-reference capture over-includes unit enum variants and associated constants (Shape::Square seeds as if callable); they stay edge-free only because resolveSeedCandidates refuses to degrade an unresolved qualified name to a simple-name lookup (review finding 18, #2522). Capture-side type filtering would false-negative on tuple-variant constructors, so the guard IS the contract: documented as a hard invariant (Go's mis-shaped multi-value forms also rely on it) and pinned end-to-end. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(php): remove nonexistent optional_parameter node type tree-sitter-php has no 'optional_parameter' — defaults ride on simple_parameter — so the entry was dead weight the #1920 literal gate does not cover for capture-option Sets (review finding 19, #2522). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(cobol): detect procedure pointers on fixed-format sources Two stacked defects made the feature a no-op on classic sequence-numbered fixed format (review finding 20/H3, #2522): 1. parseDataItemClauses' USAGE alternation knew POINTER but not PROCEDURE-POINTER/FUNCTION-POINTER, so the dataItems filter was dead. 2. The raw-line fallback scanned UNCLEANED text, where the sequence number satisfied the leading digits and the LEVEL NUMBER got captured as the pointer name. It now scans preprocessed lines and requires a letter- initial name (COBOL data names must contain a letter). 161 COBOL preprocessor/copy-expander tests stay green; free-format matrix case unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(cobol): skip comment lines in SET seed/copy scans A commented-out SET (indicator-column '*'/'/' or free-format '*>') produced a live seed and a false CALLS edge from dead code (review finding 21/M1, #2522). The scan now skips indicator-column comment lines and strips inline '*>' tails before matching. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(architecture): document callable-flow-only mode and skipped-key reporting The Callable-value flow section omitted scopeResolutionEdgeMode: 'callable-flow-only' — a real emit-pipeline branch that suppresses all ordinary emission for standalone providers (review finding 22, #2522) — and predated the skipped-key names/stats surfacing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(scope-resolution): correct value-ref resolution attribution and stale pdg-gating comments The value-ref contract comment claimed MethodRegistry resolution — the mechanism is the post-finalize findCallableBindingInScope walker owned by emitPropertyDispatchCalls (resolveReferenceSites skips these sites). Three 'only under --pdg' calleeIdSink comments were falsified by the #2437 gating change (callee-id-sink.ts's header was updated; these copies were missed). Review finding 23, #2522. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(ingestion): direct unit coverage for synthesizeCallableFlowCaptures The 1,100-line shared synthesizer had no test naming it — only downstream consumers were covered (review finding 24, #2522). Pins seed/invoke/ formal/argument emission, subscript container binding, store-gated member invokes, produced-value guards, and the bareNamesAreCalls knob over a minimal options object so assertions target the synthesizer's own semantics. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(resolvers): deepen shallow-language coverage; fix Kotlin/Swift reassignment gaps it exposed Adds the COBOL SET x TO y copy-branch scenario and conditional-assignment scenarios for Kotlin, C#, Swift, and Dart (10 languages previously had one generic case each — review finding 25, #2522). The new scenarios exposed two real capture gaps, fixed here: - tree-sitter-kotlin's 'assignment' node is fieldless, so nested reassignments (chosen = ::target inside a block) produced no flow facts; Kotlin's extractAssignment now decomposes it positionally. - tree-sitter-swift fields its assignment as target:/result:, neither in the shared fallback's field lists; both added. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(infra): literal-validation gate for callable-capture option Sets The #1920 gate validates query literals and exported configs but not the module-private *_CALLABLE_CAPTURE_OPTIONS Sets consumed by the shared synthesizer — a typo'd node type silently captures nothing (PHP shipped a dead 'optional_parameter'; review finding 26, #2522). Every <key>NodeTypes Set literal is now validated against its language's grammar; name-carrying sets (callableProtocolMethods, memberPointerOperators) are deliberately outside the contract. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(storage): centralize corrupt-fixture casts into makeStoreEntry The callable-flow store tests scattered 'as unknown as' double-casts per fixture (review finding 27, #2522; standing no-as-any rule). One typed helper now owns the single controlled escape hatch for building malformed serialization-boundary payloads. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(bench): refresh capture fingerprints after review fixes python-scope: the committed baseline (8d5c3699) never matched this branch's code — CI's benchmarks arm was red on the PR head (review finding 2/HIGH, #2522); regenerated (a99e69ab), scaling 1.04 in budget. scope-capture: ruby/cpp/swift/java/kotlin drifted from the review-fix commits (bare-name suppression, passing modes + ->* recovery, assignment fields, protocol narrowing, positional assignment); all 14 languages re-verified PASS with ratios <= 1.18 against the 1.5 budget. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(docs): untrack docs/plans working documents docs/ is gitignored (local working docs); the plan files were force-added past the ignore. Untracked from the index only — they stay on disk. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(golden): regenerate captures goldens after callable-flow review fixes The per-language digest guards (csharp/go/php/python/ruby/rust/swift) locked the pre-fix capture output; the review-fix series intentionally changed it — store-gated member invokes, subscript container binding, Ruby bare-name suppression, Swift assignment fields, positional pairing. Regenerated with UPDATE_GOLDEN=1; clean verification run 59/59; all other parity/golden guards (pipeline-graph, spring-route, python parity) pass untouched at 33/33. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ingestion): prototypes are callees, not callable value cells The cross-function visibility fix indexed EVERY signature-bearing declaration as a value binding — including plain function/method prototypes (void f(int);). Every call to a declared function then became an indirect invoke, and with emitCanonicalInvokeReference (C/C++) minted a free-call reference that resolved through the registry, bypassing the precise passes' two-phase/ambiguity/subobject suppression — eight phantom CALLS edges in the cpp resolver suite on CI. Only declarations whose binding identifier sits under a pointer/ parenthesized declarator (callable-typed variables like void (*fp)(int);) create value cells now. cpp resolver suite 331/331; callable-value-flow + C/C++ suites 181/181 (the cross-function fp regression still passes); cpp fingerprint rebaselined, both bench gates PASS. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
083aedbc41
|
refactor(ingestion): delete legacy call-resolution DAG + heritage processor (RING4-1, #942) (#2023)
* refactor(ingestion): delete legacy call-resolution DAG + heritage processor (#942) RING4-1: all 16 production languages (incl. Vue #940) are registry-primary, so the legacy resolution legs only ran under the now-removed CI parity gate. Calls and inheritance now resolve exclusively through scope-resolution (Registry.lookup, preEmitInheritanceEdges, emitHeritageEdges, buildMro → MethodDispatchIndex). Removed: - Call-resolution DAG: call-processor.ts legacy body (processCalls, processCallsFromExtracted, resolveCallTarget + all resolver/dispatch/chain helpers), model/resolve.ts MRO-via-HeritageMap, model/heritage-map.ts, type-env DAG types; inferImplicitReceiver/selectDispatch LanguageProvider hooks + Ruby impls; DispatchDecision/ImplicitReceiverOverride/ReceiverEnriched. - Legacy heritage path: heritage-processor.ts, heritage-types.ts, heritage-extractors/, @heritage.* tree-sitter queries, heritageExtractor/ heritageDefaultEdge/interfaceNamePattern wiring, worker + parse-impl heritage passes (parse-worker/parsing-processor lockstep), cross-file-impl DAG pass. - Scope-parity infrastructure entirely (no legacy↔registry parity left to run): scripts/run-parity.ts, scripts/ci-list-migrated-languages.ts, ci-scope-parity.yml, test:parity, and the scope-parity ci.yml gate. Resolver integration tests still run via the normal tests job. Kept (shared infra, NOT call-DAG-only): type-env.ts buildTypeEnv (field extraction / structure phase / embeddings), model/resolve.ts c3Linearize + gatherAncestors (mro-processor mroPhase), route/fetch/exported-type-map helpers in call-processor.ts, preEmitInheritanceEdges (legacy-edge dedup simplified). Acceptance: grep for resolveCallTarget/inferImplicitReceiver/selectDispatch/ buildHeritageMap/HeritageMap/processHeritage/heritageExtractor/@heritage. is zero across src + test. tsc clean (both packages); resolver integration suite green (bit-compatible EXTENDS/IMPLEMENTS/CALLS); scope-capture fingerprints unchanged (python re-baselined: removed redundant ignored captures). ARCHITECTURE.md updated to scope-resolution-only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(review): apply autofix feedback (#942) ce-code-review autofix pass on the RING4-1 deletion: - parse-cache.ts: bump SCHEMA_BUMP 2→3 — ParseWorkerResult lost its `heritage` field, so stale on-disk caches must invalidate (prevents a rollback replaying a heritage-less cache into legacy code) [api-contract P2]. - parse-impl.ts: drop 3 now-unused type imports (ExtractedCall, ExtractedAssignment, FileConstructorBindings) left by the deferred-block removal — would fail the eslint CI gate [correctness+maintainability P1]. - AGENTS.md / CLAUDE.md / scope-resolver.ts contract doc: fix stale pointers to the deleted "§ Call-Resolution DAG" section + removed hooks; preserve the language-neutrality rule [project-standards P1]. - registry-primary-flag.ts / cross-file.ts / parse-impl.ts: refresh stale comments referencing deleted symbols (legacy DAG, runCrossFileBindingPropagation). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(ingestion): remove the vestigial isRegistryPrimary flag (#942) With the legacy call-resolution DAG deleted, the per-language `REGISTRY_PRIMARY_<LANG>` / `isRegistryPrimary` / `MIGRATED_LANGUAGES` flag had only one meaningful state — every production language resolves via scope-resolution — and an explicit `=0` override could only *disable* resolution with no fallback (a footgun the review flagged). Removing it. - Delete `registry-primary-flag.ts` and the now-dead `shadow-harness.ts` (legacy↔registry shadow-parity tool) + its test. - Collapse the three flag gates to their behavior-preserving outcome (`SCOPE_RESOLVERS == MIGRATED_LANGUAGES`, so this is a no-op): - scope-resolution phase now runs for every registered `SCOPE_RESOLVERS` entry (was `∩ MIGRATED_LANGUAGES`). - import-processor `addImportGraphEdge` + parse-impl `shouldAccumulate`: the legacy emit/accumulate paths were already inert for migrated languages (scope-resolution owns IMPORTS via the imports-to-edges bridge); drop the flag term. - Collapse flag-branching tests to the scope-resolution path and delete the csharp legacy-`=0`-leg describe blocks; remove the ruby/rust-scope env-forcing hooks (no-ops now). - Refresh docs/comments (ARCHITECTURE.md "one registration", scope-resolver cookbook, phase deps) — adding a language is now a single `SCOPE_RESOLVERS` registration. Verified: tsc clean (both packages); resolver integration tests green (747 assertions across cobol/csharp/ruby/rust/typescript/go, IMPORTS edges intact); grep for the flag symbols is zero across src + test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(format): prettier formatting on #942 changes Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ci): drop legacy heritage-capture tests + re-baseline scope-capture fingerprints (#942) Two CI failures from the #942 cleanup, surfaced by the tri-review + CI: - tree-sitter-languages.test.ts: two tests asserted `@heritage.*` captures (Rust trait-impl, Dart extends/implements/with) that this PR removed. The acceptance grep used `@heritage\.` (with `@`); these reference the runtime capture name `heritage.trait` (no `@`), so they slipped the earlier sweep. Inheritance is now covered by the resolver integration suite. (fixed macos-latest) - Re-baselined the scope-capture bench fingerprints for csharp/rust/ruby/java/ javascript/kotlin (baselines.json) + python (python-scope/baseline-fingerprint.txt). The earlier test-cleanup reworded comments inside the lang-resolution fixture files (Shapes.cs, child.rs, derived.rb, IA.java/Plain.java, Service.js, F.kt, app.py) to scrub deleted-symbol references for the acceptance grep; those are the bench corpus, so capture node positions shifted. Capture LOGIC is unchanged — verified `--check` passes for all 14 langs + python. (fixed benchmarks) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs/chore: scrub remaining REGISTRY_PRIMARY + deleted-symbol references (#942) Tri-review P3 follow-ups (verified): - TESTING.md: rewrite the "Scope-resolution parity" section — the legacy dual-leg (REGISTRY_PRIMARY_<LANG>=0/1) and `npm run test:parity` no longer exist; resolver tests run once on the sole scope-resolution path in the normal tests job. - scripts/bench-scope-resolution.ts: drop the inert `REGISTRY_PRIMARY_PYTHON=1` env set + usage hint (the flag is gone). - ruby/scope-resolver.ts, php/captures.ts: re-point doc-comments off the deleted heritage-map.ts / heritage-processor.ts to the current behavior. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ci): prettier format + regenerate scope-capture goldens (#942) Two more CI failures, same root cause as the bench re-baseline (the test-cleanup reworded comments in lang-resolution bench/golden-corpus fixtures): - quality/format: prettier on tree-sitter-languages.test.ts (blank line left by the deleted heritage-capture tests) + TESTING.md (the rewritten section). - tests/ubuntu/coverage: `csharp-captures-golden` (and python/ruby/rust) drifted because the edited fixtures feed the per-language capture-golden snapshots too (not just the bench). Regenerated via UPDATE_GOLDEN=1. Verified safe: only the edited-fixture entries changed; csharp `captureGroups` unchanged (38) — digest shifted from comment-position only; capture LOGIC untouched. 1168 scope- resolution tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(resolvers): drop createResolverParityIt wrapper, use vitest it directly The parity-aware `it` wrapper became a no-op when #942 removed the legacy call-resolution DAG (it just returned vitest's `it`). Remove it entirely so the resolver tests call vitest's `it` directly instead of shadowing it with a local `const it` (or `pit`/`rustParityIt`): - helpers.ts: delete createResolverParityIt + its now-unused vitestIt import and VitestIt type. - 16 files: drop `const it = createResolverParityIt('x')` and import `it` from vitest instead. - ruby.test.ts (pit) + rust.test.ts (rustParityIt): rename calls to `it`. - Scrub every comment that described the removed wrapper / dual-mode parity skip / legacy_skip gate (vue-scope, js/ts/dart/php/python headers, rust x2, cpp, swift x4, rust-coverage). Genuine test rationale is kept; only the vestigial two-leg framing is dropped. Accurate "legacy DAG (removed in #942)" historical notes are retained. No fixtures touched (no bench/golden re-baseline). tsc clean; rust+ruby resolver suites green (323 tests, incl. #1992 worker-path parity after a local dist build). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c11f50a06e
|
fix(ingestion): own generic Rust inherent-impl methods through the mod-qualified Impl node (#1992) (#2003)
* fix(ingestion): own generic Rust inherent-impl methods through the mod-qualified Impl node (#1992) A generic inherent-impl target (`impl<T> Inner<T>`) is a `generic_type` node, which the inherent-impl owner walk (findEnclosingClassInfo) did not match — so the walk returned null and the method got `File -> DEFINES` with NO HAS_METHOD edge (orphaned, and invisible to findDanglingEdges). The Impl node was already correctly mod-qualified (the @name capture drills into the inner type_identifier, tree-sitter-queries.ts), so this is an owner-walk-only fix: drill into the generic base and mirror the node gate so the owner id == the node id byte-for-byte. A scoped-generic target (`impl<T> a::Inner<T>`) materializes no Impl node and is left orphaned (deferred) rather than minting a phantom owner. The owner walk is shared by the sequential and worker paths. New fixture + tests assert positive HAS_METHOD ownership through distinct `a.Inner` / `b.Inner` nodes on both resolver legs and the worker path, plus a negative scoped-generic guard. rust-captures-golden regenerated additively for the new fixture. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ingestion): qualify className for same-tail Rust generic impls + regen rust bench baseline (#1992) F3 follow-up to #1992: two same-tail generic inherent impls under sibling mods that ALSO share a method name (`mod a { impl Inner { fn m } }` + `mod b { impl Inner { fn m } }`) keyed the method node id `${className}.${name}` with the bare tail (`Inner.m`) and collapsed onto one Function node (graph addNode is first-write-wins), silently dropping the second. The owner Impl `classId` was already mod-qualified, masking the collision behind distinct HAS_METHOD sources. Qualify `className` (`a.Inner` / `b.Inner`) in the bare inherent-impl arm so the node id inherits the mod scope; symmetric with the call-resolution fallback, and the HAS_METHOD owner anchors on the unchanged qualified classId. New same-method-name fixture + sequential & worker-parity tests; holds on both legs. Also regenerate the rust scope-capture bench baseline: the new rust-nested-tail-collision-generic (#1992) + rust-generic-impl-same-method-name (F3) fixtures grow the rust-* corpus, so the order-independent fingerprint drifts (56ffc1c0 -> b00aea0f, fixture_count 127 -> 129). Pure fixture-corpus drift — no scope-extractor change; existing fixtures' captures byte-identical. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(rust): regenerate rust-captures golden for the F3 same-method-name fixture (#1992) The rust-* scope-capture corpus is fingerprinted by TWO gates: the bench baseline (bench/scope-capture/baselines.json, already updated) and the rust-captures-golden unit test (test/fixtures/rust-captures-golden/expected-captures.json). Adding the F3 fixture rust-generic-impl-same-method-name grew the corpus 128->129 entries, so the committed golden drifted too. Regenerated additively (UPDATE_GOLDEN=1) — only the new fixture's entry is added; existing fixtures' captures are byte-identical. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c60ad9f7ab
|
fix(ingestion): fully-qualified nested-type identity for C++/Ruby — structure (#1978) + resolution (#1982) (#1981)
* fix(ingestion): qualify nested-type node identity for C++/Ruby (#1978) Nested types sharing a tail name in one file — C++ `Outer::Inner` vs `Other::Inner`, Ruby `Outer::Inner` vs `Other::Inner` modules — silently merged into a single graph node keyed by the simple tail (`Struct:file:Inner`), cross-wiring their methods/properties onto one owner. Key class-like type nodes (Class/Struct/Interface/Enum/Record) by their normalized fully-qualified path (`Struct:file:Outer.Inner`) instead of the simple name. Gated per-language by a new `qualifiedNodeId` config flag (default false → byte-identical for every other language); enabled here for C++ and Ruby. - class-types.ts / generic.ts: `qualifiedNodeId` flag on ClassExtractor + config - ast-helpers.ts: findEnclosingClassInfo gains an optional getQualifiedOwnerName hook + EnclosingClassInfo.qualifiedClassId, so member-owner edges resolve to the qualified class node id (owner id == node id by construction) - parsing-processor.ts + parse-worker.ts: flag-gated qualified node-id + owner edges on both the sequential and worker parse paths (incl. routed properties) - call-processor.ts: same qualifier in the routed-property pre-pass (lockstep with the worker `kind === 'properties'` block) - configs/c-cpp.ts, configs/ruby.ts: qualifiedNodeId: true Method/Property node ids stay simple-qualified; only type nodes get the qualified id. Deferred to a resolution-side follow-up: Ruby SAME-TAIL routed-property/mixin owner identity under registry-primary (`emitRubyMixinEdges` keys owners by the simple tail name, last-wins); and Rust inherent-impl methods (impl_item is not a typeDeclaration — its #1978 test is describe.skip). Tests: same-tail collision fixtures + #1978 resolver tests for C++/Ruby (positive owner identity, R7), a worker-path parity block, and an unambiguous nested attr_accessor case; the C++ #1975 out-of-line test updated to assert qualified-id distinctness (forward-decl + out-of-line now unify). Verified green on both parity legs, the worker path, and tsc. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(ingestion): scope #1978 resolver tests to registry-primary leg; fix lint - helpers.ts: exclude the new #1978 C++/Ruby resolver tests from the legacy parity leg (LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES). They PASS on legacy too — the fix lives in the SHARED structure phase, not the legacy resolution path — so this is a deliberate registry-primary-only scoping (not a legacy gap), keeping the legacy path untouched and uncoupled from the new node-identity behavior. - rust.test.ts: drop the `eslint-disable vitest/no-disabled-tests` directive. That rule isn't configured in this repo, so eslint errored "Definition for rule 'vitest/no-disabled-tests' was not found" and failed `quality / lint`. The describe.skip needs no disable directive. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(test): satisfy CI for the new #1978 fixtures (format + golden + fingerprint) Adding the {cpp,ruby,rust}-nested-tail-collision fixtures changed the lang-resolution corpus, which the scope-capture golden snapshots and the fingerprint baselines gate on. These are pure fixture-corpus additions — #1978 does not touch the scope-capture phase (captures.ts / emit*ScopeCaptures are unchanged). Verified: the regenerated ruby/rust golden diffs are additive-only (no existing fixture's capture digest changed), so the cpp/ruby/ rust fingerprint drift is solely the new fixtures. - prettier --write test/integration/resolvers/{ruby,rust}.test.ts - regenerate ruby/rust captures-golden snapshots (UPDATE_GOLDEN=1; +1 fixture each) - rebaseline cpp/ruby/rust scope-capture fingerprints (bench/scope-capture/baselines.json) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(ingestion): extract shared qualified-name normalizer (#1982) Move normalizeQualifiedName/splitQualifiedName out of class-extractors/ generic.ts into utils/qualified-name.ts so the structure-phase buildQualifiedName, the scope-resolution inheritance resolver, and the per-language capture emitters can all key against ONE normalizer. A raw '::' qualifier must normalize to the exact '.'-joined key the QualifiedNameIndex already holds, or the qualified lookup silently misses (the #1982 resolution-side foundation). Pure relocation — byte-identical function bodies; tsc clean; existing C++ nested-collision tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ingestion): resolve same-tail C++ nested-type heritage to the correct qualified node (#1982) Registry-primary C++ inheritance (preEmitInheritanceEdges -> resolveInheritanceBaseInScope) resolved a same-tail nested base by its SIMPLE TAIL with first-wins, so `struct DerivedB : Other::Inner` mis-resolved EXTENDS to Outer.Inner (the wrong sibling; 0 dangling, so undetected). The namespace qualifier was discarded at the C++ inheritance capture. Fix (additive, qualified-first): - ReferenceSite gains an optional `rawQualifiedName`; the C++ inheritance capture emits `@reference.qualified-name` (qualifier-preserving, template-stripped: Other::Inner, ns::Base<T> -> ns::Base) only when the base is qualified, registered as a sub-tag so it can't shadow the `@reference.inherits` anchor. - resolveInheritanceBaseInScope resolves the qualifier against the full-path QualifiedNameIndex FIRST (which already carries Outer.Inner / Other.Inner keys from the structure phase), with progressive-prefix lookup for relative bases and refuse-on-tie, falling through to the existing simple-tail walk on miss — so unqualified bases and the single-candidate cross-file case are unchanged. Registry-primary cpp.test.ts 278/278 (incl. worker-path: rawQualifiedName survives worker serialization). Legacy leg unaffected (207 pass / 71 skip) — the new resolution-side assertions are registry-primary-only via helpers.ts. tsc clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ingestion): resolve same-tail Ruby mixin/attr_accessor owners to the correct qualified node (#1982) emitRubyMixinEdges keyed its owner map by the SIMPLE tail (def.qualifiedName split-popped) with last-wins, and the __heritage__/__property__ markers carried only the immediate owner name — so `module Outer; class Inner` and `module Other; class Inner` collapsed onto one `Inner` key and cross-wired their include/attr_accessor edges onto whichever Inner was processed last. Fix (lockstep, full-qualified): - ruby/captures.ts: build the marker owner from the FULL enclosing class/module chain (buildEnclosingQualifiedName walks all ancestors, normalizing the compact `class Outer::Inner` scope_resolution form via the shared splitQualifiedName) so the marker owner byte-matches the resolution def's qualifiedName. - ruby/scope-resolver.ts: key graphIdByName by the full def.qualifiedName instead of the simple tail. Top-level owners/mixins are unchanged (full == simple). Registry-primary ruby.test.ts 142/142 incl. a new worker-path block (the deferred note's duplicate-edge concern: markers survive worker serialization, exactly one HAS_PROPERTY per attr). Legacy leg unaffected (136 pass / 6 skip) — new assertions registry-primary-only via helpers.ts. tsc clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(ingestion): rebaseline #1982 golden/fingerprint + lint/format sweep Cross-cutting verification artifacts for the #1982 same-tail resolution fix: - ruby capture golden regenerated: ONLY the ruby-nested-tail-collision fixture drifts (+10 capture groups from its new include/attr_accessor + the now full-qualified __heritage__/__property__ marker owner). All other ruby fixtures byte-identical (proves the owner-qualification is localized to nested owners). - bench/scope-capture/baselines.json: rebaseline cpp + ruby fingerprints (the only two that drift; 12 other languages byte-identical). cpp = additive @reference.qualified-name capture; ruby = the localized owner change. Provenance notes record both. scaling linear (~1.0), 14/14 PASS. - generic.ts: drop the now-unused normalizeQualifiedName import (lint error). - walkers.ts / ruby.test.ts: prettier formatting. Verified: cpp 278/278 + ruby 142/142 (registry-primary), both legacy legs clean (skips registry-primary-only assertions), go/java/csharp 542 (cross-language regression — the qualified-first branch is gated on rawQualifiedName, set only by C++, so non-C++ inheritance resolution is unchanged). tsc + eslint(0 errors) + prettier clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ingestion): resolve nested Ruby mixin included by short name (#1982) emitRubyMixinEdges keyed graphIdByName by the full def.qualifiedName on the owner side, but the __heritage__ marker carries the mixin target as the bare written name (arg.text). A nested mixin module included by its short name (include Loggable where it is App::Loggable) missed the full-qn map and its IMPLEMENTS edge was silently dropped (0 dangling, undetectable). The shipped same-tail fixture used only top-level mixin modules, so CI stayed green. Add a secondary simple-tail fallback map consulted only when the full-qn mixin lookup misses; owner lookups stay full-qn so same-tail owner disambiguation is preserved. Characterization test + fixture (registry-primary only); golden regenerated additively. Addresses PR #1981 review (4417182679) P1. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ingestion): normalize qualified Ruby mixin arg in heritage marker (#1982) `include Outer::Mixin` embedded the raw `Outer::Mixin` into the ':'-delimited __heritage__ marker, so the `::` collided with the field separator and emitRubyMixinEdges mis-split it (className became empty), dropping the IMPLEMENTS edge. Normalize the mixin arg via splitQualifiedName(...).join('.') before emit so the marker carries the dotted form, which both parses correctly and matches the mixin def's qualifiedName. Simple names are unchanged (no golden drift). Addresses PR #1981 review (4417182679) secondary R2. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ingestion): resolve C++ same-tail nested heritage inside a namespace (#1982) A namespace-nested C++ type's scope-model qualifiedName carried its enclosing CLASS chain (A.Inner) but dropped the enclosing NAMESPACE, while the structure-phase graph node is keyed by the full path (NS.A.Inner). resolveDefGraphId's qualifiedKey therefore missed and fell back to simpleKey('Inner'), collapsing same-tail nested bases across sibling namespace members — DB : B::Inner pointed at NS.A.Inner. The shipped fixture was top-level only, so it could not catch this. Fix without disturbing the qualifiedName-keyed resolution index (an earlier attempt that rewrote qualifiedName regressed brace-init / UDC / two-phase namespace resolution): tagNamespacePrefixes records each namespace-nested def's enclosing-namespace prefix on a sidecar field, and resolveDefGraphId retries the node lookup with the namespace-prefixed key before the simpleKey fallback. The helper is language-agnostic (acts only on Namespace scopes) and opt-in — only the C++ provider calls it. Namespaced fixture + sequential & worker tests (registry-primary only). All 280 cpp resolver tests pass; tsc clean. Addresses PR #1981 review (4417182679) P2. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(ingestion): worker-path parity for Ruby mixin IMPLEMENTS + C++ DerivedA (#1982) The Ruby worker-path parity block asserted only attr_accessor (HAS_PROPERTY); add an IMPLEMENTS assertion so a dropped/cross-wired mixin owner on the worker path is caught (the __heritage__ marker owner must survive serialization). The C++ worker heritage block asserted only DerivedB; add a DerivedA assertion with a toHaveLength(1) duplicate guard. Registry-primary only. Addresses PR #1981 review (4417182679) test-coverage gap. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ingestion): distinct Rust same-tail nested-mod inherent-impl ownership (#1982) Rust methods live in `impl Inner` blocks, and findEnclosingClassInfo keyed the inherent-impl owner by the target's RAW tail (`Impl:lib.rs:Inner`), so two same-tail `impl Inner` blocks under different mods (mod outer / mod other) collapsed onto ONE Impl node and their methods cross-wired. The shipped fixture test for this was skipped/deferred. Qualify an UNSCOPED inherent-impl target by its enclosing `mod_item` scope (`outer.Inner`) in BOTH the owner walk (ast-helpers.qualifyRustImplTargetByModScope) and the Impl-node materialization (parsing-processor + parse-worker, lockstep) so the owner edge and node id agree byte-for-byte. Gated on the Impl label + impl_item + an unscoped type_identifier target — Rust-impl-exclusive, so C++/Ruby and the rust captures golden are untouched; a SCOPED `impl a::Inner` keeps its full raw text (#1975, unchanged). The previously-skipped distinct-ownership test is now active and passing; rust 170/170, cpp+ruby+golden 437/437, tsc clean. Done in-PR at maintainer request (was deferred as a follow-up). Addresses PR #1981 review (4417182679) test-coverage gap R7. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(ingestion): single qualified-name normalizer + module-scoped Ruby PROPERTY_PREFIX (#1982) Replace cpp/captures.ts's parallel normalizeCppNamespaceQName with the shared normalizeQualifiedName (behaviorally equivalent for C++ qualified-identifier inputs: '::'->'.' with leading/trailing-:: handling; no interior whitespace reaches it). Promote Ruby's PROPERTY_PREFIX to module scope alongside HERITAGE_PREFIX (was function-local — asymmetric with no behavioral effect). Maintainability only; cpp+ruby resolver suites 428/428, tsc clean. Addresses PR #1981 review (4417182679) maintainability item. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * perf+fix(ingestion): single enclosing-class walk + root-anchored base guard (#1982) U7 (perf): preEmitInheritanceEdges resolved the deriving class AND resolveQualifiedInheritanceBase re-walked findEnclosingClassDef for the same site. Resolve callerClass once and thread it into resolveInheritanceBaseInScope -> resolveQualifiedInheritanceBase -> enclosingScopeSegments, so the enclosing class is walked once per qualified site. Add a 'program' early-exit to buildEnclosingQualifiedName (ruby/captures.ts). Behavior-preserving. U8 (P3): a root-anchored C++ base ": ::A::Inner" names the GLOBAL type, but resolveQualifiedInheritanceBase prepended the deriving class's enclosing segments and could mis-bind to an enclosing-relative same-path type. Detect the leading "::" on the raw qualifier and try only the root-anchored key. Discriminating fixture + test (registry-primary only). cpp+ruby+rust resolver suites 599/599; tsc clean. Addresses PR #1981 review (4417182679) perf + P3 items. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(ingestion): rebaseline ruby+cpp scope-capture fingerprints for new #1982 fixtures The four new fixtures (ruby-nested-mixin-shortname, ruby-qualified-mixin, cpp-namespaced-collision, cpp-global-base-anchor) grow the lang-resolution corpus, drifting the ruby and cpp order-independent capture fingerprints. Verified purely additive: the ruby captures golden shows only the two new fixtures added (existing byte-identical), and removing the two cpp fixtures reverts the cpp fingerprint to the prior baseline (so the U3/U6/U8 code changes are scope-resolution / behavior-preserving, not capture-emission). measure.mjs --check PASS (14 languages). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(ingestion): prettier-wrap ruby resolver test call (#1982) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
04ade15451
|
fix(rust): scope-resolution coverage gaps — F66,F68,F71,F72 (#1934) (#1974)
* fix(rust): scope-resolution coverage gaps — F66,F68,F71,F72,F73 (#1934) * fix(rust): reviewer fixes — macro namespace, revert pattern:(_), drop variadic * fix(rust): wire macro resolution end-to-end + materialize unions (#1974 review) Addresses the outstanding #1974 review (second batch). Per maintainer decision, F72 is FULLY WIRED rather than documented capture-only. F72 macro — was a capture-only no-op (@reference.macro dropped downstream): - gitnexus-shared: add 'macro' ReferenceKind + Reference.kind; add MACRO_KINDS (['Macro']) and a MacroRegistry that resolves a macro invocation ONLY to a macro_rules! definition — never a same-named free function (the disjoint-namespace guarantee the review required). - scope-extractor: referenceKindFromAnchor @reference.macro -> 'macro'; normalizeNodeLabel 'macro' -> Macro. - resolve-references: route 'macro' sites through MacroRegistry. - emit-references / graph-bridge edges: 'macro' -> USES (kept out of the CALLS keyspace, which denotes function/method dispatch). - node-lookup isLinkableLabel: Macro is linkable, bridging the registry def to the legacy @definition.macro graph node. - rust query: capture macro_rules! as @declaration.macro; fix the scoped macro arm to capture the tail identifier, not the full path (P3). F71 union — the @declaration.struct scope capture had no graph node to resolve to (legacy RUST_QUERIES never captured union_item): - legacy query: capture union_item as @definition.struct so the union is materialized as a Struct node and is genuinely resolvable. - query.ts: document the deliberate union->Struct downgrade rationale. Tests: - rust.test.ts (parity-gated): pipeline-level union resolution + macro resolution (USES to the Macro, exactly one CALLS to fn, none to Macro). Macro resolution is registry-primary-only -> listed in LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES['rust']. - rust-coverage.test.ts: scoped-macro tail + macro-def capture assertions; reframed as capture-layer only, pointing at the pipeline tests. - new fixtures rust-macro, rust-union. F73: dropped from baselines.json _note (variadic was never implemented). Rebaselined the rust capture golden + scope-capture fingerprint (a5fdff2c..., scaling ~0.99, fixture_count 126). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(rust): prettier-format the Reference.kind union (#1974) CI quality/format gate — collapse the multi-line 'macro' addition back to one line (fits the 100-col print width). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Gergő Magyar <gergomagyar@icloud.com> Co-authored-by: Gergo Magyar <abhigyan1.patwari@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5f0d690c60
|
fix(ingestion): materialize graph nodes for scoped class/module/impl declarations (#1975) (#1977)
* test(ingestion): failing target tests + graph-integrity helper for scoped-declaration nodes (U1, #1975) Adds findDanglingEdges() and pipeline-level tests asserting that Ruby namespaced class/module declarations materialize a Class/Trait node with a resolving HAS_METHOD edge. Red by design on the pre-fix base (5 failing) — the fix lands in U2 (shared core) + U3 (Ruby enablement). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ingestion): materialize graph nodes for Ruby namespaced class/module declarations (U2/U3, #1975) Widen the Ruby legacy structure query so `class Foo::Bar` / `module Baz::Qux` (name field is a scope_resolution node) match @definition.class/.module as separate top-level patterns. The node is keyed by its full scoped name, which matches the HAS_METHOD owner id that findEnclosingClassInfo derives from the same name field — so the previously-dangling ownership edges now resolve, and distinct namespaces (Foo::Bar vs Baz::Bar) stay distinct nodes (no collision). No change to findEnclosingClassInfo (zero call-resolution blast radius) and no scope-extractor/golden/bench impact — the fix is purely the legacy structure query gate. Finalizes the U1 target assertions to the qualified-name identity. Validated: 134/134 Ruby resolver tests pass on BOTH legs; tsc --noEmit clean; dangling HAS_METHOD edges on the ruby-namespaced fixture drop from 3 to 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ingestion): resolve C++ out-of-line nested definition method ownership (U4, #1975) For an out-of-line `struct Outer::Inner { ... }`, the container name is a qualified_identifier, so findEnclosingClassInfo derived the owner id from the full `Outer::Inner` text — but the type is keyed by its in-class declaration (the nested `Inner` node), leaving the method's HAS_METHOD edge dangling. Reduce a qualified_identifier container name to its tail segment for the owner id/name, matching how inline nested definitions are already keyed. Node-type scoped, so Ruby's scope_resolution names stay full (distinct-by-namespace) and no language is named in shared code. Only out-of-line-def methods (already dangling) change behavior — zero impact on bare classes or call resolution. Validated: C++ 268/268 default leg, 205+63-skip legacy leg, no regression; 2 new target tests pass both legs; Ruby namespaced tests still pass; tsc clean; scope-capture bench rebaselined (cpp +cpp-out-of-line-class fixture) — --check PASS (13 langs). Dangling HAS_METHOD on the new fixture: 1 -> 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ingestion): resolve Rust scoped impl-target method ownership (U5, #1975) `impl path::Type` and `impl Trait for path::Type` name the target with a scoped_type_identifier. Two coordinated fixes: - findEnclosingClassInfo: reduce a scoped_type_identifier impl target to its trailing type name (both the trait-impl `for` branch and the inherent branch), matching the type's own tail-keyed declaration. - tree-sitter-queries: add a @definition.impl arm for scoped inherent impls so the Impl node is materialized (keyed by the same tail) instead of missing. Together the trait-impl method owns through the real Struct node and the inherent-impl method owns through a real Impl node — no dangling edges. Rust's scoped_type_identifier has a name: field, so the tail extraction is exact. Validated: Rust 163/163 on BOTH legs, no regression; new target test passes; C++/Ruby suites unaffected; tsc clean; scope-capture bench rebaselined (rust +rust-scoped-impl fixture) — --check PASS (13 langs). Dangling 1 -> 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(ingestion): cross-namespace collision test + regenerate ruby/rust captures goldens (U6, #1975) - Add ruby-tail-collision fixture + test: Foo::Bar and Baz::Bar share the tail 'Bar' but must stay two distinct Class nodes (locks the KTD-2 anti-collision guarantee from full-scoped-name keying). No dangling, no cross-wiring. - Regenerate the ruby + rust captures goldens for the fixtures added in U3-U6 (ruby-tail-collision, rust-scoped-impl). Both diffs are additive-only — a single new entry each, existing entries byte-identical (no capture-logic drift; the fixes are in the legacy structure query + findEnclosingClassInfo, not the scope-extractor). - Re-baseline the ruby scope-capture fingerprint (81->82 fixtures). N/A-language verification: C#/Java/PHP have no class-declaration scoped-name gap and show no regression (606 passed; the 2 C# worker-pool failures are the known worktree 'parse-worker.js not built' limitation, unrelated to this change). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * revert(ingestion): drop C++/Rust scoped-owner reduction; ship Ruby-only (#1975) The self-tri-review of PR #1977 (review 4411683756) found — and reproduced — that the C++/Rust tail-reduction in findEnclosingClassInfo collides same-tail types declared in the same file (struct Outer::Inner + struct Other::Inner -> one Struct:Inner node, methods silently mis-attributed; same-named members merge). Root cause is pre-existing: GitNexus keys nested-type nodes by their tail name within a file, so even plain inline same-tail nested types already merge. A correct fix needs fully-qualified nested-type node identity — a broad change deferred to #1978. This reverts the C++ (qualified_identifier) and Rust (scoped_type_identifier impl) owner reductions in ast-helpers.ts, the Rust @definition.impl scoped arm, and the cpp/rust fixtures+tests+golden+bench entries. The Ruby fix is unaffected (it keys the node by the full scoped text — no collision) and stays: namespaced class/module node materialization + the cross-namespace collision test. Validated Ruby-only: 136/136 both legs; ruby+rust captures goldens 19/19; bench --check PASS (14 langs); tsc clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ingestion): collision-safe C++/Rust scoped-declaration node ownership (#1975) Re-introduces the C++/Rust fix the tri-review reverted, using a collision-safe approach instead of owner tail-reduction (which merged same-tail types in one file). Key the scoped DECLARATION's node by its full qualified text so it matches the owner id and stays distinct from a same-tail type elsewhere: - C++: widen the legacy structure query to materialize a node for out-of-line defs (class/struct Outer::Inner — name is qualified_identifier), keyed by the full text. No findEnclosingClassInfo change needed — BASE already derives the full-text owner, which now matches. Outer::Inner and Other::Inner stay distinct; 3-level A::B::C resolves. (A redundant forward-decl node remains.) - Rust: @definition.impl arm for scoped inherent impls (keyed full) + findEnclosingClassInfo inherent-impl branch accepts scoped_type_identifier with full text. impl a::Inner and impl b::Inner stay distinct. Collision-aware fixtures + positive owner-identity assertions (per the tri-review) replace the single-type fixtures. Deferred to #1978: Rust trait impls on a scoped struct path (impl T for a::Inner) and the pre-existing inline same-tail node collision — both need qualified struct-node identity. Validated: Ruby 136/136, C++/Rust 434/434 both legs (371+63-skip legacy); ruby+rust captures goldens 19/19 (additive); bench --check PASS (14 langs); tsc clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(format): apply prettier to scoped-declaration changes (#1975) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
0fc0211d26
|
fix(ingestion): migrate all languages' inheritance to scope-resolution on the worker path (#1951) (#1956) | ||
|
|
d1d2a64d0f
|
perf(ingestion): linearize scope-capture across all languages + Python import resolution (O(n²)→O(n)) (#1918)
Some checks are pending
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
* bench(python-scope): build-free measure harness + baseline fingerprint for emitPythonScopeCaptures
ce-optimize scaffolding for the python-scope-capture run. Mirrors the Go
scope-capture harness (#1848): imports the .ts hotpath via tsx, times
emitPythonScopeCaptures on a synthetic DAO source at 250/800 entities, and
pins an order-independent sha256 capture fingerprint over the whole
lang-resolution/python-* corpus + a fixed 20-entity DAO as the correctness gate.
Baseline (current code) is O(n^2): 250->800 entities (3.2x) -> 10.7x time
(1062->11343ms), scaling_ratio 3.34.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* optimize(python-scope-capture): thread captured nodes to kill O(n^2) findNodeAtRange re-walks
emitPythonScopeCaptures re-derived each tree-sitter match's AST node via
findNodeAtRange(tree.rootNode, ...) on every match, scanning all of root's named
children per call -> O(matches x rootChildren) ~ O(n^2). The same #1848 bug Go
had (fixed in
|