mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-11-04 07:27:20 +00:00
fix(scope): bind same-package inheritance bases when the simple name is reused elsewhere (#3659)
* 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> * fix(java): resolve a duplicated package+type import to the importer's own root Cause: `buildJavaPackageIndex` kept the first file declaring each package+type. One repository can hold several build modules or sample projects that each declare `models.User`, so every `import models.User;` bound whichever copy was parsed first, a wrong IMPORTS edge at full confidence that carries the receiver types behind it. In the combined lang-resolution corpus 24 fixtures declare `models.User`, and e.g. `java-heritage` UserService.processUser called `java-assignment-chain/models/User.save`. Fix: the index keeps every declaring file in a sparse `duplicateTypes` map (only colliding pairs, so a normal workspace pays nothing), and a type import resolves to the declaring file sharing the longest leading path with the importer (its own module or source root). A tie, or an unknown importer, refuses rather than guessing. Wildcard imports are unchanged. Corpus A/B (previous commit vs this, same corpus): 40 CALLS edges that crossed into another fixture's User/Repo/Address/UserService are gone, 55 CALLS/ACCESSES edges now land in their own fixture (0 cross-fixture), and `java-parent-resolution` User IMPLEMENTS Serializable moves off `java-heritage` onto its own interface. Receiver-unresolved drops unchanged; import-target and receiver benches pass. Scope-capture java fingerprint re-baselined for the added fixture only (parked, --check passes on the old one). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(scope): respect base candidate visibility * fix(scope): match written JVM base arity --------- Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
169aa5d960
commit
c30449394b
54 changed files with 1202 additions and 42 deletions
|
|
@ -392,6 +392,7 @@ Single interface a language implements to plug into the pipeline. Contract fully
|
|||
| `qualifiedTypeLookupSpelling?` | `(typeRef, scopes?) → 'pkg.Type' \| null \| undefined` — the namespace-qualified spelling of a declared type whose qualifier capture cut from `rawName` (`*models.User` stored as `User`), recovered from `declaredSpelling`. Receiver typing tries it first, grounded on the qualifier's import (no unique-name guess). A failed written-qualified lookup stops instead of falling back to a bare type name. The optional scope indexes also let a provider expand a visible namespace alias; `null` refuses the written lookup and its fallback. The binding is not rewritten — 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 |
|
||||
|
|
@ -494,7 +495,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)
|
||||
|
|
|
|||
|
|
@ -995,6 +995,10 @@ Two incidental corrections from that check, both to KTD6:
|
|||
gap that was really a fixture defect — the same error class as the untyped
|
||||
`$repo` control caught earlier.
|
||||
|
||||
### Same-package inheritance review corrections
|
||||
|
||||
The fallback excludes function-local and nested JVM candidates, while local and nested deriving classes can still use package-level bases. Synthetic deriving classes do not enter this fallback; their source captures do not retain the qualifier evidence needed here. C# filters by the written generic arity before source-root proximity. Five native assertions fail before the corrections; all 14 focused checks and all 755 Java/Kotlin/C# resolver and helper tests pass afterward. Build and both TypeScript configurations pass. After merging main `d57f570d`, receiver counts and all shape states match main (135 calls, 182 total, 27 reads and 20 writes).
|
||||
|
||||
### Library-rooted receiver evidence (#3658, #3498)
|
||||
|
||||
After merging main `2b4456553`, receiver totals and all shape states are unchanged: 133 call drops, 160 across all kinds, 27 reads and 0 writes. Provider evidence now classifies 30 calls external, 45 unknown, 56 in-program and 2 unclassified.
|
||||
|
|
@ -1009,6 +1013,11 @@ Shape states remain unchanged; counts are 133 call drops and
|
|||
160 drops of all kinds (27 reads,
|
||||
0 writes). The counts include both main and this branch's fixes.
|
||||
|
||||
### Inheritance visibility review boundaries
|
||||
|
||||
Native review controls reproduced production Java imports and Java/Kotlin inheritance selecting nearer test-only declarations, an inherited nested base losing to a package homonym, a resolved Kotlin typealias import losing its claim, and a C# tuple argument counting as two type arguments. The corrected focused run passes 71 tests in five files, including positive test-source ownership controls and ordinary nested/local inheritance. The generic argument splitter now keeps tuple commas together.
|
||||
|
||||
Canonical `src/main/java` and `src/main/kotlin` sources exclude recognized non-main source-set candidates before proximity ranking. Custom layouts retain prior eligibility; this is not build-system classpath resolution. JVM package fallback refuses when an enclosing class has inheritance whose member types the pre-MRO pass cannot establish, and explicit named imports retain their claim even when the import names a typealias rather than a class. These conservative cases may remain unresolved. Java duplicate-FQN import selection is part of this same package-ownership fix. Fresh current-main integration and hosted CI remain required.
|
||||
### Rust qualified factories after #3653
|
||||
|
||||
Main `2b4456553` is merged normally. The receiver-drop guard preserves both qualified-factory evidence and the module-export exclusion. Fresh build, both TypeScript configurations and all 28 Rust boundary, capture-golden and CommonJS checks pass. The measured baseline has 135 call drops, 164 total drops, 29 reads and 0 writes.
|
||||
|
|
@ -1020,6 +1029,11 @@ Main `2b4456553` is merged normally. The field lookup retains both the declared
|
|||
Main `2b4456553` is merged normally. The measured baseline has 130 call drops and 156 across all kinds, with 26 reads and zero writes. Main has 133 calls, 27 reads and zero writes (160 total). Every shape state matches main. The fresh build and both TypeScript configurations pass, as do all 68 namespace-boundary, capture, hook and CommonJS merge checks.
|
||||
|
||||
After merging main caacc3bac normally, totals remain 130 calls, 156 across all kinds, 26 reads and zero writes. Origin labels are 30 external, 45 unknown, 53 in-program and 2 unclassified. Shape states are unchanged. The 5,328-record corpus is byte-identical to current main. Package build, both pinned TypeScript configurations, 23 CommonJS/C# precedence tests and 15 library-origin boundary tests pass.
|
||||
|
||||
### Same-package inheritance after #3655
|
||||
|
||||
Main `caacc3bac` is merged normally. The C# resolver retains both main's bare-receiver eligibility hook and this branch's inheritance hook. The combined source passes a fresh package build, both TypeScript configurations and82 focused checks in six files. Before this main merge, the final visibility fixes passed741 Java/Kotlin/C# resolver tests in three files; those runs overlap and are not a unique total. The combined C# capture baseline is remeasured at193 files and2899 groups, fingerprint3720a3d2, scaling1.039 below the unchanged1.5 budget. Receiver counts are remeasured from combined sources. Fresh hosted CI remains required.
|
||||
|
||||
### Rust qualified factories after #3655
|
||||
|
||||
Main `caacc3bac` is merged normally. The C# capture baseline retains main's fixture additions, and receiver counts are regenerated on the combined source: 132 call drops, 160 total, 28 reads and zero writes. All shape states match the preceding Rust baseline. Fresh build, both TypeScript configurations and 23 Rust factory, capture and CommonJS checks pass. Final hosted CI remains required.
|
||||
|
|
|
|||
|
|
@ -75,7 +75,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": "28a51b97c32bf95d85e5a322e443ab00cd490a1178269c2f6f9cfc4fb9776686",
|
||||
"fingerprint": "3720a3d2e3e8b8a24dd78fac36f5c51a04c5c952f819f168cac6f26a7652457c",
|
||||
"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.",
|
||||
|
|
@ -85,12 +85,14 @@
|
|||
"_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|…` instead of `1|…`. 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": 4509,
|
||||
"capture_groups_large": 14409,
|
||||
"capture_groups_fp": 2841,
|
||||
"fixture_count": 186,
|
||||
"capture_groups_fp": 2899,
|
||||
"fixture_count": 193,
|
||||
"_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_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.",
|
||||
"_rebaselined_global_namespace_precedence": "C# global-namespace precedence: fixture growth only. The new csharp-global-namespace-precedence fixture adds 8 files and 94 capture groups; no existing entry changes (the C# capture golden gains only those 8 entries). Capture emission is unchanged. Fixture entries 178 -> 186; capture groups 2746 -> 2840. Prior af724813b5183ad9c98408a9c6944a44897b97d02e7f474ffa4ae637ca9ac1e3 -> 98b066eb5a5d6b5ece83e9fb61cff19cc7bc4b2538f7bddfcd1dc416d1ce4cb4; scaling 1.108 < 1.5.",
|
||||
"_rebaselined_3498_payee_initializer": "Review follow-up initializes Voucher.Payee in the namespace-precedence fixture. Only this fixture input changes: one constructor capture is added (2840 -> 2841 groups), fixture_count stays186; only the C# fingerprint changes. Current measured synthetic counts4509/14409, scaling1.109 below unchanged1.5; all16 languages remain within budget."
|
||||
"_rebaselined_3498_payee_initializer": "Review follow-up initializes Voucher.Payee in the namespace-precedence fixture. Only this fixture input changes: one constructor capture is added (2840 -> 2841 groups), fixture_count stays186; only the C# fingerprint changes. Current measured synthetic counts4509/14409, scaling1.109 below unchanged1.5; all16 languages remain within budget.",
|
||||
"_rebaselined_3498_merge_3655": "Combined same-package inheritance branch and main #3655 sources/fixtures remeasured: 193 files,2899 capture groups; fingerprint3720a3d2. Synthetic counts4509/14409 match main; scaling1.039 stays below the unchanged1.5 budget. Both namespace precedence and inheritance hook are retained."
|
||||
},
|
||||
"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.",
|
||||
|
|
@ -189,7 +191,7 @@
|
|||
"_rebaselined_3498_prefixed_call_bindings": "#3498: a local initialized by exactly `prefix.member(...)` now gets a call-derived @type-binding.constructor whose type keeps the dotted callee, so the shared chain follow can type namespace factories (`final x = rp.make()`). Two existing fixture sites gain a binding, both in dart-method-enrichment/app.dart: `var sound = dog.speak()` and `var category = Animal.classify(\"dog\")`; neither variable is used as a receiver, and a full lang-resolution A/B against origin/main emits the identical CALLS/ACCESSES edges outside the new fixture. The new dart-prefixed-factory-return fixture adds its own captures. Prior 4690403d2be0d270e28a55791df82eaa627d26d28c52d81bfafa59761badcaf3 (897dfc745d4ba0dfe0ae5fcfffcd983563ddab6d0a870fc8ee0de1d8df2c7c87 with the existing fixtures alone) -> a8b2e674a7f695caba32b5a0912686119944e8e4874a6ceeb65b3b5ac420bebb. scaling_ratio 1.173 against the 1.5 budget. Review follow-up: the fixture gains an awaited `rp.makeAsync()` row (corpus growth only; the Dart emitter change in that commit is a docstring) -> 1c5b892a136318eddee3435225284896c2effb52dd51ca5717c92887ae4a39a3."
|
||||
},
|
||||
"java": {
|
||||
"fingerprint": "41c4338f5813a73199dd5c2609313c11076812df21fd9c429039a833325870e0",
|
||||
"fingerprint": "fbd822c3b6848dc6f5e708218c1ab9c1244f2f9f02f4e3df5ed9296137b776d8",
|
||||
"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.",
|
||||
|
|
@ -209,10 +211,12 @@
|
|||
"_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|…` instead of `1|…`. 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": 3515,
|
||||
"fixture_count": 222,
|
||||
"_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.",
|
||||
"_rebaselined_duplicate_fqn_import_fixture": "Duplicate-FQN import fixture: adds `java-duplicate-fqn-source-roots` (4 files). FIXTURE ADDITION ONLY, no capture code changed: with that directory parked, --check passes against the prior fingerprint 82a4b09708caed2350ab36d2b7a1ff00639a34bca73dd1536e37f231e800c2d2 (fixture_count 218, capture_groups_fp 3483); with it, fixture_count 222, capture_groups_fp 3515. Synthetic counts and scaling unchanged."
|
||||
},
|
||||
"java-local-types": {
|
||||
"fingerprint": "6ae163cd36c889454c8672b28944c765d3a3a5e8c49ea4db1cd6cc1787ede583",
|
||||
|
|
@ -275,7 +279,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 — 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.",
|
||||
|
|
@ -294,13 +298,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 — 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 — 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.",
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { CLASS_KINDS, SupportedLanguages } from 'gitnexus-shared';
|
|||
import { buildMro, defaultLinearize } from '../../scope-resolution/passes/mro.js';
|
||||
import {
|
||||
populateClassOwnedMembers,
|
||||
resolveSameNamespaceInheritanceBase,
|
||||
lookupNameClaim,
|
||||
} from '../../scope-resolution/scope/walkers.js';
|
||||
import { populateCsharpNamespacePrefixes } from './qualified-type-names.js';
|
||||
|
|
@ -122,6 +123,21 @@ 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, site) =>
|
||||
resolveSameNamespaceInheritanceBase(
|
||||
baseName,
|
||||
derivingClass,
|
||||
scopes,
|
||||
(def) => def.namespacePrefix,
|
||||
// This provider captures type parameters for every class-like declaration.
|
||||
(def) =>
|
||||
site !== undefined &&
|
||||
(def.typeParameters?.length ?? 0) === (site.typeArguments?.length ?? 0),
|
||||
),
|
||||
isBareClassReceiverCandidate: ({ candidate, spelling, inScope, scopes }) => {
|
||||
if (candidate.typeParameters === undefined || candidate.typeParameters.length === 0)
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ export function resolveJavaImportTarget(
|
|||
const parsedFiles = ctx.parsedFiles;
|
||||
if (parsedFiles === undefined || parsedFiles.length === 0) return null;
|
||||
|
||||
return resolveJavaModule(parsedImport.targetRaw, getJavaPackageIndex(parsedFiles));
|
||||
return resolveJavaModule(parsedImport.targetRaw, getJavaPackageIndex(parsedFiles), ctx.fromFile);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { isJvmSourceVisible } from '../jvm/source-visibility.js';
|
||||
/**
|
||||
* Java import resolution against DECLARED packages (#2953).
|
||||
*
|
||||
|
|
@ -33,8 +34,16 @@ import type { ParsedFile } from 'gitnexus-shared';
|
|||
import type { JvmPackageFact } from '../jvm/package-facts.js';
|
||||
|
||||
export interface JavaPackageIndex {
|
||||
/** Declared package -> importable type name -> the file declaring it. */
|
||||
/** Declared package -> importable type name -> the first file declaring it. */
|
||||
readonly typesByPackage: ReadonlyMap<string, ReadonlyMap<string, string>>;
|
||||
/**
|
||||
* Declared package -> type name -> EVERY file declaring it, only for the
|
||||
* package+type pairs more than one file claims. Not legal within one
|
||||
* compilation, but one repository can hold several build modules or sample
|
||||
* projects that each declare `models.User`. Sparse, so a workspace without
|
||||
* such a collision pays nothing for it.
|
||||
*/
|
||||
readonly duplicateTypes: ReadonlyMap<string, ReadonlyMap<string, readonly string[]>>;
|
||||
/** Declared package -> every file declaring it, for wildcard imports. */
|
||||
readonly filesByPackage: ReadonlyMap<string, readonly string[]>;
|
||||
/**
|
||||
|
|
@ -47,6 +56,7 @@ export interface JavaPackageIndex {
|
|||
|
||||
const EMPTY_INDEX: JavaPackageIndex = {
|
||||
typesByPackage: new Map(),
|
||||
duplicateTypes: new Map(),
|
||||
filesByPackage: new Map(),
|
||||
unreadablePackageFiles: 0,
|
||||
};
|
||||
|
|
@ -68,6 +78,7 @@ export function buildJavaPackageIndex(
|
|||
if (parsedFiles.length === 0) return EMPTY_INDEX;
|
||||
|
||||
const typesByPackage = new Map<string, Map<string, string>>();
|
||||
const duplicateTypes = new Map<string, Map<string, string[]>>();
|
||||
const filesByPackage = new Map<string, string[]>();
|
||||
let unreadablePackageFiles = 0;
|
||||
|
||||
|
|
@ -92,9 +103,21 @@ export function buildJavaPackageIndex(
|
|||
types = new Map();
|
||||
typesByPackage.set(packageName, types);
|
||||
}
|
||||
// First declaration wins. Two files claiming the same package+type is not
|
||||
// legal Java; picking either is as correct as the input allows.
|
||||
if (!types.has(typeName)) types.set(typeName, filePath);
|
||||
const first = types.get(typeName);
|
||||
if (first === undefined) {
|
||||
types.set(typeName, filePath);
|
||||
} else {
|
||||
// A second claim: keep every declaring file so the importer's own
|
||||
// location can choose (see `nearestDeclaringFile`).
|
||||
let duplicates = duplicateTypes.get(packageName);
|
||||
if (duplicates === undefined) {
|
||||
duplicates = new Map();
|
||||
duplicateTypes.set(packageName, duplicates);
|
||||
}
|
||||
const files = duplicates.get(typeName);
|
||||
if (files === undefined) duplicates.set(typeName, [first, filePath]);
|
||||
else files.push(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
const files = filesByPackage.get(packageName);
|
||||
|
|
@ -102,18 +125,21 @@ export function buildJavaPackageIndex(
|
|||
else files.push(filePath);
|
||||
}
|
||||
|
||||
return { typesByPackage, filesByPackage, unreadablePackageFiles };
|
||||
return { typesByPackage, duplicateTypes, filesByPackage, unreadablePackageFiles };
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve one import specifier to the file(s) it names, or `null`.
|
||||
*
|
||||
* A wildcard answers with every file in the package; a type import answers with
|
||||
* one file. Anything the workspace does not declare answers `null`.
|
||||
* one file. Anything the workspace does not declare answers `null`. `fromFile`
|
||||
* is the importing file, which decides between several files declaring the
|
||||
* same package+type.
|
||||
*/
|
||||
export function resolveJavaModule(
|
||||
targetRaw: string,
|
||||
index: JavaPackageIndex,
|
||||
fromFile?: string,
|
||||
): string | readonly string[] | null {
|
||||
if (targetRaw === '') return null;
|
||||
|
||||
|
|
@ -125,7 +151,7 @@ export function resolveJavaModule(
|
|||
return inPackage !== undefined && stem !== '' ? inPackage : null;
|
||||
}
|
||||
|
||||
return resolveTypeName(targetRaw, index);
|
||||
return resolveTypeName(targetRaw, index, fromFile);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -141,7 +167,11 @@ export function resolveJavaModule(
|
|||
* The second is a static member import; its trailing segments name members
|
||||
* inside the type, and the file the import binds to is the type's.
|
||||
*/
|
||||
function resolveTypeName(qualified: string, index: JavaPackageIndex): string | null {
|
||||
function resolveTypeName(
|
||||
qualified: string,
|
||||
index: JavaPackageIndex,
|
||||
fromFile: string | undefined,
|
||||
): string | null {
|
||||
const parts = qualified.split('.').filter((part) => part !== '');
|
||||
// A single bare segment names a type in the default package, which Java
|
||||
// forbids importing. Nothing to resolve, and nothing to guess at.
|
||||
|
|
@ -151,12 +181,53 @@ function resolveTypeName(qualified: string, index: JavaPackageIndex): string | n
|
|||
const packageName = parts.slice(0, split).join('.');
|
||||
const types = index.typesByPackage.get(packageName);
|
||||
if (types === undefined) continue;
|
||||
const file = types.get(parts[split]);
|
||||
if (file !== undefined) return file;
|
||||
const typeName = parts[split];
|
||||
const file = types.get(typeName);
|
||||
if (file === undefined) continue;
|
||||
const duplicates = index.duplicateTypes.get(packageName)?.get(typeName);
|
||||
return duplicates === undefined ? file : nearestDeclaringFile(duplicates, fromFile);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The one file a type import names when several declare its package+type. The
|
||||
* import cannot say which, so the importer's location does: the file sharing
|
||||
* the longest leading path with it (its own module or source root) wins, and a
|
||||
* tie refuses — a fabricated IMPORTS edge at full confidence is worse than
|
||||
* none, which is also the answer when the importer is unknown.
|
||||
*/
|
||||
function nearestDeclaringFile(
|
||||
files: readonly string[],
|
||||
fromFile: string | undefined,
|
||||
): string | null {
|
||||
if (fromFile === undefined) return null;
|
||||
const ownSegments = fromFile.replace(/\\/g, '/').split('/');
|
||||
let nearest: string | null = null;
|
||||
let nearestShared = -1;
|
||||
let tied = false;
|
||||
for (const file of files) {
|
||||
if (!isJvmSourceVisible(fromFile, file)) continue;
|
||||
const segments = file.replace(/\\/g, '/').split('/');
|
||||
let shared = 0;
|
||||
while (
|
||||
shared < segments.length &&
|
||||
shared < ownSegments.length &&
|
||||
segments[shared] === ownSegments[shared]
|
||||
) {
|
||||
shared++;
|
||||
}
|
||||
if (shared > nearestShared) {
|
||||
nearest = file;
|
||||
nearestShared = shared;
|
||||
tied = false;
|
||||
} else if (shared === nearestShared) {
|
||||
tied = true;
|
||||
}
|
||||
}
|
||||
return tied ? null : nearest;
|
||||
}
|
||||
|
||||
/** `src/main/java/com/example/User.java` -> `User`. */
|
||||
function baseTypeName(filePath: string): string | null {
|
||||
const slash = filePath.replace(/\\/g, '/').lastIndexOf('/');
|
||||
|
|
|
|||
|
|
@ -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,8 +1,19 @@
|
|||
import type { BindingRef, ParsedFile, ScopeId, TypeRef } from 'gitnexus-shared';
|
||||
import type {
|
||||
BindingRef,
|
||||
ParsedFile,
|
||||
ReferenceSite,
|
||||
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';
|
||||
import { isJvmSourceVisible } from './source-visibility.js';
|
||||
|
||||
/** Packages larger than this get no implicit sibling visibility at all — every
|
||||
* file in them is marked incomplete. `GITNEXUS_MAX_INJECTED_SIBLINGS` bounds
|
||||
|
|
@ -32,6 +43,15 @@ 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,
|
||||
site?: ReferenceSite,
|
||||
) => SymbolDefinition | undefined;
|
||||
}
|
||||
|
||||
interface PackageBucket {
|
||||
|
|
@ -225,9 +245,88 @@ 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;
|
||||
};
|
||||
|
||||
// Scope indexes are immutable for this inheritance pass. Hydrate each file's
|
||||
// module children once; local and nested classes are not package siblings.
|
||||
const topLevelIds = new WeakMap<ScopeResolutionIndexes, Map<string, ReadonlySet<string>>>();
|
||||
const isPackageClass = (def: SymbolDefinition, scopes: ScopeResolutionIndexes): boolean => {
|
||||
let files = topLevelIds.get(scopes);
|
||||
if (files === undefined) topLevelIds.set(scopes, (files = new Map()));
|
||||
let ids = files.get(def.filePath);
|
||||
if (ids === undefined) {
|
||||
const found = new Set<string>();
|
||||
const module = scopes.moduleScopes.get(def.filePath);
|
||||
if (module !== undefined) {
|
||||
for (const childId of scopes.scopeTree.getChildren(module)) {
|
||||
const child = scopes.scopeTree.getScope(childId);
|
||||
if (child?.kind !== 'Class') continue;
|
||||
for (const candidate of child.ownedDefs) {
|
||||
if (isClassLike(candidate.type) || candidate.type === 'Annotation')
|
||||
found.add(candidate.nodeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
files.set(def.filePath, (ids = found));
|
||||
}
|
||||
return ids.has(def.nodeId);
|
||||
};
|
||||
|
||||
// The pre-MRO pass cannot rule out an inherited member type in an
|
||||
// enclosing class. Refuse the package fallback until that higher-priority
|
||||
// lookup is known, rather than manufacturing an unrelated package edge.
|
||||
const heritageScopes = new WeakMap<ScopeResolutionIndexes, ReadonlySet<ScopeId>>();
|
||||
const hasInheritedEnclosingClass = (site: ReferenceSite, scopes: ScopeResolutionIndexes) => {
|
||||
let inherited = heritageScopes.get(scopes);
|
||||
if (inherited === undefined) {
|
||||
inherited = new Set(
|
||||
scopes.referenceSites.filter((ref) => ref.kind === 'inherits').map((ref) => ref.inScope),
|
||||
);
|
||||
heritageScopes.set(scopes, inherited);
|
||||
}
|
||||
let cursor = scopes.scopeTree.getScope(site.inScope)?.parent ?? null;
|
||||
while (cursor !== null) {
|
||||
const scope = scopes.scopeTree.getScope(cursor);
|
||||
if (scope?.kind === 'Class' && inherited.has(cursor)) return true;
|
||||
cursor = scope?.parent ?? null;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
return {
|
||||
populateNamespaceSiblings,
|
||||
isVisibilityIncomplete: (filePath) => incompleteFiles.has(filePath),
|
||||
resolveImplicitInheritanceBase: (baseName, derivingClass, scopes, site) => {
|
||||
// An explicit typealias/non-class import can resolve without producing a
|
||||
// class binding. Its claim still takes precedence over package siblings.
|
||||
const moduleScope = scopes.moduleScopes.get(derivingClass.filePath);
|
||||
if (
|
||||
moduleScope !== undefined &&
|
||||
scopes.imports.get(moduleScope)?.some((edge) => edge.localName === baseName)
|
||||
)
|
||||
return undefined;
|
||||
if (site !== undefined && hasInheritedEnclosingClass(site, scopes)) return undefined;
|
||||
return resolveSameNamespaceInheritanceBase(
|
||||
baseName,
|
||||
derivingClass,
|
||||
scopes,
|
||||
packageOf,
|
||||
(def) =>
|
||||
isPackageClass(def, scopes) &&
|
||||
isJvmSourceVisible(derivingClass.filePath, def.filePath) &&
|
||||
// Omitted arguments may be raw or constructor-inferred. Explicit
|
||||
// arguments must match before proximity ranks same-package bases.
|
||||
(site?.typeArguments === undefined ||
|
||||
site.typeArguments.length === 0 ||
|
||||
(def.typeParameters?.length ?? 0) === site.typeArguments.length),
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
/** Conventional production source roots cannot see test or other source sets.
|
||||
* Unknown/custom layouts retain their existing eligibility; this is not a
|
||||
* build-system classpath model. */
|
||||
export function isJvmSourceVisible(fromFile: string, candidateFile: string): boolean {
|
||||
const sourceSet = (file: string) =>
|
||||
/(?:^|\/)src\/([^/]+)\/(?:java|kotlin)(?:\/|$)/.exec(file.replace(/\\/g, '/'))?.[1];
|
||||
return (
|
||||
sourceSet(fromFile) !== 'main' ||
|
||||
sourceSet(candidateFile) === undefined ||
|
||||
sourceSet(candidateFile) === 'main'
|
||||
);
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -1774,6 +1774,31 @@ 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,
|
||||
/** Written base evidence, including generic arity. Providers that require
|
||||
* this evidence must decline when a direct caller omits it. */
|
||||
site?: ReferenceSite,
|
||||
) => 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,
|
||||
|
|
@ -208,6 +220,57 @@ 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 ||
|
||||
derivingClass.isSynthetic === true ||
|
||||
!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, site);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve inheritance reference sites early and pre-emit their EXTENDS edges
|
||||
* before MRO construction. This lets template-base captures contribute to the
|
||||
|
|
@ -232,6 +295,7 @@ function preEmitInheritanceEdges(
|
|||
nodeLookup: ReturnType<typeof buildGraphNodeLookup>,
|
||||
recordTypeArguments: HeritageTypeArgumentSink,
|
||||
unresolvedInheritanceByClass: Set<string>,
|
||||
resolveImplicitBase: ScopeResolver['resolveImplicitInheritanceBase'],
|
||||
recordSourceBinding?: SourceBindingRecorder,
|
||||
): Set<string> {
|
||||
const handledSites = new Set<string>();
|
||||
|
|
@ -266,14 +330,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
|
||||
|
|
@ -944,6 +1009,7 @@ export function runScopeResolution(
|
|||
nodeLookup,
|
||||
recordHeritageTypeArguments,
|
||||
unresolvedInheritanceByClass,
|
||||
provider.resolveImplicitInheritanceBase,
|
||||
recordSourceBinding,
|
||||
);
|
||||
// Call-based heritage hook (e.g., Ruby include/extend/prepend) — emits
|
||||
|
|
|
|||
|
|
@ -1836,6 +1836,57 @@ 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,
|
||||
acceptsCandidate?: (def: SymbolDefinition) => boolean,
|
||||
): 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 || acceptsCandidate?.(def) === false) 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.
|
||||
|
|
|
|||
|
|
@ -116,8 +116,8 @@ function balancedTailList(text: string, start: number): string | undefined {
|
|||
}
|
||||
|
||||
/** Split `string, Map<int, bool>` on the commas that are not inside a nested
|
||||
* list. Tracks BOTH bracket families so a mixed spelling (`List<Dict[a, b]>`)
|
||||
* does not split inside the inner one. */
|
||||
* list or parenthesized tuple. Both bracket families and parentheses protect
|
||||
* commas inside nested type expressions. */
|
||||
function splitTopLevelArguments(inner: string): string[] {
|
||||
const args: string[] = [];
|
||||
let depth = 0;
|
||||
|
|
@ -128,8 +128,8 @@ function splitTopLevelArguments(inner: string): string[] {
|
|||
};
|
||||
for (let i = 0; i < inner.length; i++) {
|
||||
const ch = inner[i];
|
||||
if (ch === '<' || ch === '[') depth++;
|
||||
else if (ch === '>' || ch === ']') depth--;
|
||||
if (ch === '<' || ch === '[' || ch === '(') depth++;
|
||||
else if (ch === '>' || ch === ']' || ch === ')') depth--;
|
||||
else if (ch === ',' && depth === 0) {
|
||||
push(i);
|
||||
tokenStart = i + 1;
|
||||
|
|
|
|||
|
|
@ -635,6 +635,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,5 @@
|
|||
package models;
|
||||
|
||||
public class User {
|
||||
public void save() {}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package models;
|
||||
|
||||
public class User {
|
||||
public void save() {}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package services;
|
||||
|
||||
import models.User;
|
||||
|
||||
public class UserService {
|
||||
public void process(User user) {
|
||||
user.save();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package reports;
|
||||
|
||||
import models.User;
|
||||
|
||||
public class Report {
|
||||
public void run(User user) {
|
||||
user.save();
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { getRelationships, runPipelineFromRepo, writeFixtureRepo } from './resolvers/helpers.js';
|
||||
|
||||
describe.each([
|
||||
{
|
||||
language: 'Kotlin',
|
||||
extension: 'kt',
|
||||
directory: 'kotlin',
|
||||
basePrefix: 'open class',
|
||||
child: 'class Child : Base<String>()',
|
||||
},
|
||||
{
|
||||
language: 'Java',
|
||||
extension: 'java',
|
||||
directory: 'java',
|
||||
basePrefix: 'class',
|
||||
child: 'class Child extends Base<String> {}',
|
||||
},
|
||||
])('$language inherited generic arity', ({ language, extension, directory, basePrefix, child }) => {
|
||||
const source = (body: string) => 'package p' + (extension === 'java' ? ';\n' : '\n') + body;
|
||||
|
||||
it.each([
|
||||
{ label: 'a nearer two-parameter base', near: '<T, U>', far: '<T>', hasTarget: true },
|
||||
{ label: 'a nearer non-generic base', near: '', far: '<T>', hasTarget: true },
|
||||
{ label: 'only incompatible bases', near: '<T, U>', far: '<T, U, V>', hasTarget: false },
|
||||
])(
|
||||
'filters $label before source proximity',
|
||||
async ({ near, far, hasTarget }) => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-jvm-base-arity-'));
|
||||
try {
|
||||
const suffix = extension === 'java' ? ' {}' : '';
|
||||
writeFixtureRepo(root, {
|
||||
['near/src/main/' + directory + '/p/Child.' + extension]: source(child),
|
||||
['near/nested-module/src/main/' + directory + '/p/Base.' + extension]: source(
|
||||
basePrefix + ' Base' + near + suffix,
|
||||
),
|
||||
['far/src/main/' + directory + '/p/Base.' + extension]: source(
|
||||
basePrefix + ' Base' + far + suffix,
|
||||
),
|
||||
['other/src/main/' + directory + '/q/Base.' + extension]:
|
||||
'package q' + (extension === 'java' ? ';\nclass Base<T> {}' : '\nclass Base<T>'),
|
||||
});
|
||||
const result = await runPipelineFromRepo(root, () => {}, {});
|
||||
expect(
|
||||
getRelationships(result, 'EXTENDS')
|
||||
.filter((e) => e.source === 'Child')
|
||||
.map((e) => e.targetFilePath),
|
||||
).toEqual(hasTarget ? ['far/src/main/' + directory + '/p/Base.' + extension] : []);
|
||||
} finally {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
120000,
|
||||
);
|
||||
|
||||
it('retains omitted generic arguments where the language permits them', async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-jvm-base-inference-'));
|
||||
try {
|
||||
const kotlin = language === 'Kotlin';
|
||||
writeFixtureRepo(root, {
|
||||
['app/src/main/' + directory + '/p/Child.' + extension]: source(
|
||||
kotlin ? 'class Child : Base("value")' : 'class Child extends Base {}',
|
||||
),
|
||||
['model/src/main/' + directory + '/p/Base.' + extension]: source(
|
||||
kotlin ? 'open class Base<T>(val value: T)' : 'class Base<T> {}',
|
||||
),
|
||||
['other/src/main/' + directory + '/q/Base.' + extension]:
|
||||
'package q' + (kotlin ? '\nclass Base<T>' : ';\nclass Base<T> {}'),
|
||||
});
|
||||
const result = await runPipelineFromRepo(root, () => {}, {});
|
||||
expect(
|
||||
getRelationships(result, 'EXTENDS')
|
||||
.filter((e) => e.source === 'Child')
|
||||
.map((e) => e.targetFilePath),
|
||||
).toEqual(['model/src/main/' + directory + '/p/Base.' + extension]);
|
||||
} finally {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
}, 120000);
|
||||
});
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import {
|
||||
getRelationships,
|
||||
runPipelineFromRepo,
|
||||
writeFixtureRepo,
|
||||
type PipelineResult,
|
||||
} from './resolvers/helpers.js';
|
||||
|
||||
describe('review probes for candidate visibility', () => {
|
||||
let root: string;
|
||||
let result: PipelineResult;
|
||||
beforeAll(async () => {
|
||||
root = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-source-visibility-'));
|
||||
writeFixtureRepo(root, {
|
||||
'alpha/src/main/java/models/User.java':
|
||||
'package models; public class User { public void save() {} }',
|
||||
'beta/src/test/java/models/User.java':
|
||||
'package models; public class User { public void save() {} }',
|
||||
'beta/src/main/java/services/Service.java':
|
||||
'package services; import models.User; public class Service { void run(User value) { value.save(); } }',
|
||||
'alpha/src/main/java/parents/Base.java': 'package parents; public class Base {}',
|
||||
'beta/src/test/java/parents/Base.java': 'package parents; public class Base {}',
|
||||
'beta/src/main/java/parents/Child.java':
|
||||
'package parents; public class Child extends Base {}',
|
||||
'gamma/src/main/java/nested/A.java':
|
||||
'package nested; public class A { public static class B {} }',
|
||||
'gamma/src/main/java/nested/C.java':
|
||||
'package nested; public class C extends A { public static class D extends B {} }',
|
||||
'gamma/src/main/java/nested/B.java': 'package nested; public class B {}',
|
||||
'gamma/src/main/java/other/B.java': 'package other; public class B {}',
|
||||
});
|
||||
writeFixtureRepo(root, {
|
||||
'alias/p/B.kt': 'package aliasp\nopen class B',
|
||||
'alias/p/C.kt': 'package aliasp\nimport aliasq.B\nclass AliasChild : B()',
|
||||
'alias/q/Types.kt': 'package aliasq\nopen class Real\ntypealias B = Real',
|
||||
'claims/p/B.java': 'package claimsp; public class B {}',
|
||||
'claims/q/B.java': 'package claimsq; public class B {}',
|
||||
'claims/p/C.java':
|
||||
'package claimsp; import external.B; public class ExternalChild extends B {}',
|
||||
'beta/src/test/java/services/TestService.java':
|
||||
'package services; import models.User; public class TestService { void run(User value) { value.save(); } }',
|
||||
'beta/src/test/java/parents/TestChild.java':
|
||||
'package parents; public class TestChild extends Base {}',
|
||||
'alpha/src/main/kotlin/ktparents/Base.kt': 'package ktparents\nopen class Base',
|
||||
'beta/src/test/kotlin/ktparents/Base.kt': 'package ktparents\nopen class Base',
|
||||
'beta/src/main/kotlin/ktparents/Child.kt': 'package ktparents\nclass Child : Base()',
|
||||
});
|
||||
result = await runPipelineFromRepo(root, () => {}, {});
|
||||
}, 120000);
|
||||
afterAll(() => {
|
||||
if (root) fs.rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
it('does not dispatch production receivers to a test-only duplicate', () => {
|
||||
const edges = getRelationships(result, 'CALLS').filter(
|
||||
(e) => e.sourceFilePath === 'beta/src/main/java/services/Service.java' && e.target === 'save',
|
||||
);
|
||||
expect(edges.map((e) => e.targetFilePath)).toEqual(['alpha/src/main/java/models/User.java']);
|
||||
});
|
||||
it('does not bind a production base to a test-only duplicate', () => {
|
||||
const edges = getRelationships(result, 'EXTENDS').filter(
|
||||
(e) => e.source === 'Child' && e.sourceFilePath.endsWith('.java'),
|
||||
);
|
||||
expect(edges.map((e) => e.targetFilePath)).toEqual(['alpha/src/main/java/parents/Base.java']);
|
||||
});
|
||||
it('does not override a resolved Kotlin typealias import with a package sibling', () => {
|
||||
const edges = getRelationships(result, 'EXTENDS').filter((e) => e.source === 'AliasChild');
|
||||
expect(edges).not.toContainEqual(expect.objectContaining({ targetFilePath: 'alias/p/B.kt' }));
|
||||
});
|
||||
it('does not override an unresolved explicit Java import with a package sibling', () => {
|
||||
expect(getRelationships(result, 'EXTENDS').filter((e) => e.source === 'ExternalChild')).toEqual(
|
||||
[],
|
||||
);
|
||||
});
|
||||
it('preserves the nearer test declaration for a test importer', () => {
|
||||
const edges = getRelationships(result, 'CALLS').filter(
|
||||
(e) =>
|
||||
e.sourceFilePath === 'beta/src/test/java/services/TestService.java' && e.target === 'save',
|
||||
);
|
||||
expect(edges.map((e) => e.targetFilePath)).toEqual(['beta/src/test/java/models/User.java']);
|
||||
});
|
||||
it('preserves the nearer test base for a test deriving class', () => {
|
||||
const edges = getRelationships(result, 'EXTENDS').filter((e) => e.source === 'TestChild');
|
||||
expect(edges.map((e) => e.targetFilePath)).toEqual(['beta/src/test/java/parents/Base.java']);
|
||||
});
|
||||
it('excludes test-only Kotlin bases from production inheritance', () => {
|
||||
const edges = getRelationships(result, 'EXTENDS').filter(
|
||||
(e) => e.source === 'Child' && e.sourceFilePath.endsWith('.kt'),
|
||||
);
|
||||
expect(edges.map((e) => e.targetFilePath)).toEqual(['alpha/src/main/kotlin/ktparents/Base.kt']);
|
||||
});
|
||||
it('does not replace an inherited nested base with a package-level homonym', () => {
|
||||
const edges = getRelationships(result, 'EXTENDS').filter((e) => e.source === 'D');
|
||||
expect(edges).not.toContainEqual(
|
||||
expect.objectContaining({ targetFilePath: 'gamma/src/main/java/nested/B.java' }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import { it, expect } from 'vitest';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { getRelationships, runPipelineFromRepo, writeFixtureRepo } from './resolvers/helpers.js';
|
||||
it('counts a C# tuple as one inherited type argument', async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-tuple-base-'));
|
||||
try {
|
||||
writeFixtureRepo(root, {
|
||||
'Tuple.cs': 'namespace P { class Child : Base<(int, string)> {} }',
|
||||
'One.cs': 'namespace P { class Base<T> {} }',
|
||||
'Two.cs': 'namespace P { class Base<T, U> {} }',
|
||||
});
|
||||
const result = await runPipelineFromRepo(root, () => {}, {});
|
||||
expect(
|
||||
getRelationships(result, 'EXTENDS')
|
||||
.filter((e) => e.source === 'Child')
|
||||
.map((e) => e.targetFilePath),
|
||||
).toEqual(['One.cs']);
|
||||
} finally {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
}, 120000);
|
||||
|
|
@ -1596,6 +1596,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,95 @@ 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]);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// One package+type declared under several source roots. alpha and beta each
|
||||
// declare `models.User`; beta's UserService imports it from its own module,
|
||||
// gamma's Report sits equally far from both. The import must bind the
|
||||
// importer's own copy, and refuse when no copy is nearer, instead of whichever
|
||||
// file was parsed first. The import-free twin (same-package siblings) is
|
||||
// 'Java same-module priority for duplicate FQNs' above.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Java import of a type declared under several source roots', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'java-duplicate-fqn-source-roots'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it.each<[string, string, readonly string[]]>([
|
||||
[
|
||||
'beta/src/main/java/services/UserService.java',
|
||||
'process',
|
||||
['beta/src/main/java/models/User.java'],
|
||||
],
|
||||
['gamma/src/main/java/reports/Report.java', 'run', []],
|
||||
])('%s (%s) imports and calls only %j', (importer, method, userFiles) => {
|
||||
const imports = getRelationships(result, 'IMPORTS').filter(
|
||||
(e) => e.sourceFilePath === importer && e.targetFilePath.endsWith('/models/User.java'),
|
||||
);
|
||||
const saves = getRelationships(result, 'CALLS').filter(
|
||||
(c) => c.source === method && c.target === 'save',
|
||||
);
|
||||
expect(imports.map((e) => e.targetFilePath)).toEqual(userFiles);
|
||||
expect(saves.map((c) => c.targetFilePath)).toEqual(userFiles);
|
||||
});
|
||||
});
|
||||
|
||||
// ── 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,95 @@
|
|||
import { beforeAll, afterAll, describe, expect, it } from 'vitest';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import {
|
||||
getRelationships,
|
||||
runPipelineFromRepo,
|
||||
writeFixtureRepo,
|
||||
type PipelineResult,
|
||||
} from './resolvers/helpers.js';
|
||||
|
||||
describe('implicit inheritance candidate ownership', () => {
|
||||
let root: string;
|
||||
let result: PipelineResult;
|
||||
beforeAll(async () => {
|
||||
root = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-inheritance-review-'));
|
||||
writeFixtureRepo(root, {
|
||||
'kt/p/B.kt': `package p
|
||||
open class B {
|
||||
fun topLevel() {}
|
||||
}
|
||||
`,
|
||||
'kt/p/C.kt': `package p
|
||||
fun helper() {
|
||||
class B {
|
||||
fun invisible() {}
|
||||
}
|
||||
}
|
||||
class C : B()
|
||||
class Outer {
|
||||
class Nested : B()
|
||||
}
|
||||
fun localDeriving() {
|
||||
class Local : B()
|
||||
}
|
||||
`,
|
||||
'kt/q/B.kt': `package q
|
||||
open class B {}
|
||||
`,
|
||||
'java/p/B.java': `package p;
|
||||
public class B {}
|
||||
`,
|
||||
'java/q/B.java': `package q;
|
||||
public class B {}
|
||||
`,
|
||||
'java/p/Factory.java': `package p;
|
||||
public class Factory {
|
||||
Object make() { return new external.B() {}; }
|
||||
}
|
||||
`,
|
||||
'cs/src/Feature/C.cs': `namespace P {
|
||||
class C : B<int> {}
|
||||
class Plain : B {}
|
||||
}
|
||||
`,
|
||||
'cs/src/Feature/B.cs': `namespace P {
|
||||
class B {}
|
||||
}
|
||||
`,
|
||||
'cs/src/Generic/B.cs': `namespace P {
|
||||
class B<T> {}
|
||||
}
|
||||
`,
|
||||
'cs/other/B.cs': `namespace Other {
|
||||
class B {}
|
||||
}
|
||||
`,
|
||||
});
|
||||
result = await runPipelineFromRepo(root, () => {}, {});
|
||||
}, 120000);
|
||||
afterAll(() => {
|
||||
if (root) fs.rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
const bases = (source: string, file: string) =>
|
||||
getRelationships(result, 'EXTENDS')
|
||||
.filter((e) => e.source === source && e.sourceFilePath === file)
|
||||
.map((e) => e.targetFilePath);
|
||||
it.each(['C', 'Nested', 'Local'])('binds Kotlin %s to the package-level class', (name) => {
|
||||
expect(bases(name, 'kt/p/C.kt')).toEqual(['kt/p/B.kt']);
|
||||
});
|
||||
it('does not bind a qualified anonymous Java base to a package sibling', () => {
|
||||
const anonymous = getRelationships(result, 'EXTENDS').filter(
|
||||
(e) => e.sourceFilePath === 'java/p/Factory.java',
|
||||
);
|
||||
expect(anonymous).not.toContainEqual(
|
||||
expect.objectContaining({ targetFilePath: 'java/p/B.java' }),
|
||||
);
|
||||
});
|
||||
it('matches the written C# generic arity before path proximity', () => {
|
||||
expect(bases('C', 'cs/src/Feature/C.cs')).toEqual(['cs/src/Generic/B.cs']);
|
||||
});
|
||||
it('keeps the non-generic C# base distinct', () => {
|
||||
expect(bases('Plain', 'cs/src/Feature/C.cs')).toEqual(['cs/src/Feature/B.cs']);
|
||||
});
|
||||
});
|
||||
|
|
@ -81,6 +81,43 @@ describe('type imports', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('one package+type declared under several source roots', () => {
|
||||
// Not legal within one compilation, but one repository can hold several
|
||||
// build modules or sample projects that each declare `models.User`. The
|
||||
// import cannot say which one it means; the importer's location does, and a
|
||||
// tie refuses rather than binding whichever file was parsed first.
|
||||
const ROOTS = indexOf({
|
||||
'alpha/src/main/java/models/User.java': 'models',
|
||||
'beta/src/main/java/models/User.java': 'models',
|
||||
'beta/src/test/java/models/User.java': 'models',
|
||||
'beta/src/main/java/services/UserService.java': 'services',
|
||||
});
|
||||
|
||||
it.each<[string, string | undefined, string | null]>([
|
||||
[
|
||||
'its own module',
|
||||
'beta/src/main/java/services/UserService.java',
|
||||
'beta/src/main/java/models/User.java',
|
||||
],
|
||||
[
|
||||
'its own source root over a sibling root',
|
||||
'beta/src/test/java/services/UserServiceTest.java',
|
||||
'beta/src/test/java/models/User.java',
|
||||
],
|
||||
['another module', 'alpha/src/main/java/app/App.java', 'alpha/src/main/java/models/User.java'],
|
||||
['nowhere nearer one copy than another', 'gamma/src/main/java/app/App.java', null],
|
||||
['an unknown importer', undefined, null],
|
||||
])('resolves an import from %s', (_title, fromFile, expected) => {
|
||||
expect(resolveJavaModule('models.User', ROOTS, fromFile)).toBe(expected);
|
||||
});
|
||||
|
||||
it('leaves a type declared once independent of the importer', () => {
|
||||
expect(resolveJavaModule('services.UserService', ROOTS, 'gamma/App.java')).toBe(
|
||||
'beta/src/main/java/services/UserService.java',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('imports of things outside the repository (#2953)', () => {
|
||||
it('does not resolve a JDK import onto a same-named local file', () => {
|
||||
// The reported defect. `vendor/java/util/List.java` ends in `java/util/List`
|
||||
|
|
|
|||
|
|
@ -301,6 +301,14 @@ describe('heritageTypeArgumentsKey', () => {
|
|||
});
|
||||
|
||||
describe('typeApplicationArguments', () => {
|
||||
it('keeps tuple and nested tuple commas inside one type argument', () => {
|
||||
expect(typeApplicationArguments('Base<(int, string)>')).toEqual(['(int, string)']);
|
||||
expect(typeApplicationArguments('Base<(int, (string, bool)), byte>')).toEqual([
|
||||
'(int, (string, bool))',
|
||||
'byte',
|
||||
]);
|
||||
});
|
||||
|
||||
it('reads angle-bracket arguments', () => {
|
||||
expect(typeApplicationArguments('IValidator<string>')).toEqual(['string']);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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