mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-06 02:49:56 +00:00
8 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e06c2dd13e
|
fix(impact): fail closed on id-less targets and follow ??/||/?: callable values (#3354) (#3373)
* fix(impact): fail closed on id-less targets and follow ??/||/?: callable values (#3354) #3354 reports `impact` returning a byte-identical 1037/CRITICAL/`exact` result for three unrelated targets, with only `target.name` differing. The reporter's `target` had no `id` and no `filePath`, which the traversal path always emits, and a synthetic reproduction of their monorepo (pnpm, Cloudflare worker-configuration.d.ts in five packages, Hono, a Durable Object) resolves every target correctly on main. So the identical result was not reproduced. The repro did surface two real gaps and one hardening point: - `_runImpactBFS` now throws when the target has no node id. Every caller already catches, so impact reports `impactedCount: null, risk: UNKNOWN` instead of a normal-looking blast radius that cannot be about this symbol. - Callable-value flow followed only a single designator on the RHS, so `const sweep = env.__sweep ?? runSweep; await sweep(env)` produced no flow and `scheduled` was missing as a caller of `runSweep`, while the result still claimed `epistemic: exact`. Each branch of `??`, `||`, `or`, and `?:` now flows into the binding (language-neutral: operator and field names, no language checks). Parse cache bumped 104 -> 112 (105-111 are claimed by open PR #3326). - A whitespace-only `target_uid` (strict adapters materialize omitted optional strings) is treated as omitted and falls back to the name. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(mcp): treat a non-string uid as omitted instead of throwing (#3354) Review follow-up on #3373. `query.uid?.trim()` called `.trim()` on a client-supplied value, and the MCP envelope is not type-validated, so `context({uid: 42})` threw a TypeError that `context()` does not catch. Before #3373 the same input ended as a structured not_found. A non-string uid now counts as omitted, which matches how normalizeToolParams already treats a non-string `target_uid`. The impact and trace not-found messages print a trimmed uid only when it is a non-blank string, so a strict adapter sending " " sees the name it searched for instead of `' '`. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(ingestion): expand ??/?:/ternary branches through a provider hook (#3354) Review follow-up on #3373. The shared value-alternatives rule keys on tree-sitter field names, and several grammars spell the same construct differently, so the expansion never fired for them: - Kotlin `elvis_expression` has no fields. - Swift uses `value`/`if_nil` and `if_true`/`if_false`. - Dart uses `first`/`second`, and its conditional has no `condition` field. - Python `a if c else b` has no fields. The `'?:'` operator entry was dead, since no bundled grammar emits it. Add an optional `valueAlternatives` hook to CallableFlowCaptureOptions, consulted before the shared rule, and implement it in the Kotlin, Swift, Dart, Python and Ruby providers. Shared code still names no language. Ruby's statement-bodied `if`/`unless`/`elsif` also carries `condition`/`consequence`/`alternative`, so the shared ternary rule dug an identifier out of an arbitrary statement (`g = h; 0` flowed `h`) and produced a wrong CALLS edge. The Ruby hook now keeps a multi-statement branch as one opaque source, as before #3373. Tests: new provider fixtures for Python, Kotlin, Swift, Dart and Ruby (including a Ruby negative), and TS chain, parenthesized, callable-left and `&&` negative cases. Captures goldens gain one entry each for the new fixtures. SCHEMA_BUMP stays 112 (same unreleased PR). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(ingestion): keep long ||/??/?: chains linear in callable-flow capture (#3354) Expanding value-selecting sources into per-branch flows made long chains super-linear and, past a few thousand operands, a stack overflow. A generated `w === "k0" || w === "k1" || ...` keyword table took 6.7 s at 1000 operands and 48 s at 2000, and threw RangeError at 8000. Two causes, both fixed without changing any emitted capture: - valueAlternatives recursed once per operator level and spread the partial results at every level. It is now an explicit stack that writes to one output array. The left-to-right order, the paren unwrapping, and the provider-hook contract (`[node]` means opaque) are unchanged. - Every alternative ran its visibility walks from its own leaf, which can be as deep as the chain is long, all the way to the root. Also, tree-sitter's `parent` re-descends from the root, so each step costs the node's depth. The walks now jump between "anchor" nodes, the only nodes any check can match: region ids and formal owners. The nearest anchor is memoized per node across the file, and parents come from a map recorded by the one DFS the synthesizer already does. After the fix: 0.34 s / 0.23 s / 1.6 s at 1000 / 2000 / 8000 operands. That is within about 1.2x of main without the expansion; what remains is tree-sitter query time. Capture fingerprints are byte-identical before and after the fix for all 16 scope-capture bench languages and for the Python harness. A `typescript-deep-chain` case added to the scope-capture bench guards the scaling: 1.11-1.22 now, 7.5-8.0 before. A unit test pins a 10000-operand chain: it must still yield the seed for a callable operand, the copy for a formal at the deepest leaf, and the invoke. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(kotlin): see through braced if-branches in callable alternatives (#3354) tree-sitter-kotlin wraps a braced branch as `control_structure_body > statements > <expr>`, so for any non-empty block kotlinValueAlternatives saw exactly one named child (`statements`) and pushed the wrapper itself as the branch value. operandSyntax emits nothing for a `statements` node, so `val run = if (c) { ::f } else { ::g }; run()` produced no flow edges, and the "multi-statement block stays opaque" guard could never fire. Descend one level through `statements` and require exactly one non-comment expression there. Empty blocks (`{}` has no named children), multi-statement blocks, and `if` without `else` still return the whole `if` as one opaque source. The doc comment now describes that. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(ruby): skip only the multi-statement branch in callable alternatives (#3354) rubyValueAlternatives returned `[node]` (opaque) as soon as one branch held more than one statement. Two things went wrong because of that: - At the top level, `run = if c then g = h; 0 else method(:f) end` dropped the single-statement `else`, so `f` got no flow edge. - In an elsif chain, the outer `if` pushed the `elsif` node as a branch. The shared loop called the hook on it again, got `[elsif]` back, and used the whole elsif subtree as one source. So in `if a then method(:run_a) elsif b then g = h; 0 else method(:run_b) end` the `run_b` edge was lost. The hook now walks the elsif chain itself and skips each multi-statement branch, while every single-statement branch still becomes an alternative. This cannot add a wrong edge: each emitted alternative is a value the conditional really evaluates to, and nothing is taken from the skipped branch. Before, that branch did not contribute a resolvable value either. A whole conditional used as a source becomes a qualified-name seed, which resolves to nothing when it has more than one identifier leaf. With a single identifier leaf, it could even seed the condition variable. When no branch is a single statement, the conditional still stays one opaque source, as before. Ruby captures golden: ruby-callable-alternatives/app.rb goes from 33 to 58 capture groups. 23 of them come from the new fixture functions (checked against the old source). The other 2 are the new branch alternatives, `statement_if -> run_sweep` and `elsif_chain -> run_b`. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(ingestion): flow the right operand of && / and into callable bindings (#3354) `a && b` / `a and b` yields `a` when it is falsy and `b` otherwise. A falsy value is never a callable, so the right operand is the only one that can be invoked later. The capture left `&&` unexpanded, and the compound source became a qualified seed that resolves to nothing: `const run = x && f; run()` gained no edge to `f` while impact claimed `exact`. Python `x and f or g` reached only `g`. The shared expansion now maps `&&` / `and` to the right branch only. It recurses, so `x and f or g` reaches both `f` and `g`. Where `&&` yields a boolean (Java, C#, Go, Rust, C, C++, PHP, Zig), the destination cannot be invoked, so the flow never meets a call. A before/after CALLS diff over all 83 lang-resolution fixtures that contain `&&` or `and` shows exactly one new edge, logicalAnd -> runAndRight. PHP and Ruby bind low-precedence `and` looser than `=`, so `$g = $x and $y` never reaches this rule. The Python golden digest changes only for the extended python-callable-alternatives fixture. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(ingestion): keep operator branches of a value-selecting source opaque (#3354) The fan-out sent every branch of `??` / `||` / `or` / `?:` to emitAssignmentFact on its own, including branches that compute a value. `Handlers.fallback === run || fb` then emitted the comparison as a seed whose qualified text sliced to receiver `Handlers`, member `run`, and resolveBoundMemberCandidates minted a CALLS edge to `Handlers.run`. The same happened for `this.state !== run ?? this.fallback`, `this.state + run || this.fallback`, and Python `self.state != run or self.fallback`. Before the fan-out, the whole compound source was one opaque seed. A branch that is a binary operator expression now contributes nothing. The check uses the field vocabulary valueBranches already reads (a `left`/`right` pair, or an `operator`/`operators`/`op` token after the expression start), not grammar type names. Member accesses that field their `.`/`->` as `operator` (Ruby `call`, C/C++ `field_expression`) also field a member name through the list memberParts uses, now shared as memberNameNode, so they stay designators. Unary `&f`/`*fp` lead with their operator and stay designators. Call results were already dropped by emitAssignmentFact, and lambdas and callable references are unchanged. CALLS-edge diff over 87 lang-resolution fixtures (505 -> 501 edges): only the four false edges above were removed, and none were added. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(ingestion): pin the LEFT operand of ?? / or / elvis per provider (#3354) The Python, Kotlin, Swift and Dart "override ?? fn" cases put an unresolvable parameter on the left, so a fan-out that kept only the last operand still passed them. Each fixture now adds a callableLeft case with a real function on the left and a parameter on the right (`run_left or fallback`, `::runLeft ?: fallback`, `runLeft ?? fallback`), and each language asserts `callableLeft → runLeft`. Mutation check: dropping the left branch from the shared `??`/`||`/`or` rule and making the four provider hooks return only their last operand fails all four new tests, while the existing right-operand tests still pass. Capture goldens were regenerated with UPDATE_GOLDEN=1: - python app.py: 35 -> 85 groups. 35 -> 75 is stale drift from |
||
|
|
4576adfc46
|
fix(java): emit Record interface heritage (#2916)
* fix(java): emit Record interface heritage Synthesize inheritance references for Java record implements clauses so scope resolution emits canonical heritage and interface-dispatch edges. * test(java): cover Record heritage review gaps Document deferred enum and implicit-accessor behavior, make assertions order-independent, and add Record heritage to the capture benchmark. |
||
|
|
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> |
||
|
|
450cebc268
|
fix(java): JLS binary-name identities for local classes, enums, records & interfaces (#2562) (#2653)
* Initial plan * docs(plans): add Java local class naming plan * fix(java): model local class binary names * docs(java): clarify local class naming guards * fix(java): recognize local classes in compact constructors * chore: remove Java naming plan * fix(java): harden local type identities and scope * perf(java): linearize local type ordinal allocation * fix(java): harden ordinal benchmark follow-up * docs(java): clarify ordinal benchmark invariants * test(java): cover local type ownership paths --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Gergő Magyar <gergomagyar@icloud.com> |
||
|
|
de0248c5db
|
refactor(ingestion): migrate Dart to registry-primary call resolution (#939) (#1970)
* feat(scope-resolution): migrate Dart to registry-primary call resolution (#939) Add a Dart scope-resolution module (languages/dart/) mirroring the Swift template and flip Dart to registry-primary. Resolution edges (CALLS/IMPORTS/ACCESSES/EXTENDS/IMPLEMENTS/METHOD_IMPLEMENTS) now route through the shared registry pipeline with byte-for-byte parity against the legacy DAG: test/integration/resolvers/dart.test.ts passes 53/53 under both REGISTRY_PRIMARY_DART=0 and =1 (scripts/run-parity.ts --language dart: 2/2). Dart-specific handling: - Function scopes are synthesized to span signature..body (tree-sitter function_signature/function_body are siblings, not parent/child). - extends rides @reference.inherits (EXTENDS via the generic pre-pass); implements/with are carried as __heritage__ side-effect imports and emitted as IMPLEMENTS, since Dart `implements <class>` must be IMPLEMENTS regardless of the target's symbol kind. - imports are wildcard (whole-library) with expandsWildcardTo so imported return types propagate cross-file (var u = getUser(); u.save()). - getInnerSignature now self-returns a bare signature node so top-level function params/return/name extract (legacy-safe: legacy only ever passes method_signature/declaration wrappers). Also: add Dart scope-capture bench coverage (linear ~0.99 scaling); update two tests that used Dart as a non-migrated control (Vue / forced legacy). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(scope-resolution): close Dart registry-primary parity gaps from review Adversarial review of #1970 surfaced real divergences from the legacy DAG on constructs the 10 fixtures don't exercise. All fixed; parity gate still 2/2 (now 55/55 each mode): - Implicit-constructor construction (`Foo()` with no explicit ctor): the legacy DAG emits `caller -> Foo` (Class) but registry emitted nothing (callee tagged @reference.call.free never reaches constructorCallTargetsClass). Re-tag UpperCamelCase free-callees to @reference.call.constructor (Dart types are UpperCamelCase) so they link to the Class. Locked in with a regression fixture + test that passes in BOTH modes. - Cascade calls (`list..add(1)..sort()`) were dropped — cascade_section has no `selector` wrapper, so the reference walk never saw them while legacy emitted them as free calls. Add a cascade_section handler. - BUILT_INS (setState/then/push/pop/listen/...) were not suppressed on the registry path, so a user symbol shadowing one produced a spurious CALLS edge the legacy DAG suppresses. Skip built-in-named call refs at capture time (extract the set to a leaf module shared with the provider). - Enhanced-enum methods mis-parented to Module (no enum scope). Add `(enum_declaration) @scope.class` so enum members are owned by the enum. Re-baseline the Dart scope-capture fingerprint (linear ~0.95). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(scope-resolution): apply issue #1926 F24/F25 findings to the Dart scope path Issue #1926 catalogs Dart parsing-layer coverage gaps. Apply the two that the registry-primary scope-resolution path owns (call edges + call attribution), registered as legacy-expected-failures since they are scope-resolver-only wins. - F24: the scope path's unified tree-walk already captures member calls (obj.method()) in return / list-literal / named-argument / arrow-body contexts — the legacy DAG only captures them under expression_statement / initialized_variable_definition. Lock it with the dart-member-call-contexts fixture + tests. - F25 (constructor portion): a constructor's body is a sibling of the WRAPPING method_signature (class_body > method_signature > constructor_signature, then function_body), so findFunctionBody now walks up to the method_signature wrapper. Constructor bodies get a Function scope and their body-calls attribute to the Constructor (a valid caller anchor) instead of the class. Add the dart-constructor-body fixture + test. Switch dart.test.ts to createResolverParityIt('dart') and add the dart entry to LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES (5 wins). Both modes pass: run-parity --language dart → 2/2 (registry 60/60; legacy 55 pass + 5 skipped). Not applicable to the scope path (structure-phase / shared-pipeline, tracked by #1926's legacy fix): F25 getter/setter (Property is not a caller anchor) and operator (no Method node emitted by the structure phase) bodies; F26 (static field Property nodes); F27 (no generic_type reference in the scope module); F28/F29 (typedef/variable node extraction). Re-baseline the Dart scope-capture fingerprint (linear ~1.0). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(scope-resolution): fix Dart named-constructor file-drop + container-name mis-binding (tri-review) Multi-engine tri-review (GitNexus + CE personas + Codex gpt-5.5) of #1970 found a P0 the parity gate missed plus a P2 wrong-edge: - P0 (file drop): a named constructor with a body (`class A { A.named() {…} }`, idiomatic Dart) parses as ONE constructor_signature carrying multiple `name:` fields, so the scope query matched it more than once and synthesized two identical-range @scope.function captures → ScopeTreeInvariantError(duplicate- scope-id) → extractParsedFile swallowed it → the WHOLE file was dropped from registry-primary resolution (CALLS=0 vs legacy CALLS=2). Introduced by the #1926 F25 findFunctionBody change that started giving constructors body scopes. Fix: dedup function-like declarations by their statement node so each is emitted once. Add dart-named-constructor-body fixture + a parity guard test (both modes) that fails if the file is dropped, plus the named-ctor F25 attribution win (registry-only). - P2 (wrong edge): normalizeDartType's Future<X>/List<X> unwrap is unreachable (generic args are stripped upstream to a bare `Future`/`List`), so a return/ field type binding to the bare container name let a same-named user class (`class Stream {…}`) capture the receiver — a wrong CALLS edge legacy didn't emit. Suppress type bindings that normalize to a bare container name (leaving the call unresolved, matching legacy) instead of binding to the container. Both modes still pass: run-parity --language dart → 2/2 (registry 62/62; legacy 56 + 6 skipped). Re-baseline the Dart scope-capture fingerprint. Also: refresh the captures.ts module doc (constructors get scopes; cascade calls). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(scope-resolution): address Dart tri-review follow-ups (heritage collision + polish) - P2 heritage cross-file name collision: emitDartHeritageEdges resolved both child and base by a global last-write-wins simple-name map, so two files each declaring `class Logger` (one `implements Logger`) produced a wrong-file IMPLEMENTS edge. Resolve with same-file affinity (prefer a same-file class, then a workspace-unique match, else refuse to guess) — the #1951 file-affinity pattern. Add dart-heritage-name-collision fixture + a parity test (both modes resolve same-file). Also reason-qualify the dedup key so `implements X` + `with X` keep distinct edges. - Polish: buildDartMro uses Sets instead of Array.includes-in-loop; merge-bindings uses named tier constants matching swift; drop the dead no-op stripQuotes in import-target (targetRaw already arrives quote-stripped). Both modes pass: run-parity --language dart → 2/2 (registry 63/63; legacy 57 + 6 skipped). Re-baseline the Dart scope-capture fingerprint. 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) | ||
|
|
7d40156003
|
feat(swift): migrate Swift to scope-based registry resolution (#937) (#1948)
* feat(swift): migrate Swift to scope-based registry resolution (#937) Ring 3 of RFC #909 — Swift is the final language migrated to the scope-based registry resolution pipeline. Flips Swift into MIGRATED_LANGUAGES so registry-primary call resolution is the production default, with dual-mode parity proven: the resolver suite passes 77/77 under both the legacy DAG (REGISTRY_PRIMARY_SWIFT=0) and the registry-primary path (REGISTRY_PRIMARY_SWIFT=1). New language module src/core/ingestion/languages/swift/ (mirrors csharp/): query, captures, interpret, import-decomposer, receiver-binding, signature-bindings, arity (+metadata), merge-bindings, simple-hooks, import-target, target-siblings, implicit-imports, sibling-type-bindings, scope-resolver, cache-stats, index. Parse-time hooks wired into the existing flat languages/swift.ts (coexists with the swift/ dir, like kotlin) and the resolver registered in the scope-resolution registry. tree-sitter-swift 0.7.1 specifics handled in the Swift module (not in shared code): - class / struct / extension all parse to class_declaration; extensions are re-keyed onto the extended type so members hoist (like C# partial). - if-let / guard-let have no if_let_binding node — the optional binding is synthesized from if_statement / guard_statement. - the name: field is reused for func name, param labels, param types and return type, so param/return type-bindings are synthesized in code (signature-bindings.ts) rather than via a multi-name query. - no `new` keyword: Type(...) and Type.init(...) are synthesized into constructor type-bindings. Shared-pipeline additions are language-agnostic (AGENTS.md: no language names in shared ingestion code): - constructorCallTargetsClass on the ScopeResolver contract + free-call-fallback option + run.ts wiring: when true, Type(...) links to the Class def rather than its explicit init Constructor. - pickUniqueGlobalClass: constructor-branch global fallback for cross-file types absent from the call site's lexical bindings, deduped by qualifiedName so extension/partial fragments aren't seen as ambiguous. - emitImplicitImportEdges: same-module File->File IMPORTS edges (Swift whole-module visibility has no syntactic import to drive the generic ImportEdge pipeline). Import resolution: rewrote the O(n^2) module scan in import-resolvers/configs/swift.ts with a WeakMap-memoized index. Benchmarks & guards: - Swift added to bench/scope-capture/measure.mjs + baselines.json (fingerprint + 1.5x scaling budget); `--check` passes for all 7 languages, Swift scaling 0.98 (linear). - golden capture-parity test (test/unit/scope-resolution/swift/swift-captures-golden.test.ts + fixtures/swift-captures-golden/) mirrors the csharp golden. - O(n) scope-capture tripwire (test/integration/swift-scope-capture-tripwire.test.ts). Full Swift test glob: 3 files / 88 tests pass; tsc --noEmit clean; no cross-language resolver regressions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(autofix): apply prettier + eslint fixes via /autofix command * fix(swift): green CI — Dart canary, cascade-safe availability test, prettier, comment/order nits (U1) * test(swift): wire createResolverParityIt('swift') + empty legacy skip-set (U2) * fix(swift): group same-module files by SPM target subtree in registry-primary hooks (U3) * fix(swift): correct member-write, class-func self, multi-clause if-let, nested-extension (U4) * perf(scope-resolution): build global class index once for pickUniqueGlobalClass (U5) * fix(swift): re-baseline scope-capture fingerprint after member-write capture change (U4) * style(swift): prettier-format pick-unique-global-class test (U5 follow-up) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
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
|