mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-28 06:22:22 +00:00
fix(scope): bind same-namespace inheritance bases before sibling visibility exists
Cause: same-package / same-namespace visibility (`populateNamespaceSiblings`) is populated after the inheritance pre-pass and `buildMro`. A base written without an import (`class C extends B`, B in another file of the package) could bind only through the workspace-unique simple-name fallback, which any other package declaring a `B` defeats: no EXTENDS edge, an empty MRO, and lost calls through inherited members. In the combined lang-resolution corpus `java-grandparent-resolution` `c.greet().save()` dropped because `java-unrelated-method-collision` declares default-package A and B. Moving the sibling hook ahead of the pre-pass was measured and rejected. It retargeted correct edges and minted wrong ones, because the sibling channels carry candidates the pre-pass must not see: C# global-namespace types are consulted at every scope (same-file `Cat : Animal` bound another fixture's global Animal), namespace buckets merge across source roots with no proximity order (PHP, C#), and a qualified `extends app.base.Box` bound through a same-package `app.Box`. Fix: an optional `ScopeResolver.resolveImplicitInheritanceBase`, consulted by the pre-pass only when the shared chain bound nothing, the base was written bare (no qualifier, name capture at the start of the base expression), and no import claims the name. It adds edges and never retargets one. `resolveSameNamespaceInheritanceBase` picks the candidate in the deriving class's namespace, nearest by shared path prefix, refusing a tie. Java and Kotlin answer from their package facts, C# from the exact `namespacePrefix`. Corpus A/B (main vs this, same corpus): EXTENDS/IMPLEMENTS +42, all within their own fixture, 0 removed. CALLS: six `super.save()` edges keep their target but are now MRO-dispatched (`global`, as in isolation), one moves off another fixture's BaseModel.Save onto its own, and 7 are new (both grandparent `greet` calls, a default-method `validate`, the new fixtures). Receiver bench: call drops 128 -> 127, all kinds 192 -> 191, shape arm unchanged. Scope-capture fingerprints re-baselined for the added fixtures only (parked, --check passes on the old ones). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
04427e03c8
commit
082d707435
41 changed files with 609 additions and 34 deletions
|
|
@ -391,6 +391,7 @@ Single interface a language implements to plug into the pipeline. Contract fully
|
|||
| `stripTypePreservingDecoration?` | `(typeName) → strippedName \| undefined` — strip ONE layer of TYPE-PRESERVING decoration (pointer, reference, `const`, nullable, borrow, sigil) so a receiver declared `*Host` still finds the `Host` binding (#2766). Never a container: unwrapping `Repo[]` here would fold `repos.find(x)` to `Repo.find` — that is `elementTypeOf`'s job, and only after a real subscript. Consulted only after every undecorated lookup fails, and only by receiver-chain base/step resolution — default off |
|
||||
| `collapseMemberCallsByCallerTarget?` | One CALLS edge per (caller, target) instead of per-site — default off |
|
||||
| `populateNamespaceSiblings?` | Cross-file implicit visibility (compiler-implicit namespace sharing) — default off; ctx carries `treeCache` |
|
||||
| `resolveImplicitInheritanceBase?` | Fallback for a bare inheritance base the shared chain left unbound, bound by the implicit namespace rule (siblings exist only after the pre-pass); Java, Kotlin, C# opt in |
|
||||
| `hoistTypeBindingsToModule?` | Walk up to Module scope when looking up a method's return-type typeBinding — default off; enable only when bindings are stored at module level |
|
||||
| `hasFileLocalCallableLinkage?` | Precise internal-linkage predicate used only when joining callable declarations/prototypes to cross-file definitions; C/C++ use it for `static` free functions |
|
||||
| `constructorCallTargetsClass?` | A constructor-form call `Type(...)` links to the Class def rather than its explicit Constructor def — default off; Swift and Dart opt in |
|
||||
|
|
@ -504,7 +505,7 @@ Workers: `workers/worker-pool.ts`, `workers/parse-worker.ts`.
|
|||
|
||||
### Inheritance and MRO
|
||||
|
||||
Inheritance is captured by the `@reference.inherits` tag and emitted by the scope-resolution phase: `preEmitInheritanceEdges` resolves each base in scope, then `emitHeritageEdges` writes the `EXTENDS`/`IMPLEMENTS` edges. The phase then computes method resolution order via each `ScopeResolver`'s `buildMro` hook, feeding a `MethodDispatchIndex` used for owner-scoped lookups. Per-language strategy:
|
||||
Inheritance is captured by the `@reference.inherits` tag and emitted by the scope-resolution phase: `preEmitInheritanceEdges` resolves each base in scope, then `emitHeritageEdges` writes the `EXTENDS`/`IMPLEMENTS` edges. A bare base the shared chain cannot bind falls back to `ScopeResolver.resolveImplicitInheritanceBase`, because same-namespace sibling bindings (`populateNamespaceSiblings`) are populated only after this pre-pass; without it a Java `class C extends B` with no import loses its edge whenever another package also declares a `B`. The phase then computes method resolution order via each `ScopeResolver`'s `buildMro` hook, feeding a `MethodDispatchIndex` used for owner-scoped lookups. Per-language strategy:
|
||||
|
||||
- **`first-wins`** — Java, C#, C++, TS, Ruby, Go
|
||||
- **`c3`** — Python (C3 linearization)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,19 @@
|
|||
# Receiver-resolution baseline
|
||||
|
||||
## Same-namespace inheritance base fallback
|
||||
|
||||
`c.greet().save()` in `java-grandparent-resolution/src/services/App.java:8` no
|
||||
longer drops. Its `C extends B extends A` chain has no imports, so it binds only
|
||||
by the same-package rule, and that sibling visibility is populated after the
|
||||
inheritance pre-pass. The corpus's default-package `A`/`B`
|
||||
(`java-unrelated-method-collision`) made the unique-name fallback decline, so
|
||||
`C` had no EXTENDS edge and an empty MRO. The pre-pass now asks
|
||||
`ScopeResolver.resolveImplicitInheritanceBase` (Java, Kotlin, C#) for a bare,
|
||||
unclaimed base the shared chain left unbound. Call drops move from 128 to 127
|
||||
and all-kind drops from 192 to 191 (`.java` 49 to 48, `chain-call` 30 to 29,
|
||||
`in-program` 55 to 54). The three new collision fixtures add no drop; on main
|
||||
the C# one would add `leaf.Greet().Save()`. Every shape-arm state is unchanged.
|
||||
|
||||
## Express inline-handler fixture (#3488)
|
||||
|
||||
The new `express-inline-handlers` fixture reads `req.query.path` inside an
|
||||
|
|
|
|||
|
|
@ -199,15 +199,15 @@
|
|||
}
|
||||
},
|
||||
"countArm": {
|
||||
"callDrops": 128,
|
||||
"totalDropsAllKinds": 192,
|
||||
"callDrops": 127,
|
||||
"totalDropsAllKinds": 191,
|
||||
"bySiteKind": {
|
||||
"call": 128,
|
||||
"call": 127,
|
||||
"read": 44,
|
||||
"write": 20
|
||||
},
|
||||
"callDropsByExtension": {
|
||||
".java": 49,
|
||||
".java": 48,
|
||||
".py": 20,
|
||||
".ts": 16,
|
||||
".zig": 11,
|
||||
|
|
@ -222,14 +222,14 @@
|
|||
},
|
||||
"callDropsByShape": {
|
||||
"chain-field": 57,
|
||||
"chain-call": 30,
|
||||
"chain-call": 29,
|
||||
"no-chain": 27,
|
||||
"<<unclassified>>": 11,
|
||||
"chain-mixed": 2,
|
||||
"chain-unwrap": 1
|
||||
},
|
||||
"callDropsByOrigin": {
|
||||
"in-program": 55,
|
||||
"in-program": 54,
|
||||
"external": 44,
|
||||
"unknown": 29
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@
|
|||
},
|
||||
"csharp": {
|
||||
"_rebaselined": "#1956 synth-widening: + csharp-qualified-base fixture; the synth now walks record_declaration + struct_declaration base_lists and handles alias_qualified_name (matching the #1940 legacy leg), so record/struct heritage now emits. csharp-record-base gains a record inherits capture. (record->record SAME-namespace EXTENDS is a separate registry resolution gap, tracked as follow-up.) Linear (~1.00). (Earlier #1956: heritage-bearing scale source.) | #942: scope-resolution-only cleanup reworded fixture comments; capture byte-positions shift, capture LOGIC unchanged. | #1924 F16: record primary-constructor base bindings now exclude constructor arguments; capture fingerprint changes, scaling remains linear. | #2036 review follow-up: csharp-record-base now exercises primary-constructor base dispatch end to end; +2 capture groups, scaling remains linear.",
|
||||
"fingerprint": "af724813b5183ad9c98408a9c6944a44897b97d02e7f474ffa4ae637ca9ac1e3",
|
||||
"fingerprint": "4c7a9195735a0b36de708786361c7675ffe2e4764a47fb414026100df1f00e99",
|
||||
"scaling_budget": 1.5,
|
||||
"_rebaselined_callable_flow_2522_review": "PR #2522 review hardening: callable operands retain expression/qualified identity and formals retain signature metadata. Prior f31544530924748f9aa37d11cec570bc10c3ddf9d9b237e6df7a17623fd2bb3a -> 75cf380209fa7d1a8a3ec873be1a9424b4e5173be0b08234c2291e8521a9b3c1; scaling 1.061 < 1.5.",
|
||||
"_rebaselined_callable_flow_2522_followup": "PR #2522 follow-up: C# method-group/delegate callable flow facts with invocation-result suppression. Prior 2bb5bc8c19cb8eb08c9590545ad8a1968a7152951f7e12746e2d7901d542fed9 -> f31544530924748f9aa37d11cec570bc10c3ddf9d9b237e6df7a17623fd2bb3a; scaling 1.115 < 1.5.",
|
||||
|
|
@ -77,10 +77,11 @@
|
|||
"_rebaselined_2766_receiver_chain_wire_v2": "#2766: receiver-chain wire format v1 -> v2 (name-free `await` / `index` step kinds). The VERSION prefix is part of every emitted `@reference.receiver-chain` capture, so every chain-minting language's capture text changed. WIRE-FORMAT CHANGE, NOT A CAPTURE-SET CHANGE: the same chains are minted for the same sites, spelled `2|\u2026` instead of `1|\u2026`. Exactly the 12 chain-minting languages drifted; c, cobol and dart did not, which is the check that this is the prefix and not a capture regression. Accompanied by SCHEMA_BUMP 34 -> 37 and INCREMENTAL_SCHEMA_VERSION 28 -> 31 so a stale index is rejected rather than replaying chains a v2 decoder refuses. Prior 8a282254b93b3ef2ff34c2fdba819ebc95c53c4fcb09942cbad99f96d3687855 -> 476d98a7cc659951c315d63319c8077bbcf0e5f3ec12d32ed773992a1f3a2adc.",
|
||||
"capture_groups_small": 4259,
|
||||
"capture_groups_large": 13609,
|
||||
"capture_groups_fp": 2657,
|
||||
"fixture_count": 178,
|
||||
"capture_groups_fp": 2804,
|
||||
"fixture_count": 185,
|
||||
"_rebaselined_1432_member_call_callee_name": "#1432 (Zig): the shared callable-flow reader no longer names a callee by simple name for a MEMBER call (`@callable-flow.direct-callee-name` requires a direct designator: `f(x)`, `ns.f(x)`), and a member call is a field-stored-callable invoke only when a MEMBER store (`o.f = handler`) or a declared callable-typed field is visible - a same-named plain binding no longer gates it. CAPTURE-EMISSION CHANGE, not fixture growth (fixture_count unchanged). Only drift: `string.Join(\", \", args)` (csharp-variadic-resolution/Utils/Logger.cs) loses `direct-callee-name|Join`; the argument fact itself is unchanged, capture_groups_fp 2657 (unchanged). Prior 2930ef49fdce984a4c051409880bddfe8445e30e1c6bf802bd90a0a0f8f6b094 -> 9c4d2ca55707a03185ea45e554462e81e6d83c9f8d0ea3cd63db5c362ad1bfec.",
|
||||
"_rebaselined_3486_semantic_rename": "#3486: Compared actual origin/main (191a954b), committed dd3bf and frozen provider source over the same unchanged corpus. 89 constructor-parameter type bindings are added, 46 field declarations become addressable properties, and 3 existing member reads gain receiver-chain tags. Reversing exactly these three changes restores the complete main capture set. Fixture entries 178; changed entries 47; capture groups 2657 -> 2746. Fingerprint regenerated from the runner's canonicalized capture records. Scaling budgets unchanged."
|
||||
"_rebaselined_3486_semantic_rename": "#3486: Compared actual origin/main (191a954b), committed dd3bf and frozen provider source over the same unchanged corpus. 89 constructor-parameter type bindings are added, 46 field declarations become addressable properties, and 3 existing member reads gain receiver-chain tags. Reversing exactly these three changes restores the complete main capture set. Fixture entries 178; changed entries 47; capture groups 2657 -> 2746. Fingerprint regenerated from the runner's canonicalized capture records. Scaling budgets unchanged.",
|
||||
"_rebaselined_same_namespace_heritage_fixture": "Same-namespace heritage fixture: adds `csharp-same-namespace-base-collision` (7 files). FIXTURE ADDITION ONLY, no capture code changed: with that directory parked, --check passes against the prior fingerprint af724813b5183ad9c98408a9c6944a44897b97d02e7f474ffa4ae637ca9ac1e3 (measured fixture_count 178, capture_groups_fp 2746); with it, fixture_count 185, capture_groups_fp 2804. Synthetic counts and scaling unchanged."
|
||||
},
|
||||
"rust": {
|
||||
"_lexical_ownership_3532": "#3532: lexical ownership captures and fixture updates; fixture identities are relative to lang-resolution so serialized scope IDs are independent of checkout location. Full capture content remains hashed; scaling_budget is unchanged.",
|
||||
|
|
@ -176,7 +177,7 @@
|
|||
"_rebaselined_3633_native_extension_types": "#3633: Compared the prior grammar and emitter at 89602560d with native extension-type capture over the same 42 Dart fixture files. Only dart-extension-types/models.dart changes: 11 module/type scope and declaration matches retain the original header text, and Box's declaration gains type-parameter metadata. All method, receiver and heritage captures are unchanged. Total capture groups remain 895 across 43 fixture/synthetic entries; synthetic scale counts stay 5011/16011. Measured scaling 1.127 < 1.5; budget unchanged. Prior 557bdc505af40590b21e53298584f14227b7d79220806425f8e7da25d78f9712 -> 4690403d2be0d270e28a55791df82eaa627d26d28c52d81bfafa59761badcaf3."
|
||||
},
|
||||
"java": {
|
||||
"fingerprint": "41c4338f5813a73199dd5c2609313c11076812df21fd9c429039a833325870e0",
|
||||
"fingerprint": "82a4b09708caed2350ab36d2b7a1ff00639a34bca73dd1536e37f231e800c2d2",
|
||||
"scaling_budget": 1.5,
|
||||
"_rebaselined_3190_ctor_qualified_name": "#3190: qualified constructors (`new pkg.Foo()`, `new pkg.Box<T>()`) now copy @reference.call.constructor.qualified onto @reference.qualified-name so constructor fallback treats the written qualifier as precise. DIGEST DRIFT ONLY: the tag is added to existing F35 matches. Prior 2bf47cc19b595a9889ac21ec0154c6ce6786271d68551f21d1bc14c626bcd4ff -> e22a753b9f59331898a3a27c43072b588c33639937b1f618f6c70ce04f072a9f; capture_groups_fp 3586, fixture_count 209.",
|
||||
"_rebaselined_2935_synthetic_declarations": "PR #2935 review follow-up: synthesized Java anonymous classes and bodied enum constants now carry the presence-only @declaration.is-synthetic sidecar used to preserve source-written dispatch targets at the fanout cap. DIGEST DRIFT ONLY, NOT A CAPTURE-SET CHANGE: the tag is attached to existing synthetic declaration matches; capture groups and fixture count remain 5755/18405, 3512, and 206. Prior 36d689c58526c4482fbd701d1d9ca156623a3970734ead145717858712271ab5 -> 2e2150b4f4d64519e3f4c6d7a2c12259178d3117872203c904fab8cba96a694a; CI scaling 0.971 < 1.5.",
|
||||
|
|
@ -196,10 +197,11 @@
|
|||
"_rebaselined_2766_receiver_chain_wire_v2": "#2766: receiver-chain wire format v1 -> v2 (name-free `await` / `index` step kinds). The VERSION prefix is part of every emitted `@reference.receiver-chain` capture, so every chain-minting language's capture text changed. WIRE-FORMAT CHANGE, NOT A CAPTURE-SET CHANGE: the same chains are minted for the same sites, spelled `2|\u2026` instead of `1|\u2026`. Exactly the 12 chain-minting languages drifted; c, cobol and dart did not, which is the check that this is the prefix and not a capture regression. Accompanied by SCHEMA_BUMP 34 -> 37 and INCREMENTAL_SCHEMA_VERSION 28 -> 31 so a stale index is rejected rather than replaying chains a v2 decoder refuses. Prior 310adbc2e0827b5ac749acaa981cd12d256fc5b7cbc5592c5bee219e92abf9ee -> a9943355e945e03ddb87c800f4cc1f62b3d04feefb3ec64c258d8e0bb3b3fcd9.",
|
||||
"capture_groups_small": 6255,
|
||||
"capture_groups_large": 20005,
|
||||
"capture_groups_fp": 3586,
|
||||
"fixture_count": 209,
|
||||
"capture_groups_fp": 3483,
|
||||
"fixture_count": 218,
|
||||
"_rebaselined_2910_declared_package_fixtures": "#2910 adds three Java resolver fixture files covering an external JDK lookalike, a path/package mismatch, and wildcard package membership. Fixture-corpus growth only: Java query rules and synthetic scaling sources are unchanged; capture_groups_small/large remain 6255/20005. capture_groups_fp 3560 -> 3586 and fixture_count 206 -> 209.",
|
||||
"_rebaselined_3486_semantic_rename": "#3486: Compared actual origin/main (191a954b), committed dd3bf and frozen provider source over the same unchanged corpus. 39 constructor-parameter type bindings are added, 6 member reads gain receiver chains, and 196 duplicate qualified free-call matches are removed. Every removed free-call match retains a member-call match with the identical call anchor and name capture; no qualified call disappears. Fixture entries 209; changed entries 89; capture groups 3586 -> 3429. Fingerprint regenerated from the runner's canonicalized capture records. Scaling budgets unchanged."
|
||||
"_rebaselined_3486_semantic_rename": "#3486: Compared actual origin/main (191a954b), committed dd3bf and frozen provider source over the same unchanged corpus. 39 constructor-parameter type bindings are added, 6 member reads gain receiver chains, and 196 duplicate qualified free-call matches are removed. Every removed free-call match retains a member-call match with the identical call anchor and name capture; no qualified call disappears. Fixture entries 209; changed entries 89; capture groups 3586 -> 3429. Fingerprint regenerated from the runner's canonicalized capture records. Scaling budgets unchanged.",
|
||||
"_rebaselined_same_namespace_heritage_fixture": "Same-namespace heritage fixture: adds `java-same-package-extends-collision` (9 files). FIXTURE ADDITION ONLY, no capture code changed: with that directory parked, --check passes against the prior fingerprint 41c4338f5813a73199dd5c2609313c11076812df21fd9c429039a833325870e0 (measured fixture_count 209, capture_groups_fp 3429); with it, fixture_count 218, capture_groups_fp 3483. Synthetic counts and scaling unchanged."
|
||||
},
|
||||
"java-local-types": {
|
||||
"fingerprint": "6ae163cd36c889454c8672b28944c765d3a3a5e8c49ea4db1cd6cc1787ede583",
|
||||
|
|
@ -262,7 +264,7 @@
|
|||
"_rebaselined_blind_spots_2856": "#2856 blind-spots series: the JS/TS SCOPE queries gained capture rules, so fingerprint drift is expected and additive. Verified before re-baselining by diffing the capture-name sets in both scope queries against origin/main: TypeScript gained exactly @reference.read.identifier (A2 bare-identifier reads in value positions) and @reference.type (R2-2 type references, so a declared contract stops reporting incoming:{}); JavaScript gained exactly @reference.read.identifier, @reference.read.destructured (R2-1c) and @reference.write.property-key (R2-1b record-construction writes). NOTHING was removed on either side \u2014 the delta is a pure superset, which is the check that no existing capture moved. capture_groups_small/large are unchanged (4503/14403) because those measure the SYNTHETIC scaling source, which this branch does not touch; only the fixture-corpus count moves. capture_groups_fp 2097 -> 2338 and fixture_count 146 -> 151 from 21 new lang-resolution fixtures. Scaling stayed linear and inside budget: typescript 1.116 < 1.5, javascript 1.010 < 1.5. Prior typescript ed92588e0fc7b28b3a0174339ac378b4dd85965fe007db1208dea97a65ce0571 -> f66a3e6f1e096431e7046505129a627deaa00ca0de5bc846b080591b397248f7; prior javascript 806f70ad3cce5fc849f6d06a08ace8a95f92a1ea84a2418fddabb1eef5846594 -> 2026993b81b873839dd2ef8797d9c14d9c48516b2b57b05ac17d8d43f2f4eba3."
|
||||
},
|
||||
"kotlin": {
|
||||
"fingerprint": "026392f9a60d9808dd993fa6d26f3a2ca6240ed6ac63981d614058ae7acd29e2",
|
||||
"fingerprint": "9b410bd5ad629cb8d6c5fef542ffae67cefa1b66782e24a68a26c49f3032f5ab",
|
||||
"scaling_budget": 1.5,
|
||||
"_rebaselined_3486_bodyless_jvm_accessors": "#3486 validation repair: synthetic JVM accessors have no lexical body; nested getter/setter scopes hid their class bindings. A/B over the same 142 benchmark entries removes exactly 98 standalone @scope.function groups across 27 entries (40 in the 20-entity DAO, 58 in fixtures), with zero additions and every declaration capture, text and range byte-identical. Actual fingerprint groups 2634 -> 2536; single-emission scaling-size counts 5753/18403 -> 5253/16803. Java, java-local-types and Swift fingerprints are unchanged. Existing count metadata is synchronized with the current corpus. Scaling budgets unchanged.",
|
||||
"_rebaselined_interface_abstract_2885": "#2885: Kotlin interface property accessors stay in the capture set (groups still 5753/18403 and capture_groups_fp 2563) but Method isAbstract is now true for body-less interface properties, which changes accessor-plan identity in the fixture digest. Prior 82ae5e1f750580383344d4c84c400a290474528cd502be4af8cd56705819a683 -> aeafc7a87402c933786ef582b7c98683b1822b78fa909e605cb97552867fa0d5; CI scaling 0.838 < 1.5.",
|
||||
|
|
@ -281,13 +283,14 @@
|
|||
"_rebaselined_2960_declared_package_fixture": "#2960 adds four Kotlin declared-package import-resolution fixture files. This is fixture-corpus growth only: fixture_count 137 -> 141 and capture_groups_fp 2334 -> 2367; the synthetic capture counts remain 4753/15203, no Kotlin scope-capture query or implementation changed, and package resolution runs after capture. Other language fingerprints matched their baselines in the same CI run. Prior a184f8ff0ae40d246db855b63f7ff26bda3afac03e5f4c76e4593c7e2cefce54 -> f98e7e936afbce0e99588285cfc603bf945fd58c5de45271860509a5d90eb832.",
|
||||
"capture_groups_small": 5253,
|
||||
"capture_groups_large": 16803,
|
||||
"capture_groups_fp": 2536,
|
||||
"fixture_count": 142,
|
||||
"capture_groups_fp": 2581,
|
||||
"fixture_count": 149,
|
||||
"_rebaselined_1432_member_call_callee_name": "#1432 (Zig): the shared callable-flow reader no longer names a callee by simple name for a MEMBER call (`@callable-flow.direct-callee-name` requires a direct designator: `f(x)`, `ns.f(x)`), and a member call is a field-stored-callable invoke only when a MEMBER store (`o.f = handler`) or a declared callable-typed field is visible - a same-named plain binding no longer gates it. CAPTURE-EMISSION CHANGE, not fixture growth (fixture_count unchanged). Only drift: `users.map { it.name }.forEach { name -> println(name) }` (kotlin-lambda-scopes/App.kt) loses `direct-callee-name|forEach` (member call). capture_groups_fp 2334 (unchanged). Prior a184f8ff0ae40d246db855b63f7ff26bda3afac03e5f4c76e4593c7e2cefce54 -> 5a181af0dbc9451937da0964c40d3f3f9820914ca429d873bb5c812b5e2b9284.",
|
||||
"_rebaselined_1432_rebase_onto_2960": "#1432 rebase onto main @ aac7515d: the kotlin fingerprint is a COMBINATION of two independent changes, so neither side of the merge conflict was correct on its own and resolving it by picking a side would have committed a fingerprint no run can reproduce. main's #2960 added four declared-package fixture files (fixture_count 137 -> 141, capture_groups_fp 2334 -> 2367); this branch's `_rebaselined_1432_member_call_callee_name` drops `direct-callee-name|forEach` from one member call. Recomputed under both: capture_groups_fp 2367 and fixture_count 141 match main's committed counts EXACTLY (this branch's change is emission-only and moves no count), capture_groups_small/large stay 4753/15203 (the SYNTHETIC scaling source, which neither change touches), scaling 1.003 < 1.5, and the other 14 languages report ok against their committed baselines in the same run - which is the check that the rebase replayed nothing else into the capture stream. Attribution is exact rather than inferred: moving test/fixtures/lang-resolution/kotlin-import-package-evidence aside and re-running returns kotlin to 5a181af0dbc9451937da0964c40d3f3f9820914ca429d873bb5c812b5e2b9284 byte-for-byte with fixture_count back at 137 and capture_groups_fp back at 2334 - this branch's pre-rebase value - so the whole delta is #2960's corpus growth layered on top, with nothing else moving. Prior (this branch, pre-rebase) 5a181af0dbc9451937da0964c40d3f3f9820914ca429d873bb5c812b5e2b9284 and (main) f98e7e936afbce0e99588285cfc603bf945fd58c5de45271860509a5d90eb832 -> 973d702510002dda76166e017c5eca90cae37a139f5a512877b7a1b04ad19dc5.",
|
||||
"_rebaselined_1432_merge_main_2885": "#1432 merge of main @ 212e007a: the kotlin fingerprint is again a COMBINATION of two independent changes \u2014 main's #2885 JVM property accessors / interface-abstract (4753/15203 -> 5753/18403, capture_groups_fp 2367 -> 2563) and this branch's `_rebaselined_1432_member_call_callee_name` (drops `direct-callee-name|forEach` from one member call). Neither side's value reproduces under the merged tree. Recomputed under both: counts 5753/18403/2563 and fixture_count 141 match main's committed counts EXACTLY (this branch's change is emission-only), scaling 1.061 < 1.5, and csharp / cpp / typescript measure byte-for-byte at this branch's committed values (main did not touch them since the merge-base) while the other 11 languages report ok \u2014 the check that the merge replayed nothing else into the capture stream. Prior (main) aeafc7a87402c933786ef582b7c98683b1822b78fa909e605cb97552867fa0d5 and (this branch) 973d702510002dda76166e017c5eca90cae37a139f5a512877b7a1b04ad19dc5 -> a9d3f0db7547ff47856159debf15d2a6f427efca97a6af27b4004174ed432132.",
|
||||
"_rebaselined_3354_callable_alternatives": "#3354 callable alternatives: a callable chosen by a value-selecting source now flows every branch it can yield (`a ?? b`, `a || b`, `a or b`, `c ? a : b`, statement `if`, elvis), and an operator branch (`x == f || g`) stays opaque instead of seeding a qualified name. Verified by running the BASE (merge-base 233ca2849) and HEAD emitters over the SAME HEAD fixture corpus: every added or removed match is an `@callable-flow.*` match on one of those sources, and the pre-existing corpus is byte-identical (all other languages: zero delta). The rest of the drift is corpus growth from this PR's regression fixture, which this bench globs. Corpus growth: kotlin-callable-alternatives/App.kt (+1 file, +71 groups). Emitter delta: +8 / -2: elvis and `if` expression (bare and braced) seeds and copies now name each operand; the 2 removed matches are the whole-expression seed and copy carrying a qualified name of the compound source; the multi-statement `if` stays opaque. capture_groups_fp 2563 -> 2634, fixture_count 141 -> 142; synthetic counts unchanged; scaling 1.17 < 1.5. Prior a9d3f0db7547ff47856159debf15d2a6f427efca97a6af27b4004174ed432132 -> f3fefe058484823761b52df5715f41d65fa4ff0223843bcc86893d7b0a6b3de2.",
|
||||
"_rebaselined_3486_semantic_rename": "#3486: Compared actual origin/main (191a954b), committed dd3bf and frozen provider source over the same unchanged corpus. 1 existing member-read match gains a receiver-chain tag; removing only that added tag restores the complete main capture set. No group is added or removed. Fixture entries 142; changed entries 1; capture groups 2634 -> 2634. Fingerprint regenerated from the runner's canonicalized capture records. Scaling budgets unchanged."
|
||||
"_rebaselined_3486_semantic_rename": "#3486: Compared actual origin/main (191a954b), committed dd3bf and frozen provider source over the same unchanged corpus. 1 existing member-read match gains a receiver-chain tag; removing only that added tag restores the complete main capture set. No group is added or removed. Fixture entries 142; changed entries 1; capture groups 2634 -> 2634. Fingerprint regenerated from the runner's canonicalized capture records. Scaling budgets unchanged.",
|
||||
"_rebaselined_same_namespace_heritage_fixture": "Same-namespace heritage fixture: adds `kotlin-same-package-extends-collision` (7 files). FIXTURE ADDITION ONLY, no capture code changed: with that directory parked, --check passes against the prior fingerprint 026392f9a60d9808dd993fa6d26f3a2ca6240ed6ac63981d614058ae7acd29e2 (measured fixture_count 142, capture_groups_fp 2536); with it, fixture_count 149, capture_groups_fp 2581. Synthetic counts and scaling unchanged."
|
||||
},
|
||||
"typescript-deep-chain": {
|
||||
"_lexical_ownership_3532": "#3532: lexical ownership captures and fixture updates; fixture identities are relative to lang-resolution so serialized scope IDs are independent of checkout location. Full capture content remains hashed; scaling_budget is unchanged.",
|
||||
|
|
|
|||
|
|
@ -9,7 +9,10 @@
|
|||
import type { ParsedFile } from 'gitnexus-shared';
|
||||
import { CLASS_KINDS, SupportedLanguages } from 'gitnexus-shared';
|
||||
import { buildMro, defaultLinearize } from '../../scope-resolution/passes/mro.js';
|
||||
import { populateClassOwnedMembers } from '../../scope-resolution/scope/walkers.js';
|
||||
import {
|
||||
populateClassOwnedMembers,
|
||||
resolveSameNamespaceInheritanceBase,
|
||||
} from '../../scope-resolution/scope/walkers.js';
|
||||
import { populateCsharpNamespacePrefixes } from './qualified-type-names.js';
|
||||
import type { ScopeResolver } from '../../scope-resolution/contract/scope-resolver.js';
|
||||
import {
|
||||
|
|
@ -119,6 +122,18 @@ const csharpScopeResolver: ScopeResolver = {
|
|||
// `namespace-siblings.ts` for the implementation.
|
||||
populateNamespaceSiblings: populateCsharpNamespaceSiblings,
|
||||
|
||||
// The inheritance pre-pass runs before those siblings exist, so a base
|
||||
// declared in another file of the SAME named namespace binds here. Exact
|
||||
// namespace only (the `namespacePrefix` sidecar from `populateOwners`):
|
||||
// enclosing-namespace and global visibility stay with the shared chain.
|
||||
resolveImplicitInheritanceBase: (baseName, derivingClass, scopes) =>
|
||||
resolveSameNamespaceInheritanceBase(
|
||||
baseName,
|
||||
derivingClass,
|
||||
scopes,
|
||||
(def) => def.namespacePrefix,
|
||||
),
|
||||
|
||||
// C# is statically typed — type information is reliable. Field-
|
||||
// fallback heuristic stays off (the type-binding layer already
|
||||
// produces precise owner types); return-type propagation on is fine
|
||||
|
|
|
|||
|
|
@ -10,3 +10,6 @@ export const populateJavaPackageSiblings = javaPackageSiblingVisibility.populate
|
|||
|
||||
export const isJavaPackageSiblingVisibilityIncomplete =
|
||||
javaPackageSiblingVisibility.isVisibilityIncomplete;
|
||||
|
||||
export const resolveJavaSamePackageBase =
|
||||
javaPackageSiblingVisibility.resolveImplicitInheritanceBase;
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import {
|
|||
resolveJavaImportTarget,
|
||||
type JavaResolveContext,
|
||||
} from './index.js';
|
||||
import { populateJavaPackageSiblings } from './package-siblings.js';
|
||||
import { populateJavaPackageSiblings, resolveJavaSamePackageBase } from './package-siblings.js';
|
||||
import { attachSpringBeanCandidateMetadata } from './spring-bean-metadata.js';
|
||||
import { attachJavaSpringAopMetadata } from './spring-aop.js';
|
||||
import { attachJavaSpringConfigBindings } from './spring-config-bindings.js';
|
||||
|
|
@ -90,6 +90,7 @@ const javaScopeResolver: ScopeResolver = {
|
|||
freeCallsRequireInstanceOwnership: true,
|
||||
|
||||
populateNamespaceSiblings: populateJavaPackageSiblings,
|
||||
resolveImplicitInheritanceBase: resolveJavaSamePackageBase,
|
||||
populateRangeBindings: populateJavaCrossFileReturnTypes,
|
||||
emitPostResolutionEdges: (graph, parsedFiles, nodeLookup, indexes, ctx) => {
|
||||
attachSpringBeanCandidateMetadata(graph, parsedFiles, nodeLookup, indexes);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import type { BindingRef, ParsedFile, ScopeId, TypeRef } from 'gitnexus-shared';
|
||||
import type { BindingRef, ParsedFile, ScopeId, SymbolDefinition, TypeRef } from 'gitnexus-shared';
|
||||
import { logger } from '../../../logger.js';
|
||||
import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js';
|
||||
import { isClassLike } from '../../scope-resolution/scope/walkers.js';
|
||||
import {
|
||||
isClassLike,
|
||||
resolveSameNamespaceInheritanceBase,
|
||||
} from '../../scope-resolution/scope/walkers.js';
|
||||
import type { JvmPackageFact } from './package-facts.js';
|
||||
|
||||
/** Packages larger than this get no implicit sibling visibility at all — every
|
||||
|
|
@ -32,6 +35,14 @@ export interface JvmPackageSiblingVisibility {
|
|||
},
|
||||
) => void;
|
||||
readonly isVisibilityIncomplete: (filePath: string) => boolean;
|
||||
/** `ScopeResolver.resolveImplicitInheritanceBase`: an unqualified base bound
|
||||
* by the same-package rule, which the sibling bindings above only provide
|
||||
* after the inheritance pre-pass has run. */
|
||||
readonly resolveImplicitInheritanceBase: (
|
||||
baseName: string,
|
||||
derivingClass: SymbolDefinition,
|
||||
scopes: ScopeResolutionIndexes,
|
||||
) => SymbolDefinition | undefined;
|
||||
}
|
||||
|
||||
interface PackageBucket {
|
||||
|
|
@ -225,9 +236,19 @@ export function createJvmPackageSiblingVisibility(
|
|||
}
|
||||
}
|
||||
|
||||
// A file whose package is unknown matches nothing, so it can neither claim a
|
||||
// base nor be offered one. Top-level types only: `qualifiedNames` keys a
|
||||
// nested type by its dotted `Outer.Inner` path, never by the bare name.
|
||||
const packageOf = (def: SymbolDefinition): string | undefined => {
|
||||
const fact = options.getPackageFact(def.filePath);
|
||||
return fact?.status === 'known' ? fact.packageName : undefined;
|
||||
};
|
||||
|
||||
return {
|
||||
populateNamespaceSiblings,
|
||||
isVisibilityIncomplete: (filePath) => incompleteFiles.has(filePath),
|
||||
resolveImplicitInheritanceBase: (baseName, derivingClass, scopes) =>
|
||||
resolveSameNamespaceInheritanceBase(baseName, derivingClass, scopes, packageOf),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,3 +11,6 @@ export const populateKotlinPackageSiblings =
|
|||
|
||||
export const isKotlinPackageSiblingVisibilityIncomplete =
|
||||
kotlinPackageSiblingVisibility.isVisibilityIncomplete;
|
||||
|
||||
export const resolveKotlinSamePackageBase =
|
||||
kotlinPackageSiblingVisibility.resolveImplicitInheritanceBase;
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import {
|
|||
clearKotlinClassAnnotationFacts,
|
||||
} from './capture-side-channel.js';
|
||||
import { isKotlinStaticOnly } from './owners.js';
|
||||
import { populateKotlinPackageSiblings } from './package-siblings.js';
|
||||
import { populateKotlinPackageSiblings, resolveKotlinSamePackageBase } from './package-siblings.js';
|
||||
import { attachKotlinSpringBeanCandidateMetadata } from './spring-bean-metadata.js';
|
||||
import { attachKotlinSpringAopMetadata } from './spring-aop.js';
|
||||
import { clearKotlinPackageFacts } from './package-facts.js';
|
||||
|
|
@ -152,6 +152,7 @@ export const kotlinScopeResolver: ScopeResolver = {
|
|||
freeCallsRequireInstanceOwnership: true,
|
||||
postExtractSourceTextPolicy: 'uncached-files',
|
||||
populateNamespaceSiblings: populateKotlinPackageSiblings,
|
||||
resolveImplicitInheritanceBase: resolveKotlinSamePackageBase,
|
||||
emitPostResolutionEdges: (graph, parsedFiles, nodeLookup, indexes) => {
|
||||
attachKotlinSpringBeanCandidateMetadata(graph, parsedFiles, nodeLookup, indexes);
|
||||
attachKotlinSpringAopMetadata(graph, parsedFiles, nodeLookup, indexes);
|
||||
|
|
|
|||
|
|
@ -1633,6 +1633,28 @@ export interface ScopeResolver {
|
|||
},
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* Optional fallback for an UNQUALIFIED inheritance base that the shared
|
||||
* chain (`resolveInheritanceBaseInScope`) could not bind. Consulted only by
|
||||
* the inheritance pre-pass, which runs before `populateNamespaceSiblings`:
|
||||
* a base visible solely through that implicit rule (`class C extends B`,
|
||||
* `B` declared in another file of the same namespace) otherwise reaches only
|
||||
* the workspace-unique simple-name fallback, which any other namespace
|
||||
* declaring a `B` defeats — no inheritance edge, so an empty MRO.
|
||||
*
|
||||
* The pre-pass skips the hook when the chain bound a base, when the base is
|
||||
* written qualified, or when the site holds an explicit name claim (an
|
||||
* import that shadows the implicit rule), so it can add an edge but never
|
||||
* retarget one. Answer with `resolveSameNamespaceInheritanceBase`
|
||||
* (`scope/walkers.ts`) given the provider's namespace fact, or `undefined`
|
||||
* to decline.
|
||||
*/
|
||||
readonly resolveImplicitInheritanceBase?: (
|
||||
baseName: string,
|
||||
derivingClass: SymbolDefinition,
|
||||
scopes: ScopeResolutionIndexes,
|
||||
) => SymbolDefinition | undefined;
|
||||
|
||||
/**
|
||||
* Whether the compound-receiver resolver should walk up from a
|
||||
* class scope to ancestor (Module) scopes when looking up a
|
||||
|
|
|
|||
|
|
@ -23,7 +23,13 @@
|
|||
* Plan: `docs/plans/2026-04-20-001-refactor-emit-pipeline-generalization-plan.md`.
|
||||
*/
|
||||
|
||||
import type { ParsedFile, ReferenceSite, RegistryProviders, Scope } from 'gitnexus-shared';
|
||||
import type {
|
||||
ParsedFile,
|
||||
ReferenceSite,
|
||||
RegistryProviders,
|
||||
Scope,
|
||||
SymbolDefinition,
|
||||
} from 'gitnexus-shared';
|
||||
import type { TypeRef } from 'gitnexus-shared';
|
||||
import type { KnowledgeGraph } from '../../../graph/types.js';
|
||||
import { generateId } from '../../../../lib/utils.js';
|
||||
|
|
@ -103,7 +109,13 @@ import {
|
|||
emitCallableValueFlow,
|
||||
} from '../passes/callable-value-flow.js';
|
||||
import type { ScopeResolver, UndecidedSatisfaction } from '../contract/scope-resolver.js';
|
||||
import { findEnclosingClassDef, resolveInheritanceBaseInScope } from '../scope/walkers.js';
|
||||
import {
|
||||
findEnclosingClassDef,
|
||||
hasExplicitNameClaim,
|
||||
lookupNameClaim,
|
||||
resolveInheritanceBaseInScope,
|
||||
} from '../scope/walkers.js';
|
||||
import { normalizeQualifiedName, stripTrailingTypeArguments } from '../../utils/qualified-name.js';
|
||||
import {
|
||||
heritageTypeArgumentsKey,
|
||||
type HeritageTypeArgumentSink,
|
||||
|
|
@ -207,6 +219,52 @@ function emitInheritanceEdgeDirect(
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether an `inherits` site's base was written as a bare name, with no
|
||||
* qualifier (`a.b.Base`, `Ns::Base`). Captures that keep only the simple tail
|
||||
* carry no `rawQualifiedName`, so the name capture must also start where the
|
||||
* base expression does: `Base<T>` and `Base(args)` lead with the name,
|
||||
* `pkg.Base` does not. A site whose anchor is not the base expression at all
|
||||
* fails the same test, which only costs it the implicit-namespace fallback.
|
||||
*/
|
||||
function isBareInheritanceBase(site: ReferenceSite): boolean {
|
||||
if (
|
||||
site.rawQualifiedName !== undefined &&
|
||||
stripTrailingTypeArguments(normalizeQualifiedName(site.rawQualifiedName)).includes('.')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const name = site.nameRange;
|
||||
return (
|
||||
name !== undefined &&
|
||||
name.startLine === site.atRange.startLine &&
|
||||
name.startCol === site.atRange.startCol
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The provider's implicit same-namespace base for an `inherits` site the shared
|
||||
* chain left unbound. That visibility (`populateNamespaceSiblings`) is only
|
||||
* populated after the inheritance pre-pass, so without this a base it alone
|
||||
* makes visible reaches nothing but the workspace-unique name fallback. Bare,
|
||||
* unclaimed bases only: for a written qualifier or a shadowing import the
|
||||
* chain has already decided, and its miss stands.
|
||||
*/
|
||||
function resolveImplicitInheritanceBaseAtSite(
|
||||
site: ReferenceSite,
|
||||
derivingClass: SymbolDefinition,
|
||||
scopes: ScopeResolutionIndexes,
|
||||
resolveImplicitBase: ScopeResolver['resolveImplicitInheritanceBase'],
|
||||
): SymbolDefinition | undefined {
|
||||
if (resolveImplicitBase === undefined || !isBareInheritanceBase(site)) return undefined;
|
||||
const claim = lookupNameClaim(site.lookupScope ?? site.inScope, site.name, scopes, {
|
||||
position: site.atRange,
|
||||
purpose: site.lookupPurpose ?? 'type',
|
||||
});
|
||||
if (hasExplicitNameClaim(claim, site.name)) return undefined;
|
||||
return resolveImplicitBase(site.name, derivingClass, scopes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve inheritance reference sites early and pre-emit their EXTENDS edges
|
||||
* before MRO construction. This lets template-base captures contribute to the
|
||||
|
|
@ -231,6 +289,7 @@ function preEmitInheritanceEdges(
|
|||
nodeLookup: ReturnType<typeof buildGraphNodeLookup>,
|
||||
recordTypeArguments: HeritageTypeArgumentSink,
|
||||
unresolvedInheritanceByClass: Set<string>,
|
||||
resolveImplicitBase: ScopeResolver['resolveImplicitInheritanceBase'],
|
||||
recordSourceBinding?: SourceBindingRecorder,
|
||||
): Set<string> {
|
||||
const handledSites = new Set<string>();
|
||||
|
|
@ -265,14 +324,15 @@ function preEmitInheritanceEdges(
|
|||
const callerClass = findEnclosingClassDef(site.inScope, scopes);
|
||||
if (callerClass === undefined) continue;
|
||||
|
||||
const targetDef = resolveInheritanceBaseInScope(
|
||||
site.lookupScope ?? site.inScope,
|
||||
site.name,
|
||||
scopes,
|
||||
site.rawQualifiedName,
|
||||
callerClass,
|
||||
{ position: site.atRange, purpose: site.lookupPurpose ?? 'type' },
|
||||
);
|
||||
const targetDef =
|
||||
resolveInheritanceBaseInScope(
|
||||
site.lookupScope ?? site.inScope,
|
||||
site.name,
|
||||
scopes,
|
||||
site.rawQualifiedName,
|
||||
callerClass,
|
||||
{ position: site.atRange, purpose: site.lookupPurpose ?? 'type' },
|
||||
) ?? resolveImplicitInheritanceBaseAtSite(site, callerClass, scopes, resolveImplicitBase);
|
||||
if (targetDef === undefined || targetDef.nodeId === callerClass.nodeId) {
|
||||
// Static lookup can mistake the current declaration for an earlier
|
||||
// same-named base. A self-edge is not inheritance evidence and would
|
||||
|
|
@ -941,6 +1001,7 @@ export function runScopeResolution(
|
|||
nodeLookup,
|
||||
recordHeritageTypeArguments,
|
||||
unresolvedInheritanceByClass,
|
||||
provider.resolveImplicitInheritanceBase,
|
||||
recordSourceBinding,
|
||||
);
|
||||
// Call-based heritage hook (e.g., Ruby include/extend/prepend) — emits
|
||||
|
|
|
|||
|
|
@ -1722,6 +1722,56 @@ function dirnameOf(filePath: string): string {
|
|||
return idx === -1 ? '' : filePath.slice(0, idx);
|
||||
}
|
||||
|
||||
/**
|
||||
* The class-like def named `baseName` that `derivingClass` sees through an
|
||||
* implicit same-namespace rule — the shared body of
|
||||
* `ScopeResolver.resolveImplicitInheritanceBase`. `namespaceOf` is the
|
||||
* provider's namespace fact for a def (`undefined` = unknown, never matches).
|
||||
*
|
||||
* Candidates must share the deriving class's namespace. Several can when one
|
||||
* namespace is declared under more than one source root (a multi-module
|
||||
* build), so the nearest by shared leading path segments wins — the order the
|
||||
* sibling hooks inject them in — and a tie at that distance refuses: a missing
|
||||
* edge is recoverable, a wrong EXTENDS edge silently corrupts impact analysis.
|
||||
*/
|
||||
export function resolveSameNamespaceInheritanceBase(
|
||||
baseName: string,
|
||||
derivingClass: SymbolDefinition,
|
||||
scopes: ScopeResolutionIndexes,
|
||||
namespaceOf: (def: SymbolDefinition) => string | undefined,
|
||||
): SymbolDefinition | undefined {
|
||||
const namespace = namespaceOf(derivingClass);
|
||||
if (namespace === undefined) return undefined;
|
||||
const ownSegments = derivingClass.filePath.split('/');
|
||||
let nearest: SymbolDefinition | undefined;
|
||||
let nearestShared = -1;
|
||||
let tied = false;
|
||||
for (const id of scopes.qualifiedNames.get(baseName)) {
|
||||
const def = scopes.defs.get(id);
|
||||
if (def === undefined || def.nodeId === derivingClass.nodeId || !isClassLike(def.type)) {
|
||||
continue;
|
||||
}
|
||||
if (namespaceOf(def) !== namespace) continue;
|
||||
const segments = def.filePath.split('/');
|
||||
let shared = 0;
|
||||
while (
|
||||
shared < segments.length &&
|
||||
shared < ownSegments.length &&
|
||||
segments[shared] === ownSegments[shared]
|
||||
) {
|
||||
shared++;
|
||||
}
|
||||
if (shared > nearestShared) {
|
||||
nearest = def;
|
||||
nearestShared = shared;
|
||||
tied = false;
|
||||
} else if (shared === nearestShared) {
|
||||
tied = true;
|
||||
}
|
||||
}
|
||||
return tied ? undefined : nearest;
|
||||
}
|
||||
|
||||
/**
|
||||
* True when the caller can see `classDef` through import evidence, not
|
||||
* merely because the class name is workspace-unique.
|
||||
|
|
|
|||
|
|
@ -603,6 +603,34 @@
|
|||
"captureGroups": 11,
|
||||
"digest": "daf34e132bd27b86fe012d64e6cc6fcf4f5e39dd168e9bd30dc1151644235d45"
|
||||
},
|
||||
"csharp-same-namespace-base-collision/App/App.cs": {
|
||||
"captureGroups": 14,
|
||||
"digest": "23a4e39b9e81bde699e0ea97801591b269565e22f6bd200641fd7e9d3119ec63"
|
||||
},
|
||||
"csharp-same-namespace-base-collision/Core/A.cs": {
|
||||
"captureGroups": 10,
|
||||
"digest": "e3feae117230f7458726093097b396ca752737bf46d377a8416591553014fb52"
|
||||
},
|
||||
"csharp-same-namespace-base-collision/Core/B.cs": {
|
||||
"captureGroups": 6,
|
||||
"digest": "51ec8184444d674f9dc646cac0cb3b88b0fc916edfb27d91f5e12a8cb1aa6803"
|
||||
},
|
||||
"csharp-same-namespace-base-collision/Core/Leaf.cs": {
|
||||
"captureGroups": 6,
|
||||
"digest": "0cf391c146ada3f91172e0c484ad2857868ac686dcdcc6058a61a50e2be94204"
|
||||
},
|
||||
"csharp-same-namespace-base-collision/Core/Stamp.cs": {
|
||||
"captureGroups": 8,
|
||||
"digest": "e8422aaf8c91bcaa8ed7bfac1c087f94dc5b912b7c5f86cb92a9d8547b0b94ef"
|
||||
},
|
||||
"csharp-same-namespace-base-collision/Legacy/A.cs": {
|
||||
"captureGroups": 8,
|
||||
"digest": "57ff887da3157871ba8c549c5fa80a31dd72039da9841a4057ca1a5b9cc9a196"
|
||||
},
|
||||
"csharp-same-namespace-base-collision/Legacy/B.cs": {
|
||||
"captureGroups": 6,
|
||||
"digest": "b20bc3cd08070a4242d3530e8fc6e55e5bf003989c56ac96f78c43cf9cdd6193"
|
||||
},
|
||||
"csharp-self-this-resolution/src/Models/Repo.cs": {
|
||||
"captureGroups": 8,
|
||||
"digest": "bc3ded7bee753993b7bb10a33f3fe4e4499cca0489fcfd58d8f80c811375a4a9"
|
||||
|
|
|
|||
13
gitnexus/test/fixtures/lang-resolution/csharp-same-namespace-base-collision/App/App.cs
vendored
Normal file
13
gitnexus/test/fixtures/lang-resolution/csharp-same-namespace-base-collision/App/App.cs
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
using Acme.Core;
|
||||
|
||||
namespace Acme.App
|
||||
{
|
||||
public class App
|
||||
{
|
||||
public void Process()
|
||||
{
|
||||
var leaf = new Leaf();
|
||||
leaf.Greet().Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
7
gitnexus/test/fixtures/lang-resolution/csharp-same-namespace-base-collision/Core/A.cs
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/csharp-same-namespace-base-collision/Core/A.cs
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
namespace Acme.Core
|
||||
{
|
||||
public class A
|
||||
{
|
||||
public Stamp Greet() => new Stamp();
|
||||
}
|
||||
}
|
||||
4
gitnexus/test/fixtures/lang-resolution/csharp-same-namespace-base-collision/Core/B.cs
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/csharp-same-namespace-base-collision/Core/B.cs
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
namespace Acme.Core
|
||||
{
|
||||
public class B : A {}
|
||||
}
|
||||
4
gitnexus/test/fixtures/lang-resolution/csharp-same-namespace-base-collision/Core/Leaf.cs
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/csharp-same-namespace-base-collision/Core/Leaf.cs
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
namespace Acme.Core
|
||||
{
|
||||
public class Leaf : B {}
|
||||
}
|
||||
7
gitnexus/test/fixtures/lang-resolution/csharp-same-namespace-base-collision/Core/Stamp.cs
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/csharp-same-namespace-base-collision/Core/Stamp.cs
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
namespace Acme.Core
|
||||
{
|
||||
public class Stamp
|
||||
{
|
||||
public void Save() {}
|
||||
}
|
||||
}
|
||||
7
gitnexus/test/fixtures/lang-resolution/csharp-same-namespace-base-collision/Legacy/A.cs
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/csharp-same-namespace-base-collision/Legacy/A.cs
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
namespace Acme.Legacy
|
||||
{
|
||||
public class A
|
||||
{
|
||||
public void Greet() {}
|
||||
}
|
||||
}
|
||||
4
gitnexus/test/fixtures/lang-resolution/csharp-same-namespace-base-collision/Legacy/B.cs
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/csharp-same-namespace-base-collision/Legacy/B.cs
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
namespace Acme.Legacy
|
||||
{
|
||||
public class B : A {}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.acme.app;
|
||||
|
||||
import com.acme.core.C;
|
||||
|
||||
public class App {
|
||||
public void process() {
|
||||
C c = new C();
|
||||
c.greet().save();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.acme.core;
|
||||
|
||||
public class A {
|
||||
public Stamp greet() { return new Stamp(); }
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package com.acme.core;
|
||||
|
||||
public class B extends A {}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package com.acme.core;
|
||||
|
||||
public class C extends B {}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package com.acme.core;
|
||||
|
||||
public class Qualified extends com.acme.legacy.B {}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.acme.core;
|
||||
|
||||
import com.acme.legacy.A;
|
||||
|
||||
public class Shadowed extends A {}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.acme.core;
|
||||
|
||||
public class Stamp {
|
||||
public void save() {}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.acme.legacy;
|
||||
|
||||
public class A {
|
||||
public void greet() {}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package com.acme.legacy;
|
||||
|
||||
public class B extends A {}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.acme.app
|
||||
|
||||
import com.acme.core.C
|
||||
|
||||
class App {
|
||||
fun process() {
|
||||
val c = C()
|
||||
c.greet().save()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.acme.core
|
||||
|
||||
open class A {
|
||||
fun greet(): Stamp = Stamp()
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package com.acme.core
|
||||
|
||||
open class B : A()
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package com.acme.core
|
||||
|
||||
class C : B()
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.acme.core
|
||||
|
||||
class Stamp {
|
||||
fun save() {}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.acme.legacy
|
||||
|
||||
open class A {
|
||||
fun greet() {}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package com.acme.legacy
|
||||
|
||||
class B : A()
|
||||
|
|
@ -1595,6 +1595,46 @@ describe('C# grandparent method resolution via MRO (Phase B)', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Same-namespace base whose simple name another namespace also declares.
|
||||
// Acme.Core's `B : A` / `Leaf : B` carry no `using` (same-namespace visibility),
|
||||
// while Acme.Legacy declares its own A and B. The base must bind through the
|
||||
// namespace, not through a workspace-unique simple name the legacy pair makes
|
||||
// ambiguous.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('C# same-namespace base with a colliding simple name', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'csharp-same-namespace-base-collision'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('binds every base inside its own namespace', () => {
|
||||
const extends_ = getRelationships(result, 'EXTENDS').map(
|
||||
(e) => `${e.sourceFilePath}:${e.source} → ${e.targetFilePath}:${e.target}`,
|
||||
);
|
||||
expect(extends_.sort()).toEqual([
|
||||
'Core/B.cs:B → Core/A.cs:A',
|
||||
'Core/Leaf.cs:Leaf → Core/B.cs:B',
|
||||
'Legacy/B.cs:B → Legacy/A.cs:A',
|
||||
]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['Greet', 'Core/A.cs'],
|
||||
['Save', 'Core/Stamp.cs'],
|
||||
])('resolves App.Process → %s through the Leaf → B → A chain (%s)', (target, targetFile) => {
|
||||
const calls = getRelationships(result, 'CALLS').filter(
|
||||
(c) => c.source === 'Process' && c.target === target,
|
||||
);
|
||||
expect(calls.map((c) => c.targetFilePath)).toEqual([targetFile]);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase C: C# null-check narrowing — if (x != null) and if (x is not null)
|
||||
// Both patterns emit patternOverrides for the if-body position range
|
||||
|
|
|
|||
|
|
@ -2101,6 +2101,57 @@ describe('Java grandparent method resolution via MRO (Phase B)', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Same-package `extends` whose base name another package also declares.
|
||||
// com.acme.core's `B extends A` / `C extends B` carry no import (the Java
|
||||
// same-package rule), while com.acme.legacy declares its own A and B. The base
|
||||
// must bind through the package, not through a workspace-unique simple name
|
||||
// that the legacy pair makes ambiguous. Two guards: `Shadowed` single-type
|
||||
// imports legacy.A, which beats the same-package A; `Qualified` names
|
||||
// `com.acme.legacy.B`, which the same-package B must never answer.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Java same-package extends with a colliding simple name', () => {
|
||||
let result: PipelineResult;
|
||||
const core = 'src/main/java/com/acme/core';
|
||||
const legacy = 'src/main/java/com/acme/legacy';
|
||||
const extendsEdges = (): string[] =>
|
||||
getRelationships(result, 'EXTENDS').map(
|
||||
(e) => `${e.sourceFilePath}:${e.source} → ${e.targetFilePath}:${e.target}`,
|
||||
);
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'java-same-package-extends-collision'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('binds unqualified bases by the same-package rule, a single-type import first', () => {
|
||||
const unqualified = extendsEdges().filter((e) => !e.startsWith(`${core}/Qualified.java:`));
|
||||
expect(unqualified.sort()).toEqual([
|
||||
`${core}/B.java:B → ${core}/A.java:A`,
|
||||
`${core}/C.java:C → ${core}/B.java:B`,
|
||||
`${core}/Shadowed.java:Shadowed → ${legacy}/A.java:A`,
|
||||
`${legacy}/B.java:B → ${legacy}/A.java:A`,
|
||||
]);
|
||||
});
|
||||
|
||||
it('never binds a qualified base through the same-package simple name', () => {
|
||||
expect(extendsEdges()).not.toContain(`${core}/Qualified.java:Qualified → ${core}/B.java:B`);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['greet', `${core}/A.java`],
|
||||
['save', `${core}/Stamp.java`],
|
||||
])('resolves App.process → %s through the C → B → A chain (%s)', (target, targetFile) => {
|
||||
const calls = getRelationships(result, 'CALLS').filter(
|
||||
(c) => c.source === 'process' && c.target === target,
|
||||
);
|
||||
expect(calls.map((c) => c.targetFilePath)).toEqual([targetFile]);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Phase P: Overload Disambiguation via Parameter Types ─────────────────
|
||||
|
||||
describe('Java overload disambiguation by parameter types', () => {
|
||||
|
|
|
|||
|
|
@ -1637,6 +1637,48 @@ describe('Kotlin grandparent method resolution via MRO (Phase B)', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Same-package supertype whose simple name another package also declares.
|
||||
// com.acme.core's `B : A()` / `C : B()` carry no import (same-package
|
||||
// visibility), while com.acme.legacy declares its own A and B. The supertype
|
||||
// must bind through the package, not through a workspace-unique simple name
|
||||
// the legacy pair makes ambiguous.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Kotlin same-package supertype with a colliding simple name', () => {
|
||||
let result: PipelineResult;
|
||||
const core = 'src/main/kotlin/com/acme/core';
|
||||
const legacy = 'src/main/kotlin/com/acme/legacy';
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'kotlin-same-package-extends-collision'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('binds every supertype inside its own package', () => {
|
||||
const extends_ = getRelationships(result, 'EXTENDS').map(
|
||||
(e) => `${e.sourceFilePath}:${e.source} → ${e.targetFilePath}:${e.target}`,
|
||||
);
|
||||
expect(extends_.sort()).toEqual([
|
||||
`${core}/B.kt:B → ${core}/A.kt:A`,
|
||||
`${core}/C.kt:C → ${core}/B.kt:B`,
|
||||
`${legacy}/B.kt:B → ${legacy}/A.kt:A`,
|
||||
]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['greet', `${core}/A.kt`],
|
||||
['save', `${core}/Stamp.kt`],
|
||||
])('resolves App.process → %s through the C → B → A chain (%s)', (target, targetFile) => {
|
||||
const calls = getRelationships(result, 'CALLS').filter(
|
||||
(c) => c.source === 'process' && c.target === target,
|
||||
);
|
||||
expect(calls.map((c) => c.targetFilePath)).toEqual([targetFile]);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase C: Kotlin null-check narrowing — if (x != null) { x.save() }
|
||||
// NOTE: depends on nullable_type capture being fixed in jvm.ts
|
||||
|
|
|
|||
|
|
@ -0,0 +1,97 @@
|
|||
/**
|
||||
* Unit coverage for `resolveSameNamespaceInheritanceBase`, the shared body of
|
||||
* `ScopeResolver.resolveImplicitInheritanceBase`. The inheritance pre-pass runs
|
||||
* before same-namespace sibling bindings exist, so this is what binds
|
||||
* `class C extends B` to the `B` of C's own namespace when another namespace
|
||||
* also declares a `B`. Rules pinned here:
|
||||
*
|
||||
* - only candidates sharing the deriving class's namespace count
|
||||
* - an unknown namespace (deriving class or candidate) never matches
|
||||
* - several same-namespace candidates: nearest by shared leading path
|
||||
* segments wins, a tie at that distance refuses
|
||||
* - the deriving class itself and non-class-like defs are never answers
|
||||
*
|
||||
* Drives the function through a minimal cast `ScopeResolutionIndexes` (only
|
||||
* `qualifiedNames` / `defs`, the accessors it reads).
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import type { SymbolDefinition } from 'gitnexus-shared';
|
||||
import { resolveSameNamespaceInheritanceBase } from '../../../src/core/ingestion/scope-resolution/scope/walkers.js';
|
||||
import type { ScopeResolutionIndexes } from '../../../src/core/ingestion/model/scope-resolution-indexes.js';
|
||||
|
||||
const BASE = 'B';
|
||||
const DERIVING_FILE = 'app/src/main/java/com/acme/core/C.java';
|
||||
|
||||
interface Candidate {
|
||||
readonly nodeId: string;
|
||||
readonly filePath: string;
|
||||
readonly type?: string;
|
||||
}
|
||||
|
||||
/** `filePath -> namespace`; a path missing from the table has no known namespace. */
|
||||
const NAMESPACES: Readonly<Record<string, string>> = {
|
||||
[DERIVING_FILE]: 'com.acme.core',
|
||||
'app/src/main/java/com/acme/core/B.java': 'com.acme.core',
|
||||
'app/src/test/java/com/acme/core/B.java': 'com.acme.core',
|
||||
'lib/src/main/java/com/acme/core/B.java': 'com.acme.core',
|
||||
'tools/src/main/java/com/acme/core/B.java': 'com.acme.core',
|
||||
'app/src/main/java/com/acme/legacy/B.java': 'com.acme.legacy',
|
||||
};
|
||||
|
||||
function resolve(
|
||||
candidates: readonly Candidate[],
|
||||
derivingFile = DERIVING_FILE,
|
||||
): string | undefined {
|
||||
const defs = new Map<string, SymbolDefinition>();
|
||||
for (const c of candidates) {
|
||||
defs.set(c.nodeId, {
|
||||
nodeId: c.nodeId,
|
||||
filePath: c.filePath,
|
||||
type: c.type ?? 'Class',
|
||||
} as SymbolDefinition);
|
||||
}
|
||||
const ids = candidates.map((c) => c.nodeId);
|
||||
const scopes = {
|
||||
qualifiedNames: { get: (name: string) => (name === BASE ? ids : []) },
|
||||
defs: { get: (id: string) => defs.get(id) },
|
||||
} as unknown as ScopeResolutionIndexes;
|
||||
const deriving = { nodeId: 'd:self', filePath: derivingFile, type: 'Class' } as SymbolDefinition;
|
||||
return resolveSameNamespaceInheritanceBase(
|
||||
BASE,
|
||||
deriving,
|
||||
scopes,
|
||||
(def) => NAMESPACES[def.filePath],
|
||||
)?.nodeId;
|
||||
}
|
||||
|
||||
const CORE_B = { nodeId: 'd:core', filePath: 'app/src/main/java/com/acme/core/B.java' };
|
||||
const LEGACY_B = { nodeId: 'd:legacy', filePath: 'app/src/main/java/com/acme/legacy/B.java' };
|
||||
const TEST_ROOT_B = { nodeId: 'd:test', filePath: 'app/src/test/java/com/acme/core/B.java' };
|
||||
const LIB_B = { nodeId: 'd:lib', filePath: 'lib/src/main/java/com/acme/core/B.java' };
|
||||
const TOOLS_B = { nodeId: 'd:tools', filePath: 'tools/src/main/java/com/acme/core/B.java' };
|
||||
|
||||
describe('resolveSameNamespaceInheritanceBase', () => {
|
||||
it.each<[string, readonly Candidate[], string | undefined]>([
|
||||
['binds the one candidate in its own namespace', [LEGACY_B, CORE_B], 'd:core'],
|
||||
['declines when no candidate shares its namespace', [LEGACY_B], undefined],
|
||||
['prefers the nearest of several same-namespace candidates', [LIB_B, TEST_ROOT_B], 'd:test'],
|
||||
['refuses a tie at the nearest distance', [LIB_B, TOOLS_B], undefined],
|
||||
[
|
||||
'never answers with a candidate whose namespace is unknown',
|
||||
[{ nodeId: 'd:unknown', filePath: 'gen/B.java' }],
|
||||
undefined,
|
||||
],
|
||||
[
|
||||
'never answers with the deriving class itself',
|
||||
[{ nodeId: 'd:self', filePath: DERIVING_FILE }],
|
||||
undefined,
|
||||
],
|
||||
['ignores a non-class-like def', [{ ...CORE_B, type: 'Method' }], undefined],
|
||||
])('%s', (_title, candidates, expected) => {
|
||||
expect(resolve(candidates)).toBe(expected);
|
||||
});
|
||||
|
||||
it('declines when the deriving class has no known namespace', () => {
|
||||
expect(resolve([CORE_B], 'gen/C.java')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue