diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml
index aab78e679..7e91c8201 100644
--- a/.github/workflows/ci-tests.yml
+++ b/.github/workflows/ci-tests.yml
@@ -1352,6 +1352,9 @@ jobs:
GITNEXUS_DEBUG_HEAP: '1'
run: node --import tsx bench/scope-resolution/dense-java.mts 300 20 10 --check 2>&1 | tee scope-resolution-dense-java.log
working-directory: gitnexus
+ - name: Check Java overload and ingestion scaling
+ run: node bench/java-overload-resolution/measure.js --check --output java-overload-resolution-benchmark.json
+ working-directory: gitnexus
- name: Upload scope-resolution measurements
if: always()
uses: ./.github/actions/upload-test-artifact
@@ -1361,6 +1364,7 @@ jobs:
gitnexus/scope-resolution-benchmark.json
gitnexus/scope-resolution-call-signature-memory.json
gitnexus/scope-resolution-dense-java.log
+ gitnexus/java-overload-resolution-benchmark.json
retention-days: 5
# Locked eval suite. setup-uv and uv itself are immutable so CI exercises
diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index 4635dbff8..b0ae8a977 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -432,6 +432,56 @@ Wire format is **v2**: `2|||…`, one-character version prefix
Receivers the resolver could not type are not silently dropped. Each records a `ResolutionOutcome` (`scope-resolution/resolution-outcome.ts`) carrying the receiver's *shape* (`classifyReceiverShape`: `chain-call` / `chain-field` / `chain-mixed` / `chain-unwrap` / `no-chain` — the bench censuses these) and its *origin* (`in-program` / `external` / `unknown`). `scope-resolution/unresolved-receivers.ts` aggregates them per member name into the index-persisted `unresolvedReceiverMembers` summary, keeping in-program and external counts under separate keys. Only in-program drops make a count short: an external-rooted call (`System.out.println`, `fetch(...)`) has no in-graph node an edge could have reached, so it is reported but does not hedge. `impact` / `context` read that summary and publish `epistemic: 'exact' | 'lower-bound'`, prose `boundaries`, and the machine-readable `causes` split (`EpistemicCauses` in `mcp/local/local-backend.ts`).
+### Java overload selection and missing call edges (#3789)
+
+Java resolves an overload family through provider hooks before the shared passes
+prune candidates by owner or conversion rank. The provider gathers eligible
+members across the receiver's compile-time hierarchy, removes overridden
+signatures, and applies the JLS 15.12.2 phases in order: strict invocation, loose
+invocation, then variable arity. Within the first applicable phase it compares
+parameter types pairwise for specificity. Boxing, primitive widening, arrays,
+null, and concrete invariant generic arguments retain their distinct meanings.
+Lexically scoped argument facts travel in Java's existing `captureSideChannel`,
+so parse workers and durable cache replay make the same decisions.
+Static imports use accessible overload families; an explicit import shadows only
+the same signature from an on-demand import. Enum constant bodies and implicit
+record accessors reuse the provider's existing synthetic identities. Field,
+hierarchy, and subtype walks memoize shared ancestors while preserving cycle and
+depth bounds.
+
+The provider models a bounded set of qualified JDK type relationships and
+`Arrays.asList` / `Collections.singletonList` calls. These factories are poly
+expressions: their type arguments depend on the candidate's parameter type.
+Source declarations and lexical shadowing take precedence over these summaries.
+Unsupported generic inference, unavailable argument types, and incomplete
+inherited inventories leave an overload unresolved. A complete original family
+containing only one declaration may retain the existing unique-member inference
+for opaque arguments; this exception does not apply to a family narrowed to one
+candidate. Runtime implementation dispatch follows the selected signature.
+It preserves the original receiver's subtype bound and excludes static and private
+methods from virtual dispatch.
+
+Captures also retain callable-owned return annotations and structural identities
+for `var` initializers. The shared passes keep the selected compile-time method
+separate from runtime dispatch targets, then propagate owned return types to
+untyped assignments and revisit waiting receiver calls through a dependency
+queue. Sequential assignments have no expression-depth cutoff. Unresolved producers
+cannot restore an earlier name-based return guess. Generic implementation
+signatures substitute a whole formal type parameter only when concrete source
+heritage proves its binding; unsupported substitutions remain unknown.
+
+Suppressed overload calls record graph identities for their candidate methods,
+declaring owners, and caller. `unresolved-overloads.ts` reconciles decisions per
+call site and resolution phase and persists a bounded `unresolvedOverloads`
+receipt, including an explicit complete-empty receipt. `impact` and `context`
+report matching omitted sites through `causes.overloadResolution` and
+`epistemic: 'lower-bound'`; possible callers never become ordinary `CALLS`
+edges or inflate known caller counts. Impact checks the nodes whose neighbors
+were actually expanded before pagination, using candidate identities upstream
+and caller identities downstream. Missing, malformed, or capped receipts retain
+an uncertainty boundary. Re-analyze with the current build (`--force` when
+repairing metadata) to replace a legacy receipt.
+
### Optional CFG/PDG emission (`--pdg`, #2081–#2086)
On a `--pdg` run providers build control-flow graphs through `LanguageProvider.cfgVisitor` (Tree-sitter ASTs) or `collectSourceCfgs` (standalone source processing). Every supported language supplies one of these hooks; Vue covers executable scripts. AST visitors run in the parse worker, while source-backed providers run during fresh scope extraction. Both produce plain `ParsedFile.cfgSideChannel` data. Scope-resolution emits the program-dependence layers **inside Phase 4 of `runScopeResolution`, while the disk-backed ParsedFile store is still live**. The store is cleared after that phase, so a later standalone phase cannot consume it.
diff --git a/gitnexus/bench/java-overload-resolution/README.md b/gitnexus/bench/java-overload-resolution/README.md
new file mode 100644
index 000000000..96e5a098e
--- /dev/null
+++ b/gitnexus/bench/java-overload-resolution/README.md
@@ -0,0 +1,67 @@
+# Java overload and ingestion scaling
+
+Build the package, then measure the current implementation:
+
+```sh
+npm run build
+node bench/java-overload-resolution/measure.js --check --output /tmp/java-overload-check.json
+```
+
+CI runs this check in the scope-resolution benchmark job and uploads
+`java-overload-resolution-benchmark.json` with that job's measurements.
+
+`measure.js` runs three fresh-process samples at 128, 512 and 2,048 units. Each
+unit adds seven methods and four calls. Two shapes exercise increasing file
+counts and increasing declarations within one class. Every overload family has
+two candidates; package width and inheritance depth stay fixed. The spread
+shape uses separate packages with an explicit import so package-size limits
+cannot silently reduce resolution work.
+
+Each sample runs real worker parsing, scope resolution, graph construction and
+database writes, with a cold cache, a warm cache and one edited source file.
+The edit casts one argument to `Object`, changing exactly one selected overload.
+The calls cover reference specificity, widening before boxing, `var` receiver
+inference through a factory call and cross-file member resolution. Expected target IDs
+are generated independently of the implementation. Missing methods or calls,
+incorrect targets, suppressed calls, extraction failures, incorrect cache reuse
+and truncated process detection all fail the run.
+
+The harness uses one parser worker, private temporary storage, a 128 MiB database
+buffer pool, and no FTS or embeddings. It raises the file-size and process limits
+to include the entire generated corpus. Each scenario forces graph reconstruction
+and persistence; warm and changed scenarios reuse parse-cache chunks. Editing one
+file can reparse other files in its cache chunk. This measures full graph writes,
+not the separate selective database-update path.
+
+The report includes raw samples, median wall time and process CPU time for the
+whole analysis and the scope-resolution phase, main-thread heap at completion,
+and process peak RSS (including workers/native memory). Peak RSS is cumulative
+across the three scenarios in each child. Heap is sampled before an explicit
+post-analysis collection. The built JavaScript digest identifies the measured
+runtime and detects changes to it during the run.
+
+`--check` verifies the committed corpus, call-target and cache fingerprints, then
+gates every adjacent size pair and the full span. For every time and memory
+metric, `(large cost / small cost) / (large size / small size)` must be at most
+**1.5**. The shared gate and its quadratic-growth tests come from
+`../method-declaration-facts/scaling.mjs`. Absolute baseline timings are reference
+measurements, not machine-independent thresholds.
+
+These are bounded scaling checks, not a proof of linear complexity for arbitrary
+Java programs. In particular, this corpus does not increase overload-family
+width, inheritance depth or generic constraint complexity. Startup and database
+costs can hide growth at small sizes; the separate scope measurements and
+deterministic lookup-work regression tests help expose it.
+
+After reviewing an intentional workload or semantic change, record a baseline:
+
+```sh
+node bench/java-overload-resolution/measure.js --record --output /tmp/java-overload-record.json
+```
+
+Recording also requires all scaling and correctness checks to pass, uses at least
+three samples, and is disabled in CI. `--root /path/to/built/gitnexus` selects an
+isolated runtime. `--sizes 128,512,2048 --samples 1` is useful for diagnostic runs;
+one-sample runs cannot record or check a baseline. Completed samples are saved
+after each child so a later failure leaves usable diagnostic evidence.
+The output path must not already exist; existing files and symlinks are rejected.
diff --git a/gitnexus/bench/java-overload-resolution/baseline.json b/gitnexus/bench/java-overload-resolution/baseline.json
new file mode 100644
index 000000000..354fc0795
--- /dev/null
+++ b/gitnexus/bench/java-overload-resolution/baseline.json
@@ -0,0 +1,926 @@
+{
+ "_note": "Measured baseline. --check gates same-run normalized growth at 1.5 and exact call targets/cache counts. Absolute timings and memory are references, not portable budgets.",
+ "schemaVersion": 1,
+ "recordedAt": "2026-10-31T19:46:00.972Z",
+ "runtimeSha256": "4b3e97e7ebb9f21e6aa4048f584fe10fd5ac049d5a4b60488947f45b01fc2057",
+ "workload": {
+ "sizes": [128, 512, 2048],
+ "shapes": ["spread", "concentrated"],
+ "scenarios": ["cold", "warm", "changed"],
+ "overloadsPerFamily": 2,
+ "callsPerUnit": 4,
+ "workers": 1
+ },
+ "environment": {
+ "node": "v22.23.3",
+ "platform": "linux",
+ "arch": "x64",
+ "cpu": "12th Gen Intel(R) Core(TM) i9-12900K"
+ },
+ "correctness": [
+ {
+ "shape": "spread",
+ "size": 128,
+ "scenario": "cold",
+ "corpus": "ba00fdce998f6c93ded7b9e48249deb2195f435cd9f498d2585c028a4745b18c",
+ "correctness": {
+ "files": 129,
+ "methods": 898,
+ "calls": 512,
+ "fingerprint": "9c7584e6908a263beed06089876413d7d73a0d787d1eff7681dde0dd26f7b672",
+ "reparsed": 129,
+ "hits": 0
+ }
+ },
+ {
+ "shape": "spread",
+ "size": 128,
+ "scenario": "warm",
+ "corpus": "ba00fdce998f6c93ded7b9e48249deb2195f435cd9f498d2585c028a4745b18c",
+ "correctness": {
+ "files": 129,
+ "methods": 898,
+ "calls": 512,
+ "fingerprint": "9c7584e6908a263beed06089876413d7d73a0d787d1eff7681dde0dd26f7b672",
+ "reparsed": 0,
+ "hits": 129
+ }
+ },
+ {
+ "shape": "spread",
+ "size": 128,
+ "scenario": "changed",
+ "corpus": "ba00fdce998f6c93ded7b9e48249deb2195f435cd9f498d2585c028a4745b18c",
+ "correctness": {
+ "files": 129,
+ "methods": 898,
+ "calls": 512,
+ "fingerprint": "def7d18a8b462dcf231bd2192afebe764911163d3ffc65e75fe5fefeb7e3a54c",
+ "reparsed": 2,
+ "hits": 127
+ }
+ },
+ {
+ "shape": "spread",
+ "size": 512,
+ "scenario": "cold",
+ "corpus": "dd070e75fcba816f5775dff5efbfdd3fdda28e4b01d0c1603fd9e294587896c7",
+ "correctness": {
+ "files": 513,
+ "methods": 3586,
+ "calls": 2048,
+ "fingerprint": "f126c9b94d15f6c85f55e802db43430abbb378a6e82a78492b3d6e8fcc18a69b",
+ "reparsed": 513,
+ "hits": 0
+ }
+ },
+ {
+ "shape": "spread",
+ "size": 512,
+ "scenario": "warm",
+ "corpus": "dd070e75fcba816f5775dff5efbfdd3fdda28e4b01d0c1603fd9e294587896c7",
+ "correctness": {
+ "files": 513,
+ "methods": 3586,
+ "calls": 2048,
+ "fingerprint": "f126c9b94d15f6c85f55e802db43430abbb378a6e82a78492b3d6e8fcc18a69b",
+ "reparsed": 0,
+ "hits": 513
+ }
+ },
+ {
+ "shape": "spread",
+ "size": 512,
+ "scenario": "changed",
+ "corpus": "dd070e75fcba816f5775dff5efbfdd3fdda28e4b01d0c1603fd9e294587896c7",
+ "correctness": {
+ "files": 513,
+ "methods": 3586,
+ "calls": 2048,
+ "fingerprint": "360cdbc01738746de63cca79adba7410797749c171d3e324579a7a53deee56d0",
+ "reparsed": 5,
+ "hits": 508
+ }
+ },
+ {
+ "shape": "spread",
+ "size": 2048,
+ "scenario": "cold",
+ "corpus": "5566903502880e054a6e2779f37d4c71da7333cfef65fb51e60d7ce3d03f8c77",
+ "correctness": {
+ "files": 2049,
+ "methods": 14338,
+ "calls": 8192,
+ "fingerprint": "9c07a075de05a15a73b0649a9fb45b730e6a6bf68d5152a315e5fae4fd1365a7",
+ "reparsed": 2049,
+ "hits": 0
+ }
+ },
+ {
+ "shape": "spread",
+ "size": 2048,
+ "scenario": "warm",
+ "corpus": "5566903502880e054a6e2779f37d4c71da7333cfef65fb51e60d7ce3d03f8c77",
+ "correctness": {
+ "files": 2049,
+ "methods": 14338,
+ "calls": 8192,
+ "fingerprint": "9c07a075de05a15a73b0649a9fb45b730e6a6bf68d5152a315e5fae4fd1365a7",
+ "reparsed": 0,
+ "hits": 2049
+ }
+ },
+ {
+ "shape": "spread",
+ "size": 2048,
+ "scenario": "changed",
+ "corpus": "5566903502880e054a6e2779f37d4c71da7333cfef65fb51e60d7ce3d03f8c77",
+ "correctness": {
+ "files": 2049,
+ "methods": 14338,
+ "calls": 8192,
+ "fingerprint": "8b2269dfa3d017f04e05f86afd8f6cac6364c1228acab0507de5287feb249969",
+ "reparsed": 15,
+ "hits": 2034
+ }
+ },
+ {
+ "shape": "concentrated",
+ "size": 128,
+ "scenario": "cold",
+ "corpus": "4043c0f69f54bcb6409bfeb70b974c2aa82b373d28f92f294a60c8199a8272fb",
+ "correctness": {
+ "files": 2,
+ "methods": 898,
+ "calls": 512,
+ "fingerprint": "f2f037938ed3b85bfac7ea91c8460bcef0bea3b73c38b84630e2ac36d8185b5d",
+ "reparsed": 2,
+ "hits": 0
+ }
+ },
+ {
+ "shape": "concentrated",
+ "size": 128,
+ "scenario": "warm",
+ "corpus": "4043c0f69f54bcb6409bfeb70b974c2aa82b373d28f92f294a60c8199a8272fb",
+ "correctness": {
+ "files": 2,
+ "methods": 898,
+ "calls": 512,
+ "fingerprint": "f2f037938ed3b85bfac7ea91c8460bcef0bea3b73c38b84630e2ac36d8185b5d",
+ "reparsed": 0,
+ "hits": 2
+ }
+ },
+ {
+ "shape": "concentrated",
+ "size": 128,
+ "scenario": "changed",
+ "corpus": "4043c0f69f54bcb6409bfeb70b974c2aa82b373d28f92f294a60c8199a8272fb",
+ "correctness": {
+ "files": 2,
+ "methods": 898,
+ "calls": 512,
+ "fingerprint": "5e4d7c334a4a1f6de8b594ae62d0792bd5895b828893103af8bbc4789f1f11fb",
+ "reparsed": 1,
+ "hits": 1
+ }
+ },
+ {
+ "shape": "concentrated",
+ "size": 512,
+ "scenario": "cold",
+ "corpus": "d6c830c77f6515d7e9157bc48d96a2bfa3ec3b7c81e315c61738ae5a5079a579",
+ "correctness": {
+ "files": 2,
+ "methods": 3586,
+ "calls": 2048,
+ "fingerprint": "e1e74e9da813db36ccea611bf2374dbcc079b4dc29466d5da127879d08cdaf00",
+ "reparsed": 2,
+ "hits": 0
+ }
+ },
+ {
+ "shape": "concentrated",
+ "size": 512,
+ "scenario": "warm",
+ "corpus": "d6c830c77f6515d7e9157bc48d96a2bfa3ec3b7c81e315c61738ae5a5079a579",
+ "correctness": {
+ "files": 2,
+ "methods": 3586,
+ "calls": 2048,
+ "fingerprint": "e1e74e9da813db36ccea611bf2374dbcc079b4dc29466d5da127879d08cdaf00",
+ "reparsed": 0,
+ "hits": 2
+ }
+ },
+ {
+ "shape": "concentrated",
+ "size": 512,
+ "scenario": "changed",
+ "corpus": "d6c830c77f6515d7e9157bc48d96a2bfa3ec3b7c81e315c61738ae5a5079a579",
+ "correctness": {
+ "files": 2,
+ "methods": 3586,
+ "calls": 2048,
+ "fingerprint": "c9beb4f12867432b2e53a6e52d337b39dfc0b05e1a18939575579157b425d29f",
+ "reparsed": 1,
+ "hits": 1
+ }
+ },
+ {
+ "shape": "concentrated",
+ "size": 2048,
+ "scenario": "cold",
+ "corpus": "b0698581ac113f8b46b4cc13cc1a8744b37aa41945e40245423eef383ca296b3",
+ "correctness": {
+ "files": 2,
+ "methods": 14338,
+ "calls": 8192,
+ "fingerprint": "7f349aefb5d96c2d46cda31484a7d696e26c4e55253336625d001462799b0de6",
+ "reparsed": 2,
+ "hits": 0
+ }
+ },
+ {
+ "shape": "concentrated",
+ "size": 2048,
+ "scenario": "warm",
+ "corpus": "b0698581ac113f8b46b4cc13cc1a8744b37aa41945e40245423eef383ca296b3",
+ "correctness": {
+ "files": 2,
+ "methods": 14338,
+ "calls": 8192,
+ "fingerprint": "7f349aefb5d96c2d46cda31484a7d696e26c4e55253336625d001462799b0de6",
+ "reparsed": 0,
+ "hits": 2
+ }
+ },
+ {
+ "shape": "concentrated",
+ "size": 2048,
+ "scenario": "changed",
+ "corpus": "b0698581ac113f8b46b4cc13cc1a8744b37aa41945e40245423eef383ca296b3",
+ "correctness": {
+ "files": 2,
+ "methods": 14338,
+ "calls": 8192,
+ "fingerprint": "1786baf149b46e933b59c54f2db3f6b3e1cb4d6e356497029546cd1afbe738fb",
+ "reparsed": 1,
+ "hits": 1
+ }
+ }
+ ],
+ "measured": [
+ {
+ "shape": "spread",
+ "size": 128,
+ "scenario": "cold",
+ "corpus": "ba00fdce998f6c93ded7b9e48249deb2195f435cd9f498d2585c028a4745b18c",
+ "correctness": {
+ "files": 129,
+ "methods": 898,
+ "calls": 512,
+ "fingerprint": "9c7584e6908a263beed06089876413d7d73a0d787d1eff7681dde0dd26f7b672",
+ "reparsed": 129,
+ "hits": 0
+ },
+ "wallMs": 6382.313993999999,
+ "cpuMs": 6573.025,
+ "scopeWallMs": 265.60164499999973,
+ "scopeCpuMs": 422.705,
+ "heapBytes": 72082216,
+ "peakRssBytes": 563265536
+ },
+ {
+ "shape": "spread",
+ "size": 128,
+ "scenario": "warm",
+ "corpus": "ba00fdce998f6c93ded7b9e48249deb2195f435cd9f498d2585c028a4745b18c",
+ "correctness": {
+ "files": 129,
+ "methods": 898,
+ "calls": 512,
+ "fingerprint": "9c7584e6908a263beed06089876413d7d73a0d787d1eff7681dde0dd26f7b672",
+ "reparsed": 0,
+ "hits": 129
+ },
+ "wallMs": 2412.3159590000005,
+ "cpuMs": 2055.392,
+ "scopeWallMs": 236.99802499999987,
+ "scopeCpuMs": 423.928,
+ "heapBytes": 106224584,
+ "peakRssBytes": 693342208
+ },
+ {
+ "shape": "spread",
+ "size": 128,
+ "scenario": "changed",
+ "corpus": "ba00fdce998f6c93ded7b9e48249deb2195f435cd9f498d2585c028a4745b18c",
+ "correctness": {
+ "files": 129,
+ "methods": 898,
+ "calls": 512,
+ "fingerprint": "def7d18a8b462dcf231bd2192afebe764911163d3ffc65e75fe5fefeb7e3a54c",
+ "reparsed": 2,
+ "hits": 127
+ },
+ "wallMs": 3324.9829769999997,
+ "cpuMs": 2986.027,
+ "scopeWallMs": 279.4751209999995,
+ "scopeCpuMs": 570.481,
+ "heapBytes": 74642032,
+ "peakRssBytes": 747954176
+ },
+ {
+ "shape": "spread",
+ "size": 512,
+ "scenario": "cold",
+ "corpus": "dd070e75fcba816f5775dff5efbfdd3fdda28e4b01d0c1603fd9e294587896c7",
+ "correctness": {
+ "files": 513,
+ "methods": 3586,
+ "calls": 2048,
+ "fingerprint": "f126c9b94d15f6c85f55e802db43430abbb378a6e82a78492b3d6e8fcc18a69b",
+ "reparsed": 513,
+ "hits": 0
+ },
+ "wallMs": 9657.072656,
+ "cpuMs": 11590.296,
+ "scopeWallMs": 787.7755969999998,
+ "scopeCpuMs": 1342.358,
+ "heapBytes": 83321312,
+ "peakRssBytes": 641908736
+ },
+ {
+ "shape": "spread",
+ "size": 512,
+ "scenario": "warm",
+ "corpus": "dd070e75fcba816f5775dff5efbfdd3fdda28e4b01d0c1603fd9e294587896c7",
+ "correctness": {
+ "files": 513,
+ "methods": 3586,
+ "calls": 2048,
+ "fingerprint": "f126c9b94d15f6c85f55e802db43430abbb378a6e82a78492b3d6e8fcc18a69b",
+ "reparsed": 0,
+ "hits": 513
+ },
+ "wallMs": 4034.202557999999,
+ "cpuMs": 4545.678,
+ "scopeWallMs": 876.6461720000007,
+ "scopeCpuMs": 1818.994,
+ "heapBytes": 140587192,
+ "peakRssBytes": 788180992
+ },
+ {
+ "shape": "spread",
+ "size": 512,
+ "scenario": "changed",
+ "corpus": "dd070e75fcba816f5775dff5efbfdd3fdda28e4b01d0c1603fd9e294587896c7",
+ "correctness": {
+ "files": 513,
+ "methods": 3586,
+ "calls": 2048,
+ "fingerprint": "360cdbc01738746de63cca79adba7410797749c171d3e324579a7a53deee56d0",
+ "reparsed": 5,
+ "hits": 508
+ },
+ "wallMs": 4542.365702000001,
+ "cpuMs": 4986.714,
+ "scopeWallMs": 745.671574,
+ "scopeCpuMs": 1462.018,
+ "heapBytes": 141094984,
+ "peakRssBytes": 897118208
+ },
+ {
+ "shape": "spread",
+ "size": 2048,
+ "scenario": "cold",
+ "corpus": "5566903502880e054a6e2779f37d4c71da7333cfef65fb51e60d7ce3d03f8c77",
+ "correctness": {
+ "files": 2049,
+ "methods": 14338,
+ "calls": 8192,
+ "fingerprint": "9c07a075de05a15a73b0649a9fb45b730e6a6bf68d5152a315e5fae4fd1365a7",
+ "reparsed": 2049,
+ "hits": 0
+ },
+ "wallMs": 22421.167622,
+ "cpuMs": 28309.111,
+ "scopeWallMs": 2729.000151,
+ "scopeCpuMs": 4387.326,
+ "heapBytes": 110154224,
+ "peakRssBytes": 760012800
+ },
+ {
+ "shape": "spread",
+ "size": 2048,
+ "scenario": "warm",
+ "corpus": "5566903502880e054a6e2779f37d4c71da7333cfef65fb51e60d7ce3d03f8c77",
+ "correctness": {
+ "files": 2049,
+ "methods": 14338,
+ "calls": 8192,
+ "fingerprint": "9c07a075de05a15a73b0649a9fb45b730e6a6bf68d5152a315e5fae4fd1365a7",
+ "reparsed": 0,
+ "hits": 2049
+ },
+ "wallMs": 7275.022228999998,
+ "cpuMs": 9205.167,
+ "scopeWallMs": 2452.0143179999977,
+ "scopeCpuMs": 4076.376,
+ "heapBytes": 216411400,
+ "peakRssBytes": 931454976
+ },
+ {
+ "shape": "spread",
+ "size": 2048,
+ "scenario": "changed",
+ "corpus": "5566903502880e054a6e2779f37d4c71da7333cfef65fb51e60d7ce3d03f8c77",
+ "correctness": {
+ "files": 2049,
+ "methods": 14338,
+ "calls": 8192,
+ "fingerprint": "8b2269dfa3d017f04e05f86afd8f6cac6364c1228acab0507de5287feb249969",
+ "reparsed": 15,
+ "hits": 2034
+ },
+ "wallMs": 8042.547480000001,
+ "cpuMs": 10680.432,
+ "scopeWallMs": 2782.9499240000005,
+ "scopeCpuMs": 4797.166,
+ "heapBytes": 221611264,
+ "peakRssBytes": 1018769408
+ },
+ {
+ "shape": "concentrated",
+ "size": 128,
+ "scenario": "cold",
+ "corpus": "4043c0f69f54bcb6409bfeb70b974c2aa82b373d28f92f294a60c8199a8272fb",
+ "correctness": {
+ "files": 2,
+ "methods": 898,
+ "calls": 512,
+ "fingerprint": "f2f037938ed3b85bfac7ea91c8460bcef0bea3b73c38b84630e2ac36d8185b5d",
+ "reparsed": 2,
+ "hits": 0
+ },
+ "wallMs": 5724.462823000001,
+ "cpuMs": 5990.56,
+ "scopeWallMs": 216.9987230000006,
+ "scopeCpuMs": 359.581,
+ "heapBytes": 66736864,
+ "peakRssBytes": 549969920
+ },
+ {
+ "shape": "concentrated",
+ "size": 128,
+ "scenario": "warm",
+ "corpus": "4043c0f69f54bcb6409bfeb70b974c2aa82b373d28f92f294a60c8199a8272fb",
+ "correctness": {
+ "files": 2,
+ "methods": 898,
+ "calls": 512,
+ "fingerprint": "f2f037938ed3b85bfac7ea91c8460bcef0bea3b73c38b84630e2ac36d8185b5d",
+ "reparsed": 0,
+ "hits": 2
+ },
+ "wallMs": 2307.437070000001,
+ "cpuMs": 1697.642,
+ "scopeWallMs": 165.59543699999995,
+ "scopeCpuMs": 366.701,
+ "heapBytes": 89981272,
+ "peakRssBytes": 673837056
+ },
+ {
+ "shape": "concentrated",
+ "size": 128,
+ "scenario": "changed",
+ "corpus": "4043c0f69f54bcb6409bfeb70b974c2aa82b373d28f92f294a60c8199a8272fb",
+ "correctness": {
+ "files": 2,
+ "methods": 898,
+ "calls": 512,
+ "fingerprint": "5e4d7c334a4a1f6de8b594ae62d0792bd5895b828893103af8bbc4789f1f11fb",
+ "reparsed": 1,
+ "hits": 1
+ },
+ "wallMs": 3410.6907119999996,
+ "cpuMs": 3338.962,
+ "scopeWallMs": 166.35203,
+ "scopeCpuMs": 366.192,
+ "heapBytes": 69753416,
+ "peakRssBytes": 766402560
+ },
+ {
+ "shape": "concentrated",
+ "size": 512,
+ "scenario": "cold",
+ "corpus": "d6c830c77f6515d7e9157bc48d96a2bfa3ec3b7c81e315c61738ae5a5079a579",
+ "correctness": {
+ "files": 2,
+ "methods": 3586,
+ "calls": 2048,
+ "fingerprint": "e1e74e9da813db36ccea611bf2374dbcc079b4dc29466d5da127879d08cdaf00",
+ "reparsed": 2,
+ "hits": 0
+ },
+ "wallMs": 9268.474702000001,
+ "cpuMs": 10475.483,
+ "scopeWallMs": 625.0715480000008,
+ "scopeCpuMs": 1136.577,
+ "heapBytes": 116100256,
+ "peakRssBytes": 584364032
+ },
+ {
+ "shape": "concentrated",
+ "size": 512,
+ "scenario": "warm",
+ "corpus": "d6c830c77f6515d7e9157bc48d96a2bfa3ec3b7c81e315c61738ae5a5079a579",
+ "correctness": {
+ "files": 2,
+ "methods": 3586,
+ "calls": 2048,
+ "fingerprint": "e1e74e9da813db36ccea611bf2374dbcc079b4dc29466d5da127879d08cdaf00",
+ "reparsed": 0,
+ "hits": 2
+ },
+ "wallMs": 2986.604102000001,
+ "cpuMs": 3221.225,
+ "scopeWallMs": 582.4847769999997,
+ "scopeCpuMs": 1302.585,
+ "heapBytes": 115896680,
+ "peakRssBytes": 776753152
+ },
+ {
+ "shape": "concentrated",
+ "size": 512,
+ "scenario": "changed",
+ "corpus": "d6c830c77f6515d7e9157bc48d96a2bfa3ec3b7c81e315c61738ae5a5079a579",
+ "correctness": {
+ "files": 2,
+ "methods": 3586,
+ "calls": 2048,
+ "fingerprint": "c9beb4f12867432b2e53a6e52d337b39dfc0b05e1a18939575579157b425d29f",
+ "reparsed": 1,
+ "hits": 1
+ },
+ "wallMs": 6360.436007999999,
+ "cpuMs": 7743.869,
+ "scopeWallMs": 510.34782500000074,
+ "scopeCpuMs": 1284.846,
+ "heapBytes": 123689144,
+ "peakRssBytes": 958959616
+ },
+ {
+ "shape": "concentrated",
+ "size": 2048,
+ "scenario": "cold",
+ "corpus": "b0698581ac113f8b46b4cc13cc1a8744b37aa41945e40245423eef383ca296b3",
+ "correctness": {
+ "files": 2,
+ "methods": 14338,
+ "calls": 8192,
+ "fingerprint": "7f349aefb5d96c2d46cda31484a7d696e26c4e55253336625d001462799b0de6",
+ "reparsed": 2,
+ "hits": 0
+ },
+ "wallMs": 21677.11593,
+ "cpuMs": 25262.452,
+ "scopeWallMs": 2939.4483809999983,
+ "scopeCpuMs": 4160.173,
+ "heapBytes": 97430168,
+ "peakRssBytes": 883941376
+ },
+ {
+ "shape": "concentrated",
+ "size": 2048,
+ "scenario": "warm",
+ "corpus": "b0698581ac113f8b46b4cc13cc1a8744b37aa41945e40245423eef383ca296b3",
+ "correctness": {
+ "files": 2,
+ "methods": 14338,
+ "calls": 8192,
+ "fingerprint": "7f349aefb5d96c2d46cda31484a7d696e26c4e55253336625d001462799b0de6",
+ "reparsed": 0,
+ "hits": 2
+ },
+ "wallMs": 4958.608856999999,
+ "cpuMs": 6291.745,
+ "scopeWallMs": 2304.6741980000006,
+ "scopeCpuMs": 3572.083,
+ "heapBytes": 123585896,
+ "peakRssBytes": 1016422400
+ },
+ {
+ "shape": "concentrated",
+ "size": 2048,
+ "scenario": "changed",
+ "corpus": "b0698581ac113f8b46b4cc13cc1a8744b37aa41945e40245423eef383ca296b3",
+ "correctness": {
+ "files": 2,
+ "methods": 14338,
+ "calls": 8192,
+ "fingerprint": "1786baf149b46e933b59c54f2db3f6b3e1cb4d6e356497029546cd1afbe738fb",
+ "reparsed": 1,
+ "hits": 1
+ },
+ "wallMs": 18368.502542000002,
+ "cpuMs": 22049.709,
+ "scopeWallMs": 2615.8797609999965,
+ "scopeCpuMs": 4184.537,
+ "heapBytes": 126797336,
+ "peakRssBytes": 1626734592
+ }
+ ],
+ "scaling": {
+ "spread/cold": {
+ "budget": 1.5,
+ "comparisons": [
+ {
+ "from": 128,
+ "to": 512,
+ "inputRatio": 4,
+ "ratios": {
+ "wallMs": 0.37827473958029156,
+ "cpuMs": 0.44082808143891133,
+ "scopeWallMs": 0.7415010522619322,
+ "scopeCpuMs": 0.7939094640470304,
+ "heapBytes": 0.2889801279139365,
+ "peakRssBytes": 0.28490502923296196
+ },
+ "passed": true
+ },
+ {
+ "from": 512,
+ "to": 2048,
+ "inputRatio": 4,
+ "ratios": {
+ "wallMs": 0.5804338545612368,
+ "cpuMs": 0.6106209668847111,
+ "scopeWallMs": 0.8660461689193454,
+ "scopeCpuMs": 0.8170931301485893,
+ "heapBytes": 0.33051035010106417,
+ "peakRssBytes": 0.29599721789734296
+ },
+ "passed": true
+ },
+ {
+ "from": 128,
+ "to": 2048,
+ "inputRatio": 16,
+ "ratios": {
+ "wallMs": 0.21956346517773664,
+ "cpuMs": 0.2691788693181602,
+ "scopeWallMs": 0.6421741455611097,
+ "scopeCpuMs": 0.648697969032777,
+ "heapBytes": 0.09551092324908546,
+ "peakRssBytes": 0.08433109601791791
+ },
+ "passed": true
+ }
+ ],
+ "passed": true
+ },
+ "spread/warm": {
+ "budget": 1.5,
+ "comparisons": [
+ {
+ "from": 128,
+ "to": 512,
+ "inputRatio": 4,
+ "ratios": {
+ "wallMs": 0.41808397268079406,
+ "cpuMs": 0.5528967223770453,
+ "scopeWallMs": 0.9247399551114415,
+ "scopeCpuMs": 1.0727022041478742,
+ "heapBytes": 0.33087254076702244,
+ "peakRssBytes": 0.284196239211215
+ },
+ "passed": true
+ },
+ {
+ "from": 512,
+ "to": 2048,
+ "inputRatio": 4,
+ "ratios": {
+ "wallMs": 0.45083397055592267,
+ "cpuMs": 0.506259297292945,
+ "scopeWallMs": 0.6992599740685332,
+ "scopeCpuMs": 0.5602514356836802,
+ "heapBytes": 0.38483484327647716,
+ "peakRssBytes": 0.29544450622833596
+ },
+ "passed": true
+ },
+ {
+ "from": 128,
+ "to": 2048,
+ "inputRatio": 16,
+ "ratios": {
+ "wallMs": 0.1884864574294763,
+ "cpuMs": 0.27990910614617553,
+ "scopeWallMs": 0.6466336370313632,
+ "scopeCpuMs": 0.6009829499348947,
+ "heapBytes": 0.12733128237056687,
+ "peakRssBytes": 0.08396421756570746
+ },
+ "passed": true
+ }
+ ],
+ "passed": true
+ },
+ "spread/changed": {
+ "budget": 1.5,
+ "comparisons": [
+ {
+ "from": 128,
+ "to": 512,
+ "inputRatio": 4,
+ "ratios": {
+ "wallMs": 0.3415330043357393,
+ "cpuMs": 0.4175040949060407,
+ "scopeWallMs": 0.6670285814099364,
+ "scopeCpuMs": 0.6406953079944818,
+ "heapBytes": 0.47257215612779674,
+ "peakRssBytes": 0.29985734313220813
+ },
+ "passed": true
+ },
+ {
+ "from": 512,
+ "to": 2048,
+ "inputRatio": 4,
+ "ratios": {
+ "wallMs": 0.44264090606238904,
+ "cpuMs": 0.5354443828140134,
+ "scopeWallMs": 0.9330347370865449,
+ "scopeCpuMs": 0.8202987240923163,
+ "heapBytes": 0.3926632572565443,
+ "peakRssBytes": 0.28390054925738395
+ },
+ "passed": true
+ },
+ {
+ "from": 128,
+ "to": 2048,
+ "inputRatio": 16,
+ "ratios": {
+ "wallMs": 0.15117647848938148,
+ "cpuMs": 0.22355022241928824,
+ "scopeWallMs": 0.6223608370850311,
+ "scopeCpuMs": 0.525561543679807,
+ "heapBytes": 0.18556172211388886,
+ "peakRssBytes": 0.08512966441409373
+ },
+ "passed": true
+ }
+ ],
+ "passed": true
+ },
+ "concentrated/cold": {
+ "budget": 1.5,
+ "comparisons": [
+ {
+ "from": 128,
+ "to": 512,
+ "inputRatio": 4,
+ "ratios": {
+ "wallMs": 0.4047748665936266,
+ "cpuMs": 0.43716626659277263,
+ "scopeWallMs": 0.7201327493526298,
+ "scopeCpuMs": 0.7902092991565183,
+ "heapBytes": 0.43491800873352393,
+ "peakRssBytes": 0.26563454234006106
+ },
+ "passed": true
+ },
+ {
+ "from": 512,
+ "to": 2048,
+ "inputRatio": 4,
+ "ratios": {
+ "wallMs": 0.5847001968220941,
+ "cpuMs": 0.6028946827559168,
+ "scopeWallMs": 1.1756447683489806,
+ "scopeCpuMs": 0.9150662471614329,
+ "heapBytes": 0.20979748744051005,
+ "peakRssBytes": 0.37816383606580356
+ },
+ "passed": true
+ },
+ {
+ "from": 128,
+ "to": 2048,
+ "inputRatio": 16,
+ "ratios": {
+ "wallMs": 0.23667194416593032,
+ "cpuMs": 0.2635652176090382,
+ "scopeWallMs": 0.8466202992931869,
+ "scopeCpuMs": 0.7230938578512212,
+ "heapBytes": 0.09124470547492312,
+ "peakRssBytes": 0.10045337752290162
+ },
+ "passed": true
+ }
+ ],
+ "passed": true
+ },
+ "concentrated/warm": {
+ "budget": 1.5,
+ "comparisons": [
+ {
+ "from": 128,
+ "to": 512,
+ "inputRatio": 4,
+ "ratios": {
+ "wallMs": 0.3235845671405461,
+ "cpuMs": 0.4743675344978505,
+ "scopeWallMs": 0.87937926846378,
+ "scopeCpuMs": 0.8880429832479322,
+ "heapBytes": 0.3220022273079225,
+ "peakRssBytes": 0.2881828570733872
+ },
+ "passed": true
+ },
+ {
+ "from": 512,
+ "to": 2048,
+ "inputRatio": 4,
+ "ratios": {
+ "wallMs": 0.4150708202067551,
+ "cpuMs": 0.4883037509022189,
+ "scopeWallMs": 0.9891564076017054,
+ "scopeCpuMs": 0.6855757973567944,
+ "heapBytes": 0.2665863595057253,
+ "peakRssBytes": 0.32713816396589274
+ },
+ "passed": true
+ },
+ {
+ "from": 128,
+ "to": 2048,
+ "inputRatio": 16,
+ "ratios": {
+ "wallMs": 0.1343105116892743,
+ "cpuMs": 0.23163544640153813,
+ "scopeWallMs": 0.8698436381130483,
+ "scopeCpuMs": 0.6088207763273075,
+ "heapBytes": 0.08584140153075409,
+ "peakRssBytes": 0.09427561074943316
+ },
+ "passed": true
+ }
+ ],
+ "passed": true
+ },
+ "concentrated/changed": {
+ "budget": 1.5,
+ "comparisons": [
+ {
+ "from": 128,
+ "to": 512,
+ "inputRatio": 4,
+ "ratios": {
+ "wallMs": 0.4662131914821363,
+ "cpuMs": 0.579811105966465,
+ "scopeWallMs": 0.766969638122241,
+ "scopeCpuMs": 0.8771668960545288,
+ "heapBytes": 0.4433085542362542,
+ "peakRssBytes": 0.3128119822564267
+ },
+ "passed": true
+ },
+ {
+ "from": 512,
+ "to": 2048,
+ "inputRatio": 4,
+ "ratios": {
+ "wallMs": 0.7219828372967103,
+ "cpuMs": 0.7118440730337767,
+ "scopeWallMs": 1.281420059446708,
+ "scopeCpuMs": 0.8142098352643041,
+ "heapBytes": 0.25628226515982677,
+ "peakRssBytes": 0.42408839873398796
+ },
+ "passed": true
+ },
+ {
+ "from": 128,
+ "to": 2048,
+ "inputRatio": 16,
+ "ratios": {
+ "wallMs": 0.33659792277142725,
+ "cpuMs": 0.4127350992613872,
+ "scopeWallMs": 0.9828102792764222,
+ "scopeCpuMs": 0.7141979139358589,
+ "heapBytes": 0.11361212044439516,
+ "peakRssBytes": 0.13265993265993267
+ },
+ "passed": true
+ }
+ ],
+ "passed": true
+ }
+ }
+}
diff --git a/gitnexus/bench/java-overload-resolution/measure.js b/gitnexus/bench/java-overload-resolution/measure.js
new file mode 100644
index 000000000..a954c84d2
--- /dev/null
+++ b/gitnexus/bench/java-overload-resolution/measure.js
@@ -0,0 +1,382 @@
+#!/usr/bin/env node
+/** Build first. Each sample measures real worker parsing, resolution and DB writes. */
+import assert from 'node:assert/strict';
+import { execFileSync } from 'node:child_process';
+import { createHash } from 'node:crypto';
+import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises';
+import os from 'node:os';
+import path from 'node:path';
+import { performance } from 'node:perf_hooks';
+import { fileURLToPath, pathToFileURL } from 'node:url';
+import { parseArgs } from 'node:util';
+import { evaluateScaling } from '../method-declaration-facts/scaling.mjs';
+
+const script = fileURLToPath(import.meta.url);
+const baselinePath = new URL('./baseline.json', import.meta.url);
+const { values } = parseArgs({
+ options: {
+ check: { type: 'boolean' },
+ record: { type: 'boolean' },
+ root: { type: 'string', default: path.resolve(path.dirname(script), '../..') },
+ sizes: { type: 'string', default: '128,512,2048' },
+ samples: { type: 'string', default: '3' },
+ output: { type: 'string' },
+ child: { type: 'string' },
+ size: { type: 'string' },
+ },
+});
+const root = path.resolve(values.root);
+const load = (name) => import(pathToFileURL(path.join(root, 'dist', name)));
+const hash = (rows) => createHash('sha256').update(JSON.stringify(rows)).digest('hex');
+const marker = 'JAVA_OVERLOAD_BENCH=';
+const shapes = ['spread', 'concentrated'];
+const scenarios = ['cold', 'warm', 'changed'];
+const metrics = ['wallMs', 'cpuMs', 'scopeWallMs', 'scopeCpuMs', 'heapBytes', 'peakRssBytes'];
+const median = (numbers) => [...numbers].sort((a, b) => a - b)[Math.floor(numbers.length / 2)];
+
+async function runtimeFingerprint() {
+ const digest = createHash('sha256');
+ const dist = path.join(root, 'dist');
+ for (const file of (await readdir(dist, { recursive: true }))
+ .filter((file) => file.endsWith('.js'))
+ .sort()) {
+ digest.update(file.replaceAll(path.sep, '/')).update('\0');
+ digest.update(await readFile(path.join(dist, file))).update('\0');
+ }
+ return digest.digest('hex');
+}
+
+function corpus(shape, size, changed = false) {
+ const files = new Map([
+ [
+ 'Product.java',
+ `package bench;
+public class Product {
+ public void accept(Object value) {}
+ public void accept(String value) {}
+}
+`,
+ ],
+ ]);
+ const expected = [];
+ const declarations = [];
+ for (let i = 0; i < size; i++) {
+ const owner = shape === 'spread' ? `Case${i}` : 'Cases';
+ const file = `${owner}.java`;
+ const suffix = shape === 'spread' ? '' : `_${i}`;
+ const body = `
+ String choose${suffix}(Object value) { return "object"; }
+ String choose${suffix}(String value) { return "string"; }
+ long widen${suffix}(long value) { return value; }
+ long widen${suffix}(Integer value) { return 0; }
+ Product make${suffix}(Object value) { return null; }
+ Product make${suffix}(String value) { return null; }
+ void run${suffix}() {
+ choose${suffix}(${changed && i === 0 ? '(Object) ' : ''}"text");
+ widen${suffix}(1);
+ var product = make${suffix}("text");
+ product.accept("text");
+ }
+`;
+ if (shape === 'spread')
+ files.set(
+ file,
+ `package clients.p${i}; import bench.Product; public class ${owner} {${body}}\n`,
+ );
+ else declarations.push(body);
+ const caller = `Method:${file}:${owner}.run${suffix}#0`;
+ for (const [name, type] of [
+ ['choose', changed && i === 0 ? 'Object' : 'String'],
+ ['widen', 'long'],
+ ['make', 'String'],
+ ])
+ expected.push([caller, `Method:${file}:${owner}.${name}${suffix}#1~${type}`]);
+ expected.push([caller, 'Method:Product.java:Product.accept#1~String']);
+ }
+ if (shape === 'concentrated')
+ files.set('Cases.java', `package bench; public class Cases {${declarations.join('')}}\n`);
+ return {
+ files,
+ expected: expected.sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b))),
+ };
+}
+
+async function sample(shape, size) {
+ assert(shapes.includes(shape) && Number.isSafeInteger(size) && size > 0);
+ const directory = await mkdtemp(path.join(os.tmpdir(), 'gitnexus-java-overload-bench-'));
+ const repo = path.join(directory, 'repo');
+ const input = corpus(shape, size);
+ Object.assign(process.env, {
+ GITNEXUS_HOME: path.join(directory, 'home'),
+ GITNEXUS_SHARED_STORE: 'off',
+ GITNEXUS_MEMORY: 'off',
+ GITNEXUS_LANG: 'en',
+ GITNEXUS_LBUG_BUFFER_POOL_SIZE: '134217728',
+ GITNEXUS_LBUG_EXTENSION_INSTALL: 'never',
+ XDG_RUNTIME_DIR: path.join(directory, 'runtime'),
+ GITNEXUS_MAX_FILE_SIZE: String(
+ Math.ceil(
+ Math.max(...[...input.files.values()].map((source) => Buffer.byteLength(source))) / 1024,
+ ) + 1,
+ ),
+ });
+ delete process.env.GITNEXUS_STORAGE_PATH;
+ delete process.env.GITNEXUS_STORAGE_ROOT;
+ await mkdir(repo);
+ await mkdir(process.env.XDG_RUNTIME_DIR, { mode: 0o700 });
+ const { runFullAnalysis } = await load('core/run-analyze.js');
+ const { scopeResolutionPhase } = await load('core/ingestion/scope-resolution/pipeline/phase.js');
+ const execute = scopeResolutionPhase.execute;
+ let scopeWallMs = 0,
+ scopeCpuMs = 0,
+ scopeRuns = 0;
+ scopeResolutionPhase.execute = async function (...args) {
+ const cpu = process.cpuUsage(),
+ start = performance.now();
+ try {
+ return await execute.apply(this, args);
+ } finally {
+ scopeWallMs += performance.now() - start;
+ const used = process.cpuUsage(cpu);
+ scopeCpuMs += (used.user + used.system) / 1000;
+ scopeRuns++;
+ }
+ };
+ try {
+ for (const [file, source] of input.files) await writeFile(path.join(repo, file), source);
+ const runs = [];
+ for (const scenario of scenarios) {
+ const fixture = scenario === 'changed' ? corpus(shape, size, true) : input;
+ if (scenario === 'changed') {
+ const edited = shape === 'spread' ? 'Case0.java' : 'Cases.java';
+ await writeFile(path.join(repo, edited), fixture.files.get(edited));
+ }
+ global.gc();
+ scopeWallMs = scopeCpuMs = scopeRuns = 0;
+ const cpu = process.cpuUsage(),
+ start = performance.now();
+ const analysis = await runFullAnalysis(
+ repo,
+ {
+ force: true,
+ skipAgentsMd: true,
+ skipSkills: true,
+ skipFts: true,
+ streamGraphEmit: false,
+ workerPoolSize: 1,
+ maxEntryPointCandidates: 7 * size + 2,
+ maxProcesses: 7 * size + 2,
+ },
+ { onProgress: () => {} },
+ );
+ const wallMs = performance.now() - start;
+ const used = process.cpuUsage(cpu);
+ const heapBytes = process.memoryUsage().heapUsed;
+ const peakRssBytes = process.resourceUsage().maxRSS * 1024;
+ const pipeline = analysis.pipelineResult;
+ assert(pipeline, 'Missing pipeline output');
+ assert.equal(scopeRuns, 1, 'Every analysis must execute scope resolution');
+ assert.deepEqual(pipeline.scopeExtractionFailures, []);
+ assert.equal(pipeline.unavailableScopeLanguageFiles, 0);
+ assert.equal(
+ pipeline.processResult?.stats.truncation.truncated,
+ false,
+ 'Process detection must cover the workload',
+ );
+ const calls = [...pipeline.graph.iterRelationshipsByType('CALLS')]
+ .map((edge) => [edge.sourceId, edge.targetId])
+ .sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
+ assert.equal(
+ calls.length,
+ fixture.expected.length,
+ `${shape}/${size}/${scenario}: lost calls`,
+ );
+ assert.equal(
+ hash(calls),
+ hash(fixture.expected),
+ `${shape}/${size}/${scenario}: incorrect overload targets`,
+ );
+ const methodCount = [...pipeline.graph.iterNodes()].filter(
+ (node) => node.label === 'Method',
+ ).length;
+ assert.equal(methodCount, 7 * size + 2, 'Workload lost method declarations');
+ const reparsed = pipeline.reparsedFileCount;
+ if (scenario === 'cold') assert.equal(reparsed, input.files.size);
+ else if (scenario === 'warm') assert.equal(reparsed, 0);
+ else {
+ // Cache entries contain chunks: editing one file can reparse its neighbors.
+ assert(reparsed > 0, 'The edited source must reach parser workers');
+ if (shape === 'spread')
+ assert(reparsed < input.files.size, 'The edit must retain cache hits');
+ }
+ assert.equal(pipeline.parseCacheHitFileCount, input.files.size - reparsed);
+ assert.equal(pipeline.usedWorkerPool, scenario !== 'warm');
+ assert(
+ !pipeline.resolutionOutcomes.some((outcome) => outcome.kind === 'suppressed'),
+ 'Unexpected suppressed calls',
+ );
+ runs.push({
+ scenario,
+ wallMs,
+ cpuMs: (used.user + used.system) / 1000,
+ scopeWallMs,
+ scopeCpuMs,
+ heapBytes,
+ peakRssBytes,
+ correctness: {
+ files: input.files.size,
+ methods: methodCount,
+ calls: calls.length,
+ fingerprint: hash(calls),
+ reparsed,
+ hits: pipeline.parseCacheHitFileCount,
+ },
+ });
+ }
+ return { shape, size, corpus: hash([...input.files]), runs };
+ } finally {
+ scopeResolutionPhase.execute = execute;
+ await rm(directory, { recursive: true, force: true });
+ }
+}
+
+async function measure() {
+ assert(!(values.check && values.record), '--check cannot record a baseline');
+ assert(!values.record || !process.env.CI, 'CI must not record baselines');
+ assert(
+ !values.output || path.resolve(values.output) !== fileURLToPath(baselinePath),
+ '--output must not overwrite the baseline',
+ );
+ const sizes = values.sizes.split(',').map(Number),
+ samples = Number(values.samples);
+ assert(
+ sizes.length >= 3 &&
+ sizes.every((n, i) => Number.isSafeInteger(n) && n > 0 && (!i || n > sizes[i - 1])),
+ 'Supply at least three increasing positive sizes',
+ );
+ assert(
+ Number.isSafeInteger(samples) && samples >= (values.check || values.record ? 3 : 1),
+ 'Checks and baseline recording require at least three samples',
+ );
+ // Refuse existing files (including symlinks), as in the other ingestion harness.
+ if (values.output) await writeFile(values.output, '', { flag: 'wx' });
+ const report = {
+ schemaVersion: 1,
+ recordedAt: new Date().toISOString(),
+ runtimeSha256: await runtimeFingerprint(),
+ workload: { sizes, shapes, scenarios, overloadsPerFamily: 2, callsPerUnit: 4, workers: 1 },
+ environment: {
+ node: process.version,
+ platform: process.platform,
+ arch: process.arch,
+ cpu: os.cpus()[0]?.model,
+ },
+ samples: [],
+ measured: [],
+ scaling: {},
+ };
+ for (let repetition = 0; repetition < samples; repetition++) {
+ for (const size of repetition % 2 ? [...sizes].reverse() : sizes) {
+ for (const shape of repetition % 2 ? [...shapes].reverse() : shapes) {
+ console.error(
+ `java-overload-resolution: ${shape}/${size}, sample ${repetition + 1}/${samples}`,
+ );
+ const output = execFileSync(
+ process.execPath,
+ ['--expose-gc', script, '--child', shape, '--size', String(size), '--root', root],
+ {
+ encoding: 'utf8',
+ timeout: 300_000,
+ maxBuffer: 16 * 1024 * 1024,
+ env: { ...process.env, GITNEXUS_MEMORY: 'off' },
+ },
+ );
+ const result = output.split('\n').find((line) => line.startsWith(marker));
+ assert(result, 'Child did not report measurements');
+ report.samples.push(JSON.parse(result.slice(marker.length)));
+ if (values.output) await writeFile(values.output, JSON.stringify(report, null, 2) + '\n');
+ }
+ }
+ }
+ for (const shape of shapes)
+ for (const size of sizes) {
+ const group = report.samples.filter((row) => row.shape === shape && row.size === size);
+ assert.equal(group.length, samples);
+ for (const scenario of scenarios) {
+ const rows = group.map((row) => row.runs.find((run) => run.scenario === scenario));
+ for (const row of rows) assert.deepEqual(row.correctness, rows[0].correctness);
+ report.measured.push({
+ shape,
+ size,
+ scenario,
+ corpus: group[0].corpus,
+ correctness: rows[0].correctness,
+ ...Object.fromEntries(
+ metrics.map((metric) => [metric, median(rows.map((row) => row[metric]))]),
+ ),
+ });
+ }
+ }
+ assert.equal(
+ await runtimeFingerprint(),
+ report.runtimeSha256,
+ 'Built runtime changed during measurement',
+ );
+ for (const shape of shapes)
+ for (const scenario of scenarios) {
+ report.scaling[`${shape}/${scenario}`] = evaluateScaling(
+ report.measured.filter((row) => row.shape === shape && row.scenario === scenario),
+ metrics,
+ );
+ }
+ if (values.output) await writeFile(values.output, JSON.stringify(report, null, 2) + '\n');
+ console.log(JSON.stringify({ measured: report.measured, scaling: report.scaling }, null, 2));
+ if (values.check || values.record) {
+ assert(
+ Object.values(report.scaling).every((row) => row.passed),
+ 'Time or memory growth exceeds the 1.5 normalized scaling budget',
+ );
+ const correctness = report.measured.map(({ shape, size, scenario, corpus, correctness }) => ({
+ shape,
+ size,
+ scenario,
+ corpus,
+ correctness,
+ }));
+ if (values.check) {
+ const baseline = JSON.parse(await readFile(baselinePath, 'utf8'));
+ assert.equal(baseline.schemaVersion, report.schemaVersion);
+ assert.deepEqual(report.workload, baseline.workload, 'Workload differs from baseline');
+ assert.deepEqual(
+ correctness,
+ baseline.correctness,
+ 'Semantic or cache results differ from baseline',
+ );
+ } else {
+ await writeFile(
+ baselinePath,
+ JSON.stringify(
+ {
+ _note:
+ 'Measured baseline. --check gates same-run normalized growth at 1.5 and exact call targets/cache counts. Absolute timings and memory are references, not portable budgets.',
+ schemaVersion: report.schemaVersion,
+ recordedAt: report.recordedAt,
+ runtimeSha256: report.runtimeSha256,
+ workload: report.workload,
+ environment: report.environment,
+ correctness,
+ measured: report.measured,
+ scaling: report.scaling,
+ },
+ null,
+ 2,
+ ) + '\n',
+ );
+ }
+ console.log(`[java-overload-resolution --${values.record ? 'record' : 'check'}] PASS`);
+ }
+}
+
+if (values.child)
+ console.log(marker + JSON.stringify(await sample(values.child, Number(values.size))));
+else await measure();
diff --git a/gitnexus/bench/receiver-resolution/baseline.json b/gitnexus/bench/receiver-resolution/baseline.json
index a973f6f58..7de5ee51f 100644
--- a/gitnexus/bench/receiver-resolution/baseline.json
+++ b/gitnexus/bench/receiver-resolution/baseline.json
@@ -202,14 +202,14 @@
}
},
"countArm": {
- "callDrops": 140,
- "totalDropsAllKinds": 171,
+ "callDrops": 141,
+ "totalDropsAllKinds": 172,
"bySiteKind": {
- "call": 140,
+ "call": 141,
"read": 31
},
"callDropsByExtension": {
- ".java": 49,
+ ".java": 50,
".py": 24,
".ts": 18,
".zig": 10,
@@ -224,16 +224,16 @@
".swift": 1
},
"callDropsByShape": {
- "chain-field": 56,
- "no-chain": 38,
- "chain-call": 32,
+ "chain-field": 57,
+ "no-chain": 39,
+ "chain-call": 31,
"<>": 13,
"chain-unwrap": 1
},
"callDropsByOrigin": {
- "in-program": 58,
- "unknown": 49,
- "external": 31,
+ "unknown": 64,
+ "in-program": 53,
+ "external": 22,
"<>": 2
}
}
diff --git a/gitnexus/bench/scope-capture/baselines.json b/gitnexus/bench/scope-capture/baselines.json
index e7f6ebf07..ca9d42101 100644
--- a/gitnexus/bench/scope-capture/baselines.json
+++ b/gitnexus/bench/scope-capture/baselines.json
@@ -194,7 +194,8 @@
"_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": "fbd822c3b6848dc6f5e708218c1ab9c1244f2f9f02f4e3df5ed9296137b776d8",
+ "fingerprint": "a0fc49d8d1e80678ab05004eabc4e2229b5954df3b884a8149017e48a6ebe425",
+ "_rebaselined_3789_overloads": "#3789: Compared base 82966afa captures with this provider on the same corpus. Existing entries differ only in 387 method return-type captures and four added var call-result assignments. Seven new fixture files add 284 capture groups; total groups 3515 -> 3803, entries 222 -> 229. Removing those two capture changes reproduces every existing entry. The base emitter reproduces the prior fingerprint exactly. Scaling budget remains 1.5.",
"scaling_budget": 1.5,
"_rebaselined_3190_ctor_qualified_name": "#3190: qualified constructors (`new pkg.Foo()`, `new pkg.Box()`) 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.",
@@ -212,24 +213,25 @@
"_rebaselined_2562_local_classes": "#2562: Java block-local classes, enums, records, and interfaces use source-type-relative JLS 13.1 Host$NLocal identities with javac-compatible per-(host, simple-name) numbering; anonymous numbering remains separate. Lexical aliases begin at each declaration and end with its immediate block. Expanded java-local-class-naming fixtures cover declaration order, disjoint blocks, initializers, lambdas, local type kinds, and recursive local/member/anonymous host chains. Prior d04298a91beec76d0fa7099b3d71265723be60c1df688969aa954f135dd49686 -> 6dd5913a58400a191ff54abf9b852b03d5add657d16c11e60a7c4608ba186197; scaling 1.204 < 1.5.",
"_rebaselined_receiver_chain_2747": "#2747 receiver-chain rollout: call matches whose receiver is itself an expression now carry `@reference.receiver-chain`, a compact encoding of the receiver's structure, so resolution types it by folding instead of re-parsing receiver source text. Capture GROUP counts are unchanged \u2014 the tag is added to existing call matches, never a new match \u2014 so this is digest drift only. Prior 6dd5913a58400a191ff54abf9b852b03d5add657d16c11e60a7c4608ba186197 -> 310adbc2e0827b5ac749acaa981cd12d256fc5b7cbc5592c5bee219e92abf9ee.",
"_rebaselined_2766_receiver_chain_wire_v2": "#2766: receiver-chain wire format v1 -> v2 (name-free `await` / `index` step kinds). The VERSION prefix is part of every emitted `@reference.receiver-chain` capture, so every chain-minting language's capture text changed. WIRE-FORMAT CHANGE, NOT A CAPTURE-SET CHANGE: the same chains are minted for the same sites, spelled `2|\u2026` instead of `1|\u2026`. Exactly the 12 chain-minting languages drifted; c, cobol and dart did not, which is the check that this is the prefix and not a capture regression. Accompanied by SCHEMA_BUMP 34 -> 37 and INCREMENTAL_SCHEMA_VERSION 28 -> 31 so a stale index is rejected rather than replaying chains a v2 decoder refuses. Prior 310adbc2e0827b5ac749acaa981cd12d256fc5b7cbc5592c5bee219e92abf9ee -> a9943355e945e03ddb87c800f4cc1f62b3d04feefb3ec64c258d8e0bb3b3fcd9.",
- "capture_groups_small": 6255,
- "capture_groups_large": 20005,
- "capture_groups_fp": 3515,
- "fixture_count": 222,
+ "capture_groups_small": 6505,
+ "capture_groups_large": 20805,
+ "capture_groups_fp": 3803,
+ "fixture_count": 229,
"_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_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",
+ "fingerprint": "41f1d8a9dcac9fad0889ae698b5a6710ffb43b264ad383a3fd0c172f2da2b596",
+ "_rebaselined_3789_overloads": "#3789: All six entries differ only in 53 method return-type captures; capture groups remain 644. The base 82966afa emitter reproduces the prior fingerprint exactly, and removing the return-type captures makes every entry identical. Corrects the stale group-count field left at 680 after #3486. Scaling budget remains 1.5.",
"scaling_budget": 1.5,
"_rebaselined_2935_synthetic_declarations": "PR #2935 review follow-up: the local-type stress corpus includes synthesized anonymous declarations, which now carry the presence-only @declaration.is-synthetic sidecar. DIGEST DRIFT ONLY, NOT A CAPTURE-SET CHANGE. Prior 8c50bbc83dff4f7f5abd06078aa6abc6b64af05fddb17ee826b5f3df3d346633 -> 560734cd053fb4f4b23aa04bc7870c22089a8deedb0217fa9c1b4db689e02a97; CI scaling 1.002 < 1.5.",
"_rebaselined_2917_record_component_accessors": "#2917: the focused local-type fixture corpus contains local records, so their implicit component accessors add the same bounded scope/declaration captures as the general Java corpus. No local-type naming logic changed. Prior 8c50bbc83dff4f7f5abd06078aa6abc6b64af05fddb17ee826b5f3df3d346633 -> 3e22f368a4ee139be7cb91ff4fb77ddadf60c55efe8d66955ec81f366a46e460; scaling 1.032 < 1.5, capture_groups_fp 680. Re-measured on top of #2935's is-synthetic sidecar after merging origin/main: 560734cd053fb4f4b23aa04bc7870c22089a8deedb0217fa9c1b4db689e02a97 -> bdde823fa725e636e257940efb4c8655aa23124c1727cbaa8856d1ad8f71729e; scaling 0.997 < 1.5, capture_groups_fp 680.",
"_added": "#2562 performance follow-up: co-scales same-host, same-name local classes and anonymous classes to gate JLS binary-name ordinal allocation. Precomputed per-sequence ordinals reduce the focused 100->800 workload from 176->6655ms to 141->752ms; normalized 250->800 scaling is 1.054.",
"_rebaselined_receiver_chain_2747": "#2747 receiver-chain rollout: call matches whose receiver is itself an expression now carry `@reference.receiver-chain`, a compact encoding of the receiver's structure, so resolution types it by folding instead of re-parsing receiver source text. Capture GROUP counts are unchanged \u2014 the tag is added to existing call matches, never a new match \u2014 so this is digest drift only. Prior a9ad88de21ca6747a923260dbdf677fb74a004abbf9d57781f745e3a9027530b -> 3ca67847ea2b9a71b0a41e09f943767e5a2d3a113d3e203499ee364e37f40236.",
"_rebaselined_2766_receiver_chain_wire_v2": "#2766: receiver-chain wire format v1 -> v2 (name-free `await` / `index` step kinds). The VERSION prefix is part of every emitted `@reference.receiver-chain` capture, so every chain-minting language's capture text changed. WIRE-FORMAT CHANGE, NOT A CAPTURE-SET CHANGE: the same chains are minted for the same sites, spelled `2|\u2026` instead of `1|\u2026`. Exactly the 12 chain-minting languages drifted; c, cobol and dart did not, which is the check that this is the prefix and not a capture regression. Accompanied by SCHEMA_BUMP 34 -> 37 and INCREMENTAL_SCHEMA_VERSION 28 -> 31 so a stale index is rejected rather than replaying chains a v2 decoder refuses. Prior 3ca67847ea2b9a71b0a41e09f943767e5a2d3a113d3e203499ee364e37f40236 -> 8c50bbc83dff4f7f5abd06078aa6abc6b64af05fddb17ee826b5f3df3d346633.",
- "capture_groups_fp": 680,
+ "capture_groups_fp": 644,
"_rebaselined_3486_semantic_rename": "#3486: Compared actual origin/main (191a954b), committed dd3bf and frozen provider source over the same unchanged corpus. 2 constructor-parameter type bindings are added and 38 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. Fixture entries 6; changed entries 6; capture groups 680 -> 644. Fingerprint regenerated from the runner's canonicalized capture records. Scaling budgets unchanged."
},
"typescript": {
diff --git a/gitnexus/bench/tool-accuracy/run.ts b/gitnexus/bench/tool-accuracy/run.ts
index dd8dd9e44..d40de7baa 100644
--- a/gitnexus/bench/tool-accuracy/run.ts
+++ b/gitnexus/bench/tool-accuracy/run.ts
@@ -125,6 +125,7 @@ try {
pool,
{ LocalBackend },
{ summarizeUnresolvedReceivers },
+ { summarizeUnresolvedOverloads },
{ taintModelVersion },
] = await Promise.all([
import('../../src/core/ingestion/pipeline.js'),
@@ -132,6 +133,7 @@ try {
import('../../src/core/lbug/pool-adapter.js'),
import('../../src/mcp/local/local-backend.js'),
import('../../src/core/ingestion/scope-resolution/unresolved-receivers.js'),
+ import('../../src/core/ingestion/scope-resolution/unresolved-overloads.js'),
import('../../src/core/ingestion/taint/typescript-model.js'),
]);
adapter = await import('../../src/core/lbug/lbug-adapter.js');
@@ -174,6 +176,10 @@ try {
indexedAt: '2026-10-06T00:00:00.000Z',
scopeExtractionReceipt: 1,
unresolvedReceiverMembers: summarizeUnresolvedReceivers(pipeline.resolutionOutcomes),
+ unresolvedOverloads: summarizeUnresolvedOverloads(
+ pipeline.resolutionOutcomes,
+ pipeline.resolutionOutcomes !== undefined,
+ ),
pdg: { maxFunctionLines: 2000, maxEdgesPerFunction: 0, taintModelVersion },
stats: {
files: pipeline.totalFileCount,
diff --git a/gitnexus/scripts/cross-platform-tests.ts b/gitnexus/scripts/cross-platform-tests.ts
index 90d68b674..07dde400f 100644
--- a/gitnexus/scripts/cross-platform-tests.ts
+++ b/gitnexus/scripts/cross-platform-tests.ts
@@ -164,6 +164,7 @@ const PLATFORM_LOGIC = [
// N-API addon which has known platform-specific behavior (Windows
// file-lock lag after close, macOS N-API destructor segfaults)
const LBUG_NATIVE = [
+ 'test/integration/java-overload-impact-persistence.test.ts',
'test/integration/dangling-owner-edges.test.ts',
'test/integration/impact-private-type-file-seed.test.ts',
'test/integration/impact-private-type-source.test.ts',
@@ -274,6 +275,7 @@ const LBUG_NATIVE = [
// process spawning, which behaves differently across platforms (shell
// quoting, path resolution, signal handling)
const SPAWN_CLI = [
+ 'test/integration/resolvers/java-javac-overloads.test.ts',
// Real shard/blob/JSON roundtrip: path identities and child invocation vary by OS.
'test/unit/shard-timings-roundtrip.test.ts',
'test/integration/cli-e2e.test.ts',
@@ -352,6 +354,7 @@ const WORKER_THREADS = [
'test/integration/cfg/interproc-taint.test.ts',
'test/integration/function-local-identity.test.ts',
'test/integration/java-imported-return-types.test.ts',
+ 'test/integration/resolvers/java-overload-jls.test.ts',
'test/unit/community-processor.test.ts',
'test/integration/worker-pool.test.ts',
'test/integration/class-expression-owners.test.ts',
diff --git a/gitnexus/src/core/ingestion/export-detection.ts b/gitnexus/src/core/ingestion/export-detection.ts
index c436e5d23..ff9003ed0 100644
--- a/gitnexus/src/core/ingestion/export-detection.ts
+++ b/gitnexus/src/core/ingestion/export-detection.ts
@@ -53,12 +53,15 @@ export const javaExportChecker: ExportChecker = (node, _name) => {
let isPublic = javaPublicModifierCache.get(parent);
if (isPublic === undefined) {
isPublic = false;
- for (let i = 0; i < parent.childCount; i++) {
- const child = parent.child(i);
+ // Java modifiers precede the declaration's other named children.
+ // Skip comments, but never scan a class body full of sibling methods.
+ for (let i = 0; i < parent.namedChildCount; i++) {
+ const child = parent.namedChild(i);
+ if (child?.type === 'line_comment' || child?.type === 'block_comment') continue;
if (child?.type === 'modifiers' && child.text?.includes('public')) {
isPublic = true;
- break;
}
+ break;
}
if (parent.type === 'method_declaration' || parent.type === 'constructor_declaration') {
isPublic ||= parent.text?.trimStart().startsWith('public') ?? false;
diff --git a/gitnexus/src/core/ingestion/languages/java/capture-side-channel.ts b/gitnexus/src/core/ingestion/languages/java/capture-side-channel.ts
index a8c5e268e..3753744f5 100644
--- a/gitnexus/src/core/ingestion/languages/java/capture-side-channel.ts
+++ b/gitnexus/src/core/ingestion/languages/java/capture-side-channel.ts
@@ -16,11 +16,19 @@ import type { JavaSpringDiClassFact } from './spring-di.js';
import type { SpringDynamicLookupFact } from '../../frameworks/spring/dynamic-lookups.js';
import type { SpringMessageProducerFact } from '../../frameworks/spring/message-producers.js';
import type { JavaSpringNonHttpHandlerFact } from './spring-non-http-handlers.js';
+import {
+ clearJavaOverloadFacts,
+ getJavaOverloadFacts,
+ isJavaOverloadFileFacts,
+ setJavaOverloadFacts,
+ type JavaOverloadFileFacts,
+} from './overload-argument-facts.js';
export type JavaClassAnnotationFact = ClassAnnotationFact;
export interface JavaCaptureSideChannel {
readonly kind: 'java';
+ readonly overloadFacts?: JavaOverloadFileFacts;
readonly packageFact: JvmPackageFact;
readonly classAnnotations: readonly JavaClassAnnotationFact[];
readonly springAopFacts?: readonly JavaSpringAopFact[];
@@ -43,6 +51,7 @@ const springMessageProducerFacts = new Map 0 ? { springAopFacts: aopFacts } : {}),
@@ -208,6 +220,7 @@ export function applyJavaCaptureSideChannel(parsed: ParsedFile): void {
data.kind !== 'java' ||
!Array.isArray(data.classAnnotations)
) {
+ setJavaOverloadFacts(parsed.filePath, undefined);
setJavaClassAnnotationFacts(parsed.filePath, []);
setJavaSpringAopFacts(parsed.filePath, []);
setJavaSpringConfigConsumerFacts(parsed.filePath, []);
@@ -219,6 +232,10 @@ export function applyJavaCaptureSideChannel(parsed: ParsedFile): void {
setJavaPackageFact(parsed.filePath, UNKNOWN_JVM_PACKAGE_FACT);
return;
}
+ setJavaOverloadFacts(
+ parsed.filePath,
+ isJavaOverloadFileFacts(data.overloadFacts) ? data.overloadFacts : undefined,
+ );
setJavaClassAnnotationFacts(parsed.filePath, data.classAnnotations);
setJavaSpringAopFacts(
parsed.filePath,
diff --git a/gitnexus/src/core/ingestion/languages/java/captures.ts b/gitnexus/src/core/ingestion/languages/java/captures.ts
index f47758b8e..751383059 100644
--- a/gitnexus/src/core/ingestion/languages/java/captures.ts
+++ b/gitnexus/src/core/ingestion/languages/java/captures.ts
@@ -63,6 +63,7 @@ import {
} from './spring-non-http-handlers.js';
import { synthesizeJavaRecordComponentAccessorCaptures } from './record-components.js';
import { synthesizeLombokAccessorCaptures } from './lombok-synthesizer.js';
+import { captureJavaOverloadFacts, setJavaOverloadFacts } from './overload-argument-facts.js';
/** Declaration anchors that carry function-like arity metadata. */
const FUNCTION_DECL_TAGS = ['@declaration.method', '@declaration.constructor'] as const;
@@ -143,6 +144,8 @@ export function emitJavaScopeCaptures(
recordCacheHit();
}
captureJavaPackageFact(filePath, tree.rootNode);
+ const accessorCaptures = synthesizeJavaRecordComponentAccessorCaptures(tree.rootNode);
+ setJavaOverloadFacts(filePath, captureJavaOverloadFacts(tree.rootNode, accessorCaptures));
const rawMatches = getJavaScopeQuery().matches(tree.rootNode);
const out: CaptureMatch[] = [];
@@ -333,6 +336,14 @@ export function emitJavaScopeCaptures(
// directly on the method/constructor node.
const fnNode = findFunctionNode(nodeMap[declTag]);
if (fnNode !== null) {
+ const returnType = fnNode.childForFieldName('type');
+ if (returnType !== null && fnNode.type === 'method_declaration') {
+ grouped['@declaration.return-type'] = syntheticCapture(
+ '@declaration.return-type',
+ fnNode,
+ returnType.text + (fnNode.childForFieldName('dimensions')?.text ?? ''),
+ );
+ }
const arity = computeJavaArityMetadata(fnNode);
if (arity.parameterCount !== undefined) {
grouped['@declaration.parameter-count'] = syntheticCapture(
@@ -392,6 +403,30 @@ export function emitJavaScopeCaptures(
'object_creation_expression',
);
if (callNode !== null) {
+ if (callNode.type === 'method_invocation') {
+ let initializer = callNode;
+ while (initializer.parent?.type === 'parenthesized_expression')
+ initializer = initializer.parent;
+ const declarator = initializer.parent;
+ const declaration = declarator?.parent;
+ const name = declarator?.childForFieldName('name');
+ if (
+ declarator?.type === 'variable_declarator' &&
+ declaration?.type === 'local_variable_declaration' &&
+ declaration.childForFieldName('type')?.text === 'var' &&
+ declarator.childForFieldName('value')?.id === initializer.id &&
+ name !== null &&
+ name !== undefined
+ ) {
+ out.push({
+ '@call-result-assignment.call': nodeToCapture(
+ '@call-result-assignment.call',
+ callNode,
+ ),
+ '@call-result-assignment.lhs': nodeToCapture('@call-result-assignment.lhs', name),
+ });
+ }
+ }
const argList = callNode.childForFieldName('arguments');
// Exclude interleaved comments — tree-sitter-java emits `block_comment` /
// `line_comment` as named children of argument_list, which would inflate
@@ -452,7 +487,7 @@ export function emitJavaScopeCaptures(
...synthesizeJavaInheritanceReferences(tree.rootNode),
...synthesizeJavaExplicitConstructorReferences(tree.rootNode),
...synthesizeJavaAnonymousClassDeclarations(tree.rootNode),
- ...synthesizeJavaRecordComponentAccessorCaptures(tree.rootNode),
+ ...accessorCaptures,
...synthesizeLombokAccessorCaptures(tree.rootNode),
...synthesizeCallableFlowCaptures(tree.rootNode, JAVA_CALLABLE_CAPTURE_OPTIONS),
];
diff --git a/gitnexus/src/core/ingestion/languages/java/overload-argument-facts.ts b/gitnexus/src/core/ingestion/languages/java/overload-argument-facts.ts
new file mode 100644
index 000000000..0f2605c96
--- /dev/null
+++ b/gitnexus/src/core/ingestion/languages/java/overload-argument-facts.ts
@@ -0,0 +1,799 @@
+import type { CaptureMatch, Range } from 'gitnexus-shared';
+import { synthesizeJavaTypeIdentity, type SyntaxNode } from '../../utils/ast-helpers.js';
+import { synthesizeJavaRecordComponentAccessorCaptures } from './record-components.js';
+
+/** Written types retain their declaration context; a caller's imports cannot
+ * reinterpret a parameter declared in another file. These are capture facts,
+ * not the receiver-oriented (container-unwrapped) TypeRef hints. */
+export type JavaArgumentFact =
+ | { readonly kind: 'typed'; readonly spelling: string; readonly at: Range }
+ | { readonly kind: 'null' }
+ | {
+ readonly kind: 'factory';
+ readonly owner: string;
+ readonly name: 'asList' | 'singletonList';
+ readonly arguments: readonly JavaArgumentFact[];
+ readonly typeArguments: readonly string[];
+ readonly at: Range;
+ readonly shadowed: boolean;
+ }
+ | { readonly kind: 'unknown' };
+
+export interface JavaOverloadTypeFact {
+ readonly name: string;
+ readonly simpleName: string;
+ readonly kind: string;
+ readonly range: Range;
+ readonly visible: Range;
+ readonly bases: readonly string[];
+ readonly typeParameters: readonly string[];
+ readonly modifiers?: readonly string[];
+}
+
+export interface JavaOverloadMethodFact {
+ readonly name: string;
+ readonly owner: string;
+ readonly range: Range;
+ readonly parameterTypes: readonly string[];
+ readonly modifiers: readonly string[];
+ readonly typeParameters: readonly string[];
+}
+
+export interface JavaOverloadCallFact {
+ readonly name: string;
+ readonly range: Range;
+ readonly arguments: readonly JavaArgumentFact[];
+ readonly receiver?: JavaArgumentFact;
+ readonly receiverSpelling?: string;
+ readonly receiverCall?: { readonly name: string; readonly range: Range };
+ readonly receiverIsValue: boolean;
+ readonly staticContext: boolean;
+}
+
+export interface JavaOverloadFileFacts {
+ readonly version: 1;
+ readonly packageName: string;
+ readonly imports: readonly {
+ readonly name: string;
+ readonly wildcard: boolean;
+ readonly static: boolean;
+ }[];
+ readonly types: readonly JavaOverloadTypeFact[];
+ readonly methods: readonly JavaOverloadMethodFact[];
+ readonly calls: readonly JavaOverloadCallFact[];
+}
+
+const files = new Map();
+const callIndexes = new Map>();
+const UNKNOWN: JavaArgumentFact = { kind: 'unknown' };
+const typeNodes = new Set([
+ 'class_declaration',
+ 'interface_declaration',
+ 'enum_declaration',
+ 'record_declaration',
+ 'annotation_type_declaration',
+]);
+const callableNodes = new Set([
+ 'method_declaration',
+ 'constructor_declaration',
+ 'compact_constructor_declaration',
+ 'lambda_expression',
+]);
+const lexicalScopes = new Set([
+ 'block',
+ 'for_statement',
+ 'enhanced_for_statement',
+ 'catch_clause',
+ 'lambda_expression',
+ 'switch_block',
+]);
+
+export const javaOverloadRangeKey = (range: Range): string =>
+ `${range.startLine}:${range.startCol}:${range.endLine}:${range.endCol}`;
+
+export function setJavaOverloadFacts(
+ filePath: string,
+ facts: JavaOverloadFileFacts | undefined,
+): void {
+ if (facts === undefined) files.delete(filePath);
+ else files.set(filePath, facts);
+ callIndexes.delete(filePath);
+}
+
+export function clearJavaOverloadFacts(): void {
+ files.clear();
+ callIndexes.clear();
+}
+
+export function getJavaOverloadFacts(filePath: string): JavaOverloadFileFacts | undefined {
+ return files.get(filePath);
+}
+
+export function getJavaOverloadCall(
+ filePath: string,
+ range: Range,
+ name: string,
+): JavaOverloadCallFact | undefined {
+ let index = callIndexes.get(filePath);
+ if (index === undefined) {
+ const facts = files.get(filePath);
+ if (facts === undefined) return undefined;
+ index = new Map(
+ facts.calls.map((call) => [`${javaOverloadRangeKey(call.range)}:${call.name}`, call]),
+ );
+ callIndexes.set(filePath, index);
+ }
+ return index.get(`${javaOverloadRangeKey(range)}:${name}`);
+}
+
+export function javaRangeContains(outer: Range, inner: Range): boolean {
+ return (
+ (outer.startLine < inner.startLine ||
+ (outer.startLine === inner.startLine && outer.startCol <= inner.startCol)) &&
+ (outer.endLine > inner.endLine ||
+ (outer.endLine === inner.endLine && outer.endCol >= inner.endCol))
+ );
+}
+
+function rangeOf(node: SyntaxNode): Range {
+ return {
+ startLine: node.startPosition.row + 1,
+ startCol: node.startPosition.column,
+ endLine: node.endPosition.row + 1,
+ endCol: node.endPosition.column,
+ };
+}
+
+function enclosing(
+ node: SyntaxNode,
+ predicate: (node: SyntaxNode) => boolean,
+): SyntaxNode | undefined {
+ let parent = node.parent;
+ while (parent !== null) {
+ if (predicate(parent)) return parent;
+ parent = parent.parent;
+ }
+ return undefined;
+}
+
+function isType(node: SyntaxNode): boolean {
+ return (
+ typeNodes.has(node.type) ||
+ ((node.type === 'object_creation_expression' || node.type === 'enum_constant') &&
+ node.namedChildren.some((child) => child.type === 'class_body'))
+ );
+}
+
+function typeIdentity(node: SyntaxNode): string | undefined {
+ return synthesizeJavaTypeIdentity(node)?.name ?? node.childForFieldName('name')?.text;
+}
+
+function parameterTypes(node: SyntaxNode): string[] {
+ const parameters = node.childForFieldName('parameters')?.namedChildren ?? [];
+ const result: string[] = [];
+ let variadic = false;
+ for (const parameter of parameters) {
+ if (!['formal_parameter', 'spread_parameter'].includes(parameter.type)) continue;
+ const declarator = parameter.namedChildren.find(
+ (child) => child.type === 'variable_declarator',
+ );
+ const name = parameter.childForFieldName('name') ?? declarator?.childForFieldName('name');
+ const type =
+ parameter.childForFieldName('type') ??
+ parameter.namedChildren.find(
+ (child) =>
+ child.type !== 'modifiers' &&
+ child.type !== 'variable_declarator' &&
+ child.id !== name?.id,
+ );
+ const dimensions =
+ parameter.childForFieldName('dimensions')?.text ??
+ declarator?.childForFieldName('dimensions')?.text ??
+ '';
+ result.push(type === undefined || type === null ? '?' : type.text + dimensions);
+ variadic ||= parameter.type === 'spread_parameter';
+ }
+ if (variadic) result.push('varargs');
+ return result;
+}
+
+function typeParameters(node: SyntaxNode): string[] {
+ const parameters = node.childForFieldName('type_parameters');
+ return (
+ parameters?.namedChildren.flatMap((parameter) => {
+ const name = parameter.namedChildren.find((child) => child.type === 'type_identifier');
+ return name === undefined ? [] : [name.text];
+ }) ?? []
+ );
+}
+
+function modifiers(node: SyntaxNode): string[] {
+ return (
+ node.namedChildren
+ .find((child) => child.type === 'modifiers')
+ ?.children.filter((child) => !child.isNamed)
+ .map((child) => child.text) ?? []
+ );
+}
+
+interface Binding {
+ readonly type?: string;
+ readonly at: Range;
+ readonly scope: Range;
+ readonly depth: number;
+ readonly available: number;
+ readonly owner?: SyntaxNode;
+ readonly inheritable: boolean;
+}
+
+/** One AST walk records declarations and lexical bindings. Calls are evaluated
+ * against those bindings after the walk, so fields are visible before their
+ * declaration while local declarations retain their source-order boundary. */
+export function captureJavaOverloadFacts(
+ root: SyntaxNode,
+ accessorCaptures: readonly CaptureMatch[] = synthesizeJavaRecordComponentAccessorCaptures(root),
+): JavaOverloadFileFacts | undefined {
+ const types: JavaOverloadTypeFact[] = [];
+ const typeFactsByNode = new Map();
+ const typeNodesByFact = new Map();
+ const methods: JavaOverloadMethodFact[] = [];
+ // Reuse the scope synthesizer's validated component names and explicit
+ // accessor suppression so generated declarations have the same evidence as
+ // source-written methods without creating a second accessor authority.
+ const implicitAccessors = new Map(
+ accessorCaptures.flatMap((capture) => {
+ const declaration = capture['@declaration.method'];
+ const name = capture['@declaration.name'];
+ return declaration === undefined || name === undefined
+ ? []
+ : [[javaOverloadRangeKey(declaration.range), name.text] as const];
+ }),
+ );
+ const callNodes: SyntaxNode[] = [];
+ const bindings = new Map();
+ const localBindings = new Map>();
+ const imports: { name: string; wildcard: boolean; static: boolean }[] = [];
+ let packageName = '';
+ const stack = [{ node: root, depth: 0 }];
+ const addBinding = (
+ name: string | undefined,
+ type: string | undefined,
+ node: SyntaxNode,
+ scope: SyntaxNode,
+ depth: number,
+ available: number,
+ field = false,
+ ) => {
+ if (name === undefined) return;
+ const binding: Binding = {
+ type,
+ at: rangeOf(node),
+ scope: rangeOf(scope),
+ depth,
+ available,
+ owner: enclosing(node, isType),
+ inheritable: !field || node.parent === null || !modifiers(node.parent).includes('private'),
+ };
+ if (field) {
+ const list = bindings.get(name) ?? [];
+ list.push(binding);
+ bindings.set(name, list);
+ } else {
+ const scoped = localBindings.get(scope.id) ?? new Map();
+ const locals = scoped.get(name) ?? [];
+ locals.push(binding);
+ scoped.set(name, locals);
+ localBindings.set(scope.id, scoped);
+ }
+ };
+ while (stack.length > 0) {
+ const { node, depth } = stack.pop()!;
+ if (node.type === 'package_declaration') {
+ packageName =
+ node.namedChildren.find(
+ (child) => child.type === 'scoped_identifier' || child.type === 'identifier',
+ )?.text ?? '';
+ } else if (node.type === 'import_declaration') {
+ const name = node.namedChildren.find(
+ (child) => child.type === 'scoped_identifier' || child.type === 'identifier',
+ )?.text;
+ if (name !== undefined)
+ imports.push({
+ name,
+ wildcard: node.namedChildren.some((child) => child.type === 'asterisk'),
+ static: node.children.some((child) => child.type === 'static'),
+ });
+ }
+ if (isType(node)) {
+ const identity = typeIdentity(node);
+ const simpleName =
+ node.type === 'enum_constant'
+ ? identity
+ : (node.childForFieldName('name')?.text ?? identity);
+ if (identity !== undefined && simpleName !== undefined) {
+ const visibilityScope =
+ enclosing(
+ node,
+ (parent) => lexicalScopes.has(parent.type) || parent.type === 'class_body',
+ ) ?? root;
+ const visible = lexicalScopes.has(visibilityScope.type)
+ ? {
+ ...rangeOf(visibilityScope),
+ startLine: node.startPosition.row + 1,
+ startCol: node.startPosition.column,
+ }
+ : rangeOf(visibilityScope);
+ const bases: string[] = [];
+ for (const child of node.namedChildren) {
+ if (child.type === 'superclass')
+ bases.push(...child.namedChildren.map((part) => part.text));
+ if (child.type === 'super_interfaces' || child.type === 'extends_interfaces') {
+ const list = child.namedChildren.find((part) => part.type === 'type_list');
+ bases.push(...(list?.namedChildren ?? child.namedChildren).map((part) => part.text));
+ }
+ }
+ if (node.type === 'object_creation_expression') {
+ const base = node.childForFieldName('type');
+ if (base !== null) bases.push(base.text);
+ } else if (node.type === 'enum_constant') {
+ const host = enclosing(node, (parent) => parent.type === 'enum_declaration');
+ const base = host?.childForFieldName('name');
+ if (base !== undefined && base !== null) bases.push(base.text);
+ }
+ const flags = modifiers(node);
+ if (
+ (node.parent?.type === 'interface_body' ||
+ node.parent?.type === 'annotation_type_body') &&
+ !flags.some((flag) => ['public', 'protected', 'private'].includes(flag))
+ )
+ flags.push('public');
+ const fact: JavaOverloadTypeFact = {
+ name: identity,
+ simpleName,
+ kind: node.type,
+ range: rangeOf(node),
+ visible,
+ bases,
+ typeParameters: typeParameters(node),
+ modifiers: flags,
+ };
+ types.push(fact);
+ typeFactsByNode.set(node.id, fact);
+ typeNodesByFact.set(fact, node);
+ }
+ }
+ const implicitAccessor =
+ node.type === 'formal_parameter' || node.type === 'spread_parameter'
+ ? implicitAccessors.get(javaOverloadRangeKey(rangeOf(node)))
+ : undefined;
+ if (
+ (callableNodes.has(node.type) && node.type !== 'lambda_expression') ||
+ implicitAccessor !== undefined
+ ) {
+ const owner = enclosing(node, isType);
+ const ownerIdentity = owner === undefined ? undefined : typeIdentity(owner);
+ const name = implicitAccessor ?? node.childForFieldName('name')?.text;
+ if (ownerIdentity !== undefined && name !== undefined) {
+ const flags = implicitAccessor === undefined ? modifiers(node) : ['public'];
+ if (
+ owner?.type === 'interface_declaration' &&
+ !flags.includes('private') &&
+ !flags.includes('public')
+ )
+ flags.push('public');
+ methods.push({
+ name,
+ owner: ownerIdentity,
+ range: rangeOf(node),
+ parameterTypes: implicitAccessor === undefined ? parameterTypes(node) : [],
+ modifiers: flags,
+ typeParameters: typeParameters(node),
+ });
+ }
+ }
+ if (node.type === 'method_invocation') callNodes.push(node);
+ if (
+ node.type === 'formal_parameter' ||
+ node.type === 'spread_parameter' ||
+ node.type === 'catch_formal_parameter'
+ ) {
+ const scope = enclosing(
+ node,
+ (parent) => callableNodes.has(parent.type) || parent.type === 'catch_clause',
+ );
+ const declarator = node.namedChildren.find((child) => child.type === 'variable_declarator');
+ const name = node.childForFieldName('name') ?? declarator?.childForFieldName('name');
+ const type =
+ node.childForFieldName('type') ??
+ node.namedChildren.find(
+ (child) =>
+ child.type !== 'modifiers' &&
+ child.type !== 'variable_declarator' &&
+ child.id !== name?.id,
+ );
+ const dimensions =
+ node.childForFieldName('dimensions')?.text ??
+ declarator?.childForFieldName('dimensions')?.text ??
+ '';
+ if (scope !== undefined)
+ addBinding(
+ name?.text,
+ type === undefined || type === null
+ ? undefined
+ : type.text + dimensions + (node.type === 'spread_parameter' ? '[]' : ''),
+ node,
+ scope,
+ depth,
+ scope.startIndex,
+ );
+ } else if (
+ node.type === 'variable_declarator' &&
+ (node.parent?.type === 'field_declaration' ||
+ node.parent?.type === 'constant_declaration' ||
+ node.parent?.type === 'local_variable_declaration')
+ ) {
+ const declaration = node.parent;
+ const field = declaration.type !== 'local_variable_declaration';
+ const scope = enclosing(node, (parent) =>
+ field
+ ? ['class_body', 'interface_body', 'enum_body'].includes(parent.type)
+ : lexicalScopes.has(parent.type),
+ );
+ const type = declaration.childForFieldName('type');
+ const dimensions = node.childForFieldName('dimensions')?.text ?? '';
+ if (scope !== undefined)
+ addBinding(
+ node.childForFieldName('name')?.text,
+ type?.text === 'var'
+ ? undefined
+ : type?.text === undefined
+ ? undefined
+ : type.text + dimensions,
+ node,
+ scope,
+ depth,
+ field ? scope.startIndex : node.startIndex,
+ field,
+ );
+ } else if (node.type === 'enhanced_for_statement') {
+ const type = node.childForFieldName('type')?.text;
+ const body = node.childForFieldName('body');
+ if (body !== null)
+ addBinding(
+ node.childForFieldName('name')?.text,
+ type === undefined
+ ? undefined
+ : type + (node.childForFieldName('dimensions')?.text ?? ''),
+ node,
+ body,
+ depth + 1,
+ body.startIndex,
+ );
+ } else if (node.type === 'lambda_expression') {
+ const parameters = node.childForFieldName('parameters');
+ if (parameters?.type === 'identifier')
+ addBinding(parameters.text, undefined, parameters, node, depth + 1, node.startIndex);
+ else if (parameters?.type === 'inferred_parameters') {
+ for (const parameter of parameters.namedChildren)
+ addBinding(parameter.text, undefined, parameter, node, depth + 1, node.startIndex);
+ }
+ }
+ const children = node.namedChildren;
+ for (let i = children.length - 1; i >= 0; i--)
+ stack.push({ node: children[i]!, depth: depth + 1 });
+ }
+ if (types.length === 0 && methods.length === 0 && callNodes.length === 0) return undefined;
+
+ // Resolve only inheritance visible in this AST. A missing superclass or a
+ // generic substitution is an uncertainty barrier, never permission to bind
+ // an enclosing instance's same-named field instead (JLS 6.5.6.1, 8.3).
+ // Include traversal depth so a cached shallow proof cannot bypass the bound
+ // on a longer path. Caching unknown results also bounds repeated failures.
+ const fieldLookupCache = new Map();
+ const fieldsFor = (
+ owner: SyntaxNode,
+ name: string,
+ inherited: boolean,
+ seen = new Set(),
+ ): readonly Binding[] | undefined => {
+ if (seen.has(owner.id) || seen.size >= 64) return undefined;
+ const cacheKey = `${owner.id}:${name}:${inherited}:${seen.size}`;
+ if (fieldLookupCache.has(cacheKey)) return fieldLookupCache.get(cacheKey);
+ fieldLookupCache.set(cacheKey, undefined);
+ seen.add(owner.id);
+ const fact = typeFactsByNode.get(owner.id);
+ if (fact === undefined) return undefined;
+ const own = (bindings.get(name) ?? []).filter((binding) => binding.owner?.id === owner.id);
+ if (own.length > 0) {
+ if (inherited && fact.typeParameters.length > 0) return undefined;
+ const result = inherited ? own.filter((binding) => binding.inheritable) : own;
+ fieldLookupCache.set(cacheKey, result);
+ return result;
+ }
+ if (fact.kind !== 'class_declaration' && fact.kind !== 'interface_declaration')
+ return undefined;
+ const found = new Map();
+ for (const spelling of fact.bases) {
+ let nameOfBase = spelling.split('<')[0]!.trim();
+ if (packageName !== '' && nameOfBase.startsWith(`${packageName}.`))
+ nameOfBase = nameOfBase.slice(packageName.length + 1);
+ const visible = types.filter(
+ (candidate) =>
+ candidate.simpleName === nameOfBase && javaRangeContains(candidate.visible, fact.range),
+ );
+ let nearest: JavaOverloadTypeFact | undefined;
+ for (const candidate of visible) {
+ if (nearest === undefined || javaRangeContains(nearest.visible, candidate.visible))
+ nearest = candidate;
+ }
+ if (nearest === undefined) {
+ if (nameOfBase === 'java.lang.Object') continue;
+ return undefined;
+ }
+ if (
+ visible.some(
+ (candidate) =>
+ candidate !== nearest &&
+ javaOverloadRangeKey(candidate.visible) === javaOverloadRangeKey(nearest!.visible),
+ )
+ )
+ return undefined;
+ const base = typeNodesByFact.get(nearest);
+ const inheritedFields =
+ base === undefined ? undefined : fieldsFor(base, name, true, new Set(seen));
+ if (inheritedFields === undefined) return undefined;
+ for (const binding of inheritedFields) found.set(javaOverloadRangeKey(binding.at), binding);
+ }
+ const result = [...found.values()];
+ fieldLookupCache.set(cacheKey, result);
+ return result;
+ };
+ const bindingAt = (name: string, node: SyntaxNode, fieldOnly = false): Binding | undefined => {
+ const at = rangeOf(node);
+ const locals: Binding[] = [];
+ if (!fieldOnly) {
+ // Every recorded local scope is an AST ancestor of its uses. Restrict
+ // candidates before applying the existing shadowing and owner checks.
+ for (let scope: SyntaxNode | null = node; scope !== null; scope = scope.parent)
+ locals.push(...(localBindings.get(scope.id)?.get(name) ?? []));
+ }
+ let owner = enclosing(node, isType);
+ while (owner !== undefined) {
+ if (!fieldOnly) {
+ let local: Binding | undefined;
+ for (const binding of locals) {
+ if (
+ binding.owner?.id !== owner.id ||
+ binding.available > node.startIndex ||
+ !javaRangeContains(binding.scope, at)
+ )
+ continue;
+ if (
+ local === undefined ||
+ binding.depth > local.depth ||
+ (binding.depth === local.depth && binding.available > local.available)
+ )
+ local = binding;
+ }
+ if (local !== undefined) return local;
+ }
+ const fields = fieldsFor(owner, name, false);
+ if (fields === undefined || fields.length > 1) return undefined;
+ if (fields.length === 1) return fields[0];
+ if (fieldOnly || modifiers(owner).includes('static')) return undefined;
+ owner = enclosing(owner, isType);
+ }
+ return undefined;
+ };
+ const expression = (node: SyntaxNode, depth = 0): JavaArgumentFact => {
+ if (depth > 12) return UNKNOWN;
+ const typed = (spelling: string, at = rangeOf(node)): JavaArgumentFact => ({
+ kind: 'typed',
+ spelling,
+ at,
+ });
+ switch (node.type) {
+ case 'decimal_integer_literal':
+ case 'hex_integer_literal':
+ case 'octal_integer_literal':
+ case 'binary_integer_literal':
+ return typed(/[lL]$/.test(node.text) ? 'long' : 'int');
+ case 'decimal_floating_point_literal':
+ case 'hex_floating_point_literal':
+ return typed(/[fF]$/.test(node.text) ? 'float' : 'double');
+ case 'string_literal':
+ return typed('java.lang.String');
+ case 'character_literal':
+ return typed('char');
+ case 'true':
+ case 'false':
+ return typed('boolean');
+ case 'null_literal':
+ return { kind: 'null' };
+ case 'identifier': {
+ const binding = bindingAt(node.text, node);
+ return binding?.type === undefined ? UNKNOWN : typed(binding.type, binding.at);
+ }
+ case 'field_access': {
+ if (node.childForFieldName('object')?.type !== 'this') return UNKNOWN;
+ const name = node.childForFieldName('field')?.text;
+ const binding = name === undefined ? undefined : bindingAt(name, node, true);
+ return binding?.type === undefined ? UNKNOWN : typed(binding.type, binding.at);
+ }
+ case 'parenthesized_expression':
+ return node.namedChildren[0] === undefined
+ ? UNKNOWN
+ : expression(node.namedChildren[0], depth + 1);
+ case 'cast_expression':
+ case 'object_creation_expression': {
+ const type = node.childForFieldName('type');
+ return type === null ? UNKNOWN : typed(type.text);
+ }
+ case 'array_creation_expression': {
+ const type = node.childForFieldName('type');
+ const dimensions =
+ node.namedChildren.filter((child) => child.type === 'dimensions_expr').length +
+ (node.namedChildren.find((child) => child.type === 'dimensions')?.text.match(/\[/g)
+ ?.length ?? 0);
+ return type === null || dimensions === 0
+ ? UNKNOWN
+ : typed(type.text + '[]'.repeat(dimensions));
+ }
+ case 'unary_expression': {
+ const operand = node.childForFieldName('operand') ?? node.namedChildren[0];
+ if (operand === undefined || operand === null) return UNKNOWN;
+ const value = expression(operand, depth + 1);
+ if (
+ value.kind !== 'typed' ||
+ !['byte', 'short', 'char', 'int', 'long', 'float', 'double', 'boolean'].includes(
+ value.spelling,
+ )
+ )
+ return UNKNOWN;
+ return typed(['byte', 'short', 'char'].includes(value.spelling) ? 'int' : value.spelling);
+ }
+ case 'method_invocation': {
+ const name = node.childForFieldName('name')?.text;
+ const owner = node.childForFieldName('object');
+ if ((name !== 'asList' && name !== 'singletonList') || owner === null) return UNKNOWN;
+ const args =
+ node
+ .childForFieldName('arguments')
+ ?.namedChildren.filter(
+ (child) => child.type !== 'line_comment' && child.type !== 'block_comment',
+ ) ?? [];
+ const firstName = owner.text.split('.')[0]!;
+ return {
+ kind: 'factory',
+ owner: owner.text,
+ name,
+ arguments: args.map((arg) => expression(arg, depth + 1)),
+ typeArguments:
+ node.childForFieldName('type_arguments')?.namedChildren.map((type) => type.text) ?? [],
+ at: rangeOf(node),
+ shadowed: bindingAt(firstName, owner) !== undefined,
+ };
+ }
+ default:
+ return UNKNOWN;
+ }
+ };
+ const calls = callNodes.map((node): JavaOverloadCallFact => {
+ const receiver = node.childForFieldName('object');
+ let directReceiver = receiver;
+ while (directReceiver?.type === 'parenthesized_expression')
+ directReceiver = directReceiver.namedChildren[0] ?? null;
+ const receiverName =
+ directReceiver?.type === 'method_invocation'
+ ? directReceiver.childForFieldName('name')?.text
+ : undefined;
+ const callable = enclosing(
+ node,
+ (parent) => callableNodes.has(parent.type) && parent.type !== 'lambda_expression',
+ );
+ return {
+ name: node.childForFieldName('name')?.text ?? '',
+ range: rangeOf(node),
+ arguments: (node.childForFieldName('arguments')?.namedChildren ?? [])
+ .filter((arg) => arg.type !== 'line_comment' && arg.type !== 'block_comment')
+ .map((arg) => expression(arg)),
+ ...(receiver === null
+ ? {}
+ : { receiver: expression(receiver), receiverSpelling: receiver.text }),
+ ...(receiverName === undefined || directReceiver === null
+ ? {}
+ : { receiverCall: { name: receiverName, range: rangeOf(directReceiver) } }),
+ receiverIsValue:
+ receiver !== null &&
+ (receiver.type !== 'identifier' || bindingAt(receiver.text, receiver) !== undefined),
+ staticContext: callable !== undefined && modifiers(callable).includes('static'),
+ };
+ });
+ return { version: 1, packageName, imports, types, methods, calls };
+}
+
+/** Side-channel revival is fail-closed: malformed data never revives stale
+ * facts from a preceding file generation in a long-lived process. */
+export function isJavaOverloadFileFacts(value: unknown): value is JavaOverloadFileFacts {
+ if (value === null || typeof value !== 'object') return false;
+ const facts = value as JavaOverloadFileFacts;
+ const range = (r: Range): boolean =>
+ r !== null &&
+ typeof r === 'object' &&
+ [r.startLine, r.startCol, r.endLine, r.endCol].every(Number.isSafeInteger);
+ const strings = (items: readonly string[]): boolean =>
+ Array.isArray(items) && items.every((item) => typeof item === 'string');
+ const argument = (arg: JavaArgumentFact, depth = 0): boolean => {
+ if (depth > 12 || arg === null || typeof arg !== 'object') return false;
+ if (arg.kind === 'null' || arg.kind === 'unknown') return true;
+ if (arg.kind === 'typed') return typeof arg.spelling === 'string' && range(arg.at);
+ return (
+ arg.kind === 'factory' &&
+ typeof arg.owner === 'string' &&
+ ['asList', 'singletonList'].includes(arg.name) &&
+ typeof arg.shadowed === 'boolean' &&
+ range(arg.at) &&
+ strings(arg.typeArguments) &&
+ Array.isArray(arg.arguments) &&
+ arg.arguments.every((item) => argument(item, depth + 1))
+ );
+ };
+ return (
+ facts.version === 1 &&
+ typeof facts.packageName === 'string' &&
+ Array.isArray(facts.imports) &&
+ facts.imports.every(
+ (item) =>
+ item !== null &&
+ typeof item === 'object' &&
+ typeof item.name === 'string' &&
+ typeof item.wildcard === 'boolean' &&
+ typeof item.static === 'boolean',
+ ) &&
+ Array.isArray(facts.types) &&
+ facts.types.every(
+ (type) =>
+ type !== null &&
+ typeof type === 'object' &&
+ typeof type.name === 'string' &&
+ typeof type.simpleName === 'string' &&
+ typeof type.kind === 'string' &&
+ range(type.range) &&
+ range(type.visible) &&
+ strings(type.bases) &&
+ strings(type.typeParameters) &&
+ (type.modifiers === undefined || strings(type.modifiers)),
+ ) &&
+ Array.isArray(facts.methods) &&
+ facts.methods.every(
+ (method) =>
+ method !== null &&
+ typeof method === 'object' &&
+ typeof method.name === 'string' &&
+ typeof method.owner === 'string' &&
+ range(method.range) &&
+ strings(method.parameterTypes) &&
+ strings(method.modifiers) &&
+ strings(method.typeParameters),
+ ) &&
+ Array.isArray(facts.calls) &&
+ facts.calls.every(
+ (call) =>
+ call !== null &&
+ typeof call === 'object' &&
+ typeof call.name === 'string' &&
+ range(call.range) &&
+ typeof call.receiverIsValue === 'boolean' &&
+ typeof call.staticContext === 'boolean' &&
+ Array.isArray(call.arguments) &&
+ call.arguments.every((arg) => argument(arg)) &&
+ (call.receiver === undefined || argument(call.receiver)) &&
+ (call.receiverSpelling === undefined || typeof call.receiverSpelling === 'string') &&
+ (call.receiverCall === undefined ||
+ (call.receiverCall !== null &&
+ typeof call.receiverCall === 'object' &&
+ typeof call.receiverCall.name === 'string' &&
+ range(call.receiverCall.range))),
+ )
+ );
+}
diff --git a/gitnexus/src/core/ingestion/languages/java/overload-resolution.ts b/gitnexus/src/core/ingestion/languages/java/overload-resolution.ts
new file mode 100644
index 000000000..9b8bb5612
--- /dev/null
+++ b/gitnexus/src/core/ingestion/languages/java/overload-resolution.ts
@@ -0,0 +1,1244 @@
+import type { Range, ReferenceSite, SymbolDefinition } from 'gitnexus-shared';
+import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js';
+import type { SemanticModel } from '../../model/semantic-model.js';
+import type { ReceiverMemberResolution } from '../../scope-resolution/contract/scope-resolver.js';
+import {
+ findEnclosingClassDef,
+ isClassLike,
+ lookupNameClaim,
+} from '../../scope-resolution/scope/walkers.js';
+import { getJavaPackageFact } from './package-facts.js';
+import {
+ getJavaOverloadCall,
+ getJavaOverloadFacts,
+ javaRangeContains,
+ type JavaArgumentFact,
+ type JavaOverloadCallFact,
+ type JavaOverloadFileFacts,
+ type JavaOverloadMethodFact,
+ type JavaOverloadTypeFact,
+} from './overload-argument-facts.js';
+import {
+ boxedJavaType,
+ JAVA_PRIMITIVES,
+ javaInvocationConversion,
+ javaSubtype,
+ KNOWN_JAVA_PLATFORM_TYPES,
+ MAX_JAVA_TYPE_DEPTH,
+ sameJavaType,
+ selectJavaOverload,
+ splitJavaTypeArguments,
+ UNKNOWN_JAVA_TYPE,
+ type JavaOverloadCandidate,
+ type JavaPrimitive,
+ type JavaReferenceType,
+ type JavaType,
+ type JavaTypeRelations,
+ type JavaVerdict,
+} from './overload-types.js';
+
+interface TypeContext {
+ readonly filePath: string;
+ readonly at: Range;
+}
+interface FileFactIndex {
+ readonly methods: Map;
+ readonly ownedMethods: Map>;
+ readonly typeParameters: Map;
+}
+interface State {
+ readonly model: SemanticModel;
+ readonly scopes: ScopeResolutionIndexes;
+ readonly types: Map;
+ readonly methodFacts: Map;
+ readonly files: WeakMap;
+ readonly typeFacts: Map;
+ readonly hierarchyCompleteness: Map;
+ readonly relations: JavaTypeRelations;
+}
+const states = new WeakMap();
+const UNKNOWN_RANGE: Range = { startLine: 0, startCol: 0, endLine: 0, endCol: 0 };
+const objectMethods = new Set([
+ 'equals',
+ 'hashCode',
+ 'toString',
+ 'getClass',
+ 'clone',
+ 'finalize',
+ 'notify',
+ 'notifyAll',
+ 'wait',
+]);
+
+function stateFor(scopes: ScopeResolutionIndexes, model: SemanticModel): State {
+ const existing = states.get(scopes);
+ if (existing !== undefined && existing.model === model) return existing;
+ const activeSubtype = new Set();
+ const subtypeVerdicts = new Map();
+ const state: State = {
+ scopes,
+ model,
+ types: new Map(),
+ methodFacts: new Map(),
+ files: new WeakMap(),
+ typeFacts: new Map(),
+ hierarchyCompleteness: new Map(),
+ relations: {
+ sourceSubtype: (from, to) => {
+ const key = JSON.stringify([from, to]);
+ if (activeSubtype.has(key) || activeSubtype.size >= 64) return 'unknown';
+ const cacheKey = `${key}\0${activeSubtype.size}`;
+ const cached = subtypeVerdicts.get(cacheKey);
+ if (cached !== undefined) return cached;
+ activeSubtype.add(key);
+ try {
+ const verdict = sourceSubtype(from, to, state);
+ subtypeVerdicts.set(cacheKey, verdict);
+ return verdict;
+ } finally {
+ activeSubtype.delete(key);
+ }
+ },
+ },
+ };
+ states.set(scopes, state);
+ return state;
+}
+
+function packageName(filePath: string): string | undefined {
+ const fact = getJavaPackageFact(filePath);
+ return fact?.status === 'known' ? fact.packageName : getJavaOverloadFacts(filePath)?.packageName;
+}
+
+function typeFact(definition: SymbolDefinition, state: State): JavaOverloadTypeFact | undefined {
+ if (state.typeFacts.has(definition.nodeId)) return state.typeFacts.get(definition.nodeId);
+ const facts = getJavaOverloadFacts(definition.filePath)?.types ?? [];
+ const candidates = facts.filter(
+ (type) => definition.nodeId.endsWith(`:${type.name}`) || definition.qualifiedName === type.name,
+ );
+ const byPosition =
+ definition.declarationRange === undefined
+ ? undefined
+ : candidates.find((type) => javaRangeContains(type.range, definition.declarationRange!));
+ const fact = byPosition ?? (candidates.length === 1 ? candidates[0] : undefined);
+ state.typeFacts.set(definition.nodeId, fact);
+ return fact;
+}
+
+function methodName(definition: SymbolDefinition): string {
+ return definition.nodeId
+ .slice(definition.nodeId.lastIndexOf(':') + 1)
+ .split('#')[0]!
+ .split('.')
+ .at(-1)!;
+}
+
+function fileFactIndex(facts: JavaOverloadFileFacts, state: State): FileFactIndex {
+ const cached = state.files.get(facts);
+ if (cached !== undefined) return cached;
+ const index: FileFactIndex = {
+ methods: new Map(),
+ ownedMethods: new Map(),
+ typeParameters: new Map(),
+ };
+ for (const method of facts.methods) {
+ const named = index.methods.get(method.name) ?? [];
+ named.push(method);
+ index.methods.set(method.name, named);
+ const owner =
+ index.ownedMethods.get(method.owner) ?? new Map();
+ const owned = owner.get(method.name) ?? [];
+ owned.push(method);
+ owner.set(method.name, owned);
+ index.ownedMethods.set(method.owner, owner);
+ }
+ for (const declaration of [...facts.methods, ...facts.types]) {
+ for (const name of declaration.typeParameters) {
+ const ranges = index.typeParameters.get(name) ?? { starts: [], widest: [] };
+ ranges.starts.push(declaration.range);
+ index.typeParameters.set(name, ranges);
+ }
+ }
+ for (const ranges of index.typeParameters.values()) {
+ ranges.starts.sort((a, b) => a.startLine - b.startLine || a.startCol - b.startCol);
+ let widest = ranges.starts[0]!;
+ for (const range of ranges.starts) {
+ if (
+ range.endLine > widest.endLine ||
+ (range.endLine === widest.endLine && range.endCol > widest.endCol)
+ )
+ widest = range;
+ ranges.widest.push(widest);
+ }
+ }
+ state.files.set(facts, index);
+ return index;
+}
+
+function hasTypeParameter(index: FileFactIndex, name: string, at: Range): boolean {
+ const ranges = index.typeParameters.get(name);
+ if (ranges === undefined) return false;
+ // Find declarations starting before this context. The greatest end in that
+ // prefix answers containment, including nested and same-named parameters.
+ let low = 0,
+ high = ranges.starts.length;
+ while (low < high) {
+ const mid = (low + high) >>> 1;
+ const start = ranges.starts[mid]!;
+ if (
+ start.startLine < at.startLine ||
+ (start.startLine === at.startLine && start.startCol <= at.startCol)
+ )
+ low = mid + 1;
+ else high = mid;
+ }
+ return low > 0 && javaRangeContains(ranges.widest[low - 1]!, at);
+}
+
+function methodFact(
+ definition: SymbolDefinition,
+ state: State,
+): JavaOverloadMethodFact | undefined {
+ if (state.methodFacts.has(definition.nodeId)) return state.methodFacts.get(definition.nodeId);
+ const owner =
+ definition.ownerId === undefined ? undefined : state.scopes.defs.get(definition.ownerId);
+ const ownerFact = owner === undefined ? undefined : typeFact(owner, state);
+ const facts = getJavaOverloadFacts(definition.filePath);
+ const index = facts === undefined ? undefined : fileFactIndex(facts, state);
+ const candidates =
+ (ownerFact === undefined
+ ? index?.methods.get(methodName(definition))
+ : index?.ownedMethods.get(ownerFact.name)?.get(methodName(definition))) ?? [];
+ const byPosition =
+ definition.declarationRange === undefined
+ ? undefined
+ : candidates.find((method) => javaRangeContains(method.range, definition.declarationRange!));
+ const exact = candidates.filter(
+ (method) => method.parameterTypes.join('\0') === (definition.parameterTypes ?? []).join('\0'),
+ );
+ const fact =
+ byPosition ??
+ (exact.length === 1 ? exact[0] : candidates.length === 1 ? candidates[0] : undefined);
+ state.methodFacts.set(definition.nodeId, fact);
+ return fact;
+}
+
+function sourceQualifiedName(definition: SymbolDefinition): string | undefined {
+ const pkg = packageName(definition.filePath);
+ if (pkg === undefined) return undefined;
+ const name =
+ definition.qualifiedName ?? definition.nodeId.slice(definition.nodeId.lastIndexOf(':') + 1);
+ return pkg === '' || name.startsWith(`${pkg}.`)
+ ? name.replaceAll('$', '.')
+ : `${pkg}.${name.replaceAll('$', '.')}`;
+}
+
+function sourceReference(definition: SymbolDefinition): JavaReferenceType {
+ return { kind: 'reference', name: definition.nodeId, sourceId: definition.nodeId, arguments: [] };
+}
+
+function sourceDefinitions(name: string, state: State): readonly SymbolDefinition[] {
+ // Scope and graph identities are distinct. Use the finalized name index,
+ // whose definitions share the inheritance and owner lookup key space.
+ const qualifiedNames = new Set([
+ name,
+ ...state.model.types.lookupClassByName(name).flatMap((definition) => {
+ const qualified = definition.qualifiedName;
+ if (qualified === undefined) return [];
+ const pkg = packageName(definition.filePath);
+ // Model names include the package; nested scope names retain only owners.
+ return pkg && qualified.startsWith(`${pkg}.`)
+ ? [qualified, qualified.slice(pkg.length + 1)]
+ : [qualified];
+ }),
+ ]);
+ return [
+ ...new Set(
+ [...qualifiedNames].flatMap((qualified) => state.scopes.qualifiedNames.get(qualified)),
+ ),
+ ].flatMap((id) => {
+ const definition = state.scopes.defs.get(id);
+ return definition !== undefined && isClassLike(definition.type) ? [definition] : [];
+ });
+}
+
+function qualifiedReference(name: string, state: State): JavaType {
+ const matches = sourceDefinitions(name.split('.').at(-1)!, state).filter(
+ (definition) => sourceQualifiedName(definition) === name,
+ );
+ const unique = [
+ ...new Map(matches.map((definition) => [definition.nodeId, definition])).values(),
+ ];
+ if (unique.length > 1) return UNKNOWN_JAVA_TYPE;
+ if (unique.length === 1) return sourceReference(unique[0]!);
+ return { kind: 'reference', name, arguments: [] };
+}
+
+function resolveTypeName(name: string, context: TypeContext, state: State): JavaType {
+ const facts = getJavaOverloadFacts(context.filePath);
+ if (facts === undefined)
+ return name.includes('.') ? qualifiedReference(name, state) : UNKNOWN_JAVA_TYPE;
+ if (hasTypeParameter(fileFactIndex(facts, state), name, context.at)) return UNKNOWN_JAVA_TYPE;
+ const first = name.split('.')[0]!;
+ const lexical = facts.types.filter(
+ (type) => type.simpleName === first && javaRangeContains(type.visible, context.at),
+ );
+ let nearest: JavaOverloadTypeFact | undefined;
+ for (const type of lexical)
+ if (nearest === undefined || javaRangeContains(nearest.visible, type.visible)) nearest = type;
+ if (nearest !== undefined) {
+ if (name.includes('.')) return UNKNOWN_JAVA_TYPE; // qualified nested lookup needs a proven identity
+ const matches = [
+ ...sourceDefinitions(nearest.name, state),
+ ...sourceDefinitions(nearest.simpleName, state),
+ ].filter(
+ (definition) =>
+ definition.filePath === context.filePath &&
+ (definition.nodeId.endsWith(`:${nearest!.name}`) ||
+ definition.qualifiedName === nearest!.name),
+ );
+ const unique = [
+ ...new Map(matches.map((definition) => [definition.nodeId, definition])).values(),
+ ];
+ return unique.length === 1 ? sourceReference(unique[0]!) : UNKNOWN_JAVA_TYPE;
+ }
+ if (name.includes('.')) return qualifiedReference(name, state);
+ // Static imports can name types, values or methods. Require a source type
+ // identity before resolving one; an unproven import must not become a JDK type.
+ if (
+ facts.imports.some(
+ (item) => item.static && (item.wildcard || item.name.split('.').at(-1) === name),
+ )
+ ) {
+ const explicit = facts.imports.filter(
+ (item) => item.static && !item.wildcard && item.name.split('.').at(-1) === name,
+ );
+ if (explicit.length === 1) {
+ const imported = qualifiedReference(explicit[0]!.name, state);
+ if (imported.kind === 'reference' && imported.sourceId !== undefined) return imported;
+ }
+ return UNKNOWN_JAVA_TYPE;
+ }
+ for (const enclosing of facts.types.filter((type) => javaRangeContains(type.range, context.at))) {
+ for (const owner of sourceDefinitions(enclosing.name, state).filter(
+ (definition) => definition.filePath === context.filePath,
+ )) {
+ for (const ancestorId of state.scopes.methodDispatch.mroFor(owner.nodeId)) {
+ const ancestor = state.scopes.defs.get(ancestorId);
+ const ancestorFact = ancestor === undefined ? undefined : typeFact(ancestor, state);
+ if (
+ ancestor !== undefined &&
+ ancestorFact !== undefined &&
+ getJavaOverloadFacts(ancestor.filePath)?.types.some(
+ (type) =>
+ type.simpleName === name &&
+ type.name !== ancestorFact.name &&
+ javaRangeContains(ancestorFact.range, type.range),
+ )
+ )
+ return UNKNOWN_JAVA_TYPE;
+ }
+ }
+ }
+ const explicit = facts.imports.filter(
+ (item) => !item.static && !item.wildcard && item.name.split('.').at(-1) === name,
+ );
+ if (explicit.length > 0)
+ return explicit.length === 1 ? qualifiedReference(explicit[0]!.name, state) : UNKNOWN_JAVA_TYPE;
+ const inPackage = facts.packageName === '' ? name : `${facts.packageName}.${name}`;
+ const local = sourceDefinitions(name, state).filter(
+ (definition) => sourceQualifiedName(definition) === inPackage,
+ );
+ if (local.length > 0) {
+ const unique = [
+ ...new Map(local.map((definition) => [definition.nodeId, definition])).values(),
+ ];
+ return unique.length === 1 ? sourceReference(unique[0]!) : UNKNOWN_JAVA_TYPE;
+ }
+ const platform = `java.lang.${name}`;
+ const imported: JavaType[] = KNOWN_JAVA_PLATFORM_TYPES.has(platform)
+ ? [qualifiedReference(platform, state)]
+ : [];
+ let unknownWildcard = false;
+ for (const item of facts.imports) {
+ if (item.static || !item.wildcard) continue;
+ const qualified = `${item.name}.${name}`;
+ const candidates = sourceDefinitions(name, state).filter(
+ (definition) => sourceQualifiedName(definition) === qualified,
+ );
+ if (candidates.length > 0) imported.push(...candidates.map(sourceReference));
+ else if (KNOWN_JAVA_PLATFORM_TYPES.has(qualified))
+ imported.push(qualifiedReference(qualified, state));
+ else if (!['java.lang', 'java.util', 'java.io'].includes(item.name)) unknownWildcard = true;
+ }
+ if (unknownWildcard) return UNKNOWN_JAVA_TYPE;
+ const unique = [...new Map(imported.map((type) => [JSON.stringify(type), type])).values()];
+ if (unique.length === 1) return unique[0]!;
+ // An unqualified external declaration without a source/import witness is not
+ // promoted to a platform class merely because its spelling looks familiar.
+ return UNKNOWN_JAVA_TYPE;
+}
+
+function parseType(spelling: string, context: TypeContext, state: State, depth = 0): JavaType {
+ if (depth >= MAX_JAVA_TYPE_DEPTH || spelling.length > 16_384) return UNKNOWN_JAVA_TYPE;
+ const key = `${context.filePath}:${context.at.startLine}:${context.at.startCol}:${depth}:${spelling}`;
+ const cached = state.types.get(key);
+ if (cached !== undefined) return cached;
+ let text = spelling
+ .trim()
+ .replace(/@[\w.]+(?:\([^()]*\))?\s*/g, '')
+ .replaceAll(/\s+/g, '');
+ let dimensions = 0;
+ while (text.endsWith('[]')) {
+ dimensions++;
+ if (depth + dimensions >= MAX_JAVA_TYPE_DEPTH) return UNKNOWN_JAVA_TYPE;
+ text = text.slice(0, -2);
+ }
+ let type: JavaType;
+ if (JAVA_PRIMITIVES.has(text as JavaPrimitive))
+ type = { kind: 'primitive', name: text as JavaPrimitive };
+ else {
+ const open = text.indexOf('<');
+ const name = open < 0 ? text : text.slice(0, open);
+ if (
+ !/^[\p{L}_$][\p{L}\p{N}_$]*(?:\.[\p{L}_$][\p{L}\p{N}_$]*)*$/u.test(name) ||
+ (open >= 0 && !text.endsWith('>'))
+ )
+ type = UNKNOWN_JAVA_TYPE;
+ else {
+ type = resolveTypeName(name, context, state);
+ if (open >= 0) {
+ const args = splitJavaTypeArguments(text.slice(open + 1, -1));
+ type =
+ args === undefined || type.kind !== 'reference'
+ ? UNKNOWN_JAVA_TYPE
+ : {
+ ...type,
+ arguments: args.map((argument) =>
+ parseType(argument, context, state, depth + dimensions + 1),
+ ),
+ };
+ }
+ }
+ }
+ while (dimensions-- > 0) type = { kind: 'array', component: type };
+ state.types.set(key, type);
+ return type;
+}
+
+function sourceSubtype(from: JavaReferenceType, to: JavaReferenceType, state: State): JavaVerdict {
+ if (from.sourceId === undefined)
+ return KNOWN_JAVA_PLATFORM_TYPES.has(from.name) && to.sourceId !== undefined ? 'no' : 'unknown';
+ const definition = state.scopes.defs.get(from.sourceId);
+ if (definition === undefined) return 'unknown';
+ if (
+ to.sourceId !== undefined &&
+ state.scopes.methodDispatch.mroFor(from.sourceId).includes(to.sourceId)
+ ) {
+ return from.arguments.length === 0 && to.arguments.length === 0 ? 'yes' : 'unknown';
+ }
+ const fact = typeFact(definition, state);
+ if (fact === undefined) return 'unknown';
+ if (from.arguments.length > 0 || fact.typeParameters.length > 0) return 'unknown';
+ // Enum's implicit Enum superclass and Record's superclass are not source
+ // heritage edges. Do not prove disjointness from an empty explicit base list.
+ if (fact.kind === 'enum_declaration' || fact.kind === 'record_declaration') return 'unknown';
+ let uncertain = false;
+ for (const base of fact.bases) {
+ const result = javaSubtype(
+ parseType(base, { filePath: definition.filePath, at: fact.range }, state),
+ to,
+ state.relations,
+ );
+ if (result === 'yes') return 'yes';
+ if (result === 'unknown') uncertain = true;
+ }
+ return uncertain ? 'unknown' : 'no';
+}
+
+function normalizedCandidate(
+ definition: SymbolDefinition,
+ state: State,
+): JavaOverloadCandidate {
+ const fact = methodFact(definition, state);
+ const raw = fact?.parameterTypes ?? definition.parameterTypes ?? [];
+ const variadic = raw.at(-1) === 'varargs';
+ const formalTypes = variadic ? raw.slice(0, -1) : raw;
+ // Synthetic scope definitions can carry arity without formal type spellings.
+ // Missing or inconsistent metadata cannot prove a zero-parameter signature
+ // (or remove an unknown declaration from an inherited overload family).
+ const missingParameterTypes =
+ fact === undefined &&
+ definition.parameterCount !== undefined &&
+ definition.parameterCount !== formalTypes.length;
+ const parameters = missingParameterTypes
+ ? Array.from({ length: definition.parameterCount! }, () => UNKNOWN_JAVA_TYPE)
+ : formalTypes.map((type) =>
+ parseType(
+ type,
+ {
+ filePath: definition.filePath,
+ at: fact?.range ?? definition.declarationRange ?? UNKNOWN_RANGE,
+ },
+ state,
+ ),
+ );
+ if (variadic && parameters.length > 0)
+ parameters[parameters.length - 1] = { kind: 'array', component: parameters.at(-1)! };
+ return {
+ definition,
+ parameters,
+ variadic,
+ unsupportedGeneric:
+ missingParameterTypes ||
+ (fact?.typeParameters.length ?? 0) > 0 ||
+ (fact === undefined && (definition.parameterCount ?? parameters.length) !== 0),
+ };
+}
+
+function argumentType(fact: JavaArgumentFact, filePath: string, state: State): JavaType {
+ return fact.kind === 'typed'
+ ? parseType(fact.spelling, { filePath, at: fact.at }, state)
+ : fact.kind === 'null'
+ ? { kind: 'null' }
+ : UNKNOWN_JAVA_TYPE;
+}
+
+function factoryConversion(
+ fact: Extract,
+ target: JavaType,
+ filePath: string,
+ site: ReferenceSite,
+ state: State,
+): JavaVerdict {
+ if (
+ fact.shadowed ||
+ fact.arguments.length === 0 ||
+ (fact.name === 'singletonList' && fact.arguments.length !== 1) ||
+ fact.typeArguments.length > 1
+ )
+ return 'unknown';
+ const rootName = fact.owner.split('.')[0]!;
+ const claim = lookupNameClaim(site.inScope, rootName, state.scopes);
+ if (claim.bindings.some((binding) => !isClassLike(binding.def.type))) return 'unknown';
+ const enclosing = findEnclosingClassDef(site.inScope, state.scopes);
+ if (
+ enclosing !== undefined &&
+ [enclosing.nodeId, ...state.scopes.methodDispatch.mroFor(enclosing.nodeId)].some(
+ (owner) => state.model.fields.lookupAllByOwner(owner, rootName).length > 0,
+ )
+ )
+ return 'unknown';
+ const owner = resolveTypeName(fact.owner, { filePath, at: fact.at }, state);
+ const expectedOwner = fact.name === 'asList' ? 'java.util.Arrays' : 'java.util.Collections';
+ if (owner.kind !== 'reference' || owner.sourceId !== undefined || owner.name !== expectedOwner)
+ return 'unknown';
+ let elements = fact.arguments.map((argument) => argumentType(argument, filePath, state));
+ if (elements.some((element) => element.kind === 'unknown' || element.kind === 'null'))
+ return 'unknown';
+ const explicit =
+ fact.typeArguments[0] === undefined
+ ? undefined
+ : parseType(fact.typeArguments[0], { filePath, at: fact.at }, state);
+ if (explicit?.kind === 'primitive' || explicit?.kind === 'unknown') return 'unknown';
+ if (fact.name === 'asList' && elements.length === 1 && elements[0]?.kind === 'array') {
+ const component = elements[0].component;
+ // Fixed-array applicability precedes varargs. Target typing must not turn
+ // asList(String[]) into List unless explicit T requires it.
+ if (explicit === undefined && component.kind !== 'primitive') elements = [component];
+ else if (
+ explicit !== undefined &&
+ javaSubtype(elements[0], { kind: 'array', component: explicit }, state.relations) === 'yes'
+ )
+ elements = [component];
+ }
+ const boxed = elements.map(boxedJavaType);
+ if (boxed.some((element) => sameJavaType(element, boxed[0]!) !== 'yes')) return 'unknown';
+ if (target.kind === 'unknown') return 'unknown';
+ if (target.kind !== 'reference') return 'no';
+ const elementTarget =
+ explicit ??
+ (['java.util.List', 'java.util.Collection', 'java.lang.Iterable'].includes(target.name) &&
+ target.sourceId === undefined &&
+ target.arguments.length === 1
+ ? target.arguments[0]
+ : boxed[0]);
+ if (elementTarget === undefined) return 'unknown';
+ const converted = elements.map((element) =>
+ javaInvocationConversion(element, elementTarget, 'loose', state.relations),
+ );
+ if (converted.includes('no')) return 'no';
+ if (converted.includes('unknown')) return 'unknown';
+ return javaSubtype(
+ { kind: 'reference', name: 'java.util.List', arguments: [elementTarget] },
+ target,
+ state.relations,
+ );
+}
+
+function enclosingTopLevel(definition: SymbolDefinition, state: State): string | undefined {
+ const fact = typeFact(definition, state);
+ if (fact === undefined) return undefined;
+ let outermost = fact;
+ for (const candidate of getJavaOverloadFacts(definition.filePath)?.types ?? []) {
+ if (javaRangeContains(candidate.range, outermost.range)) outermost = candidate;
+ }
+ const pkg = packageName(definition.filePath);
+ return pkg === undefined ? undefined : `${pkg}:${outermost.name}`;
+}
+
+function isTypeQualifier(
+ call: JavaOverloadCallFact,
+ caller: SymbolDefinition | undefined,
+ state: State,
+): boolean {
+ const name = call.receiverSpelling;
+ if (
+ caller === undefined ||
+ call.receiverIsValue ||
+ name === undefined ||
+ name === 'this' ||
+ name === 'super'
+ )
+ return false;
+ if (
+ [caller.nodeId, ...state.scopes.methodDispatch.mroFor(caller.nodeId)].some(
+ (ownerId) => state.model.fields.lookupAllByOwner(ownerId, name).length > 0,
+ )
+ )
+ return false;
+ return (
+ resolveTypeName(name, { filePath: caller.filePath, at: call.range }, state).kind === 'reference'
+ );
+}
+
+function inheritsPackageMember(
+ receiver: SymbolDefinition,
+ declaringOwner: SymbolDefinition,
+ state: State,
+ seen = new Set(),
+): JavaVerdict {
+ if (receiver.nodeId === declaringOwner.nodeId) return 'yes';
+ if (seen.has(receiver.nodeId) || seen.size >= 64) return 'unknown';
+ seen.add(receiver.nodeId);
+ const ownerPackage = packageName(declaringOwner.filePath);
+ const receiverPackage = packageName(receiver.filePath);
+ if (ownerPackage === undefined || receiverPackage === undefined) return 'unknown';
+ // Once a class outside the declaration's package fails to inherit this
+ // member, a later subclass cannot regain it by returning to that package.
+ if (ownerPackage !== receiverPackage) return 'no';
+ const fact = typeFact(receiver, state);
+ if (fact === undefined) return 'unknown';
+ let uncertain = false;
+ for (const spelling of fact.bases) {
+ const base = parseType(spelling, { filePath: receiver.filePath, at: fact.range }, state);
+ if (base.kind !== 'reference' || base.sourceId === undefined) {
+ uncertain = true;
+ continue;
+ }
+ const definition = state.scopes.defs.get(base.sourceId);
+ const inherited =
+ definition === undefined
+ ? 'unknown'
+ : inheritsPackageMember(definition, declaringOwner, state, new Set(seen));
+ if (inherited === 'yes') return 'yes';
+ if (inherited === 'unknown') uncertain = true;
+ }
+ return uncertain ? 'unknown' : 'no';
+}
+
+function eligible(
+ definition: SymbolDefinition,
+ receiver: SymbolDefinition,
+ site: ReferenceSite,
+ call: JavaOverloadCallFact | undefined,
+ state: State,
+): JavaVerdict {
+ const fact = methodFact(definition, state);
+ if (fact === undefined) return definition.isSynthetic ? 'yes' : 'unknown';
+ const owner =
+ definition.ownerId === undefined ? undefined : state.scopes.defs.get(definition.ownerId);
+ if (owner === undefined) return 'unknown';
+ const inherited = owner.nodeId !== receiver.nodeId;
+ const isStatic = fact.modifiers.includes('static');
+ const ownerKind = typeFact(owner, state)?.kind;
+ if (
+ inherited &&
+ (fact.modifiers.includes('private') || (ownerKind === 'interface_declaration' && isStatic))
+ )
+ return 'no';
+ if (fact.modifiers.includes('public')) return 'yes';
+ const caller = findEnclosingClassDef(site.inScope, state.scopes);
+ const callerPackage = caller === undefined ? undefined : packageName(caller.filePath);
+ const ownerPackage = packageName(owner.filePath);
+ const receiverPackage = packageName(receiver.filePath);
+ if (fact.modifiers.includes('private')) {
+ const callerNest = caller === undefined ? undefined : enclosingTopLevel(caller, state);
+ const ownerNest = enclosingTopLevel(owner, state);
+ return callerNest === undefined || ownerNest === undefined
+ ? 'unknown'
+ : callerNest === ownerNest
+ ? 'yes'
+ : 'no';
+ }
+ if (!fact.modifiers.includes('protected')) {
+ if (inherited && receiverPackage !== ownerPackage) return 'no';
+ if (inherited) {
+ const membership = inheritsPackageMember(receiver, owner, state);
+ if (membership !== 'yes') return membership;
+ }
+ return callerPackage === undefined || ownerPackage === undefined
+ ? 'unknown'
+ : callerPackage === ownerPackage
+ ? 'yes'
+ : 'no';
+ }
+ if (callerPackage !== undefined && callerPackage === ownerPackage) return 'yes';
+ if (caller === undefined) return 'unknown';
+ // JLS 6.6.2.1 permits access within any enclosing subclass body, but
+ // an instance receiver must be that qualifying subclass or its subtype.
+ const receiverMro = state.scopes.methodDispatch.mroFor(receiver.nodeId);
+ for (const scopeId of [site.inScope, ...state.scopes.scopeTree.getAncestors(site.inScope)]) {
+ const scope = state.scopes.scopeTree.getScope(scopeId);
+ if (scope?.kind !== 'Class') continue;
+ const enclosing = scope.ownedDefs.find((def) => isClassLike(def.type));
+ if (
+ enclosing === undefined ||
+ !state.scopes.methodDispatch.mroFor(enclosing.nodeId).includes(owner.nodeId)
+ )
+ continue;
+ if (
+ isStatic ||
+ call?.receiverSpelling === 'super' ||
+ receiver.nodeId === enclosing.nodeId ||
+ receiverMro.includes(enclosing.nodeId)
+ )
+ return 'yes';
+ }
+ return 'no';
+}
+
+function coversObjectSignature(owner: SymbolDefinition, name: string, state: State): boolean {
+ if (!['equals', 'hashCode', 'toString', 'clone', 'finalize'].includes(name)) return false;
+ return [owner.nodeId, ...state.scopes.methodDispatch.mroFor(owner.nodeId)].some((ownerId) =>
+ state.model.methods.lookupAllByOwner(ownerId, name).some((method) => {
+ const fact = methodFact(method, state);
+ if (
+ fact === undefined ||
+ fact.modifiers.includes('static') ||
+ fact.modifiers.includes('private')
+ )
+ return false;
+ const parameters = normalizedCandidate(method, state).parameters;
+ return name === 'equals'
+ ? parameters.length === 1 &&
+ sameJavaType(parameters[0]!, {
+ kind: 'reference',
+ name: 'java.lang.Object',
+ arguments: [],
+ }) === 'yes'
+ : parameters.length === 0;
+ }),
+ );
+}
+
+function hierarchyComplete(
+ owner: SymbolDefinition,
+ memberName: string,
+ state: State,
+ seen = new Set(),
+ objectCovered = coversObjectSignature(owner, memberName, state),
+): boolean {
+ if (seen.has(owner.nodeId) || seen.size >= 64) return false;
+ // Reuse shared ancestors without letting a shallower proof bypass the
+ // traversal bound on another path. Failed/cyclic inventories are cached too.
+ const key = `${owner.nodeId}\0${memberName}\0${objectCovered}\0${seen.size}`;
+ const cached = state.hierarchyCompleteness.get(key);
+ if (cached !== undefined) return cached;
+ state.hierarchyCompleteness.set(key, false);
+ seen.add(owner.nodeId);
+ const fact = typeFact(owner, state);
+ if (fact === undefined) return false;
+ if (objectMethods.has(memberName) && !objectCovered) return false;
+ for (const raw of fact.bases) {
+ const base = parseType(raw, { filePath: owner.filePath, at: fact.range }, state);
+ if (base.kind !== 'reference') return false;
+ if (base.sourceId === undefined) {
+ if (base.name === 'java.lang.Object' && (!objectMethods.has(memberName) || objectCovered))
+ continue;
+ // Runnable declares only run(). An indexed zero-argument override
+ // represents that signature; its other member names come from Object.
+ if (base.name === 'java.lang.Runnable' && !objectMethods.has(memberName)) {
+ if (
+ memberName !== 'run' ||
+ state.model.methods
+ .lookupAllByOwner(owner.nodeId, memberName)
+ .some((method) => methodFact(method, state)?.parameterTypes.length === 0)
+ )
+ continue;
+ }
+ return false;
+ }
+ const definition = state.scopes.defs.get(base.sourceId);
+ if (
+ definition === undefined ||
+ !hierarchyComplete(definition, memberName, state, new Set(seen), objectCovered)
+ )
+ return false;
+ }
+ state.hierarchyCompleteness.set(key, true);
+ return true;
+}
+
+function receiverInventory(
+ owner: SymbolDefinition,
+ name: string,
+ site: ReferenceSite,
+ call: JavaOverloadCallFact | undefined,
+ state: State,
+) {
+ const declarations: SymbolDefinition[] = [];
+ const originalDeclarationIds = new Set();
+ const normalized: JavaOverloadCandidate[] = [];
+ let complete = hierarchyComplete(owner, name, state);
+ for (const ownerId of [owner.nodeId, ...state.scopes.methodDispatch.mroFor(owner.nodeId)]) {
+ for (const declaration of state.model.methods.lookupAllByOwner(ownerId, name)) {
+ if (originalDeclarationIds.has(declaration.nodeId)) continue;
+ originalDeclarationIds.add(declaration.nodeId);
+ const verdict = eligible(declaration, owner, site, call, state);
+ if (verdict === 'no') continue;
+ declarations.push(declaration);
+ if (verdict === 'unknown') complete = false;
+ const candidate = normalizedCandidate(declaration, state);
+ // A more-derived override removes only its override-equivalent ancestor,
+ // never the other overloads on that ancestor.
+ const overridden = normalized.some(
+ (prior) =>
+ prior.parameters.length === candidate.parameters.length &&
+ prior.parameters.every(
+ (parameter, i) => sameJavaType(parameter, candidate.parameters[i]!) === 'yes',
+ ) &&
+ prior.definition.ownerId !== undefined &&
+ declaration.ownerId !== undefined &&
+ state.scopes.methodDispatch
+ .mroFor(prior.definition.ownerId)
+ .includes(declaration.ownerId),
+ );
+ if (!overridden) normalized.push(candidate);
+ }
+ }
+ return {
+ declarations,
+ normalized,
+ complete,
+ originalDeclarationCount: originalDeclarationIds.size,
+ };
+}
+
+function selectCallOverload(
+ candidates: readonly JavaOverloadCandidate[],
+ call: JavaOverloadCallFact,
+ filePath: string,
+ site: ReferenceSite,
+ state: State,
+) {
+ return selectJavaOverload(
+ candidates,
+ call.arguments.length,
+ (index, target, phase) => {
+ const argument = call.arguments[index]!;
+ return argument.kind === 'factory'
+ ? factoryConversion(argument, target, filePath, site, state)
+ : javaInvocationConversion(
+ argumentType(argument, filePath, state),
+ target,
+ phase,
+ state.relations,
+ );
+ },
+ state.relations,
+ );
+}
+
+/** Java claims full overload families before shared owner/rank pruning. A
+ * complete original singleton with opaque arguments may retain the historical
+ * unique-member inference; no pruned singleton or incomplete inventory may. */
+export function resolveJavaReceiverMember(
+ ownerDef: SymbolDefinition,
+ memberName: string,
+ site: ReferenceSite,
+ scopes: ScopeResolutionIndexes,
+ model: SemanticModel,
+): ReceiverMemberResolution | undefined {
+ if (site.kind !== 'call' || site.callForm === 'constructor') return undefined;
+ const state = stateFor(scopes, model);
+ const filePath = scopes.scopeTree.getScope(site.inScope)?.filePath;
+ const call =
+ filePath === undefined ? undefined : getJavaOverloadCall(filePath, site.atRange, site.name);
+ let owner = ownerDef;
+ let runnableReceiver = false;
+ if (call?.receiver?.kind === 'typed' && filePath !== undefined) {
+ const declared = argumentType(call.receiver, filePath, state);
+ if (declared.kind === 'reference' && declared.sourceId !== undefined)
+ owner = scopes.defs.get(declared.sourceId) ?? owner;
+ else if (declared.kind === 'reference' && declared.sourceId === undefined) {
+ runnableReceiver =
+ declared.name === 'java.lang.Runnable' &&
+ memberName === 'run' &&
+ typeFact(owner, state)?.bases.some((base) => {
+ const type = parseType(
+ base,
+ { filePath: owner.filePath, at: typeFact(owner, state)!.range },
+ state,
+ );
+ return (
+ type.kind === 'reference' &&
+ type.sourceId === undefined &&
+ type.name === 'java.lang.Runnable'
+ );
+ }) === true;
+ if (!runnableReceiver) {
+ const candidateIds = [owner.nodeId, ...scopes.methodDispatch.mroFor(owner.nodeId)].flatMap(
+ (id) =>
+ model.methods.lookupAllByOwner(id, memberName).map((candidate) => candidate.nodeId),
+ );
+ return candidateIds.length === 0
+ ? undefined
+ : {
+ kind: 'unknown',
+ candidateIds,
+ candidateSetComplete: false,
+ detail: 'Declared receiver identity does not match the indexed owner',
+ };
+ }
+ }
+ }
+ const caller = findEnclosingClassDef(site.inScope, scopes);
+ const inventory = receiverInventory(owner, memberName, site, call, state);
+ if (inventory.declarations.length === 0) return undefined;
+ const candidateIds = inventory.declarations.map((candidate) => candidate.nodeId);
+ if (!inventory.complete)
+ return {
+ kind: 'unknown',
+ candidateIds,
+ candidateSetComplete: false,
+ detail: 'Incomplete Java inherited member inventory or accessibility evidence',
+ };
+ if (filePath === undefined || call === undefined) {
+ return inventory.originalDeclarationCount === 1
+ ? undefined
+ : {
+ kind: 'unknown',
+ candidateIds,
+ candidateSetComplete: true,
+ detail: 'Java argument capture facts are unavailable',
+ };
+ }
+ const decision = selectCallOverload(
+ runnableReceiver
+ ? inventory.normalized.filter(
+ (candidate) => candidate.parameters.length === 0 && !candidate.variadic,
+ )
+ : inventory.normalized,
+ call,
+ filePath,
+ site,
+ state,
+ );
+ if (decision.kind === 'resolved') {
+ const selected = methodFact(decision.definition, state);
+ // JLS 15.12.3 checks invocation form AFTER choosing the most-specific
+ // method. Removing instance methods earlier could invent a less-specific
+ // static target for a program javac rejects.
+ if (
+ selected !== undefined &&
+ !selected.modifiers.includes('static') &&
+ ((call.staticContext && call.receiverSpelling === undefined) ||
+ isTypeQualifier(call, caller, state))
+ ) {
+ return {
+ kind: 'inapplicable',
+ candidateIds,
+ candidateSetComplete: true,
+ detail: 'Selected instance method requires an instance receiver',
+ };
+ }
+ return { kind: 'resolved', definition: decision.definition };
+ }
+ if (decision.kind === 'unknown' && inventory.originalDeclarationCount === 1) return undefined;
+ return {
+ kind: decision.kind,
+ candidateIds,
+ candidateSetComplete: true,
+ ...(decision.kind === 'unknown'
+ ? { detail: 'Unsupported Java argument, generic constraint, or subtype evidence' }
+ : {}),
+ };
+}
+
+function staticImportOwnerAccess(
+ owner: SymbolDefinition,
+ filePath: string,
+ state: State,
+): JavaVerdict {
+ const fact = typeFact(owner, state);
+ if (fact === undefined) return 'unknown';
+ if (owner.isSynthetic) return 'no';
+ const samePackage = packageName(owner.filePath) === packageName(filePath);
+ // Imports are outside every class body. Check the named type and all its
+ // enclosing types; nestmate/subclass privileges do not apply here.
+ for (const enclosing of getJavaOverloadFacts(owner.filePath)?.types ?? []) {
+ if (!javaRangeContains(enclosing.range, fact.range)) continue;
+ if (enclosing.modifiers === undefined) return 'unknown';
+ if (enclosing.modifiers.includes('private')) return 'no';
+ if (!enclosing.modifiers.includes('public') && !samePackage) return 'no';
+ }
+ return 'yes';
+}
+
+export function resolveJavaUnqualifiedCall(
+ site: ReferenceSite,
+ scopes: ScopeResolutionIndexes,
+ model: SemanticModel,
+): ReceiverMemberResolution | undefined {
+ if (site.kind !== 'call' || site.callForm === 'constructor') return undefined;
+ let scopeId: string | null = site.inScope;
+ const visited = new Set();
+ while (scopeId !== null && !visited.has(scopeId)) {
+ visited.add(scopeId);
+ const scope = scopes.scopeTree.getScope(scopeId);
+ if (scope === undefined) break;
+ if (scope.kind === 'Class') {
+ const owner = scope.ownedDefs.find((definition) => isClassLike(definition.type));
+ if (
+ owner !== undefined &&
+ [owner.nodeId, ...scopes.methodDispatch.mroFor(owner.nodeId)].some(
+ (id) => model.methods.lookupAllByOwner(id, site.name).length > 0,
+ )
+ ) {
+ return resolveJavaReceiverMember(owner, site.name, site, scopes, model);
+ }
+ }
+ scopeId = scope.parent;
+ }
+ const filePath = scopes.scopeTree.getScope(site.inScope)?.filePath;
+ const imported =
+ filePath === undefined
+ ? []
+ : (getJavaOverloadFacts(filePath)?.imports.filter(
+ (item) => item.static && (item.wildcard || item.name.split('.').at(-1) === site.name),
+ ) ?? []);
+ if (imported.length > 0) {
+ const state = stateFor(scopes, model);
+ const call = getJavaOverloadCall(filePath!, site.atRange, site.name);
+ const claim = lookupNameClaim(site.inScope, site.name, scopes);
+ const candidates = new Map<
+ string,
+ { candidate: JavaOverloadCandidate; explicit: boolean }
+ >();
+ let complete = true;
+ for (const item of imported) {
+ const ownerName = item.wildcard ? item.name : item.name.slice(0, item.name.lastIndexOf('.'));
+ const type = qualifiedReference(ownerName, state);
+ const owner =
+ type.kind === 'reference' && type.sourceId !== undefined
+ ? scopes.defs.get(type.sourceId)
+ : undefined;
+ if (owner === undefined) {
+ complete = false;
+ continue;
+ }
+ const ownerAccess = staticImportOwnerAccess(owner, filePath!, state);
+ if (ownerAccess !== 'yes') {
+ if (ownerAccess === 'unknown') complete = false;
+ continue;
+ }
+ const inventory = receiverInventory(owner, site.name, site, call, state);
+ complete &&= inventory.complete;
+ for (const candidate of inventory.normalized) {
+ const fact = methodFact(candidate.definition, state);
+ if (fact === undefined) {
+ complete = false;
+ continue;
+ }
+ if (!fact.modifiers.includes('static') || fact.modifiers.includes('private')) continue;
+ // Static-import access is checked at the compilation unit, outside
+ // a subclass body (JLS 7.5.3), so protected does not grant cross-package access.
+ if (
+ !fact.modifiers.includes('public') &&
+ packageName(candidate.definition.filePath) !== packageName(filePath!)
+ )
+ continue;
+ const previous = candidates.get(candidate.definition.nodeId);
+ candidates.set(candidate.definition.nodeId, {
+ candidate,
+ explicit: !item.wildcard || previous?.explicit === true,
+ });
+ }
+ }
+ const all = [...candidates.values()];
+ const visible = all.filter(
+ ({ candidate, explicit }) =>
+ explicit ||
+ !all.some(
+ (other) =>
+ other.explicit &&
+ other.candidate.parameters.length === candidate.parameters.length &&
+ other.candidate.parameters.every(
+ (parameter, index) => sameJavaType(parameter, candidate.parameters[index]!) === 'yes',
+ ),
+ ),
+ );
+ const candidateIds = [
+ ...new Set([
+ ...all.map(({ candidate }) => candidate.definition.nodeId),
+ ...claim.bindings
+ .filter((binding) => binding.def.type === 'Method' || binding.def.type === 'Function')
+ .map((binding) => binding.def.nodeId),
+ ]),
+ ];
+ if (complete && call !== undefined) {
+ const decision = selectCallOverload(
+ visible.map(({ candidate }) => candidate),
+ call,
+ filePath!,
+ site,
+ state,
+ );
+ if (decision.kind === 'resolved')
+ return { kind: 'resolved', definition: decision.definition };
+ if (decision.kind === 'unknown' && all.length === 1) return undefined;
+ return { kind: decision.kind, candidateIds, candidateSetComplete: true };
+ }
+ return {
+ kind: 'unknown',
+ candidateIds,
+ candidateSetComplete: false,
+ detail: 'Incomplete static-import overload inventory or argument evidence',
+ };
+ }
+ return undefined;
+}
+
+export function resolveJavaReceiverCallSite(
+ site: ReferenceSite,
+ scopes: ScopeResolutionIndexes,
+): { readonly name: string; readonly range: Range } | undefined {
+ const filePath = scopes.scopeTree.getScope(site.inScope)?.filePath;
+ return filePath === undefined
+ ? undefined
+ : getJavaOverloadCall(filePath, site.atRange, site.name)?.receiverCall;
+}
+
+/** Ground only a whole formal owner type variable from explicit source
+ * heritage. This preserves generic interface fan-out without treating a
+ * concrete implementation's competing overload as the selected signature.
+ * Nested substitutions and generic method inference remain unknown. */
+function signatureForImplementation(
+ primary: SymbolDefinition,
+ owner: SymbolDefinition,
+ state: State,
+): JavaOverloadCandidate {
+ const signature = normalizedCandidate(primary, state);
+ const method = methodFact(primary, state);
+ const primaryOwner =
+ primary.ownerId === undefined ? undefined : state.scopes.defs.get(primary.ownerId);
+ const declaration = primaryOwner === undefined ? undefined : typeFact(primaryOwner, state);
+ if (
+ method === undefined ||
+ method.typeParameters.length > 0 ||
+ declaration === undefined ||
+ declaration.typeParameters.length === 0
+ )
+ return signature;
+ const witnesses: (readonly JavaType[])[] = [];
+ for (const id of [owner.nodeId, ...state.scopes.methodDispatch.mroFor(owner.nodeId)]) {
+ const implementation = state.scopes.defs.get(id);
+ const fact = implementation === undefined ? undefined : typeFact(implementation, state);
+ if (implementation === undefined || fact === undefined) continue;
+ for (const spelling of fact.bases) {
+ const base = parseType(
+ spelling,
+ { filePath: implementation.filePath, at: fact.range },
+ state,
+ );
+ if (
+ base.kind === 'reference' &&
+ base.sourceId === primary.ownerId &&
+ base.arguments.length === declaration.typeParameters.length
+ )
+ witnesses.push(base.arguments);
+ }
+ }
+ const arguments_ = witnesses[0];
+ if (
+ arguments_ === undefined ||
+ !witnesses.every((witness) =>
+ witness.every((type, i) => sameJavaType(type, arguments_[i]!) === 'yes'),
+ )
+ )
+ return signature;
+ return {
+ ...signature,
+ parameters: signature.parameters.map((parameter, index) => {
+ const position = declaration.typeParameters.indexOf(
+ method.parameterTypes[index]?.trim() ?? '',
+ );
+ return position < 0
+ ? parameter
+ : signature.variadic && index === signature.parameters.length - 1
+ ? { kind: 'array', component: arguments_[position]! }
+ : arguments_[position]!;
+ }),
+ };
+}
+
+export function resolveJavaSelectedMethodImplementation(
+ primary: SymbolDefinition,
+ owner: SymbolDefinition,
+ scopes: ScopeResolutionIndexes,
+ model: SemanticModel,
+): ReceiverMemberResolution | undefined {
+ const state = stateFor(scopes, model);
+ const primaryFact = methodFact(primary, state);
+ if (
+ primaryFact?.modifiers.some(
+ (modifier) => modifier === 'static' || modifier === 'private' || modifier === 'final',
+ )
+ )
+ return undefined;
+ const signature = signatureForImplementation(primary, owner, state);
+ const candidates: SymbolDefinition[] = [];
+ for (const ownerId of [owner.nodeId, ...scopes.methodDispatch.mroFor(owner.nodeId)]) {
+ for (const candidate of model.methods.lookupAllByOwner(ownerId, methodName(primary))) {
+ if (candidate.nodeId === primary.nodeId) continue;
+ const fact = methodFact(candidate, state);
+ const declaredOwner =
+ candidate.ownerId === undefined ? undefined : scopes.defs.get(candidate.ownerId);
+ if (
+ fact?.modifiers.includes('abstract') ||
+ (declaredOwner !== undefined &&
+ typeFact(declaredOwner, state)?.kind === 'interface_declaration' &&
+ !fact?.modifiers.includes('default'))
+ )
+ continue;
+ const other = normalizedCandidate(candidate, state);
+ if (other.parameters.length !== signature.parameters.length) continue;
+ const comparisons = other.parameters.map((parameter, i) =>
+ sameJavaType(parameter, signature.parameters[i]!),
+ );
+ if (comparisons.includes('no')) continue;
+ if (!comparisons.includes('unknown') && candidates.length === 0)
+ return { kind: 'resolved', definition: candidate };
+ candidates.push(candidate);
+ }
+ }
+ return candidates.length === 0
+ ? undefined
+ : {
+ kind: 'unknown',
+ candidateIds: candidates.map((candidate) => candidate.nodeId),
+ candidateSetComplete: true,
+ detail: 'Generic override signature could not be established',
+ };
+}
diff --git a/gitnexus/src/core/ingestion/languages/java/overload-types.ts b/gitnexus/src/core/ingestion/languages/java/overload-types.ts
new file mode 100644
index 000000000..eea2cb544
--- /dev/null
+++ b/gitnexus/src/core/ingestion/languages/java/overload-types.ts
@@ -0,0 +1,345 @@
+/** Java method-invocation conversions (JLS 5.3) and the phase ordering and
+ * specificity rules of JLS 15.12.2. Unknown evidence is never a conversion. */
+export type JavaVerdict = 'yes' | 'no' | 'unknown';
+export type JavaPrimitive =
+ | 'boolean'
+ | 'byte'
+ | 'short'
+ | 'char'
+ | 'int'
+ | 'long'
+ | 'float'
+ | 'double';
+export type JavaType =
+ | { readonly kind: 'primitive'; readonly name: JavaPrimitive }
+ | {
+ readonly kind: 'reference';
+ readonly name: string;
+ readonly arguments: readonly JavaType[];
+ readonly sourceId?: string;
+ }
+ | { readonly kind: 'array'; readonly component: JavaType }
+ | { readonly kind: 'null' }
+ | { readonly kind: 'unknown' };
+
+export type JavaReferenceType = Extract;
+export type JavaInvocationPhase = 'strict' | 'loose' | 'variable';
+export interface JavaTypeRelations {
+ readonly sourceSubtype: (from: JavaReferenceType, to: JavaReferenceType) => JavaVerdict;
+}
+
+export interface JavaOverloadCandidate {
+ readonly definition: T;
+ /** A variadic declaration's final entry is its ARRAY type. */
+ readonly parameters: readonly JavaType[];
+ readonly variadic: boolean;
+ readonly unsupportedGeneric?: boolean;
+}
+
+export type JavaOverloadDecision =
+ | { readonly kind: 'resolved'; readonly definition: T; readonly phase: JavaInvocationPhase }
+ | { readonly kind: 'ambiguous' | 'unknown' | 'inapplicable'; readonly candidates: readonly T[] };
+
+export const UNKNOWN_JAVA_TYPE: JavaType = { kind: 'unknown' };
+export const MAX_JAVA_TYPE_DEPTH = 32;
+export const JAVA_PRIMITIVES = new Set([
+ 'boolean',
+ 'byte',
+ 'short',
+ 'char',
+ 'int',
+ 'long',
+ 'float',
+ 'double',
+]);
+
+const widening: Readonly> = {
+ boolean: [],
+ byte: ['short', 'int', 'long', 'float', 'double'],
+ short: ['int', 'long', 'float', 'double'],
+ char: ['int', 'long', 'float', 'double'],
+ int: ['long', 'float', 'double'],
+ long: ['float', 'double'],
+ float: ['double'],
+ double: [],
+};
+const boxes: Readonly> = {
+ boolean: 'java.lang.Boolean',
+ byte: 'java.lang.Byte',
+ short: 'java.lang.Short',
+ char: 'java.lang.Character',
+ int: 'java.lang.Integer',
+ long: 'java.lang.Long',
+ float: 'java.lang.Float',
+ double: 'java.lang.Double',
+};
+const unboxes = new Map(
+ Object.entries(boxes).map(([primitive, box]) => [box, primitive as JavaPrimitive]),
+);
+
+/** These summaries are only used after a provider proves qualified JDK type
+ * identity. Collection edges preserve their invariant element arguments. */
+const jdkParents: Readonly> = {
+ 'java.lang.Object': [],
+ 'java.lang.String': ['java.lang.Object', 'java.lang.CharSequence', 'java.io.Serializable'],
+ 'java.lang.Number': ['java.lang.Object', 'java.io.Serializable'],
+ 'java.lang.Byte': ['java.lang.Number'],
+ 'java.lang.Short': ['java.lang.Number'],
+ 'java.lang.Integer': ['java.lang.Number'],
+ 'java.lang.Long': ['java.lang.Number'],
+ 'java.lang.Float': ['java.lang.Number'],
+ 'java.lang.Double': ['java.lang.Number'],
+ 'java.lang.Boolean': ['java.lang.Object', 'java.io.Serializable'],
+ 'java.lang.Character': ['java.lang.Object', 'java.io.Serializable'],
+ 'java.lang.CharSequence': ['java.lang.Object'],
+ 'java.lang.Cloneable': ['java.lang.Object'],
+ 'java.io.Serializable': ['java.lang.Object'],
+ 'java.lang.Iterable': ['java.lang.Object'],
+ 'java.lang.Runnable': ['java.lang.Object'],
+ 'java.util.Collection': ['java.lang.Iterable'],
+ 'java.util.List': ['java.util.Collection'],
+ 'java.util.Set': ['java.util.Collection'],
+ 'java.util.ArrayList': ['java.util.List', 'java.lang.Cloneable', 'java.io.Serializable'],
+ 'java.util.LinkedList': ['java.util.List', 'java.lang.Cloneable', 'java.io.Serializable'],
+ 'java.util.HashSet': ['java.util.Set', 'java.lang.Cloneable', 'java.io.Serializable'],
+ 'java.util.Map': ['java.lang.Object'],
+ 'java.util.HashMap': ['java.util.Map', 'java.lang.Cloneable', 'java.io.Serializable'],
+};
+const parameterizedJdk = new Set([
+ 'java.lang.Iterable',
+ 'java.util.Collection',
+ 'java.util.List',
+ 'java.util.Set',
+ 'java.util.ArrayList',
+ 'java.util.LinkedList',
+ 'java.util.HashSet',
+ 'java.util.Map',
+ 'java.util.HashMap',
+]);
+const finalJdk = new Set(['java.lang.String', ...Object.values(boxes)]);
+export const KNOWN_JAVA_PLATFORM_TYPES = new Set([
+ ...Object.keys(jdkParents),
+ 'java.util.Arrays',
+ 'java.util.Collections',
+]);
+
+function all(verdicts: readonly JavaVerdict[]): JavaVerdict {
+ return verdicts.includes('no') ? 'no' : verdicts.includes('unknown') ? 'unknown' : 'yes';
+}
+
+export function sameJavaType(left: JavaType, right: JavaType, depth = 0): JavaVerdict {
+ if (depth >= MAX_JAVA_TYPE_DEPTH) return 'unknown';
+ if (left.kind === 'unknown' || right.kind === 'unknown') return 'unknown';
+ if (left.kind !== right.kind) return 'no';
+ if (left.kind === 'null') return 'yes';
+ if (left.kind === 'primitive' && right.kind === 'primitive')
+ return left.name === right.name ? 'yes' : 'no';
+ if (left.kind === 'array' && right.kind === 'array')
+ return sameJavaType(left.component, right.component, depth + 1);
+ if (left.kind === 'reference' && right.kind === 'reference') {
+ if (left.name !== right.name || left.sourceId !== right.sourceId) return 'no';
+ if (left.arguments.length !== right.arguments.length) return 'unknown';
+ return all(
+ left.arguments.map((argument, i) => sameJavaType(argument, right.arguments[i]!, depth + 1)),
+ );
+ }
+ return 'no';
+}
+
+export function javaSubtype(
+ from: JavaType,
+ to: JavaType,
+ relations: JavaTypeRelations,
+ depth = 0,
+): JavaVerdict {
+ if (depth >= MAX_JAVA_TYPE_DEPTH) return 'unknown';
+ if (from.kind === 'unknown' || to.kind === 'unknown') return 'unknown';
+ if (sameJavaType(from, to, depth) === 'yes') return 'yes';
+ if (from.kind === 'null') return to.kind === 'reference' || to.kind === 'array' ? 'yes' : 'no';
+ if (from.kind === 'primitive' || to.kind === 'primitive') {
+ return from.kind === 'primitive' &&
+ to.kind === 'primitive' &&
+ widening[from.name].includes(to.name)
+ ? 'yes'
+ : 'no';
+ }
+ if (from.kind === 'array') {
+ if (to.kind === 'reference')
+ return ['java.lang.Object', 'java.lang.Cloneable', 'java.io.Serializable'].includes(
+ to.name,
+ ) && to.sourceId === undefined
+ ? 'yes'
+ : 'no';
+ if (to.kind !== 'array') return 'no';
+ if (from.component.kind === 'primitive' || to.component.kind === 'primitive')
+ return sameJavaType(from.component, to.component, depth + 1);
+ return javaSubtype(from.component, to.component, relations, depth + 1);
+ }
+ if (from.kind !== 'reference' || to.kind !== 'reference') return 'no';
+ if (to.name === 'java.lang.Object' && to.sourceId === undefined) return 'yes';
+ if (from.name === to.name && from.sourceId === to.sourceId) {
+ // Raw/unchecked conversions are deliberately not approximated by erasure.
+ if (from.arguments.length !== to.arguments.length) return 'unknown';
+ return all(
+ from.arguments.map((argument, i) => sameJavaType(argument, to.arguments[i]!, depth + 1)),
+ );
+ }
+ if (to.sourceId === undefined && finalJdk.has(to.name)) return 'no';
+ if (from.sourceId !== undefined || to.sourceId !== undefined)
+ return relations.sourceSubtype(from, to);
+ const parents = Object.hasOwn(jdkParents, from.name) ? jdkParents[from.name] : undefined;
+ if (parents === undefined) return 'unknown';
+ let uncertain = false;
+ for (const parent of parents) {
+ const result = javaSubtype(
+ {
+ kind: 'reference',
+ name: parent,
+ arguments: parameterizedJdk.has(parent) ? from.arguments : [],
+ },
+ to,
+ relations,
+ depth + 1,
+ );
+ if (result === 'yes') return 'yes';
+ if (result === 'unknown') uncertain = true;
+ }
+ return uncertain || !Object.hasOwn(jdkParents, to.name) ? 'unknown' : 'no';
+}
+
+export function javaInvocationConversion(
+ from: JavaType,
+ to: JavaType,
+ phase: JavaInvocationPhase,
+ relations: JavaTypeRelations,
+): JavaVerdict {
+ const strict = javaSubtype(from, to, relations);
+ if (phase === 'strict' || strict !== 'no') return strict;
+ if (from.kind === 'primitive' && to.kind === 'reference') {
+ return javaSubtype({ kind: 'reference', name: boxes[from.name], arguments: [] }, to, relations);
+ }
+ if (from.kind === 'reference' && to.kind === 'primitive') {
+ const primitive = from.sourceId === undefined ? unboxes.get(from.name) : undefined;
+ return primitive === undefined
+ ? 'no'
+ : javaSubtype({ kind: 'primitive', name: primitive }, to, relations);
+ }
+ return 'no';
+}
+
+export function boxedJavaType(type: JavaType): JavaType {
+ return type.kind === 'primitive'
+ ? { kind: 'reference', name: boxes[type.name], arguments: [] }
+ : type;
+}
+
+function effectiveParameters(
+ candidate: JavaOverloadCandidate,
+ phase: JavaInvocationPhase,
+ arity: number,
+): readonly JavaType[] | undefined {
+ if (phase !== 'variable')
+ return candidate.parameters.length === arity ? candidate.parameters : undefined;
+ if (!candidate.variadic || arity < candidate.parameters.length - 1) return undefined;
+ const last = candidate.parameters.at(-1);
+ if (last?.kind !== 'array') return undefined;
+ return [
+ ...candidate.parameters.slice(0, -1),
+ ...Array.from({ length: arity - candidate.parameters.length + 1 }, () => last.component),
+ ];
+}
+
+function moreSpecific(
+ left: JavaOverloadCandidate,
+ right: JavaOverloadCandidate,
+ phase: JavaInvocationPhase,
+ arity: number,
+ relations: JavaTypeRelations,
+): JavaVerdict {
+ const compareArity =
+ phase === 'variable' ? Math.max(arity, left.parameters.length, right.parameters.length) : arity;
+ const a = effectiveParameters(left, phase, compareArity);
+ const b = effectiveParameters(right, phase, compareArity);
+ if (a === undefined || b === undefined) return 'unknown';
+ return all(a.map((type, i) => javaSubtype(type, b[i]!, relations)));
+}
+
+/** The provider evaluates poly expressions against the candidate's parameter;
+ * the selector must not replace that callback with one frozen argument type. */
+export function selectJavaOverload(
+ candidates: readonly JavaOverloadCandidate[],
+ arity: number,
+ argumentConversion: (index: number, target: JavaType, phase: JavaInvocationPhase) => JavaVerdict,
+ relations: JavaTypeRelations,
+): JavaOverloadDecision {
+ for (const phase of ['strict', 'loose', 'variable'] as const) {
+ const applicable: JavaOverloadCandidate[] = [];
+ const uncertain: JavaOverloadCandidate[] = [];
+ for (const candidate of candidates) {
+ const parameters = effectiveParameters(candidate, phase, arity);
+ if (parameters === undefined) continue;
+ const verdict = all(
+ parameters.map((parameter, i) => argumentConversion(i, parameter, phase)),
+ );
+ if (verdict === 'no') continue;
+ if (verdict === 'unknown' || candidate.unsupportedGeneric === true) uncertain.push(candidate);
+ else applicable.push(candidate);
+ }
+ // An unknown candidate in an earlier phase can beat any later-phase
+ // declaration. Even a currently unique applicable method is not a proof.
+ if (uncertain.length > 0)
+ return {
+ kind: 'unknown',
+ candidates: [...applicable, ...uncertain].map((candidate) => candidate.definition),
+ };
+ if (applicable.length === 0) continue;
+ if (applicable.length === 1)
+ return { kind: 'resolved', definition: applicable[0]!.definition, phase };
+ const winners = applicable.filter((candidate) =>
+ applicable.every(
+ (other) =>
+ candidate === other || moreSpecific(candidate, other, phase, arity, relations) === 'yes',
+ ),
+ );
+ if (winners.length === 1)
+ return { kind: 'resolved', definition: winners[0]!.definition, phase };
+ if (winners.length > 1) {
+ // JLS 15.12.2.5 can choose a preferred declaration among inherited
+ // override-equivalent methods. Abstract/default and return-type facts
+ // are outside this bounded selector; this is not proven ambiguity.
+ return { kind: 'unknown', candidates: applicable.map((candidate) => candidate.definition) };
+ }
+ const unknown = applicable.some((candidate) =>
+ applicable.some(
+ (other) =>
+ candidate !== other &&
+ moreSpecific(candidate, other, phase, arity, relations) === 'unknown',
+ ),
+ );
+ return {
+ kind: unknown ? 'unknown' : 'ambiguous',
+ candidates: applicable.map((candidate) => candidate.definition),
+ };
+ }
+ return { kind: 'inapplicable', candidates: candidates.map((candidate) => candidate.definition) };
+}
+
+/** Split only balanced top-level arguments; unsupported wildcards/type uses
+ * remain unknown in the provider rather than losing generic information. */
+export function splitJavaTypeArguments(text: string): string[] | undefined {
+ const parts: string[] = [];
+ let depth = 0;
+ let start = 0;
+ for (let i = 0; i < text.length; i++) {
+ if (text[i] === '<') depth++;
+ else if (text[i] === '>') {
+ if (--depth < 0) return undefined;
+ } else if (text[i] === ',' && depth === 0) {
+ parts.push(text.slice(start, i).trim());
+ start = i + 1;
+ }
+ }
+ if (depth !== 0) return undefined;
+ parts.push(text.slice(start).trim());
+ return parts.every(Boolean) ? parts : undefined;
+}
diff --git a/gitnexus/src/core/ingestion/languages/java/scope-resolver.ts b/gitnexus/src/core/ingestion/languages/java/scope-resolver.ts
index 3879206ee..d23c3cd5c 100644
--- a/gitnexus/src/core/ingestion/languages/java/scope-resolver.ts
+++ b/gitnexus/src/core/ingestion/languages/java/scope-resolver.ts
@@ -42,6 +42,12 @@ import {
clearJavaClassAnnotationFacts,
} from './capture-side-channel.js';
import { clearJavaPackageFacts } from './package-facts.js';
+import {
+ resolveJavaReceiverMember,
+ resolveJavaSelectedMethodImplementation,
+ resolveJavaUnqualifiedCall,
+ resolveJavaReceiverCallSite,
+} from './overload-resolution.js';
const javaScopeResolver: ScopeResolver = {
language: SupportedLanguages.Java,
@@ -75,6 +81,11 @@ const javaScopeResolver: ScopeResolver = {
populateOwners: (parsed: ParsedFile) => populateClassOwnedMembers(parsed),
applyCaptureSideChannel: applyJavaCaptureSideChannel,
+ resolveReceiverMember: resolveJavaReceiverMember,
+ resolveUnqualifiedCall: resolveJavaUnqualifiedCall,
+ resolveSelectedMethodImplementation: resolveJavaSelectedMethodImplementation,
+ receiverMemberLookupCoversAllCalls: true,
+ receiverCallSite: resolveJavaReceiverCallSite,
isSuperReceiver: (text) => text.trim() === 'super',
diff --git a/gitnexus/src/core/ingestion/scope-resolution/contract/scope-resolver.ts b/gitnexus/src/core/ingestion/scope-resolution/contract/scope-resolver.ts
index 88ab0ff30..d61865bbb 100644
--- a/gitnexus/src/core/ingestion/scope-resolution/contract/scope-resolver.ts
+++ b/gitnexus/src/core/ingestion/scope-resolution/contract/scope-resolver.ts
@@ -319,7 +319,13 @@ export type ArityVerdict = 'compatible' | 'unknown' | 'incompatible';
export type ReceiverMemberResolution =
| { readonly kind: 'resolved'; readonly definition: SymbolDefinition }
- | { readonly kind: 'ambiguous'; readonly candidateIds: readonly string[] };
+ | {
+ readonly kind: 'ambiguous' | 'unknown' | 'inapplicable';
+ readonly candidateIds: readonly string[];
+ /** False when candidate enumeration itself could not be completed. */
+ readonly candidateSetComplete?: boolean;
+ readonly detail?: string;
+ };
export interface ImportResolutionContext {
readonly parsedFiles: readonly ParsedFile[];
@@ -1407,7 +1413,10 @@ export interface ScopeResolver {
readonly name: string;
readonly arity?: number;
readonly argumentTypes?: readonly string[];
- readonly atRange: { readonly startLine: number; readonly startCol: number };
+ readonly atRange: {
+ readonly startLine: number;
+ readonly startCol: number;
+ };
},
callerParsed: ParsedFile,
scopes: ScopeResolutionIndexes,
@@ -1663,6 +1672,38 @@ export interface ScopeResolver {
model: SemanticModel,
) => ReceiverMemberResolution | undefined;
+ /** Opt this member lookup into compound, super and chained call receivers.
+ * Existing providers retain their historical simple-receiver paths. */
+ readonly receiverMemberLookupCoversAllCalls?: boolean;
+
+ /** Source identity of a call whose result is this site's immediate receiver.
+ * Captured from the AST, never inferred by comparing expression strings.
+ * Enables typing from the selected compile-time declaration; an unresolved
+ * producer must not fall back to a name-based return-type guess. */
+ readonly receiverCallSite?: (
+ callsite: ReferenceSite,
+ scopes: ScopeResolutionIndexes,
+ ) => { readonly name: string; readonly range: ReferenceSite['atRange'] } | undefined;
+
+ /** Resolve an unqualified call before implicit-receiver owner pruning.
+ * Undefined declines handling; every returned result is authoritative and
+ * must not resume first-match or global-name fallback. */
+ readonly resolveUnqualifiedCall?: (
+ callsite: ReferenceSite,
+ scopes: ScopeResolutionIndexes,
+ model: SemanticModel,
+ ) => ReceiverMemberResolution | undefined;
+
+ /** Find an implementation of an already selected compile-time signature.
+ * This is override matching, not overload applicability at a runtime subtype.
+ * Undefined means no matching implementation; unknown retains a boundary. */
+ readonly resolveSelectedMethodImplementation?: (
+ selected: SymbolDefinition,
+ owner: SymbolDefinition,
+ scopes: ScopeResolutionIndexes,
+ model: SemanticModel,
+ ) => ReceiverMemberResolution | undefined;
+
/** Suppress all receiver dispatch when a captured type cannot prove its
* runtime binding. Runs before compound and simple receiver lookup; the
* caller records a receiver-unresolved outcome. */
diff --git a/gitnexus/src/core/ingestion/scope-resolution/passes/callable-value-flow.ts b/gitnexus/src/core/ingestion/scope-resolution/passes/callable-value-flow.ts
index d6d384b0b..0d91e78e7 100644
--- a/gitnexus/src/core/ingestion/scope-resolution/passes/callable-value-flow.ts
+++ b/gitnexus/src/core/ingestion/scope-resolution/passes/callable-value-flow.ts
@@ -835,7 +835,7 @@ export function buildGraphTargetIndex(
graph: KnowledgeGraph,
): ReadonlyMap {
const out = new Map();
- const { byAnchor, callableByPosition } = buildGraphCallableIndexes(graph);
+ const { byAnchor, callableByPosition, callableNames } = buildGraphCallableIndexes(graph);
for (const def of scopes.defs.byId.values()) {
const callableDef = isCallable(def) || providerTarget?.(def) === true;
if (!callableDef) {
@@ -858,7 +858,7 @@ export function buildGraphTargetIndex(
// A closure binding IS its callable node: same file, same line, same
// name. An aliasing local is not. So look the node up by position and
// admit only an exact hit.
- const positionalKey = valueBindingPositionKey(def);
+ const positionalKey = valueBindingPositionKey(def, callableNames);
const positionalId =
positionalKey === undefined ? undefined : callableByPosition.get(positionalKey);
// `''` marks an ambiguous position (two callables claiming one
@@ -894,12 +894,20 @@ const withoutSigil = (name: string): string => name.replace(/^[$@]+/, '');
* in `buildGraphCallableIndexes`. Definition lines come from the def id and are
* 1-based; graph `startLine` is 0-based, which is the `+ 1` there.
*/
-function valueBindingPositionKey(def: SymbolDefinition): string | undefined {
+function valueBindingPositionKey(
+ def: SymbolDefinition,
+ callableNames: ReadonlySet,
+): string | undefined {
if (!VALUE_BINDING_DEF_TYPES.has(def.type)) return undefined;
- const line = definitionIdPosition(def.nodeId, def.filePath)?.line;
const name = simpleQualifiedName(def);
- if (line === undefined || name === undefined) return undefined;
- return `${def.filePath}\0${line}\0${withoutSigil(name)}`;
+ if (name === undefined) return undefined;
+ const normalizedName = withoutSigil(name);
+ // Reject ordinary values before parsing IDs or allocating positional keys.
+ // A name hit still requires the exact file/line/name join.
+ if (!callableNames.has(normalizedName)) return undefined;
+ const line = definitionIdPosition(def.nodeId, def.filePath)?.line;
+ if (line === undefined) return undefined;
+ return `${def.filePath}\0${line}\0${normalizedName}`;
}
/**
@@ -1085,6 +1093,7 @@ function declarationSignatureCompatible(
interface GraphCallableIndexes {
/** Callable graph nodes by `file\0label\0line\0name` — the definition anchor. */
readonly byAnchor: ReadonlyMap;
+ readonly callableNames: ReadonlySet;
/**
* Callable graph nodes by `file\0line\0name` — the same anchor WITHOUT the
* label, because a value binding's def type never matches its callable node's
@@ -1100,6 +1109,7 @@ interface GraphCallableIndexes {
function buildGraphCallableIndexes(graph: KnowledgeGraph): GraphCallableIndexes {
const byAnchor = new Map();
const callableByPosition = new Map();
+ const callableNames = new Set();
for (const node of graph.iterNodes()) {
if (node.label !== 'Function' && node.label !== 'Method' && node.label !== 'Constructor') {
continue;
@@ -1115,7 +1125,9 @@ function buildGraphCallableIndexes(graph: KnowledgeGraph): GraphCallableIndexes
continue;
}
const oneBasedLine = zeroBasedLine + 1;
- const positionKey = `${filePath}\0${oneBasedLine}\0${withoutSigil(name)}`;
+ const normalizedName = withoutSigil(name);
+ callableNames.add(normalizedName);
+ const positionKey = `${filePath}\0${oneBasedLine}\0${normalizedName}`;
const existing = callableByPosition.get(positionKey);
// First wins would be order-dependent; mark the collision instead so an
// ambiguous position admits nothing rather than something arbitrary.
@@ -1125,7 +1137,7 @@ function buildGraphCallableIndexes(graph: KnowledgeGraph): GraphCallableIndexes
if (bucket === undefined) byAnchor.set(key, [node.id]);
else bucket.push(node.id);
}
- return { byAnchor, callableByPosition };
+ return { byAnchor, callableByPosition, callableNames };
}
function definitionAnchorKey(def: SymbolDefinition): string | undefined {
diff --git a/gitnexus/src/core/ingestion/scope-resolution/passes/free-call-fallback.ts b/gitnexus/src/core/ingestion/scope-resolution/passes/free-call-fallback.ts
index c3982d4c9..25f9748c3 100644
--- a/gitnexus/src/core/ingestion/scope-resolution/passes/free-call-fallback.ts
+++ b/gitnexus/src/core/ingestion/scope-resolution/passes/free-call-fallback.ts
@@ -32,6 +32,12 @@ import type { SemanticModel } from '../../model/semantic-model.js';
import type { WorkspaceResolutionIndex } from '../workspace-index.js';
import type { GraphNodeLookup } from '../graph-bridge/node-lookup.js';
import type { ScopeResolver } from '../contract/scope-resolver.js';
+import {
+ callSuppressionIdentities,
+ providerCallOutcome,
+ selectedCallKey,
+ type CallOutcomeContext,
+} from './provider-call-outcome.js';
import type {
ResolutionOutcomeRecorder,
ResolutionSuppressionReason,
@@ -65,7 +71,9 @@ export function emitFreeCallFallback(
scopes: ScopeResolutionIndexes,
parsedFiles: readonly ParsedFile[],
nodeLookup: GraphNodeLookup,
- _referenceIndex: { readonly bySourceScope: ReadonlyMap },
+ _referenceIndex: {
+ readonly bySourceScope: ReadonlyMap;
+ },
handledSites: Set,
model: SemanticModel,
workspaceIndex: WorkspaceResolutionIndex,
@@ -98,7 +106,10 @@ export function emitFreeCallFallback(
readonly name: string;
readonly arity?: number;
readonly argumentTypes?: readonly string[];
- readonly atRange: { readonly startLine: number; readonly startCol: number };
+ readonly atRange: {
+ readonly startLine: number;
+ readonly startCol: number;
+ };
},
callerParsed: ParsedFile,
scopes: ScopeResolutionIndexes,
@@ -107,6 +118,9 @@ export function emitFreeCallFallback(
/** Module-qualified free-call resolver (#2730) — see
* `ScopeResolver.resolveQualifiedFreeCall`. */
readonly resolveQualifiedFreeCall?: ScopeResolver['resolveQualifiedFreeCall'];
+ readonly resolveUnqualifiedCall?: ScopeResolver['resolveUnqualifiedCall'];
+ readonly selectedCalls?: Map;
+ readonly onSelectedCall?: (key: string, definition: SymbolDefinition | null) => void;
readonly conversionRankFn?: ConversionRankFn;
readonly conversionOnlyArgTypePrefixes?: readonly string[];
/** Optional per-language constraint hook threaded into
@@ -139,6 +153,17 @@ export function emitFreeCallFallback(
): number {
let emitted = 0;
const seen = new Set();
+ const outcomeContext: CallOutcomeContext = {
+ graph,
+ scopes,
+ nodeLookup,
+ model,
+ };
+ const recordSuppressed = (input: Parameters[1]): void =>
+ recordSuppressedOutcome(options.recordResolutionOutcome, {
+ ...input,
+ identityContext: outcomeContext,
+ });
// Build an O(1) simple-name -> callable defs index over scopes.defs once
// per pass so pickUniqueGlobalCallable doesn't re-scan defs.byId.values()
@@ -241,7 +266,10 @@ export function emitFreeCallFallback(
targetDefIds: [],
basis: 'free-call',
});
- const lookupOptions = { position: site.atRange, purpose: 'value' as const };
+ const lookupOptions = {
+ position: site.atRange,
+ purpose: 'value' as const,
+ };
const nameClaim = lookupNameClaim(site.inScope, site.name, scopes, lookupOptions);
const strictClaim = hasExplicitNameClaim(nameClaim, site.name);
const blocksCall =
@@ -264,7 +292,7 @@ export function emitFreeCallFallback(
basis: 'lexical-name-claim',
});
if (options.resolveAdlCandidates !== undefined) {
- recordSuppressedOutcome(options.recordResolutionOutcome, {
+ recordSuppressed({
recordSourceBinding: options.recordSourceBinding,
site,
phase: 'free-call-fallback',
@@ -284,6 +312,7 @@ export function emitFreeCallFallback(
// to the Class node itself (implicit constructor). Legacy emits
// the same two targets; see test expectations.
let fnDef: SymbolDefinition | undefined;
+ let fnDefFromProvider = false;
let writtenDefinition: SymbolDefinition | undefined;
let sourceCandidates: readonly SymbolDefinition[] | undefined;
// Guess flag starts here so the constructor unique-class pick can mark
@@ -294,6 +323,41 @@ export function emitFreeCallFallback(
// Language visibility vetoes the constructed TYPE (Class/Struct), not a
// Constructor child — Go refuses any `qualifiedName` containing `.`.
let globalFallbackVetoTarget: SymbolDefinition | undefined;
+ if (site.callForm !== 'constructor' && site.rawQualifiedName === undefined) {
+ const resolution = options.resolveUnqualifiedCall?.(site, scopes, model);
+ if (resolution !== undefined && resolution.kind !== 'resolved') {
+ options.selectedCalls?.set(selectedCallKey(parsed.filePath, site.atRange), null);
+ options.onSelectedCall?.(selectedCallKey(parsed.filePath, site.atRange), null);
+ options.recordSourceBinding?.(site, {
+ status:
+ resolution.kind === 'ambiguous'
+ ? 'ambiguous'
+ : resolution.kind === 'inapplicable'
+ ? 'blocked'
+ : 'unresolved',
+ targetDefIds: resolution.candidateIds,
+ basis:
+ resolution.kind === 'ambiguous'
+ ? 'member-lookup-ambiguous'
+ : `overload-${resolution.kind}`,
+ });
+ options.recordResolutionOutcome?.(
+ providerCallOutcome(
+ resolution,
+ site,
+ parsed.filePath,
+ 'free-call-fallback',
+ outcomeContext,
+ ),
+ );
+ handledSites.add(siteKey(parsed.filePath, site));
+ continue;
+ }
+ if (resolution?.kind === 'resolved') {
+ fnDef = resolution.definition;
+ fnDefFromProvider = true;
+ }
+ }
if (site.callForm === 'constructor') {
// `ns.Type{}` names its type through a namespace import: a type the
// language will not let that qualifier reach is not found, rather than
@@ -448,7 +512,7 @@ export function emitFreeCallFallback(
fnDef = eligibleBindingCandidates[0]?.def;
sourceCandidates = eligibleBindingCandidates.map((candidate) => candidate.def);
if (fnDef === undefined && bindingCandidates.length > 0) {
- recordSuppressedOutcome(options.recordResolutionOutcome, {
+ recordSuppressed({
recordSourceBinding: options.recordSourceBinding,
site,
phase: 'free-call-fallback',
@@ -524,7 +588,7 @@ export function emitFreeCallFallback(
// Cross-file candidates are shadowing; keep first-match.
const sameFile = narrowed.every((d) => d.filePath === narrowed[0]!.filePath);
if (sameFile) {
- recordSuppressedOutcome(options.recordResolutionOutcome, {
+ recordSuppressed({
recordSourceBinding: options.recordSourceBinding,
site,
phase: 'free-call-fallback',
@@ -562,7 +626,10 @@ export function emitFreeCallFallback(
name: site.name,
arity: site.arity,
argumentTypes: site.argumentTypes,
- atRange: { startLine: site.atRange.startLine, startCol: site.atRange.startCol },
+ atRange: {
+ startLine: site.atRange.startLine,
+ startCol: site.atRange.startCol,
+ },
},
parsed,
scopes,
@@ -571,7 +638,7 @@ export function emitFreeCallFallback(
const key = siteKey(parsed.filePath, site);
if (adlSuppressed && ordinary.length === 0) {
- recordSuppressedOutcome(options.recordResolutionOutcome, {
+ recordSuppressed({
recordSourceBinding: options.recordSourceBinding,
site,
phase: 'free-call-fallback',
@@ -616,7 +683,7 @@ export function emitFreeCallFallback(
} else {
// These candidates share the selected lexical tier. Different
// files do not establish precedence between imported overloads.
- recordSuppressedOutcome(options.recordResolutionOutcome, {
+ recordSuppressed({
recordSourceBinding: options.recordSourceBinding,
site,
phase: 'free-call-fallback',
@@ -659,7 +726,7 @@ export function emitFreeCallFallback(
handledSites.add(key);
continue;
} else if (narrowed.length > 1) {
- recordSuppressedOutcome(options.recordResolutionOutcome, {
+ recordSuppressed({
recordSourceBinding: options.recordSourceBinding,
site,
phase: 'free-call-fallback',
@@ -804,7 +871,7 @@ export function emitFreeCallFallback(
continue;
}
if (fnDef.isDeleted === true) {
- recordSuppressedOutcome(options.recordResolutionOutcome, {
+ recordSuppressed({
recordSourceBinding: options.recordSourceBinding,
site,
phase: 'free-call-fallback',
@@ -889,9 +956,22 @@ export function emitFreeCallFallback(
nodeLookup,
site.atRange,
);
- if (callerGraphId === undefined) continue;
const tgtGraphId = resolveDefGraphId(fnDef.filePath, fnDef, nodeLookup);
- if (tgtGraphId === undefined) continue;
+ options.selectedCalls?.set(selectedCallKey(parsed.filePath, site.atRange), fnDef);
+ options.onSelectedCall?.(selectedCallKey(parsed.filePath, site.atRange), fnDef);
+ if (fnDefFromProvider) {
+ handledSites.add(siteKey(parsed.filePath, site));
+ options.recordResolutionOutcome?.(
+ providerCallOutcome(
+ { kind: 'resolved', definition: fnDef },
+ site,
+ parsed.filePath,
+ 'free-call-fallback',
+ outcomeContext,
+ ),
+ );
+ }
+ if (callerGraphId === undefined || tgtGraphId === undefined) continue;
// Always mark the site as handled — even when the dedup-collapse
// means we don't add a new edge — so `emit-references` skips its
// potentially-wrong fallback for the same site.
@@ -1000,7 +1080,9 @@ export function constructionSiteReason(
function siteKey(
filePath: string,
- site: { readonly atRange: { readonly startLine: number; readonly startCol: number } },
+ site: {
+ readonly atRange: { readonly startLine: number; readonly startCol: number };
+ },
): string {
return `${filePath}:${site.atRange.startLine}:${site.atRange.startCol}`;
}
@@ -1042,6 +1124,7 @@ function recordSuppressedOutcome(
readonly candidates: readonly SymbolDefinition[];
readonly site?: ReferenceSite;
readonly recordSourceBinding?: SourceBindingRecorder;
+ readonly identityContext?: CallOutcomeContext;
},
): void {
if (input.site !== undefined)
@@ -1058,6 +1141,15 @@ function recordSuppressedOutcome(
range: input.range,
reason: input.reason,
candidateIds: input.candidates.map((d) => d.nodeId),
+ ...(input.site !== undefined && input.identityContext !== undefined
+ ? callSuppressionIdentities(
+ input.candidates.map((d) => d.nodeId),
+ input.site,
+ input.identityContext,
+ true,
+ input.candidates,
+ )
+ : { siteKind: input.site?.kind }),
});
}
diff --git a/gitnexus/src/core/ingestion/scope-resolution/passes/provider-call-outcome.ts b/gitnexus/src/core/ingestion/scope-resolution/passes/provider-call-outcome.ts
new file mode 100644
index 000000000..33284e3b2
--- /dev/null
+++ b/gitnexus/src/core/ingestion/scope-resolution/passes/provider-call-outcome.ts
@@ -0,0 +1,170 @@
+import type { ReferenceSite, SymbolDefinition } from 'gitnexus-shared';
+import type { KnowledgeGraph } from '../../../graph/types.js';
+import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js';
+import type { SemanticModel } from '../../model/semantic-model.js';
+import type { ReceiverMemberResolution } from '../contract/scope-resolver.js';
+import { resolveCallerGraphId, resolveDefGraphId } from '../graph-bridge/ids.js';
+import type { GraphNodeLookup } from '../graph-bridge/node-lookup.js';
+import type { ResolutionOutcome } from '../resolution-outcome.js';
+
+/** A nested call and its consumer can share a start position. Keep the entire
+ * AST call range so compile-time return types cannot alias those calls. */
+export function selectedCallKey(filePath: string, range: ReferenceSite['atRange']): string {
+ return `${filePath}:${range.startLine}:${range.startCol}:${range.endLine}:${range.endCol}`;
+}
+
+export function callResultBindingKey(scopeId: string, name: string): string {
+ return `${scopeId}\0${name}`;
+}
+
+export interface CallOutcomeContext {
+ readonly graph: KnowledgeGraph;
+ readonly scopes: ScopeResolutionIndexes;
+ readonly nodeLookup: GraphNodeLookup;
+ readonly model: SemanticModel;
+}
+
+/** Preserve scope IDs for diagnostics while bridging to queryable graph IDs.
+ * A failed bridge is missing evidence, never a measured empty candidate set. */
+export function callSuppressionIdentities(
+ candidateIds: readonly string[],
+ site: ReferenceSite,
+ context: CallOutcomeContext,
+ inventoryComplete = true,
+ definitions: readonly SymbolDefinition[] = [],
+): Pick<
+ Extract,
+ 'candidateGraphIds' | 'candidateOwnerIds' | 'callerId' | 'candidateSetComplete' | 'siteKind'
+> {
+ const { graph, scopes, nodeLookup, model } = context;
+ const ids = [...new Set(candidateIds)];
+ const candidateGraphIds = new Set();
+ const candidateOwnerIds = new Set();
+ let candidateSetComplete = inventoryComplete;
+ let suppliedById: Map | undefined;
+ let byId: Map | undefined;
+ for (const id of ids) {
+ let definition = scopes.defs.get(id);
+ if (definition === undefined) {
+ if (suppliedById === undefined) {
+ suppliedById = new Map();
+ for (const item of definitions) {
+ if (!suppliedById.has(item.nodeId)) suppliedById.set(item.nodeId, item);
+ }
+ }
+ definition = suppliedById.get(id);
+ }
+ if (definition === undefined) {
+ byId ??= new Map(
+ [
+ ...model.methods.lookupMethodByName(site.name),
+ ...model.symbols.lookupCallableByName(site.name),
+ ].map((item) => [item.nodeId, item]),
+ );
+ definition = byId.get(id);
+ }
+ const graphId =
+ definition === undefined
+ ? graph.getNode(id)?.id
+ : resolveDefGraphId(definition.filePath, definition, nodeLookup);
+ if (graphId === undefined || graph.getNode(graphId) === undefined) {
+ candidateSetComplete = false;
+ continue;
+ }
+ candidateGraphIds.add(graphId);
+ const ownerId = definition?.ownerId;
+ if (ownerId !== undefined) {
+ const owner = scopes.defs.get(ownerId);
+ const ownerGraphId =
+ owner === undefined
+ ? graph.getNode(ownerId)?.id
+ : resolveDefGraphId(owner.filePath, owner, nodeLookup);
+ if (ownerGraphId === undefined || graph.getNode(ownerGraphId) === undefined)
+ candidateSetComplete = false;
+ else candidateOwnerIds.add(ownerGraphId);
+ } else if (
+ definition === undefined ||
+ definition.type === 'Method' ||
+ definition.type === 'Constructor'
+ ) {
+ candidateSetComplete = false;
+ }
+ }
+ if (candidateGraphIds.size !== ids.length) candidateSetComplete = false;
+ const callerId = resolveCallerGraphId(
+ site.callerScope ?? site.inScope,
+ scopes,
+ nodeLookup,
+ site.atRange,
+ );
+ if (callerId === undefined || graph.getNode(callerId) === undefined) candidateSetComplete = false;
+ return {
+ candidateGraphIds: [...candidateGraphIds],
+ candidateOwnerIds: [...candidateOwnerIds],
+ callerId,
+ candidateSetComplete,
+ siteKind: site.kind,
+ };
+}
+
+export function providerCallOutcome(
+ result: ReceiverMemberResolution,
+ site: ReferenceSite,
+ filePath: string,
+ phase: string,
+ context: CallOutcomeContext,
+): ResolutionOutcome {
+ const location = { phase, filePath, name: site.name, range: site.atRange };
+ let suppression: Exclude;
+ if (result.kind === 'resolved') {
+ const targetId = resolveDefGraphId(
+ result.definition.filePath,
+ result.definition,
+ context.nodeLookup,
+ );
+ const callerId = resolveCallerGraphId(
+ site.callerScope ?? site.inScope,
+ context.scopes,
+ context.nodeLookup,
+ site.atRange,
+ );
+ if (
+ targetId !== undefined &&
+ callerId !== undefined &&
+ context.graph.getNode(targetId) !== undefined &&
+ context.graph.getNode(callerId) !== undefined
+ ) {
+ return {
+ kind: 'resolved',
+ targetId: result.definition.nodeId,
+ ...location,
+ };
+ }
+ suppression = {
+ kind: 'unknown',
+ candidateIds: [result.definition.nodeId],
+ candidateSetComplete: false,
+ detail: 'The selected call could not be represented by graph identities.',
+ };
+ } else {
+ suppression = result;
+ }
+ return {
+ kind: 'suppressed',
+ reason:
+ suppression.kind === 'ambiguous'
+ ? 'member-lookup-ambiguous'
+ : suppression.kind === 'unknown'
+ ? 'overload-unknown'
+ : 'overload-inapplicable',
+ candidateIds: suppression.candidateIds,
+ detail: suppression.detail,
+ ...location,
+ ...callSuppressionIdentities(
+ suppression.candidateIds,
+ site,
+ context,
+ suppression.candidateSetComplete !== false,
+ ),
+ };
+}
diff --git a/gitnexus/src/core/ingestion/scope-resolution/passes/receiver-bound-calls.ts b/gitnexus/src/core/ingestion/scope-resolution/passes/receiver-bound-calls.ts
index 5ebd0774d..44971f2f2 100644
--- a/gitnexus/src/core/ingestion/scope-resolution/passes/receiver-bound-calls.ts
+++ b/gitnexus/src/core/ingestion/scope-resolution/passes/receiver-bound-calls.ts
@@ -61,6 +61,7 @@
*/
import type {
+ CallResultAssignmentSite,
ParsedFile,
ReferenceSite,
ScopeId,
@@ -70,7 +71,7 @@ import type {
import type { KnowledgeGraph } from '../../../graph/types.js';
import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js';
import type { SemanticModel } from '../../model/semantic-model.js';
-import type { ScopeResolver } from '../contract/scope-resolver.js';
+import type { ReceiverMemberResolution, ScopeResolver } from '../contract/scope-resolver.js';
import { LINKABLE_LABELS, type GraphNodeLookup } from '../graph-bridge/node-lookup.js';
import type { WorkspaceResolutionIndex } from '../workspace-index.js';
import { createNamespaceTargetCache } from '../scope/namespace-target-cache.js';
@@ -128,6 +129,13 @@ import type {
ResolutionSuppressionReason,
} from '../resolution-outcome.js';
import { classifyReceiverShape } from '../resolution-outcome.js';
+import {
+ callSuppressionIdentities,
+ callResultBindingKey,
+ providerCallOutcome,
+ selectedCallKey,
+ type CallOutcomeContext,
+} from './provider-call-outcome.js';
import type { ReceiverOrigin } from '../resolution-outcome.js';
import { decodeReceiverChain } from '../../utils/receiver-chain-codec.js';
import type { DecodedReceiverChain } from '../../utils/receiver-chain-codec.js';
@@ -206,6 +214,9 @@ type ReceiverBoundProviderSubset = Pick<
| 'namespaceSkipsEnclosingClasses'
| 'createReceiverNamespaceResolver'
| 'resolveReceiverMember'
+ | 'receiverMemberLookupCoversAllCalls'
+ | 'receiverCallSite'
+ | 'resolveSelectedMethodImplementation'
| 'suppressReceiverLookup'
| 'isModuleExportAssignment'
| 'resolveThisViaEnclosingClass'
@@ -660,6 +671,13 @@ export function emitReceiverBoundCalls(
readonly allFilePaths?: ReadonlySet;
readonly recordResolutionOutcome?: ResolutionOutcomeRecorder;
readonly recordSourceBinding?: SourceBindingRecorder;
+ /** Compile-time primaries, separate from runtime fanout in calleeIdSink. */
+ readonly selectedCalls?: Map;
+ /** Apply only assignments fed by this newly selected compile-time primary. */
+ readonly onSelectedCall?: (key: string, definition: SymbolDefinition | null) => void;
+ /** Full source identity to the legacy handled-site key, finalized after replay. */
+ readonly pendingCallReceivers?: Map;
+ readonly callResultProducers?: ReadonlyMap;
/** Resolved-callee-id capture sink (#2227 U2). Threaded in under `--pdg`
* OR for callable-flow's direct-target index (#2437, position-filtered);
* `undefined` ⇒ zero overhead, byte-identity (R4). Per-file capture
@@ -699,6 +717,40 @@ export function emitReceiverBoundCalls(
// them resolve the same site to the same target. NEVER pre-seed
// from the reference index — see Contract Invariant I5.
const seen = new Set();
+ const outcomeContext: CallOutcomeContext = {
+ graph,
+ scopes,
+ nodeLookup,
+ model,
+ };
+ const suppressProviderResult = (
+ result: ReceiverMemberResolution | undefined,
+ site: ReferenceSite,
+ filePath: string,
+ ): boolean => {
+ if (result === undefined || result.kind === 'resolved') return false;
+ if (site.kind === 'call') {
+ const key = selectedCallKey(filePath, site.atRange);
+ options.selectedCalls?.set(key, null);
+ options.onSelectedCall?.(key, null);
+ options.pendingCallReceivers?.delete(key);
+ }
+ options.recordSourceBinding?.(site, {
+ status:
+ result.kind === 'ambiguous'
+ ? 'ambiguous'
+ : result.kind === 'inapplicable'
+ ? 'blocked'
+ : 'unresolved',
+ targetDefIds: result.candidateIds,
+ basis: result.kind === 'ambiguous' ? 'member-lookup-ambiguous' : `overload-${result.kind}`,
+ });
+ options.recordResolutionOutcome?.(
+ providerCallOutcome(result, site, filePath, 'receiver-bound-calls', outcomeContext),
+ );
+ handledSites.add(`${filePath}:${site.atRange.startLine}:${site.atRange.startCol}`);
+ return true;
+ };
// Active only while a source site is being resolved; nested class/chain
// lookups report a name guess at the actual decision that made it.
let onNameFallback: (() => void) | undefined;
@@ -1205,7 +1257,17 @@ export function emitReceiverBoundCalls(
* cases call this unconditionally, and the overwhelming majority of
* receivers are concrete classes that return at the first line. */
receiverTypeSpelling: string | undefined,
+ callerFilePath: string,
): number => {
+ // A provider may select an interface declaration from a concrete receiver.
+ // Runtime fan-out follows that selected declaration's owner.
+ const concreteReceiver = ownerDef.type === 'Interface' ? undefined : ownerDef;
+ const selectedOwner =
+ primaryMemberDef.ownerId === undefined
+ ? undefined
+ : scopes.defs.get(primaryMemberDef.ownerId);
+ if (concreteReceiver !== undefined && selectedOwner?.type === 'Interface')
+ ownerDef = selectedOwner;
if (ownerDef.type !== 'Interface') return 0;
if (subtypesBySupertypeDefId.get(ownerDef.nodeId) === undefined) return 0;
const receiverTypeArguments =
@@ -1240,6 +1302,10 @@ export function emitReceiverBoundCalls(
readonly typeArguments: readonly string[] | undefined;
};
const targetByMemberId = new Map();
+ const unresolvedDispatchCandidates = new Set();
+ const unresolvedDispatchOwners = new Set();
+ let hasUnresolvedDispatch = false;
+ let dispatchCandidatesComplete = true;
const bestIncomingCount = new Map([[ownerDef.nodeId, 0]]);
const queue: DispatchTraversal[] = [
{
@@ -1260,6 +1326,15 @@ export function emitReceiverBoundCalls(
const superGraphId =
current.typeArguments === undefined ? undefined : classGraphIdByDefId.get(current.typeId);
for (const subDef of subtypesBySupertypeDefId.get(current.typeId) ?? []) {
+ // Preserve a proven concrete receiver branch when the provider selects
+ // an interface signature; sibling implementations are not its targets.
+ if (
+ concreteReceiver !== undefined &&
+ subDef.nodeId !== concreteReceiver.nodeId &&
+ !scopes.methodDispatch.mroFor(concreteReceiver.nodeId).includes(subDef.nodeId) &&
+ !scopes.methodDispatch.mroFor(subDef.nodeId).includes(concreteReceiver.nodeId)
+ )
+ continue;
const previousIncomingCount = bestIncomingCount.get(subDef.nodeId);
if (
previousIncomingCount !== undefined &&
@@ -1305,11 +1380,54 @@ export function emitReceiverBoundCalls(
bestIncomingCount.set(subDef.nodeId, current.ancestorImplementationCount);
- const implMember = pickOverload(subDef.nodeId, memberName, site, model, provider);
+ const signatureResolution = provider.resolveSelectedMethodImplementation?.(
+ primaryMemberDef,
+ subDef,
+ scopes,
+ model,
+ );
+ const implMember =
+ provider.resolveSelectedMethodImplementation === undefined
+ ? pickOverload(subDef.nodeId, memberName, site, model, provider)
+ : signatureResolution?.kind === 'resolved'
+ ? signatureResolution.definition
+ : undefined;
+ const implementationGraphId =
+ signatureResolution?.kind === 'resolved'
+ ? resolveDefGraphId(
+ signatureResolution.definition.filePath,
+ signatureResolution.definition,
+ nodeLookup,
+ )
+ : undefined;
+ const implementationUnmapped =
+ signatureResolution?.kind === 'resolved' &&
+ (implementationGraphId === undefined ||
+ graph.getNode(implementationGraphId) === undefined);
+ if (
+ signatureResolution?.kind === 'unknown' ||
+ signatureResolution?.kind === 'ambiguous' ||
+ implementationUnmapped
+ ) {
+ hasUnresolvedDispatch = true;
+ if (signatureResolution?.kind === 'resolved') {
+ unresolvedDispatchCandidates.add(signatureResolution.definition.nodeId);
+ dispatchCandidatesComplete = false;
+ } else if (signatureResolution !== undefined) {
+ for (const id of signatureResolution.candidateIds) unresolvedDispatchCandidates.add(id);
+ if (signatureResolution.candidateSetComplete === false)
+ dispatchCandidatesComplete = false;
+ }
+ const ownerGraphId = resolveDefGraphId(subDef.filePath, subDef, nodeLookup);
+ if (ownerGraphId === undefined || graph.getNode(ownerGraphId) === undefined)
+ dispatchCandidatesComplete = false;
+ else unresolvedDispatchOwners.add(ownerGraphId);
+ }
let descendantImplementationCount = current.ancestorImplementationCount;
if (
implMember !== undefined &&
implMember !== OVERLOAD_AMBIGUOUS &&
+ !implementationUnmapped &&
implMember.isDeleted !== true &&
implMember.nodeId !== primaryMemberDef.nodeId &&
!isDeclarationOnly(implMember) &&
@@ -1340,6 +1458,29 @@ export function emitReceiverBoundCalls(
}
}
+ if (hasUnresolvedDispatch) {
+ const outcome = providerCallOutcome(
+ {
+ kind: 'unknown',
+ candidateIds: [...unresolvedDispatchCandidates],
+ candidateSetComplete: dispatchCandidatesComplete,
+ detail: 'An implementation of the selected signature could not be established.',
+ },
+ site,
+ callerFilePath,
+ 'receiver-dispatch',
+ outcomeContext,
+ );
+ if (outcome.kind === 'suppressed') {
+ options.recordResolutionOutcome?.({
+ ...outcome,
+ candidateOwnerIds: [
+ ...new Set([...(outcome.candidateOwnerIds ?? []), ...unresolvedDispatchOwners]),
+ ],
+ });
+ }
+ }
+
const targets = [...targetByMemberId.values()]
.sort(
(left, right) =>
@@ -1470,9 +1611,34 @@ export function emitReceiverBoundCalls(
// calls can share only the key (`ns.make().m()` starts where `ns.make()`
// does), so one's resolution must not hide the other's drop (#3498).
const siteIdentitiesByKey = new Map>();
- for (const site of parsed.referenceSites) {
+ // Producer dependencies are local to the call's file. A source range is
+ // attempted once initially, then only when a producer it awaited resolves.
+ // This handles reverse capture order without rescanning the workspace.
+ const workQueue = options.selectedCalls === undefined ? undefined : [...parsed.referenceSites];
+ const sites = workQueue ?? parsed.referenceSites;
+ const waitingByProducer = new Map>();
+ const waitForProducer = (producerKey: string, site: ReferenceSite): void => {
+ if (options.selectedCalls === undefined || options.selectedCalls.has(producerKey)) return;
+ let waiting = waitingByProducer.get(producerKey);
+ if (waiting === undefined) waitingByProducer.set(producerKey, (waiting = new Map()));
+ waiting.set(selectedCallKey(parsed.filePath, site.atRange), site);
+ };
+ const releaseConsumers = (producerKey: string): void => {
+ const waiting = waitingByProducer.get(producerKey);
+ if (waiting === undefined) return;
+ waitingByProducer.delete(producerKey);
+ for (const site of waiting.values()) workQueue?.push(site);
+ };
+ for (const site of sites) {
if (site.kind !== 'call' && site.kind !== 'read' && site.kind !== 'write') continue;
if (site.explicitReceiver === undefined) continue;
+ // Primaries and terminal suppressions are stable across dependency
+ // replay. Revisit only consumers whose producer was not known yet.
+ if (
+ site.kind === 'call' &&
+ options.selectedCalls?.has(selectedCallKey(parsed.filePath, site.atRange))
+ )
+ continue;
// A receiver-specific decision supersedes the provisional registry
// answer even when it deliberately declines to emit an edge.
options.recordSourceBinding?.(site, {
@@ -1481,6 +1647,7 @@ export function emitReceiverBoundCalls(
basis: 'receiver',
});
let sourceBindingGrounded = true;
+ let providerSelectedMember = false;
onNameFallback =
options.recordSourceBinding === undefined
? undefined
@@ -1488,6 +1655,24 @@ export function emitReceiverBoundCalls(
sourceBindingGrounded = false;
};
const recordMember = (definition: SymbolDefinition): void => {
+ if (site.kind === 'call') {
+ const key = selectedCallKey(parsed.filePath, site.atRange);
+ options.selectedCalls?.set(key, definition);
+ options.onSelectedCall?.(key, definition);
+ options.pendingCallReceivers?.delete(key);
+ releaseConsumers(key);
+ }
+ if (providerSelectedMember && site.kind === 'call') {
+ options.recordResolutionOutcome?.(
+ providerCallOutcome(
+ { kind: 'resolved', definition },
+ site,
+ parsed.filePath,
+ 'receiver-bound-calls',
+ outcomeContext,
+ ),
+ );
+ }
options.recordSourceBinding?.(
site,
sourceBindingGrounded
@@ -1525,11 +1710,71 @@ export function emitReceiverBoundCalls(
decodeReceiverChain(site.receiverChain)?.baseReceiverName ??
receiverName.split(/[.([\s]/, 1)[0];
+ if (
+ site.kind === 'call' &&
+ baseName !== undefined &&
+ options.callResultProducers !== undefined
+ ) {
+ const claim = lookupNameClaim(site.inScope, baseName, scopes, {
+ position: site.atRange,
+ purpose: 'value',
+ });
+ let currentScopeId: ScopeId | null = site.inScope;
+ let unresolvedAssignment = false;
+ let selectedProducer: SymbolDefinition | null | undefined;
+ let producerKey: string | undefined;
+ while (currentScopeId !== null) {
+ const currentScope = scopes.scopeTree.getScope(currentScopeId);
+ if (currentScope === undefined) break;
+ const assignment = options.callResultProducers.get(
+ callResultBindingKey(currentScopeId, baseName),
+ );
+ if (
+ assignment !== undefined &&
+ (assignment.callSite.startLine < site.atRange.startLine ||
+ (assignment.callSite.startLine === site.atRange.startLine &&
+ assignment.callSite.startCol <= site.atRange.startCol))
+ ) {
+ producerKey = selectedCallKey(parsed.filePath, assignment.callSite);
+ selectedProducer = options.selectedCalls?.get(producerKey);
+ unresolvedAssignment =
+ selectedProducer == null ||
+ !index.declaredReturnTypeByCallableId.has(selectedProducer.nodeId);
+ break;
+ }
+ // A nearer declaration owns the receiver even when it has no type.
+ if (claim.scope?.id === currentScopeId || currentScope.typeBindings.has(baseName)) break;
+ currentScopeId = currentScope.parent;
+ }
+ if (unresolvedAssignment) {
+ if (producerKey !== undefined) waitForProducer(producerKey, site);
+ options.pendingCallReceivers?.set(
+ selectedCallKey(parsed.filePath, site.atRange),
+ siteKey,
+ );
+ options.recordResolutionOutcome?.({
+ kind: 'suppressed',
+ reason: 'receiver-unresolved',
+ candidateIds: [],
+ phase: 'receiver-bound-calls',
+ filePath: parsed.filePath,
+ name: site.name,
+ range: site.atRange,
+ siteKind: 'call',
+ receiverOrigin: selectedProducer == null ? 'unknown' : 'in-program',
+ receiverShape: classifyReceiverShape(decodeReceiverChain(site.receiverChain)),
+ });
+ continue;
+ }
+ }
+
if (provider.suppressReceiverLookup !== undefined) {
const baseTypeRef =
baseName === undefined
? undefined
- : findReceiverTypeBinding(site.inScope, baseName, scopes, { position: site.atRange });
+ : findReceiverTypeBinding(site.inScope, baseName, scopes, {
+ position: site.atRange,
+ });
if (baseTypeRef !== undefined && provider.suppressReceiverLookup(baseTypeRef)) {
options.recordResolutionOutcome?.({
kind: 'suppressed',
@@ -1595,9 +1840,19 @@ export function emitReceiverBoundCalls(
extendsOnly !== undefined
? extendsOnly(enclosingClass.nodeId)
: scopes.methodDispatch.mroFor(enclosingClass.nodeId);
- let memberDef: SymbolDefinition | undefined;
+ const baseDef = ancestors.length > 0 ? scopes.defs.get(ancestors[0]!) : undefined;
+ const languageResolution =
+ provider.receiverMemberLookupCoversAllCalls === true &&
+ site.kind === 'call' &&
+ baseDef !== undefined
+ ? provider.resolveReceiverMember?.(baseDef, memberName, site, scopes, model)
+ : undefined;
+ if (suppressProviderResult(languageResolution, site, parsed.filePath)) continue;
+ providerSelectedMember = languageResolution?.kind === 'resolved';
+ let memberDef =
+ languageResolution?.kind === 'resolved' ? languageResolution.definition : undefined;
let ambiguousOwnerId: string | undefined;
- for (const ownerId of ancestors) {
+ for (const ownerId of memberDef === undefined ? ancestors : []) {
const picked =
site.kind === 'call'
? pickOverload(ownerId, memberName, site, model, provider)
@@ -1621,6 +1876,7 @@ export function emitReceiverBoundCalls(
model,
provider,
options.recordSourceBinding,
+ outcomeContext,
);
handledSites.add(siteKey);
continue;
@@ -1700,20 +1956,80 @@ export function emitReceiverBoundCalls(
receiverName.includes('(') ||
site.receiverChain !== undefined
) {
- const resolved = resolveCompoundReceiverTyped(
- receiverName,
- site.inScope,
- scopes,
- index,
- // Group A: the receiver IS this site's expression, so the site's
- // captured chain describes it and the structural fold applies.
- { ...fileCompoundOpts, receiverChain: site.receiverChain },
- );
+ const producer =
+ site.kind === 'call' ? provider.receiverCallSite?.(site, scopes) : undefined;
+ const selected =
+ producer === undefined
+ ? undefined
+ : options.selectedCalls?.get(selectedCallKey(parsed.filePath, producer.range));
+ const returned =
+ selected == null ? undefined : index.declaredReturnTypeByCallableId.get(selected.nodeId);
+ const returnedClass =
+ returned === undefined
+ ? undefined
+ : resolveClassBindingForName(
+ returned.declaredAtScope,
+ returned.declaredSpelling ?? returned.rawName,
+ scopes,
+ provider.stripTypePreservingDecoration,
+ {
+ purpose: 'type',
+ position: returned.lookupPosition,
+ qualifiedTypeSpellings: provider.resolveQualifiedTypeSpellings === true,
+ },
+ );
+ const resolved =
+ producer === undefined
+ ? resolveCompoundReceiverTyped(receiverName, site.inScope, scopes, index, {
+ ...fileCompoundOpts,
+ receiverChain: site.receiverChain,
+ })
+ : returnedClass === undefined
+ ? undefined
+ : {
+ def: returnedClass,
+ declaredSpelling: returned?.declaredSpelling ?? returned?.rawName,
+ };
+ if (producer !== undefined && resolved === undefined) {
+ // Defer without claiming the shared start-position key: the inner
+ // free call may still need that key during this pass. Finalization
+ // blocks generic fallback after the producer queue is exhausted.
+ waitForProducer(selectedCallKey(parsed.filePath, producer.range), site);
+ options.pendingCallReceivers?.set(
+ selectedCallKey(parsed.filePath, site.atRange),
+ siteKey,
+ );
+ options.recordResolutionOutcome?.({
+ kind: 'suppressed',
+ reason: 'receiver-unresolved',
+ candidateIds: [],
+ phase: 'receiver-bound-calls',
+ filePath: parsed.filePath,
+ name: site.name,
+ range: site.atRange,
+ siteKind: 'call',
+ receiverOrigin:
+ returned !== undefined && options.isBuiltInName?.(returned.rawName) === true
+ ? 'external'
+ : selected == null
+ ? 'unknown'
+ : 'in-program',
+ receiverShape: classifyReceiverShape(decodeReceiverChain(site.receiverChain)),
+ });
+ continue;
+ }
const currentClass = resolved?.def;
compoundReceiverUnresolved = currentClass === undefined;
if (resolved !== undefined && currentClass !== undefined) {
+ const languageResolution =
+ provider.receiverMemberLookupCoversAllCalls === true && site.kind === 'call'
+ ? provider.resolveReceiverMember?.(currentClass, memberName, site, scopes, model)
+ : undefined;
+ if (suppressProviderResult(languageResolution, site, parsed.filePath)) continue;
+ providerSelectedMember = languageResolution?.kind === 'resolved';
const chain = [currentClass.nodeId, ...scopes.methodDispatch.mroFor(currentClass.nodeId)];
- let memberDef: SymbolDefinition | undefined;
+ let memberDef =
+ languageResolution?.kind === 'resolved' ? languageResolution.definition : undefined;
let ambiguousOwnerId: string | undefined;
// Static-only filter (#1756 / U3): same shape as Case 4's
// overload-aware chain walk (skip-and-walk-on). When
@@ -1727,7 +2043,7 @@ export function emitReceiverBoundCalls(
// reference index has no compound-receiver entry for
// shapes like `Logger.create("a")`), so there's no wrong
// target to suppress.
- for (const ownerId of chain) {
+ for (const ownerId of memberDef === undefined ? chain : []) {
const picked =
site.kind === 'call'
? pickFirstNonStaticOnly(ownerId, memberName, site, model, provider)
@@ -1752,6 +2068,7 @@ export function emitReceiverBoundCalls(
model,
provider,
options.recordSourceBinding,
+ outcomeContext,
);
handledSites.add(siteKey);
continue;
@@ -1829,6 +2146,7 @@ export function emitReceiverBoundCalls(
0.85,
calleeCapture,
resolved.declaredSpelling,
+ parsed.filePath,
);
// Always mark handled when the site was resolved, even
// if the edge was deduplicated (collapse mode), so
@@ -1866,24 +2184,8 @@ export function emitReceiverBoundCalls(
scopes,
model,
);
- if (languageResolution?.kind === 'ambiguous') {
- options.recordSourceBinding?.(site, {
- status: 'ambiguous',
- targetDefIds: languageResolution.candidateIds,
- basis: 'member-lookup-ambiguous',
- });
- options.recordResolutionOutcome?.({
- kind: 'suppressed',
- phase: 'receiver-bound-calls',
- filePath: parsed.filePath,
- name: site.name,
- range: site.atRange,
- reason: 'member-lookup-ambiguous',
- candidateIds: languageResolution.candidateIds,
- });
- handledSites.add(siteKey);
- continue;
- }
+ if (suppressProviderResult(languageResolution, site, parsed.filePath)) continue;
+ providerSelectedMember = languageResolution?.kind === 'resolved';
if (languageResolution?.kind === 'resolved') {
const memberDef = languageResolution.definition;
if (
@@ -2256,24 +2558,8 @@ export function emitReceiverBoundCalls(
scopes,
model,
);
- if (languageResolution?.kind === 'ambiguous') {
- options.recordSourceBinding?.(site, {
- status: 'ambiguous',
- targetDefIds: languageResolution.candidateIds,
- basis: 'member-lookup-ambiguous',
- });
- options.recordResolutionOutcome?.({
- kind: 'suppressed',
- phase: 'receiver-bound-calls',
- filePath: parsed.filePath,
- name: site.name,
- range: site.atRange,
- reason: 'member-lookup-ambiguous',
- candidateIds: languageResolution.candidateIds,
- });
- handledSites.add(siteKey);
- continue;
- }
+ if (suppressProviderResult(languageResolution, site, parsed.filePath)) continue;
+ providerSelectedMember = languageResolution?.kind === 'resolved';
const chain = [classDef.nodeId, ...scopes.methodDispatch.mroFor(classDef.nodeId)];
let memberDef: SymbolDefinition | undefined =
languageResolution?.kind === 'resolved' ? languageResolution.definition : undefined;
@@ -2314,6 +2600,7 @@ export function emitReceiverBoundCalls(
model,
provider,
options.recordSourceBinding,
+ outcomeContext,
);
handledSites.add(siteKey);
continue;
@@ -2429,10 +2716,21 @@ export function emitReceiverBoundCalls(
? chainDef3
: lookupNamespaceMember(targetFile3, className);
if (classDef3 !== undefined) {
+ const languageResolution =
+ provider.receiverMemberLookupCoversAllCalls === true && site.kind === 'call'
+ ? provider.resolveReceiverMember?.(classDef3, memberName, site, scopes, model)
+ : undefined;
+ if (suppressProviderResult(languageResolution, site, parsed.filePath)) {
+ found3 = true;
+ break;
+ }
+ providerSelectedMember = languageResolution?.kind === 'resolved';
const picked =
- site.kind === 'call'
- ? pickOverload(classDef3.nodeId, memberName, site, model, provider)
- : findOwnedMember(classDef3.nodeId, memberName, model);
+ languageResolution?.kind === 'resolved'
+ ? languageResolution.definition
+ : site.kind === 'call'
+ ? pickOverload(classDef3.nodeId, memberName, site, model, provider)
+ : findOwnedMember(classDef3.nodeId, memberName, model);
if (picked === OVERLOAD_AMBIGUOUS) {
recordReceiverOverloadSuppression(
options.recordResolutionOutcome,
@@ -2443,6 +2741,7 @@ export function emitReceiverBoundCalls(
model,
provider,
options.recordSourceBinding,
+ outcomeContext,
);
handledSites.add(siteKey);
found3 = true;
@@ -2537,8 +2836,15 @@ export function emitReceiverBoundCalls(
}
const ownerDef = resolved?.def;
if (resolved !== undefined && ownerDef !== undefined) {
+ const languageResolution =
+ provider.receiverMemberLookupCoversAllCalls === true && site.kind === 'call'
+ ? provider.resolveReceiverMember?.(ownerDef, memberName, site, scopes, model)
+ : undefined;
+ if (suppressProviderResult(languageResolution, site, parsed.filePath)) continue;
+ providerSelectedMember = languageResolution?.kind === 'resolved';
const chain = [ownerDef.nodeId, ...scopes.methodDispatch.mroFor(ownerDef.nodeId)];
- let memberDef: SymbolDefinition | undefined;
+ let memberDef =
+ languageResolution?.kind === 'resolved' ? languageResolution.definition : undefined;
let ambiguousOwnerId: string | undefined;
// Static-only filter (#1756 / U3): mirrors Case 0's
// overload-aware chain walk. When
@@ -2548,7 +2854,7 @@ export function emitReceiverBoundCalls(
// by chain-typebinding receivers, not pre-emitted by
// `emitReferencesViaLookup` for compound shapes, so no
// handled-site marker is needed for chain-only-static).
- for (const ownerId of chain) {
+ for (const ownerId of memberDef === undefined ? chain : []) {
const picked =
site.kind === 'call'
? pickFirstNonStaticOnly(ownerId, memberName, site, model, provider)
@@ -2573,6 +2879,7 @@ export function emitReceiverBoundCalls(
model,
provider,
options.recordSourceBinding,
+ outcomeContext,
);
handledSites.add(siteKey);
continue;
@@ -2649,6 +2956,7 @@ export function emitReceiverBoundCalls(
0.85,
calleeCapture,
resolved.declaredSpelling,
+ parsed.filePath,
);
// Always mark handled when the site was resolved, even
// if the edge was deduplicated (collapse mode), so
@@ -2774,65 +3082,11 @@ export function emitReceiverBoundCalls(
scopes,
model,
);
- if (languageResolution?.kind === 'ambiguous') {
- options.recordSourceBinding?.(site, {
- status: 'ambiguous',
- targetDefIds: languageResolution.candidateIds,
- basis: 'member-lookup-ambiguous',
- });
- options.recordResolutionOutcome?.({
- kind: 'suppressed',
- phase: 'receiver-bound-calls',
- filePath: parsed.filePath,
- name: site.name,
- range: site.atRange,
- reason: 'member-lookup-ambiguous',
- candidateIds: languageResolution.candidateIds,
- });
- handledSites.add(siteKey);
- continue;
- }
- if (languageResolution?.kind === 'resolved') {
- const memberDef = languageResolution.definition;
- if (
- suppressDeletedCallTarget(
- options.recordResolutionOutcome,
- parsed.filePath,
- site,
- memberDef,
- options.recordSourceBinding,
- )
- ) {
- handledSites.add(siteKey);
- continue;
- }
- const reason =
- site.kind === 'write' || site.kind === 'read'
- ? site.kind
- : memberDef.filePath !== parsed.filePath
- ? 'import-resolved'
- : 'global';
- const confidence = site.kind === 'write' || site.kind === 'read' ? 1.0 : 0.85;
- recordMember(memberDef);
- const ok = tryEmitEdge(
- graph,
- scopes,
- nodeLookup,
- site,
- memberDef,
- reason,
- seen,
- confidence,
- collapse,
- calleeCapture,
- );
- if (ok) emitted++;
- handledSites.add(siteKey);
- continue;
- }
-
+ if (suppressProviderResult(languageResolution, site, parsed.filePath)) continue;
+ providerSelectedMember = languageResolution?.kind === 'resolved';
const chain = [ownerDef.nodeId, ...scopes.methodDispatch.mroFor(ownerDef.nodeId)];
- let memberDef: SymbolDefinition | undefined;
+ let memberDef =
+ languageResolution?.kind === 'resolved' ? languageResolution.definition : undefined;
let ambiguous = false;
let ambiguousOwnerId: string | undefined;
// Track whether the chain walk filtered out any static-only
@@ -2862,7 +3116,7 @@ export function emitReceiverBoundCalls(
// such a pair unambiguously to the instance method because
// companion members are not legal instance-dispatch
// candidates.
- for (const ownerId of chain) {
+ for (const ownerId of memberDef === undefined ? chain : []) {
const picked = pickFirstNonStaticOnly(ownerId, memberName, site, model, provider);
if (picked === OVERLOAD_AMBIGUOUS) {
ambiguous = true;
@@ -2897,6 +3151,7 @@ export function emitReceiverBoundCalls(
model,
provider,
options.recordSourceBinding,
+ outcomeContext,
);
handledSites.add(siteKey);
continue;
@@ -2964,6 +3219,7 @@ export function emitReceiverBoundCalls(
confidence,
calleeCapture,
typeApplication ?? typeRef.rawName,
+ parsed.filePath,
);
// Always mark handled when the site was resolved, even
// if the edge was deduplicated (collapse mode), so
@@ -3131,6 +3387,7 @@ export function emitReceiverBoundCalls(
range: site.atRange,
reason: 'member-lookup-ambiguous',
candidateIds: [...ambiguousCandidateIds],
+ ...callSuppressionIdentities([...ambiguousCandidateIds], site, outcomeContext),
});
}
@@ -3234,6 +3491,7 @@ export function emitReceiverBoundCalls(
model,
provider,
options.recordSourceBinding,
+ outcomeContext,
);
handledSites.add(siteKey);
continue;
@@ -3408,13 +3666,20 @@ export function emitReceiverBoundCalls(
},
);
if (receiverClass !== undefined) {
+ const languageResolution =
+ provider.receiverMemberLookupCoversAllCalls === true && site.kind === 'call'
+ ? provider.resolveReceiverMember?.(receiverClass, memberName, site, scopes, model)
+ : undefined;
+ if (suppressProviderResult(languageResolution, site, parsed.filePath)) continue;
+ providerSelectedMember = languageResolution?.kind === 'resolved';
const chain = [
receiverClass.nodeId,
...scopes.methodDispatch.mroFor(receiverClass.nodeId),
];
- let memberDef: SymbolDefinition | undefined;
+ let memberDef =
+ languageResolution?.kind === 'resolved' ? languageResolution.definition : undefined;
let ambiguousOwnerId: string | undefined;
- for (const ownerId of chain) {
+ for (const ownerId of memberDef === undefined ? chain : []) {
const picked = pickFirstNonStaticOnly(ownerId, memberName, site, model, provider);
if (picked === OVERLOAD_AMBIGUOUS) {
ambiguousOwnerId = ownerId;
@@ -3436,6 +3701,7 @@ export function emitReceiverBoundCalls(
model,
provider,
options.recordSourceBinding,
+ outcomeContext,
);
handledSites.add(siteKey);
continue;
@@ -3490,6 +3756,7 @@ export function emitReceiverBoundCalls(
confidence,
calleeCapture,
fieldDeclaredType?.name,
+ parsed.filePath,
);
handledSites.add(siteKey);
continue;
@@ -3814,7 +4081,8 @@ function recordReceiverOverloadSuppression(
memberName: string,
model: SemanticModel,
provider: ReceiverBoundProviderSubset,
- recordSourceBinding?: SourceBindingRecorder,
+ recordSourceBinding: SourceBindingRecorder | undefined,
+ identityContext: CallOutcomeContext,
): void {
if (record === undefined && recordSourceBinding === undefined) return;
const overloads = model.methods.lookupAllByOwner(ownerId, memberName);
@@ -3845,6 +4113,13 @@ function recordReceiverOverloadSuppression(
range: site.atRange,
reason,
candidateIds: candidates.map((d) => d.nodeId),
+ ...callSuppressionIdentities(
+ candidates.map((d) => d.nodeId),
+ site,
+ identityContext,
+ true,
+ candidates,
+ ),
});
}
diff --git a/gitnexus/src/core/ingestion/scope-resolution/pipeline/run.ts b/gitnexus/src/core/ingestion/scope-resolution/pipeline/run.ts
index 1ca5d7347..d61ef190b 100644
--- a/gitnexus/src/core/ingestion/scope-resolution/pipeline/run.ts
+++ b/gitnexus/src/core/ingestion/scope-resolution/pipeline/run.ts
@@ -24,6 +24,7 @@
*/
import type {
+ CallResultAssignmentSite,
ParsedFile,
ReferenceSite,
RegistryProviders,
@@ -124,9 +125,51 @@ import {
import { buildWorkspaceResolutionIndex } from '../workspace-index.js';
import { narrowOverloadCandidates } from '../passes/overload-narrowing.js';
import type { ResolutionOutcome, ResolutionOutcomeRecorder } from '../resolution-outcome.js';
-import { withdrawAnsweredReceiverDrops } from '../resolution-outcome.js';
+import { answeredSiteKey, withdrawAnsweredReceiverDrops } from '../resolution-outcome.js';
import { isDebugHeapEnabled, logHeapProbe } from '../../utils/heap-probe.js';
import { parseTruthyEnv } from '../../utils/env.js';
+import { callResultBindingKey, selectedCallKey } from '../passes/provider-call-outcome.js';
+
+function setCallResultBinding(scope: Scope, name: string, returned: TypeRef | undefined): boolean {
+ const bindings = scope.typeBindings as Map;
+ if (returned === undefined) return bindings.delete(name);
+ const binding: TypeRef = {
+ rawName: returned.rawName,
+ ...(returned.declaredSpelling !== undefined
+ ? { declaredSpelling: returned.declaredSpelling }
+ : {}),
+ declaredAtScope: returned.declaredAtScope,
+ ...(returned.lookupPosition !== undefined ? { lookupPosition: returned.lookupPosition } : {}),
+ source: 'assignment-inferred',
+ };
+ const previous = bindings.get(name);
+ if (
+ previous?.rawName === binding.rawName &&
+ previous.declaredSpelling === binding.declaredSpelling &&
+ previous.declaredAtScope === binding.declaredAtScope &&
+ previous.source === binding.source &&
+ previous.lookupPosition?.startLine === binding.lookupPosition?.startLine &&
+ previous.lookupPosition?.startCol === binding.lookupPosition?.startCol
+ )
+ return false;
+ bindings.set(name, binding);
+ return true;
+}
+
+function applySelectedCallResultBinding(
+ assignment: CallResultAssignmentSite,
+ indexes: ScopeResolutionIndexes,
+ workspaceIndex: ReturnType,
+ selected: SymbolDefinition | null | undefined,
+): boolean {
+ const scope = indexes.scopeTree.getScope(assignment.inScope);
+ if (scope === undefined) return false;
+ const returned =
+ selected == null
+ ? undefined
+ : workspaceIndex.declaredReturnTypeByCallableId.get(selected.nodeId);
+ return setCallResultBinding(scope, assignment.lhs, returned);
+}
/**
* Join extraction-time assignment identity to Phase-4's exact resolved callee.
@@ -143,8 +186,25 @@ export function applyPreciseCallResultBindings(
nodeLookup: ReturnType,
model: SemanticModel,
requireOwnedHoistedReturn = false,
+ selectedCalls?: ReadonlyMap,
): number {
let updated = 0;
+ if (selectedCalls !== undefined) {
+ for (const parsed of parsedFiles) {
+ for (const assignment of parsed.callResultAssignmentSites ?? []) {
+ if (
+ applySelectedCallResultBinding(
+ assignment,
+ indexes,
+ workspaceIndex,
+ selectedCalls.get(selectedCallKey(parsed.filePath, assignment.callSite)),
+ )
+ )
+ updated++;
+ }
+ }
+ return updated;
+ }
const returnTypeByGraphId = new Map();
for (const parsed of parsedFiles) {
for (const def of parsed.localDefs) {
@@ -167,13 +227,14 @@ export function applyPreciseCallResultBindings(
}
}
for (const assignment of parsed.callResultAssignmentSites ?? []) {
+ const scope = indexes.scopeTree.getScope(assignment.inScope);
+ if (scope === undefined) continue;
const targets = resolvedByPosition.get(
calleeIdPosKey(assignment.callSite.startLine, assignment.callSite.startCol),
);
- if (targets === undefined || targets.size !== 1) continue;
- const targetId = targets.values().next().value as string | undefined;
- if (targetId === undefined) continue;
- const returned = returnTypeByGraphId.get(targetId);
+ const targetId =
+ targets?.size === 1 ? (targets.values().next().value as string | undefined) : undefined;
+ const returned = targetId === undefined ? undefined : returnTypeByGraphId.get(targetId);
if (returned === undefined) continue;
if (requireOwnedHoistedReturn) {
const site = callsByPosition?.get(
@@ -225,18 +286,14 @@ export function applyPreciseCallResultBindings(
)
continue;
}
- const returnType = returned.type;
- const scope = indexes.scopeTree.getScope(assignment.inScope);
- if (scope === undefined) continue;
- (scope.typeBindings as Map).set(assignment.lhs, {
- rawName: returnType.rawName,
- ...(returnType.declaredSpelling !== undefined
- ? { declaredSpelling: returnType.declaredSpelling }
- : {}),
- declaredAtScope: assignment.inScope,
- source: 'assignment-inferred',
- });
- updated++;
+ if (
+ setCallResultBinding(scope, assignment.lhs, {
+ ...returned.type,
+ declaredAtScope: assignment.inScope,
+ lookupPosition: undefined,
+ })
+ )
+ updated++;
}
}
return updated;
@@ -540,7 +597,10 @@ interface RunScopeResolutionInput {
* `ParsedFile[]` to mirror the pipeline shape.
*/
readonly model: MutableSemanticModel;
- readonly files: readonly { readonly path: string; readonly content: string }[];
+ readonly files: readonly {
+ readonly path: string;
+ readonly content: string;
+ }[];
readonly onWarn?: (message: string) => void;
/**
* Optional pre-parsed-Tree lookup keyed by file path: a cache hit lets the
@@ -1203,6 +1263,51 @@ export function runScopeResolution(
// VALIDATE_SEMANTIC_MODEL=1.
validateBindingsImmutability(indexes, onWarn);
+ const selectedCalls =
+ provider.receiverCallSite === undefined
+ ? undefined
+ : new Map();
+ const pendingCallReceivers = selectedCalls === undefined ? undefined : new Map();
+ const callResultProducers =
+ selectedCalls === undefined ? undefined : new Map();
+ const assignmentsByProducer = new Map();
+ if (callResultProducers !== undefined) {
+ for (const parsed of parsedFiles) {
+ for (const assignment of parsed.callResultAssignmentSites ?? []) {
+ callResultProducers.set(
+ callResultBindingKey(assignment.inScope, assignment.lhs),
+ assignment,
+ );
+ const key = selectedCallKey(parsed.filePath, assignment.callSite);
+ let assignments = assignmentsByProducer.get(key);
+ if (assignments === undefined) assignmentsByProducer.set(key, (assignments = []));
+ assignments.push(assignment);
+ }
+ }
+ }
+ const onSelectedCall =
+ selectedCalls === undefined
+ ? undefined
+ : (key: string, definition: SymbolDefinition | null): void => {
+ for (const assignment of assignmentsByProducer.get(key) ?? []) {
+ applySelectedCallResultBinding(assignment, indexes, workspaceIndex, definition);
+ }
+ };
+ if (!callableFlowOnly && selectedCalls !== undefined) {
+ // Clear guessed assignment returns before Registry.lookup can cache a
+ // wrong downstream member. Exact primaries restore them after emission.
+ applyPreciseCallResultBindings(
+ parsedFiles,
+ indexes,
+ workspaceIndex,
+ createCalleeIdAccumulator(),
+ postHeritageNodeLookup,
+ readonlyModel,
+ provider.hoistedTypeBindingsRequireOwner === true,
+ selectedCalls,
+ );
+ }
+
// ── Phase 3: resolve references via Registry.lookup ────────────────────
input.onProgress?.('resolving references', files.length, files.length);
const registryProviders: RegistryProviders = {
@@ -1373,7 +1478,10 @@ export function runScopeResolution(
// propagation. Populated below at every CALLS emit path before dedup; the CFG
// join still consumes it only inside the `input.pdg` block.
const calleeIdAccumulator: CalleeIdAccumulator | undefined =
- input.pdg === true || deferredIndirectSites.size > 0 || callResultAssignmentSites.size > 0
+ input.pdg === true ||
+ deferredIndirectSites.size > 0 ||
+ callResultAssignmentSites.size > 0 ||
+ provider.receiverCallSite !== undefined
? createCalleeIdAccumulator(
input.pdg === true
? undefined
@@ -1401,6 +1509,10 @@ export function runScopeResolution(
{
recordResolutionOutcome,
recordSourceBinding,
+ selectedCalls,
+ onSelectedCall,
+ pendingCallReceivers,
+ callResultProducers,
allFilePaths,
calleeIdSink: calleeIdAccumulator,
// The pass's sources of positive EXTERNAL evidence for a dropped
@@ -1476,6 +1588,9 @@ export function runScopeResolution(
resolveAdlCandidates: provider.resolveAdlCandidates,
resolveOrdinaryCallables: provider.resolveOrdinaryCallables,
resolveQualifiedFreeCall: provider.resolveQualifiedFreeCall,
+ resolveUnqualifiedCall: provider.resolveUnqualifiedCall,
+ selectedCalls,
+ onSelectedCall,
conversionRankFn: provider.conversionRankFn,
conversionOnlyArgTypePrefixes: provider.conversionOnlyArgTypePrefixes,
constraintCompatibility: provider.constraintCompatibility,
@@ -1487,7 +1602,7 @@ export function runScopeResolution(
);
probeEmit('free-calls-end', `handled=${handledSites.size} emitted=${freeCallExtras}`);
const replayedCallResultBindings =
- callableFlowOnly || calleeIdAccumulator === undefined
+ callableFlowOnly || calleeIdAccumulator === undefined || selectedCalls !== undefined
? 0
: applyPreciseCallResultBindings(
emitParsedFiles,
@@ -1497,8 +1612,9 @@ export function runScopeResolution(
postHeritageNodeLookup,
readonlyModel,
provider.hoistedTypeBindingsRequireOwner === true,
+ selectedCalls,
);
- if (replayedCallResultBindings > 0) {
+ if (replayedCallResultBindings > 0 || selectedCalls !== undefined) {
const handledBeforeReplay = new Set(handledSites);
const replayedReceiverBound = emitReceiverBoundCalls(
graph,
@@ -1512,6 +1628,11 @@ export function runScopeResolution(
{
calleeIdSink: calleeIdAccumulator,
recordSourceBinding,
+ recordResolutionOutcome: selectedCalls === undefined ? undefined : recordResolutionOutcome,
+ selectedCalls,
+ onSelectedCall,
+ pendingCallReceivers,
+ callResultProducers,
isBuiltInName: provider.languageProvider.isBuiltInName,
externalReceiverNames: provider.languageProvider.externalReceiverNames,
isExternalModuleSpecifier: provider.languageProvider.isExternalModuleSpecifier,
@@ -1537,19 +1658,42 @@ export function runScopeResolution(
for (const key of handledSites) {
if (!handledBeforeReplay.has(key)) resolvedOnReplay.add(key);
}
- if (resolvedOnReplay.size > 0) {
- for (let i = resolutionOutcomes.length - 1; i >= 0; i--) {
- const outcome = resolutionOutcomes[i];
- if (
- outcome.kind === 'suppressed' &&
- outcome.reason === 'receiver-unresolved' &&
- resolvedOnReplay.has(callableFlowSiteKey(outcome.filePath, outcome.range))
- ) {
- resolutionOutcomes.splice(i, 1);
+ if (resolvedOnReplay.size > 0 || selectedCalls !== undefined) {
+ // Replay can retry an unresolved site. Keep its latest diagnostic once,
+ // and compact in one pass so withdrawing many drops stays linear.
+ const receiverDropPositions = new Map();
+ let kept = 0;
+ for (const outcome of resolutionOutcomes) {
+ if (outcome.kind === 'suppressed' && outcome.reason === 'receiver-unresolved') {
+ const resolved =
+ selectedCalls === undefined
+ ? resolvedOnReplay.has(callableFlowSiteKey(outcome.filePath, outcome.range))
+ : selectedCalls.get(selectedCallKey(outcome.filePath, outcome.range)) != null;
+ if (resolved) continue;
+ if (outcome.siteKind !== undefined) {
+ const key = answeredSiteKey(
+ outcome.filePath,
+ outcome.range,
+ outcome.siteKind,
+ outcome.name,
+ );
+ const previous = receiverDropPositions.get(key);
+ if (previous !== undefined) {
+ resolutionOutcomes[previous] = outcome;
+ continue;
+ }
+ receiverDropPositions.set(key, kept);
+ }
}
+ resolutionOutcomes[kept++] = outcome;
}
+ resolutionOutcomes.length = kept;
}
}
+ for (const [key, handledKey] of pendingCallReceivers ?? []) {
+ selectedCalls?.set(key, null);
+ handledSites.add(handledKey);
+ }
// Receiver/free-call passes have finished reading this set. Extend it for
// the remaining passes instead of retaining three whole-program copies.
const referenceSkipSites = handledSites;
@@ -1667,7 +1811,12 @@ export function runScopeResolution(
// propagation. `graph.addRelationship` remains first-write-wins, so precise
// edges already emitted for a site retain ownership.
const propertyDispatch = callableFlowOnly
- ? { usesEmitted: 0, callsEmitted: 0, skippedKeys: 0, skippedKeyNames: [] as readonly string[] }
+ ? {
+ usesEmitted: 0,
+ callsEmitted: 0,
+ skippedKeys: 0,
+ skippedKeyNames: [] as readonly string[],
+ }
: emitPropertyDispatchCalls(
graph,
indexes,
@@ -2176,7 +2325,11 @@ export function runScopeResolution(
logHeapProbe('sr-end', `lang=${provider.language} parsedFiles=${parsedFiles.length}`);
for (const outcome of resolutionOutcomes) input.recordResolutionOutcome?.(outcome);
- input.onResolved?.({ parsedFiles, indexes, referenceBindings: referenceBindings! });
+ input.onResolved?.({
+ parsedFiles,
+ indexes,
+ referenceBindings: referenceBindings!,
+ });
return {
filesProcessed: parsedFiles.length,
diff --git a/gitnexus/src/core/ingestion/scope-resolution/resolution-outcome.ts b/gitnexus/src/core/ingestion/scope-resolution/resolution-outcome.ts
index 9ccbcdf53..0e310dc41 100644
--- a/gitnexus/src/core/ingestion/scope-resolution/resolution-outcome.ts
+++ b/gitnexus/src/core/ingestion/scope-resolution/resolution-outcome.ts
@@ -9,6 +9,8 @@ export type ResolutionSuppressionReason =
| 'selected-callable-deleted'
| 'overload-ambiguous'
| 'overload-ambiguous-normalization'
+ | 'overload-unknown'
+ | 'overload-inapplicable'
| 'free-call-instance-ownership'
/** #2701 — the receiver is rebound by its own scope and has no type
* there (a JS/TS ordinary `function`'s `this`), so no enclosing type
@@ -41,6 +43,15 @@ export type ResolutionOutcome =
* qualified namespace resolver returns only an `ambiguous` sentinel.
*/
readonly candidateIds: readonly string[];
+ /** Public graph identities, kept separate from scope definition IDs. */
+ readonly candidateGraphIds?: readonly string[];
+ /** Public graph identities of the candidates' declaring types. */
+ readonly candidateOwnerIds?: readonly string[];
+ /** Public graph identity of the enclosing callable or initializer. */
+ readonly callerId?: string;
+ /** Candidate inventory and every required graph identity were proven. */
+ readonly candidateSetComplete?: boolean;
+ readonly detail?: string;
readonly phase: string;
readonly filePath: string;
readonly name: string;
diff --git a/gitnexus/src/core/ingestion/scope-resolution/unresolved-overloads.ts b/gitnexus/src/core/ingestion/scope-resolution/unresolved-overloads.ts
new file mode 100644
index 000000000..193fd7482
--- /dev/null
+++ b/gitnexus/src/core/ingestion/scope-resolution/unresolved-overloads.ts
@@ -0,0 +1,310 @@
+import type { Range } from 'gitnexus-shared';
+import type { ResolutionOutcome } from './resolution-outcome.js';
+
+/** Bound persisted diagnostics independently of repository size. */
+export const MAX_UNRESOLVED_OVERLOAD_SITES = 2_000;
+export const MAX_OVERLOAD_IDENTITIES_PER_SITE = 256;
+export const MAX_OVERLOAD_SUMMARY_BYTES = 2 * 1024 * 1024;
+
+export interface UnresolvedOverloadSite {
+ readonly id: string;
+ readonly filePath: string;
+ readonly name: string;
+ readonly range: Range;
+ /** Public graph identities, never scope-resolution definition IDs. */
+ readonly candidateIds: readonly string[];
+ readonly candidateOwnerIds: readonly string[];
+ readonly callerIds: readonly string[];
+ readonly reasons: readonly string[];
+}
+
+export interface UnresolvedOverloadSummary {
+ readonly schemaVersion: 1;
+ /** False for incomplete attribution, missing input, or a capped summary. */
+ readonly complete: boolean;
+ /** Unique unresolved call sites before the persistence cap. */
+ readonly totalSites: number;
+ readonly omittedSites: number;
+ /** Kept once per site; queries union matching identities, not per-target counts. */
+ readonly sites: readonly UnresolvedOverloadSite[];
+}
+
+export interface OverloadBoundary {
+ readonly notes: readonly string[];
+ readonly sites: number;
+}
+
+const overloadReasons = new Set([
+ 'overload-ambiguous',
+ 'overload-ambiguous-normalization',
+ 'conversion-rank-tied',
+ 'member-lookup-ambiguous',
+ 'overload-unknown',
+]);
+
+type Suppressed = Extract;
+
+function siteId(outcome: { filePath: string; range: Range }): string {
+ const { range } = outcome;
+ return JSON.stringify([
+ outcome.filePath,
+ range.startLine,
+ range.startCol,
+ range.endLine,
+ range.endCol,
+ ]);
+}
+
+const ordered = (items: Iterable): string[] => [...new Set(items)].sort();
+
+/**
+ * Summarize final decisions, including an explicit receipt for a clean run.
+ * A replay may resolve an earlier suppression. Reconcile within its phase:
+ * resolving a primary call does not resolve an unknown implementation dispatch.
+ */
+export function summarizeUnresolvedOverloads(
+ outcomes: readonly ResolutionOutcome[],
+ inputComplete = true,
+): UnresolvedOverloadSummary {
+ const pending = new Map>();
+ let complete = inputComplete;
+ for (const outcome of outcomes) {
+ if (outcome.kind !== 'resolved' && outcome.kind !== 'suppressed') continue;
+ const id = siteId(outcome);
+ if (outcome.kind === 'resolved' || outcome.reason === 'overload-inapplicable') {
+ const phases = pending.get(id);
+ phases?.delete(outcome.phase);
+ if (phases?.size === 0) pending.delete(id);
+ continue;
+ }
+ if (!overloadReasons.has(outcome.reason)) continue;
+ if (outcome.siteKind !== undefined && outcome.siteKind !== 'call') continue;
+ // Old member-lookup records also described property reads and writes.
+ // Without their kind we cannot count them as call sites or certify coverage.
+ if (outcome.reason === 'member-lookup-ambiguous' && outcome.siteKind === undefined) {
+ complete = false;
+ continue;
+ }
+ let phases = pending.get(id);
+ if (phases === undefined) pending.set(id, (phases = new Map()));
+ phases.set(outcome.phase, outcome);
+ }
+
+ const sites: UnresolvedOverloadSite[] = [];
+ let bytes = 256; // Header and punctuation, conservatively above their maximum.
+ for (const id of [...pending.keys()].sort()) {
+ const phases = [...pending.get(id)!.values()];
+ const first = phases[0];
+ const candidates = ordered(phases.flatMap((outcome) => outcome.candidateGraphIds ?? []));
+ const owners = ordered(phases.flatMap((outcome) => outcome.candidateOwnerIds ?? []));
+ const callers = ordered(
+ phases.flatMap((outcome) => (outcome.callerId ? [outcome.callerId] : [])),
+ );
+ if (
+ phases.some((outcome) => outcome.candidateSetComplete !== true) ||
+ candidates.length === 0 ||
+ callers.length !== 1 ||
+ phases.some((outcome) => !outcome.callerId)
+ ) {
+ complete = false;
+ }
+ if (
+ candidates.length > MAX_OVERLOAD_IDENTITIES_PER_SITE ||
+ owners.length > MAX_OVERLOAD_IDENTITIES_PER_SITE ||
+ callers.length > MAX_OVERLOAD_IDENTITIES_PER_SITE
+ ) {
+ complete = false;
+ }
+ const site: UnresolvedOverloadSite = {
+ id,
+ filePath: first.filePath,
+ name: ordered(phases.map((outcome) => outcome.name))[0],
+ range: first.range,
+ candidateIds: candidates.slice(0, MAX_OVERLOAD_IDENTITIES_PER_SITE),
+ candidateOwnerIds: owners.slice(0, MAX_OVERLOAD_IDENTITIES_PER_SITE),
+ callerIds: callers.slice(0, MAX_OVERLOAD_IDENTITIES_PER_SITE),
+ reasons: ordered(phases.map((outcome) => outcome.reason)),
+ };
+ const siteBytes = Buffer.byteLength(JSON.stringify(site), 'utf8') + 1;
+ if (
+ sites.length >= MAX_UNRESOLVED_OVERLOAD_SITES ||
+ bytes + siteBytes > MAX_OVERLOAD_SUMMARY_BYTES
+ ) {
+ complete = false;
+ continue;
+ }
+ sites.push(site);
+ bytes += siteBytes;
+ }
+ return {
+ schemaVersion: 1,
+ complete,
+ totalSites: pending.size,
+ omittedSites: pending.size - sites.length,
+ sites,
+ };
+}
+
+function record(value: unknown): value is Record {
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
+}
+
+function integer(value: unknown): value is number {
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
+}
+
+function identities(value: unknown): value is string[] {
+ return (
+ Array.isArray(value) &&
+ value.length <= MAX_OVERLOAD_IDENTITIES_PER_SITE &&
+ value.every((id) => typeof id === 'string' && id.length > 0) &&
+ new Set(value).size === value.length
+ );
+}
+
+function isRange(value: unknown): value is Range {
+ return (
+ record(value) &&
+ ['startLine', 'startCol', 'endLine', 'endCol'].every(
+ (key) => Object.hasOwn(value, key) && integer(value[key]),
+ ) &&
+ (value.endLine as number) >= (value.startLine as number) &&
+ (value.endLine !== value.startLine || (value.endCol as number) >= (value.startCol as number))
+ );
+}
+
+/** Metadata comes from JSON: validate the receipt, bounds and every measured count. */
+function readSummary(value: unknown): UnresolvedOverloadSummary | undefined {
+ if (
+ !record(value) ||
+ !['schemaVersion', 'complete', 'totalSites', 'omittedSites', 'sites'].every((key) =>
+ Object.hasOwn(value, key),
+ ) ||
+ value.schemaVersion !== 1 ||
+ typeof value.complete !== 'boolean' ||
+ !integer(value.totalSites) ||
+ !integer(value.omittedSites) ||
+ !Array.isArray(value.sites) ||
+ value.sites.length > MAX_UNRESOLVED_OVERLOAD_SITES ||
+ value.totalSites !== value.sites.length + value.omittedSites ||
+ (value.complete && value.omittedSites !== 0)
+ ) {
+ return undefined;
+ }
+ const seen = new Set();
+ const sites: UnresolvedOverloadSite[] = [];
+ for (const site of value.sites) {
+ if (
+ !record(site) ||
+ ![
+ 'id',
+ 'filePath',
+ 'name',
+ 'range',
+ 'candidateIds',
+ 'candidateOwnerIds',
+ 'callerIds',
+ 'reasons',
+ ].every((key) => Object.hasOwn(site, key)) ||
+ typeof site.id !== 'string' ||
+ typeof site.filePath !== 'string' ||
+ typeof site.name !== 'string' ||
+ !isRange(site.range) ||
+ !identities(site.candidateIds) ||
+ !identities(site.candidateOwnerIds) ||
+ !identities(site.callerIds) ||
+ !identities(site.reasons) ||
+ site.reasons.length === 0 ||
+ site.reasons.some((reason) => !overloadReasons.has(reason)) ||
+ (value.complete && (site.candidateIds.length === 0 || site.callerIds.length !== 1))
+ ) {
+ return undefined;
+ }
+ if (site.id !== siteId({ filePath: site.filePath, range: site.range }) || seen.has(site.id)) {
+ return undefined;
+ }
+ seen.add(site.id);
+ sites.push({
+ id: site.id,
+ filePath: site.filePath,
+ name: site.name,
+ range: site.range,
+ candidateIds: site.candidateIds,
+ candidateOwnerIds: site.candidateOwnerIds,
+ callerIds: site.callerIds,
+ reasons: site.reasons,
+ });
+ }
+ try {
+ if (Buffer.byteLength(JSON.stringify(value), 'utf8') > MAX_OVERLOAD_SUMMARY_BYTES) {
+ return undefined;
+ }
+ } catch {
+ return undefined;
+ }
+ return {
+ schemaVersion: 1,
+ complete: value.complete,
+ totalSites: value.totalSites,
+ omittedSites: value.omittedSites,
+ sites,
+ };
+}
+
+/**
+ * Match a whole expanded traversal in one bounded pass. A site touching several
+ * visited candidates is counted once. Owner IDs handle type queries without
+ * attributing every method in the owning file to every class in that file.
+ */
+export function probeOverloadBoundaries(
+ value: unknown,
+ symbolIds: ReadonlySet,
+ direction: 'upstream' | 'downstream' | 'both',
+ includeSite: (site: UnresolvedOverloadSite) => boolean = () => true,
+ traversalComplete = true,
+): OverloadBoundary {
+ if (symbolIds.size === 0) return { notes: [], sites: 0 };
+ const summary = readSummary(value);
+ if (summary === undefined) {
+ return {
+ notes: [
+ 'Overload-resolution completeness was not recorded or its metadata is invalid. ' +
+ 'Run analyze --force with the current analyzer before trusting zero callers; actual impact may be higher.',
+ ],
+ sites: 0,
+ };
+ }
+ let sites = 0;
+ let eligibleSites = 0;
+ for (const site of summary.sites) {
+ if (!includeSite(site)) continue;
+ eligibleSites++;
+ const incoming =
+ direction !== 'downstream' &&
+ (site.candidateIds.some((id) => symbolIds.has(id)) ||
+ site.candidateOwnerIds.some((id) => symbolIds.has(id)));
+ const outgoing = direction !== 'upstream' && site.callerIds.some((id) => symbolIds.has(id));
+ if (incoming || outgoing) sites++;
+ }
+ const notes: string[] = [];
+ if (sites > 0) {
+ notes.push(
+ `${sites} call ${sites === 1 ? 'site has' : 'sites have'} unresolved overload or implementation ` +
+ 'targets in this query. Those call edges were not recorded, so actual impact may be higher.',
+ );
+ }
+ if (!summary.complete) {
+ notes.push(
+ 'Overload-resolution coverage is partial: candidate/caller identities or retained call-site ' +
+ 'details are incomplete. The reported unresolved-site count is a lower bound; ' +
+ 'absence from this summary does not establish complete coverage.',
+ );
+ }
+ if (!traversalComplete && eligibleSites > 0) {
+ notes.push(
+ 'The impact traversal was incomplete, so unresolved overload sites outside the examined ' +
+ 'frontier may also affect this result.',
+ );
+ }
+ return { notes, sites };
+}
diff --git a/gitnexus/src/core/ingestion/workers/callable-id.ts b/gitnexus/src/core/ingestion/workers/callable-id.ts
index 7672dd8c8..39db8f81d 100644
--- a/gitnexus/src/core/ingestion/workers/callable-id.ts
+++ b/gitnexus/src/core/ingestion/workers/callable-id.ts
@@ -24,6 +24,12 @@ import type { SyntaxNode } from '../utils/ast-helpers.js';
import { definitionIdPosition } from '../scope-resolution/utils/definition-id.js';
const LOCAL_IDENTITY_SUFFIX = /@\d+:\d+$/;
+// Parsed-file definitions are finalized before worker graph extraction. Index
+// once per immutable array; weak keys release the index with its parse result.
+const definitionsByName = new WeakMap<
+ readonly SymbolDefinition[],
+ Map
+>();
function simpleDefinitionName(def: SymbolDefinition): string | undefined {
const qualifiedName = def.qualifiedName;
@@ -67,17 +73,26 @@ export function boundCallableStartPosition(
): { readonly row: number; readonly column: number } {
if (localDefs === undefined) return definitionNode.startPosition;
+ let index = definitionsByName.get(localDefs);
+ if (index === undefined) {
+ index = new Map();
+ for (const def of localDefs) {
+ for (const name of new Set([simpleDefinitionName(def), def.qualifiedName])) {
+ if (name === undefined) continue;
+ const key = `${def.type}\0${name}`;
+ const matches = index.get(key) ?? [];
+ matches.push(def);
+ index.set(key, matches);
+ }
+ }
+ definitionsByName.set(localDefs, index);
+ }
+
const origin = nameNode?.startPosition ?? definitionNode.startPosition;
let best: { row: number; column: number; distance: number } | undefined;
let tied = false;
- for (const def of localDefs) {
- if (
- def.type !== nodeLabel ||
- (simpleDefinitionName(def) !== nodeName && def.qualifiedName !== nodeName)
- ) {
- continue;
- }
+ for (const def of index.get(`${nodeLabel}\0${nodeName}`) ?? []) {
const position = definitionIdPosition(def.nodeId, def.filePath);
if (position === undefined) continue;
diff --git a/gitnexus/src/core/run-analyze.ts b/gitnexus/src/core/run-analyze.ts
index bec58c4b6..289946659 100644
--- a/gitnexus/src/core/run-analyze.ts
+++ b/gitnexus/src/core/run-analyze.ts
@@ -57,6 +57,7 @@ import {
} from './ingestion/scope-resolution/unresolved-receivers.js';
import { summarizeUndecidedSatisfaction } from './ingestion/scope-resolution/undecided-satisfaction.js';
import { summarizeScopeExtractionFailures } from './ingestion/scope-resolution/scope-extraction-failures.js';
+import { summarizeUnresolvedOverloads } from './ingestion/scope-resolution/unresolved-overloads.js';
import type { KnowledgeGraph } from './graph/types.js';
import { reconcileGraphNodeIdentities } from './incremental/write-reconciliation.js';
import { resetDegradedParseCounter } from './tree-sitter/safe-parse.js';
@@ -5362,6 +5363,12 @@ async function runFullAnalysisInner(
// Git-only: non-git repos never take the incremental path.
schemaFingerprint: hasGitDir(repoPath) ? SCHEMA_FINGERPRINT : undefined,
unresolvedReceiverMembers: summarizeUnresolvedReceivers(resolutionOutcomes),
+ // Resolution runs again on warm parse-cache hits. Replace the receipt,
+ // including complete-empty, so corrected/deleted source clears old gaps.
+ unresolvedOverloads: summarizeUnresolvedOverloads(
+ resolutionOutcomes,
+ pipelineResult.resolutionOutcomes !== undefined,
+ ),
nameFallbackEdges: nameFallbackSummary,
scopeExtractionFailures: summarizeScopeExtractionFailures(
pipelineResult.scopeExtractionFailures,
diff --git a/gitnexus/src/mcp/local/local-backend.ts b/gitnexus/src/mcp/local/local-backend.ts
index c8dea8657..989292246 100644
--- a/gitnexus/src/mcp/local/local-backend.ts
+++ b/gitnexus/src/mcp/local/local-backend.ts
@@ -172,6 +172,10 @@ import {
lookupUnresolvedCallCount,
} from '../../core/ingestion/scope-resolution/unresolved-receivers.js';
import type { UnresolvedReceiverSummary } from '../../core/ingestion/scope-resolution/unresolved-receivers.js';
+import {
+ probeOverloadBoundaries,
+ type OverloadBoundary,
+} from '../../core/ingestion/scope-resolution/unresolved-overloads.js';
import type { UndecidedSatisfactionSummary } from '../../core/ingestion/scope-resolution/undecided-satisfaction.js';
import { scopeExtractionFailureTotal } from '../../core/ingestion/scope-resolution/scope-extraction-failures.js';
import { lookupCount } from '../../core/ingestion/scope-resolution/summary-maps.js';
@@ -830,6 +834,12 @@ export interface EpistemicCauses {
* `unresolvedReceiverMembers` summary — the same number the prose note quotes.
*/
readonly receiverTyping: number;
+ /**
+ * Unique call sites whose overload or implementation targets were unresolved.
+ * Counted once across all queried candidates. A partial/unverified receipt
+ * can report zero with a boundary note; zero alone does not prove completeness.
+ */
+ readonly overloadResolution?: number;
/**
* Symbols on or beyond a dispatch boundary that the traversal could not
* attribute statically: implementations plus interface-level consumers.
@@ -914,6 +924,33 @@ export interface EpistemicCauses {
readonly callableValueReferences: number;
}
+type EpistemicBoundary = {
+ epistemic: 'exact' | 'lower-bound';
+ boundaries?: string[];
+ causes?: EpistemicCauses;
+};
+
+function mergeOverloadBoundary(
+ existing: EpistemicBoundary,
+ overloads: OverloadBoundary,
+): EpistemicBoundary {
+ if (overloads.notes.length === 0) return existing;
+ return {
+ epistemic: 'lower-bound',
+ boundaries: [...(existing.boundaries ?? []), ...overloads.notes],
+ causes: {
+ scopeExtractionFiles: 0,
+ receiverTyping: 0,
+ dispatchBoundary: 0,
+ externalBoundary: 0,
+ undecidedSatisfaction: 0,
+ callableValueReferences: 0,
+ ...existing.causes,
+ overloadResolution: overloads.sites,
+ },
+ };
+}
+
function epistemicFrom(dropped: {
notes: readonly string[];
sites: number;
@@ -8366,6 +8403,7 @@ export class LocalBackend {
symType: string,
symName: string,
direction?: 'upstream' | 'downstream',
+ includeOverloads = true,
): Promise<{
epistemic: 'exact' | 'lower-bound';
boundaries?: string[];
@@ -8391,6 +8429,11 @@ export class LocalBackend {
meta = undefined;
}
const receiverDrops = unresolvedReceiverBoundaries(meta?.unresolvedReceiverMembers, symName);
+ // Context has both incoming and outgoing references. Impact defers this
+ // probe until its expanded frontier is known, avoiding root double-counts.
+ const overloadDrops = includeOverloads
+ ? probeOverloadBoundaries(meta?.unresolvedOverloads, new Set([symId]), direction ?? 'both')
+ : { notes: [], sites: 0 };
const scopeExtractionDrops = scopeExtractionBoundaries(
meta?.scopeExtractionFailures,
meta?.scopeExtractionReceipt,
@@ -8483,7 +8526,9 @@ export class LocalBackend {
});
}
}
- if (boundary.size === 0) return epistemicFrom(droppedBoundaries);
+ if (boundary.size === 0) {
+ return mergeOverloadBoundary(epistemicFrom(droppedBoundaries), overloadDrops);
+ }
const ifaceIds = Array.from(boundary.keys());
// Count per interface id with scalar equality. A parameterized
@@ -8549,24 +8594,29 @@ export class LocalBackend {
);
}
}
- if (boundaries.length === 0) return epistemicFrom(droppedBoundaries);
- return {
- epistemic: 'lower-bound',
- boundaries: [...droppedBoundaries.notes, ...boundaries],
- causes: {
- scopeExtractionFiles: droppedBoundaries.scopeExtraction,
- receiverTyping: droppedBoundaries.sites,
- dispatchBoundary: droppedBoundaries.dispatch + dispatchBoundarySymbols,
- externalBoundary: droppedBoundaries.external,
- undecidedSatisfaction: droppedBoundaries.undecided,
- callableValueReferences: droppedBoundaries.callableValueReferences,
+ if (boundaries.length === 0) {
+ return mergeOverloadBoundary(epistemicFrom(droppedBoundaries), overloadDrops);
+ }
+ return mergeOverloadBoundary(
+ {
+ epistemic: 'lower-bound',
+ boundaries: [...droppedBoundaries.notes, ...boundaries],
+ causes: {
+ scopeExtractionFiles: droppedBoundaries.scopeExtraction,
+ receiverTyping: droppedBoundaries.sites,
+ dispatchBoundary: droppedBoundaries.dispatch + dispatchBoundarySymbols,
+ externalBoundary: droppedBoundaries.external,
+ undecidedSatisfaction: droppedBoundaries.undecided,
+ callableValueReferences: droppedBoundaries.callableValueReferences,
+ },
},
- };
+ overloadDrops,
+ );
} catch (error) {
rethrowSymbolIdentityError(error);
// Never let the heritage probe's failure suppress a drop we already know
// about — the whole point is that silence must not read as certainty.
- return epistemicFrom(droppedBoundaries);
+ return mergeOverloadBoundary(epistemicFrom(droppedBoundaries), overloadDrops);
}
}
@@ -8682,7 +8732,18 @@ export class LocalBackend {
symType,
(sym.name || sym[1]) as string,
direction,
+ false,
);
+ const checkOverloads = !opts.skipEpistemic && relationTypes.includes('CALLS');
+ // readToolMetadata is request-cached: this shares the existing probe's
+ // metadata read and needs no graph queries for individual frontier nodes.
+ const overloadMetadataPromise = checkOverloads
+ ? Promise.resolve()
+ .then(() => this.readToolMetadata(repo))
+ .then((meta) => meta?.unresolvedOverloads)
+ .catch(() => undefined)
+ : Promise.resolve(undefined);
+ const expandedOverloadIds = new Set();
const beanMetadataPromise =
opts.skipEpistemic || summaryOnly
? Promise.resolve(undefined)
@@ -8882,6 +8943,9 @@ export class LocalBackend {
for (let depth = 1; depth <= maxDepth && frontier.length > 0; depth++) {
const nextFrontier: string[] = [];
+ if (checkOverloads) {
+ for (const id of frontier) expandedOverloadIds.add(id);
+ }
// Batch frontier nodes into a single Cypher query per depth level.
// ids/types/confidence are bound parameters (see above) — no interpolation.
@@ -9554,11 +9618,25 @@ export class LocalBackend {
// #1858 — await the epistemic boundary probe kicked off alongside the BFS
// above. Additive: leaves impactedCount and every existing field untouched.
- const [epistemic, beanMetadata, aopMetadata] = await Promise.all([
+ const [targetEpistemic, beanMetadata, aopMetadata, overloadSummary] = await Promise.all([
epistemicPromise,
beanMetadataPromise,
aopMetadataPromise,
+ overloadMetadataPromise,
]);
+ const epistemic =
+ checkOverloads && targetEpistemic.epistemic !== undefined
+ ? mergeOverloadBoundary(
+ { ...targetEpistemic, epistemic: targetEpistemic.epistemic },
+ probeOverloadBoundaries(
+ overloadSummary,
+ expandedOverloadIds,
+ direction,
+ (site) => direction !== 'upstream' || includeTests || !isTestFilePath(site.filePath),
+ traversalComplete,
+ ),
+ )
+ : targetEpistemic;
const base = {
target: {
diff --git a/gitnexus/src/mcp/tools.ts b/gitnexus/src/mcp/tools.ts
index 4b65b3a8d..0ad26c729 100644
--- a/gitnexus/src/mcp/tools.ts
+++ b/gitnexus/src/mcp/tools.ts
@@ -346,12 +346,13 @@ Use uid, or name with file_path/kind. Ambiguous results return ranked candidates
include_content returns retained source. Leave chain_depth:0 unless layered CALLS neighbours are needed. routes:[{url,method?}] and is_entry_point describe HTTP handlers and process entries. ACCESSES references carry reason "read"/"write"; CALLS can follow field and method chains.
-COMPLETENESS: epistemic "lower-bound" means incoming is a floor: callers were missed OR a completeness probe failed. Read boundaries to distinguish them; branch on causes, not prose. Empty callers do not establish safety.
-- scopeExtractionFiles: files missing scope edges. Run analyze --force; inspect remaining extraction warnings.
-- receiverTyping: dropped call sites with unresolved receiver types; corroborate by text search.
-- externalBoundary: calls leaving the indexed program; not a defect, compatible with epistemic "exact".
-- dispatchBoundary: symbols across DI/interface boundaries, not sites. Runtime-proxy gaps can be uncountable (0 with lower-bound).
-- undecidedSatisfaction: unjudged interface/type pairs without IMPLEMENTS edges; missing dependencies can cause this.
+COMPLETENESS: epistemic "lower-bound" means incoming is a floor (missed callers/failed probes). Read boundaries; branch on causes. Empty callers do not prove safety.
+- scopeExtractionFiles: files missing scope edges; analyze --force, inspect warnings.
+- receiverTyping: sites with unknown receiver types; corroborate by text search.
+- overloadResolution: unique unresolved overload/implementation sites matched by symbol/declaring owner. Missing/invalid/capped receipts imply lower-bound even at 0; analyze --force.
+- externalBoundary: calls outside indexed code; compatible with "exact", not a defect.
+- dispatchBoundary: DI/interface symbols, not sites. Runtime-proxy gaps may be uncountable (0 with lower-bound).
+- undecidedSatisfaction: unjudged interface/type pairs without IMPLEMENTS; check dependencies.
- callableValueReferences: symbols holding the callable as a value via USES, without a modeled later invocation. Follow the registrations/readers. Zig registrations and JS/TS object properties are captured; bare JS/TS callback arguments are not counted. Synthesized dispatch excludes the whole symbol, so mixed followed/unfollowed registrations can report 0. Zero means no unfollowed registration was proven, not no escapes; a failed probe also reports 0 with lower-bound.
Older indexes can lack captures/metadata; re-analyze before trusting zero causes or exact coverage. Use impact() before edits; read gitnexus://repo/{name}/process/{processName} for a full flow.
@@ -552,12 +553,13 @@ RESULTS: target, impactedCount, summary, affected_processes, affected_modules, b
- staticGated:true marks a provably compile-time-unreachable edge (currently Zig); absence proves neither reachability nor coverage. Traversal/risk do not filter on it.
- affected_routes and row routes:[{url,method?}] are callgraph-only HTTP enrichment, not risk inputs.
-COMPLETENESS: epistemic "lower-bound" means impactedCount is a floor: missed callers OR a failed completeness probe. Read boundaries for which; use causes for machine decisions. Ambiguous lists/group fan-out may omit this envelope.
-- scopeExtractionFiles counts files missing scope edges. Run analyze --force; inspect remaining extraction warnings.
-- receiverTyping counts dropped call sites with unresolved receiver types. Corroborate before deletion/rename.
-- externalBoundary counts calls leaving the program; not an analyzer defect or an in-graph shortfall. Compatible with epistemic "exact".
-- dispatchBoundary counts symbols beyond DI/interface boundaries, not sites. Runtime proxies can prove uncountable gaps (0 with lower-bound).
-- undecidedSatisfaction counts unjudged interface/type pairs without IMPLEMENTS edges; missing dependencies are a common fixable cause.
+COMPLETENESS: epistemic "lower-bound" makes impactedCount a floor (missed callers/failed probes). Read boundaries; branch on causes. Ambiguous/group results may omit this envelope.
+- scopeExtractionFiles: files missing scope edges; analyze --force, inspect warnings.
+- receiverTyping: sites with unknown receiver types; corroborate before deletion/rename.
+- overloadResolution: when traversing CALLS, unique unresolved overload/implementation sites in the expanded traversal before pagination. Missing/invalid/capped receipts imply lower-bound even at 0; analyze --force.
+- externalBoundary: calls outside indexed code; compatible with "exact", not a defect.
+- dispatchBoundary: DI/interface symbols, not sites. Runtime-proxy gaps may be uncountable (0 with lower-bound).
+- undecidedSatisfaction: unjudged interface/type pairs without IMPLEMENTS; check dependencies.
- callableValueReferences counts symbols registering the callable VALUE via USES without a modeled later invocation. Follow registrations/readers; low risk/counts remain floors. Zig registrations and JS/TS object properties are captured; bare JS/TS callback arguments are not counted. Synthesized property dispatch excludes the whole symbol, so mixed followed/unfollowed registrations can report 0. Zero means no unfollowed registration was proven, not no escapes; a failed probe also reports 0 with lower-bound.
Older indexes can lack captures/metadata; re-analyze before trusting zero causes or apparently exact coverage from an older analyzer.
diff --git a/gitnexus/src/storage/repo-meta.ts b/gitnexus/src/storage/repo-meta.ts
index e97c80e36..a4ca81ac4 100644
--- a/gitnexus/src/storage/repo-meta.ts
+++ b/gitnexus/src/storage/repo-meta.ts
@@ -29,6 +29,7 @@
import fs from 'fs/promises';
import path from 'path';
import type { UnresolvedReceiverSummary } from '../core/ingestion/scope-resolution/unresolved-receivers.js';
+import type { UnresolvedOverloadSummary } from '../core/ingestion/scope-resolution/unresolved-overloads.js';
import type { NameFallbackSummary } from '../core/ingestion/scope-resolution/name-fallback-summary.js';
import type { UndecidedSatisfactionSummary } from '../core/ingestion/scope-resolution/undecided-satisfaction.js';
import { resolveStoragePath } from './storage-resolver.js';
@@ -341,6 +342,12 @@ export interface RepoMeta {
* this adds no runtime dependency from storage/ on core/.
*/
unresolvedReceiverMembers?: UnresolvedReceiverSummary;
+ /**
+ * Final unresolved overload/implementation call sites, attributed by public
+ * candidate, caller and declaring-owner identity. A complete empty summary
+ * certifies measured-clean coverage; absence or invalid metadata is unverified.
+ */
+ unresolvedOverloads?: UnresolvedOverloadSummary;
/**
* Files omitted from scope-resolution because their provider capture or
* extraction step threw. The rest of each file may still be present in the
diff --git a/gitnexus/test/fixtures/lang-resolution/java-overload-types/src/main/java/demo/BillingService.java b/gitnexus/test/fixtures/lang-resolution/java-overload-types/src/main/java/demo/BillingService.java
new file mode 100644
index 000000000..c6b8103b0
--- /dev/null
+++ b/gitnexus/test/fixtures/lang-resolution/java-overload-types/src/main/java/demo/BillingService.java
@@ -0,0 +1,17 @@
+package demo;
+
+import java.util.Arrays;
+import java.util.Map;
+
+public class BillingService {
+
+ private final LookupService lookup;
+
+ public BillingService(LookupService lookup) {
+ this.lookup = lookup;
+ }
+
+ public Map settle() {
+ return lookup.find(Arrays.asList(10L, 20L)); // -> find(Collection)
+ }
+}
diff --git a/gitnexus/test/fixtures/lang-resolution/java-overload-types/src/main/java/demo/LookupService.java b/gitnexus/test/fixtures/lang-resolution/java-overload-types/src/main/java/demo/LookupService.java
new file mode 100644
index 000000000..339d2346f
--- /dev/null
+++ b/gitnexus/test/fixtures/lang-resolution/java-overload-types/src/main/java/demo/LookupService.java
@@ -0,0 +1,38 @@
+package demo;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+public class LookupService {
+
+ // Group 1: same arity, generic parameter type (Collection vs Long)
+ public Map find(Collection ids) {
+ Map out = new HashMap<>();
+ for (Long id : ids) out.put(id, new Perm(id));
+ return out;
+ }
+
+ public Perm find(Long id) {
+ return find(Collections.singletonList(id)).get(id);
+ }
+
+ // Group 2: same arity, no generics (String vs Long)
+ public Perm resolve(String code) {
+ return new Perm(Long.valueOf(code));
+ }
+
+ public Perm resolve(Long id) {
+ return new Perm(id);
+ }
+
+ // Group 3 (control): different arity
+ public int count(Req req) {
+ return req.keyword == null ? 0 : 1;
+ }
+
+ public int count(Long id, Req req) {
+ return count(req) + (id == null ? 0 : 1);
+ }
+}
diff --git a/gitnexus/test/fixtures/lang-resolution/java-overload-types/src/main/java/demo/OrderService.java b/gitnexus/test/fixtures/lang-resolution/java-overload-types/src/main/java/demo/OrderService.java
new file mode 100644
index 000000000..16c138bff
--- /dev/null
+++ b/gitnexus/test/fixtures/lang-resolution/java-overload-types/src/main/java/demo/OrderService.java
@@ -0,0 +1,35 @@
+package demo;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+public class OrderService {
+
+ private final LookupService lookup;
+
+ public OrderService(LookupService lookup) {
+ this.lookup = lookup;
+ }
+
+ public Map listOrders() {
+ List ids = Arrays.asList(1L, 2L, 3L);
+ return lookup.find(ids); // -> find(Collection)
+ }
+
+ public Perm getOrder(Long id) {
+ return lookup.find(id); // -> find(Long)
+ }
+
+ public Perm byCode(String code) {
+ return lookup.resolve(code); // -> resolve(String)
+ }
+
+ public Perm byId(Long id) {
+ return lookup.resolve(id); // -> resolve(Long)
+ }
+
+ public int summarize(Long id, Req req) {
+ return lookup.count(req) + lookup.count(id, req); // -> count(Req), count(Long, Req)
+ }
+}
diff --git a/gitnexus/test/fixtures/lang-resolution/java-overload-types/src/main/java/demo/Perm.java b/gitnexus/test/fixtures/lang-resolution/java-overload-types/src/main/java/demo/Perm.java
new file mode 100644
index 000000000..9eb52865a
--- /dev/null
+++ b/gitnexus/test/fixtures/lang-resolution/java-overload-types/src/main/java/demo/Perm.java
@@ -0,0 +1,7 @@
+package demo;
+
+public class Perm {
+ private final Long id;
+ public Perm(Long id) { this.id = id; }
+ public Long getId() { return id; }
+}
diff --git a/gitnexus/test/fixtures/lang-resolution/java-overload-types/src/main/java/demo/Req.java b/gitnexus/test/fixtures/lang-resolution/java-overload-types/src/main/java/demo/Req.java
new file mode 100644
index 000000000..6affa5b11
--- /dev/null
+++ b/gitnexus/test/fixtures/lang-resolution/java-overload-types/src/main/java/demo/Req.java
@@ -0,0 +1,5 @@
+package demo;
+
+public class Req {
+ public String keyword;
+}
diff --git a/gitnexus/test/fixtures/lang-resolution/java-protected-overloads/a/A.java b/gitnexus/test/fixtures/lang-resolution/java-protected-overloads/a/A.java
new file mode 100644
index 000000000..60a86a1fc
--- /dev/null
+++ b/gitnexus/test/fixtures/lang-resolution/java-protected-overloads/a/A.java
@@ -0,0 +1,8 @@
+package a;
+
+public class A {
+ protected String pick(String value) { return "string"; }
+ public String pick(Object value) { return "object"; }
+ protected static String staticPick(String value) { return "static-string"; }
+ public static String staticPick(Object value) { return "static-object"; }
+}
diff --git a/gitnexus/test/fixtures/lang-resolution/java-protected-overloads/b/B.java b/gitnexus/test/fixtures/lang-resolution/java-protected-overloads/b/B.java
new file mode 100644
index 000000000..ca873e6b1
--- /dev/null
+++ b/gitnexus/test/fixtures/lang-resolution/java-protected-overloads/b/B.java
@@ -0,0 +1,32 @@
+package b;
+
+import a.A;
+
+public class B extends A {
+ class Inner {
+ String nested(B receiver) { return receiver.pick("x"); }
+ String baseReceiver(A receiver) { return receiver.pick("x"); }
+ String siblingReceiver(Sibling receiver) { return receiver.pick("x"); }
+ class Deep {
+ String deeplyNested(B receiver) { return receiver.pick("x"); }
+ }
+ }
+ class NestedSubclass extends A {
+ String enclosingSubclass(B receiver) { return receiver.pick("x"); }
+ }
+ static class Nested {
+ String staticNested(B receiver) { return receiver.pick("x"); }
+ String protectedStatic() { return A.staticPick("x"); }
+ }
+ public static void main(String[] args) {
+ B receiver = new B();
+ Inner inner = receiver.new Inner();
+ System.out.println(String.join(",", inner.nested(receiver),
+ inner.baseReceiver(receiver), inner.siblingReceiver(new Sibling()),
+ inner.new Deep().deeplyNested(receiver),
+ receiver.new NestedSubclass().enclosingSubclass(receiver),
+ new Nested().staticNested(receiver), new Nested().protectedStatic()));
+ }
+}
+
+class Sibling extends A {}
diff --git a/gitnexus/test/helpers/java-overload-persistence-child.mjs b/gitnexus/test/helpers/java-overload-persistence-child.mjs
new file mode 100644
index 000000000..6732fb1ea
--- /dev/null
+++ b/gitnexus/test/helpers/java-overload-persistence-child.mjs
@@ -0,0 +1,52 @@
+// Each invocation is a fresh process: no resolver maps or parsed-file objects
+// survive between the writer, durable-cache replay, and backend reader.
+const request = JSON.parse(process.argv[2]);
+const root = new URL('../../dist/', import.meta.url);
+const load = (name) => import(new URL(`${name}.js`, root).href);
+let receipt;
+if (request.action === 'analyze') {
+ const { runFullAnalysis } = await load('core/run-analyze');
+ const { getStoragePaths, loadMeta } = await load('storage/repo-manager');
+ const logs = [];
+ const result = await runFullAnalysis(
+ request.repo,
+ {
+ force: true,
+ skipAgentsMd: true,
+ skipSkills: true,
+ skipFts: true,
+ workerPoolSize: 1,
+ },
+ { onLog: (message) => logs.push(message), onProgress: () => {} },
+ );
+ const paths = getStoragePaths(request.repo);
+ receipt = {
+ usedWorkerPool: result.pipelineResult?.usedWorkerPool,
+ reparsed: result.pipelineResult?.reparsedFileCount,
+ hits: result.pipelineResult?.parseCacheHitFileCount,
+ storagePath: paths.storagePath,
+ meta: await loadMeta(paths.storagePath),
+ logs,
+ };
+} else if (request.action === 'inspect') {
+ const { LocalBackend } = await load('mcp/local/local-backend');
+ const backend = new LocalBackend();
+ try {
+ await backend.init();
+ receipt = {};
+ for (const uid of request.targets) {
+ receipt[uid] = await backend.callTool('impact', {
+ target_uid: uid,
+ repo: request.repo,
+ direction: 'upstream',
+ maxDepth: 1,
+ includeTests: true,
+ });
+ }
+ } finally {
+ await backend.dispose();
+ }
+} else {
+ throw new Error(`Unknown persistence action: ${request.action}`);
+}
+console.log('JAVA_OVERLOAD_RESULT=' + JSON.stringify(receipt));
diff --git a/gitnexus/test/integration/convex-impact-epistemic-e2e.test.ts b/gitnexus/test/integration/convex-impact-epistemic-e2e.test.ts
index a4b8869d7..5702a5084 100644
--- a/gitnexus/test/integration/convex-impact-epistemic-e2e.test.ts
+++ b/gitnexus/test/integration/convex-impact-epistemic-e2e.test.ts
@@ -1,3 +1,4 @@
+import { summarizeUnresolvedOverloads } from '../../src/core/ingestion/scope-resolution/unresolved-overloads.js';
import { runtimeParseIdentity } from '../helpers/runtime-parse-identity.js';
import fs from 'fs';
import path from 'path';
@@ -204,6 +205,7 @@ export const javascriptQuery = query({ handler: async () => null });
lastCommit: 'convex-e2e',
indexedAt: new Date(0).toISOString(),
scopeExtractionReceipt: 1,
+ unresolvedOverloads: summarizeUnresolvedOverloads(replay.resolutionOutcomes),
} satisfies RepoMeta);
},
poolAdapter: true,
diff --git a/gitnexus/test/integration/impact-callable-value-references.test.ts b/gitnexus/test/integration/impact-callable-value-references.test.ts
index 6124d76d3..3f89f03e9 100644
--- a/gitnexus/test/integration/impact-callable-value-references.test.ts
+++ b/gitnexus/test/integration/impact-callable-value-references.test.ts
@@ -27,6 +27,7 @@
* told an interface boundary exists where there is none, and would draw the
* opposite conclusion about whether the gap is reducible.
*/
+import { summarizeUnresolvedOverloads } from '../../src/core/ingestion/scope-resolution/unresolved-overloads.js';
import { it, expect, beforeAll, vi } from 'vitest';
import path from 'node:path';
import { LocalBackend } from '../../src/mcp/local/local-backend.js';
@@ -212,7 +213,10 @@ withTestLbugDB(
// completeness was not recorded"), which would make the controls below
// pass for the wrong reason and prove nothing about this probe.
// `saveMeta` is the only writer production uses.
- await saveMeta(path.dirname(h.dbPath), { scopeExtractionReceipt: 1 } as any);
+ await saveMeta(path.dirname(h.dbPath), {
+ scopeExtractionReceipt: 1,
+ unresolvedOverloads: summarizeUnresolvedOverloads([]),
+ } as any);
vi.mocked(listRegisteredRepos).mockResolvedValue([
{
name: 'test-repo',
diff --git a/gitnexus/test/integration/impact-context-integrity.test.ts b/gitnexus/test/integration/impact-context-integrity.test.ts
index a074968de..f19e1f56a 100644
--- a/gitnexus/test/integration/impact-context-integrity.test.ts
+++ b/gitnexus/test/integration/impact-context-integrity.test.ts
@@ -1,3 +1,4 @@
+import { summarizeUnresolvedOverloads } from '../../src/core/ingestion/scope-resolution/unresolved-overloads.js';
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
import fs from 'node:fs/promises';
import path from 'node:path';
@@ -395,6 +396,7 @@ describe('native impact/context result integrity (#3354)', () => {
lastCommit: 'integrity-fixture',
indexedAt: new Date().toISOString(),
scopeExtractionReceipt: 1 as const,
+ unresolvedOverloads: summarizeUnresolvedOverloads([]),
stats: { files: 8, nodes: 11, processes: 3, communities: 0 },
};
await saveMeta(paths.storagePath, meta);
@@ -730,6 +732,7 @@ describe.skipIf(process.platform === 'win32')('warm backend index replacement (#
lastCommit: 'graph-a',
indexedAt: new Date().toISOString(),
scopeExtractionReceipt: 1 as const,
+ unresolvedOverloads: summarizeUnresolvedOverloads([]),
stats: { files: 3, nodes: 4, processes: 1, communities: 0 },
};
await saveMeta(paths.storagePath, meta);
diff --git a/gitnexus/test/integration/impact-epistemic-lower-bound.test.ts b/gitnexus/test/integration/impact-epistemic-lower-bound.test.ts
index 4b750fc43..e5e147956 100644
--- a/gitnexus/test/integration/impact-epistemic-lower-bound.test.ts
+++ b/gitnexus/test/integration/impact-epistemic-lower-bound.test.ts
@@ -28,6 +28,8 @@ import {
summarizeUnresolvedReceivers,
} from '../../src/core/ingestion/scope-resolution/unresolved-receivers.js';
import type { ResolutionOutcome } from '../../src/core/ingestion/scope-resolution/resolution-outcome.js';
+import { summarizeUnresolvedOverloads } from '../../src/core/ingestion/scope-resolution/unresolved-overloads.js';
+import * as poolAdapter from '../../src/core/lbug/pool-adapter.js';
import { withTestLbugDB } from '../helpers/test-indexed-db.js';
vi.mock('../../src/storage/repo-manager.js', () => ({
@@ -38,6 +40,23 @@ vi.mock('../../src/storage/repo-manager.js', () => ({
}));
const SEED = [
+ ...['Overloads', 'Unrelated'].map(
+ (name) =>
+ `CREATE (:Class {id:'Class:src/Overloads.java:${name}', name:'${name}', filePath:'src/Overloads.java', startLine:1, endLine:20, content:'', description:''})`,
+ ),
+ ...['String', 'Long'].map(
+ (type) =>
+ `CREATE (:Method {id:'Method:src/Overloads.java:Overloads.choose#1~${type}', name:'choose', filePath:'src/Overloads.java', startLine:2, endLine:5, content:'', description:''})`,
+ ),
+ ...['leaf', 'invoke', 'entry'].map(
+ (name) =>
+ `CREATE (:Function {id:'Function:src/overload-${name}.ts:${name}', name:'overload${name}', filePath:'src/overload-${name}.ts', startLine:1, endLine:3, content:'', description:''})`,
+ ),
+ ...['String', 'Long'].flatMap((type) => [
+ `MATCH (owner:Class {id:'Class:src/Overloads.java:Overloads'}), (m:Method {id:'Method:src/Overloads.java:Overloads.choose#1~${type}'}) CREATE (owner)-[:CodeRelation {type:'HAS_METHOD', confidence:1, reason:'member', step:0}]->(m)`,
+ `MATCH (m:Method {id:'Method:src/Overloads.java:Overloads.choose#1~${type}'}), (leaf:Function {id:'Function:src/overload-leaf.ts:leaf'}) CREATE (m)-[:CodeRelation {type:'CALLS', confidence:1, reason:'direct', step:0}]->(leaf)`,
+ ]),
+ `MATCH (entry:Function {id:'Function:src/overload-entry.ts:entry'}), (caller:Function {id:'Function:src/overload-invoke.ts:invoke'}) CREATE (entry)-[:CodeRelation {type:'CALLS', confidence:1, reason:'direct', step:0}]->(caller)`,
// Interface + two implementations + an interface-level consumer.
`CREATE (iface:Interface {id: 'Interface:src/Logger.ts:Logger', name: 'Logger', filePath: 'src/Logger.ts', startLine: 1, endLine: 5, isExported: true, content: '', description: ''})`,
`CREATE (email:Class {id: 'Class:src/EmailLogger.ts:EmailLogger', name: 'EmailLogger', filePath: 'src/EmailLogger.ts', startLine: 1, endLine: 20, isExported: true, content: '', description: ''})`,
@@ -85,9 +104,175 @@ withTestLbugDB(
// Keep each fixture snapshot stable for every read in that test.
vi.mocked(loadMeta).mockResolvedValue({
scopeExtractionReceipt: 1,
+ unresolvedOverloads: summarizeUnresolvedOverloads([]),
} as Awaited>);
});
+ describe('overload completeness', () => {
+ const candidates = [
+ 'Method:src/Overloads.java:Overloads.choose#1~String',
+ 'Method:src/Overloads.java:Overloads.choose#1~Long',
+ ];
+ const owner = 'Class:src/Overloads.java:Overloads';
+ const caller = 'Function:src/overload-invoke.ts:invoke';
+ const outcome: ResolutionOutcome = {
+ kind: 'suppressed',
+ phase: 'receiver-bound-calls',
+ filePath: 'src/overload-invoke.ts',
+ name: 'choose',
+ range: { startLine: 2, startCol: 0, endLine: 2, endCol: 10 },
+ reason: 'overload-unknown',
+ siteKind: 'call',
+ candidateIds: ['def:choose.String', 'def:choose.Long'],
+ candidateGraphIds: candidates,
+ candidateOwnerIds: [owner],
+ callerId: caller,
+ candidateSetComplete: true,
+ };
+ const metadata = (outcomes: readonly ResolutionOutcome[] = [outcome]) => ({
+ repoPath: '',
+ lastCommit: '',
+ indexedAt: '',
+ scopeExtractionReceipt: 1 as const,
+ unresolvedOverloads: summarizeUnresolvedOverloads(outcomes),
+ });
+ const impact = (uid: string, params: Record = {}) =>
+ backend.callTool('impact', { target_uid: uid, direction: 'upstream', ...params });
+
+ it.each(['impact', 'context'] as const)(
+ 'hedges zero known callers for %s using graph candidate identity',
+ async (tool) => {
+ vi.mocked(loadMeta).mockResolvedValue(metadata());
+ const result = await backend.callTool(
+ tool,
+ tool === 'impact'
+ ? { target_uid: candidates[0], direction: 'upstream' }
+ : { uid: candidates[0] },
+ );
+ expect(result).not.toHaveProperty('error');
+ expect(result).toMatchObject({
+ epistemic: 'lower-bound',
+ causes: { overloadResolution: 1 },
+ });
+ expect(
+ tool === 'impact' ? result.impactedCount : (result.incoming.calls?.length ?? 0),
+ ).toBe(0);
+ },
+ );
+
+ it('finds omissions at expanded intermediate nodes, without double-counting candidates or terminal depths', async () => {
+ vi.mocked(loadMeta).mockResolvedValue(metadata());
+ const leaf = 'Function:src/overload-leaf.ts:leaf';
+ const shallow = await impact(leaf, { maxDepth: 1 });
+ expect(shallow).toMatchObject({ epistemic: 'exact', impactedCount: 2 });
+ const deep = await impact(leaf, { maxDepth: 2 });
+ expect(deep).toMatchObject({
+ epistemic: 'lower-bound',
+ impactedCount: 2,
+ causes: { overloadResolution: 1 },
+ });
+ const paged = await impact(leaf, { maxDepth: 2, limit: 1, offset: 1 });
+ const summary = await impact(leaf, { maxDepth: 2, summaryOnly: true });
+ expect(paged.causes.overloadResolution).toBe(1);
+ expect(summary.causes.overloadResolution).toBe(1);
+ expect(summary).not.toHaveProperty('byDepth');
+ });
+
+ it('matches class declaring owners without contaminating unrelated classes in the same file', async () => {
+ vi.mocked(loadMeta).mockResolvedValue(metadata());
+ expect(await impact(owner)).toMatchObject({
+ epistemic: 'lower-bound',
+ causes: { overloadResolution: 1 },
+ });
+ expect(await impact('Class:src/Overloads.java:Unrelated')).toMatchObject({
+ epistemic: 'exact',
+ });
+ });
+
+ it('uses caller identities downstream, including reached callers within the depth bound', async () => {
+ vi.mocked(loadMeta).mockResolvedValue(metadata());
+ expect(await impact(caller, { direction: 'downstream' })).toMatchObject({
+ epistemic: 'lower-bound',
+ causes: { overloadResolution: 1 },
+ });
+ const entry = 'Function:src/overload-entry.ts:entry';
+ expect(await impact(entry, { direction: 'downstream', maxDepth: 1 })).toMatchObject({
+ epistemic: 'exact',
+ });
+ expect(await impact(entry, { direction: 'downstream', maxDepth: 2 })).toMatchObject({
+ epistemic: 'lower-bound',
+ causes: { overloadResolution: 1 },
+ });
+ expect(
+ await impact('Function:src/overload-leaf.ts:leaf', { direction: 'downstream' }),
+ ).toMatchObject({ epistemic: 'exact' });
+ });
+
+ it('does not add call omissions to a traversal excluding CALLS', async () => {
+ vi.mocked(loadMeta).mockResolvedValue(metadata());
+ expect(await impact(candidates[0], { relationTypes: ['IMPORTS'] })).toMatchObject({
+ epistemic: 'exact',
+ });
+ });
+
+ it('honors upstream test-caller filtering', async () => {
+ vi.mocked(loadMeta).mockResolvedValue(
+ metadata([{ ...outcome, filePath: 'test/Caller.java' }]),
+ );
+ expect(await impact(candidates[0])).toMatchObject({ epistemic: 'exact' });
+ expect(await impact(candidates[0], { includeTests: true })).toMatchObject({
+ epistemic: 'lower-bound',
+ causes: { overloadResolution: 1 },
+ });
+ });
+
+ it.each([
+ undefined,
+ { schemaVersion: 99 },
+ {
+ schemaVersion: 1,
+ complete: false,
+ totalSites: 1,
+ omittedSites: 1,
+ sites: [],
+ },
+ ])(
+ 'keeps missing, malformed and capped coverage distinct from measured zero: %j',
+ async (summary) => {
+ vi.mocked(loadMeta).mockResolvedValue({
+ ...metadata([]),
+ unresolvedOverloads: summary,
+ } as Awaited>);
+ const result = await impact(candidates[0]);
+ expect(result).toMatchObject({
+ epistemic: 'lower-bound',
+ causes: { overloadResolution: 0 },
+ });
+ expect(result.boundaries.join(' ')).toMatch(/not recorded|partial/);
+ },
+ );
+
+ it('retains overload evidence when the heritage probe fails', async () => {
+ vi.mocked(loadMeta).mockResolvedValue(metadata());
+ const execute = poolAdapter.executeParameterized;
+ const spy = vi
+ .spyOn(poolAdapter, 'executeParameterized')
+ .mockImplementation((repo, query, params) =>
+ query.includes('RETURN DISTINCT iface.id AS id')
+ ? Promise.reject(new Error('heritage probe unavailable'))
+ : execute(repo, query, params),
+ );
+ try {
+ expect(await backend.callTool('context', { uid: candidates[0] })).toMatchObject({
+ epistemic: 'lower-bound',
+ causes: { overloadResolution: 1 },
+ });
+ } finally {
+ spy.mockRestore();
+ }
+ });
+ });
+
it('flags impact() on a concrete impl behind an interface as lower-bound', async () => {
const result = await backend.callTool('impact', {
target: 'EmailLogger',
@@ -155,6 +340,7 @@ withTestLbugDB(
lastCommit: '',
indexedAt: '',
scopeExtractionReceipt: 1,
+ unresolvedOverloads: summarizeUnresolvedOverloads([]),
unresolvedReceiverMembers: {
counts: { anotherMember: 3 },
totalSites: 10,
@@ -185,6 +371,7 @@ withTestLbugDB(
lastCommit: '',
indexedAt: '',
scopeExtractionReceipt: 1,
+ unresolvedOverloads: summarizeUnresolvedOverloads([]),
unresolvedReceiverMembers: {
counts: { formatDate: 3 },
totalSites: 10,
@@ -221,6 +408,7 @@ withTestLbugDB(
lastCommit: '',
indexedAt: '',
scopeExtractionReceipt: 1,
+ unresolvedOverloads: summarizeUnresolvedOverloads([]),
unresolvedReceiverMembers: summary,
} as Awaited>);
@@ -263,6 +451,7 @@ withTestLbugDB(
);
vi.mocked(loadMeta).mockResolvedValue({
scopeExtractionReceipt: 1,
+ unresolvedOverloads: summarizeUnresolvedOverloads(outcomes),
unresolvedReceiverMembers: summary,
} as Awaited>);
@@ -281,6 +470,7 @@ withTestLbugDB(
async (omittedNames) => {
vi.mocked(loadMeta).mockResolvedValue({
scopeExtractionReceipt: 1,
+ unresolvedOverloads: summarizeUnresolvedOverloads([]),
unresolvedReceiverMembers: { counts: {}, totalSites: 0, omittedNames },
} as Awaited>);
@@ -297,6 +487,7 @@ withTestLbugDB(
async (name) => {
vi.mocked(loadMeta).mockResolvedValue({
scopeExtractionReceipt: 1,
+ unresolvedOverloads: summarizeUnresolvedOverloads([]),
unresolvedReceiverMembers: { counts: {}, totalSites: 3, omittedNames: 2 },
} as Awaited>);
@@ -314,6 +505,7 @@ withTestLbugDB(
async (name) => {
vi.mocked(loadMeta).mockResolvedValue({
scopeExtractionReceipt: 1,
+ unresolvedOverloads: summarizeUnresolvedOverloads([]),
unresolvedReceiverMembers: JSON.parse(
JSON.stringify({ counts: { [name]: 3 }, totalSites: 5, omittedNames: 2 }),
),
@@ -366,6 +558,7 @@ withTestLbugDB(
);
vi.mocked(loadMeta).mockResolvedValue({
scopeExtractionReceipt: 1,
+ unresolvedOverloads: summarizeUnresolvedOverloads(analysed.resolutionOutcomes),
unresolvedReceiverMembers: summary,
} as Awaited>);
@@ -390,6 +583,7 @@ withTestLbugDB(
lastCommit: 'abc123',
indexedAt: new Date().toISOString(),
scopeExtractionReceipt: 1,
+ unresolvedOverloads: summarizeUnresolvedOverloads([]),
scopeExtractionFailures: {
total: 2,
paths: ['src/broken-a.ts', 'src/broken-b.ts'],
@@ -412,6 +606,7 @@ withTestLbugDB(
lastCommit: 'abc123',
indexedAt: new Date().toISOString(),
scopeExtractionReceipt: 1,
+ unresolvedOverloads: summarizeUnresolvedOverloads([]),
scopeExtractionFailures: {
total: 1,
paths: ['src/`break`\n\u001b[31m\u202e\u200binject.ts'],
@@ -511,6 +706,7 @@ withTestLbugDB(
lastCommit: 'abc123',
indexedAt: new Date().toISOString(),
scopeExtractionReceipt: 1,
+ unresolvedOverloads: summarizeUnresolvedOverloads([]),
scopeExtractionFailures: {
total: 2,
paths: ['src/broken-a.ts', 'src/broken-b.ts'],
diff --git a/gitnexus/test/integration/impact-scope-omission-persistence.test.ts b/gitnexus/test/integration/impact-scope-omission-persistence.test.ts
index 83661475b..829103e4e 100644
--- a/gitnexus/test/integration/impact-scope-omission-persistence.test.ts
+++ b/gitnexus/test/integration/impact-scope-omission-persistence.test.ts
@@ -2,6 +2,7 @@ import { beforeEach, expect, it, vi } from 'vitest';
import { LocalBackend } from '../../src/mcp/local/local-backend.js';
import { listRegisteredRepos, saveMeta } from '../../src/storage/repo-manager.js';
import { withTestLbugDB } from '../helpers/test-indexed-db.js';
+import { summarizeUnresolvedOverloads } from '../../src/core/ingestion/scope-resolution/unresolved-overloads.js';
vi.mock('../../src/storage/repo-manager.js', async (importOriginal) => {
const actual = await importOriginal();
@@ -34,6 +35,7 @@ withTestLbugDB(
lastCommit: 'abc123',
indexedAt: new Date(0).toISOString(),
scopeExtractionReceipt: 1 as const,
+ unresolvedOverloads: summarizeUnresolvedOverloads([]),
};
await saveMeta(handle.tmpHandle.dbPath, {
...baseMeta,
diff --git a/gitnexus/test/integration/impact-undecided-satisfaction.test.ts b/gitnexus/test/integration/impact-undecided-satisfaction.test.ts
index f40a407c1..e2c326462 100644
--- a/gitnexus/test/integration/impact-undecided-satisfaction.test.ts
+++ b/gitnexus/test/integration/impact-undecided-satisfaction.test.ts
@@ -15,6 +15,7 @@
* the query is the analyzer's own record of what it could not decide, read from
* the index metadata.
*/
+import { summarizeUnresolvedOverloads } from '../../src/core/ingestion/scope-resolution/unresolved-overloads.js';
import { it, expect, beforeAll, vi } from 'vitest';
import path from 'node:path';
import { LocalBackend } from '../../src/mcp/local/local-backend.js';
@@ -109,6 +110,7 @@ withTestLbugDB(
// file directly would pin a shape no real analyze can produce.
await saveMeta(path.dirname(h.dbPath), {
scopeExtractionReceipt: 1,
+ unresolvedOverloads: summarizeUnresolvedOverloads([]),
undecidedInterfaceSatisfaction: {
counts: { CtxStore: 2 },
totalInterfaces: 1,
diff --git a/gitnexus/test/integration/java-overload-impact-persistence.test.ts b/gitnexus/test/integration/java-overload-impact-persistence.test.ts
new file mode 100644
index 000000000..288e7482c
--- /dev/null
+++ b/gitnexus/test/integration/java-overload-impact-persistence.test.ts
@@ -0,0 +1,181 @@
+import { execFile } from 'node:child_process';
+import { cp, mkdtemp, rm, writeFile } from 'node:fs/promises';
+import os from 'node:os';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { promisify } from 'node:util';
+import { expect, it } from 'vitest';
+import type { RepoMeta } from '../../src/storage/repo-meta.js';
+
+const exec = promisify(execFile);
+const helper = fileURLToPath(
+ new URL('../helpers/java-overload-persistence-child.mjs', import.meta.url),
+);
+const fixture = fileURLToPath(
+ new URL('../fixtures/lang-resolution/java-overload-types/', import.meta.url),
+);
+const prefix = 'Method:src/main/java/demo/LookupService.java:LookupService.';
+const callers = {
+ 'find#1~Collection': ['find', 'listOrders', 'settle'],
+ 'find#1~Long': ['getOrder'],
+ 'resolve#1~String': ['byCode'],
+ 'resolve#1~Long': ['byId'],
+ 'count#1': ['count', 'summarize'],
+ 'count#2': ['summarize'],
+} as const;
+const targets = Object.keys(callers).map((suffix) => prefix + suffix);
+
+interface AnalysisReceipt {
+ usedWorkerPool: boolean;
+ reparsed: number;
+ hits: number;
+ storagePath: string;
+ meta: RepoMeta;
+}
+interface ImpactReceipt {
+ error?: string;
+ partial?: boolean;
+ epistemic: string;
+ causes?: { overloadResolution?: number };
+ summary: { direct: number };
+ byDepth: Record>;
+}
+
+it('preserves original overload callers and omission receipts across cold, durable warm, and edited analyses', async () => {
+ const directory = await mkdtemp(path.join(os.tmpdir(), 'gitnexus-java-overload-'));
+ const repo = path.join(directory, 'repo');
+ const env: NodeJS.ProcessEnv = {
+ ...process.env,
+ GITNEXUS_HOME: path.join(directory, 'home'),
+ GITNEXUS_SHARED_STORE: 'off',
+ GITNEXUS_MEMORY: 'off',
+ GITNEXUS_LANG: 'en',
+ GITNEXUS_LBUG_EXTENSION_INSTALL: 'never',
+ // Repeated impact traversals need more than the writer's 128 MiB minimum.
+ GITNEXUS_LBUG_BUFFER_POOL_SIZE: '536870912',
+ };
+ delete env.GITNEXUS_STORAGE_PATH;
+ delete env.GITNEXUS_STORAGE_ROOT;
+ async function child(action: 'analyze' | 'inspect'): Promise {
+ const { stdout } = await exec(
+ process.execPath,
+ [helper, JSON.stringify({ action, repo, targets })],
+ {
+ cwd: repo,
+ env,
+ timeout: 180_000,
+ maxBuffer: 4 * 1024 * 1024,
+ },
+ );
+ const line = stdout.split('\n').find((value) => value.startsWith('JAVA_OVERLOAD_RESULT='));
+ if (!line) throw new Error(`Missing child receipt: ${stdout}`);
+ return JSON.parse(line.slice('JAVA_OVERLOAD_RESULT='.length)) as T;
+ }
+ function assertCallers(results: Record): void {
+ for (const [suffix, expected] of Object.entries(callers)) {
+ const result = results[prefix + suffix];
+ expect(result, suffix).not.toHaveProperty('error');
+ expect(result.partial, suffix).not.toBe(true);
+ expect(result.summary.direct, suffix).toBe(expected.length);
+ expect((result.byDepth['1'] ?? []).map((caller) => caller.name).sort(), suffix).toEqual(
+ [...expected].sort(),
+ );
+ }
+ }
+ try {
+ await cp(fixture, repo, { recursive: true });
+ await exec('git', ['init', '-q', repo]);
+ const cold = await child('analyze');
+ expect(cold.usedWorkerPool).toBe(true);
+ expect(cold.reparsed).toBe(5);
+ expect(cold.hits).toBe(0);
+ const coldResults = await child>('inspect');
+ assertCallers(coldResults);
+ for (const result of Object.values(coldResults)) {
+ expect(result.causes?.overloadResolution ?? 0).toBe(0);
+ expect(result.epistemic).toBe('exact');
+ }
+
+ // A fresh analyzer process consumes only durable ParsedFile sidechannels.
+ // Force bypasses the up-to-date shortcut without discarding parser caches.
+ const warm = await child('analyze');
+ expect(warm.usedWorkerPool).toBe(false);
+ expect(warm.reparsed).toBe(0);
+ expect(warm.hits).toBe(5);
+ const warmResults = await child>('inspect');
+ assertCallers(warmResults);
+ for (const uid of targets) {
+ expect(warmResults[uid].epistemic).toBe(coldResults[uid].epistemic);
+ expect(warmResults[uid].causes).toEqual(coldResults[uid].causes);
+ }
+
+ // External.value() has no available return type. Neither overload can be
+ // chosen; known callers stay intact and each queried candidate is hedged.
+ const unknownPath = path.join(repo, 'src/main/java/demo/UnknownCalls.java');
+ const unknownSource = `package demo;
+class UnknownCalls {
+ LookupService lookup;
+ Perm unknown() { return lookup.resolve(External.value()); }
+}
+`;
+ await writeFile(unknownPath, unknownSource);
+ const unresolved = await child('analyze');
+ expect(unresolved.meta.unresolvedOverloads?.totalSites).toBeGreaterThan(0);
+ const uncertainResults = await child>('inspect');
+ assertCallers(uncertainResults);
+ for (const suffix of ['resolve#1~String', 'resolve#1~Long']) {
+ expect(uncertainResults[prefix + suffix]).toMatchObject({
+ epistemic: 'lower-bound',
+ causes: { overloadResolution: 1 },
+ });
+ }
+
+ // Correcting a retained file must replace its old omission receipt and
+ // persist the newly proven edge, even while all other parse entries hit.
+ await writeFile(
+ unknownPath,
+ `package demo;
+class UnknownCalls {
+ LookupService lookup;
+ Perm unknown() { return lookup.resolve("known"); }
+}
+`,
+ );
+ const correction = await child('analyze');
+ expect(correction.reparsed).toBe(1);
+ expect(correction.hits).toBe(5);
+ const corrected = await child>('inspect');
+ for (const uid of targets) expect(corrected[uid].partial, uid).not.toBe(true);
+ expect(corrected[prefix + 'resolve#1~String'].summary.direct).toBe(2);
+ expect(
+ corrected[prefix + 'resolve#1~String'].byDepth['1'].map((caller) => caller.name).sort(),
+ ).toEqual(['byCode', 'unknown']);
+ expect(corrected[prefix + 'resolve#1~Long'].summary.direct).toBe(1);
+ for (const uid of targets) {
+ expect(corrected[uid].epistemic).toBe(coldResults[uid].epistemic);
+ expect(corrected[uid].causes).toEqual(coldResults[uid].causes);
+ }
+
+ // Delete a file with a persisted unresolved call, so stale omission
+ // metadata cannot hide behind the earlier correction's empty receipt.
+ await writeFile(unknownPath, unknownSource);
+ const unresolvedAgain = await child('analyze');
+ expect(unresolvedAgain.meta.unresolvedOverloads?.totalSites).toBeGreaterThan(0);
+ await rm(unknownPath);
+ const deletion = await child('analyze');
+ expect(deletion.meta.unresolvedOverloads).toMatchObject({
+ complete: true,
+ totalSites: 0,
+ omittedSites: 0,
+ sites: [],
+ });
+ const restored = await child>('inspect');
+ assertCallers(restored);
+ for (const uid of targets) {
+ expect(restored[uid].epistemic).toBe(coldResults[uid].epistemic);
+ expect(restored[uid].causes).toEqual(coldResults[uid].causes);
+ }
+ } finally {
+ await rm(directory, { recursive: true, force: true });
+ }
+}, 600_000);
diff --git a/gitnexus/test/integration/resolvers/java-javac-overloads.test.ts b/gitnexus/test/integration/resolvers/java-javac-overloads.test.ts
new file mode 100644
index 000000000..d03c0c2fb
--- /dev/null
+++ b/gitnexus/test/integration/resolvers/java-javac-overloads.test.ts
@@ -0,0 +1,268 @@
+import { execFileSync, spawnSync } from 'node:child_process';
+import { mkdtempSync, mkdirSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
+import { tmpdir } from 'node:os';
+import path from 'node:path';
+import { describe, expect, it } from 'vitest';
+import { FIXTURES } from './helpers.js';
+
+const compilerLocale = ['-J-Duser.language=en', '-J-Duser.country=US'];
+const javacAvailable =
+ spawnSync('javac', [...compilerLocale, '-version'], { encoding: 'utf8' }).status === 0;
+
+describe('javac oracle for Java overload resolution', () => {
+ it.runIf(javacAvailable)(
+ 'distinguishes a statically imported source type from the boxed long type',
+ () => {
+ const root = mkdtempSync(path.join(tmpdir(), 'gitnexus-javac-static-type-'));
+ try {
+ const sources = {
+ 'names/Names.java': 'package names; public class Names { public static class Long {} }',
+ 'client/Oracle.java': `package client; import static names.Names.Long;
+public class Oracle {
+ static String select(Long value) { return "source"; }
+ static String select(Number value) { return "number"; }
+ public static void main(String[] args) {
+ System.out.println(select(1L) + "," + select(new Long()));
+ }
+}`,
+ };
+ for (const [name, source] of Object.entries(sources)) {
+ const file = path.join(root, name);
+ mkdirSync(path.dirname(file), { recursive: true });
+ writeFileSync(file, source);
+ }
+ execFileSync('javac', [
+ ...compilerLocale,
+ '-d',
+ root,
+ ...Object.keys(sources).map((name) => path.join(root, name)),
+ ]);
+ expect(
+ execFileSync('java', ['-cp', root, 'client.Oracle'], { encoding: 'utf8' }).trim(),
+ ).toBe('number,source');
+ } finally {
+ rmSync(root, { recursive: true, force: true });
+ }
+ },
+ );
+
+ it.runIf(javacAvailable)(
+ 'selects the accessible Object overload for a declared method return and known arguments',
+ () => {
+ const root = mkdtempSync(path.join(tmpdir(), 'gitnexus-javac-accessible-'));
+ try {
+ const source = path.join(root, 'Oracle.java');
+ writeFileSync(
+ source,
+ `class AccessFilter {
+ private String accessFiltered(String value) { return "private"; }
+ public String accessFiltered(Object value) { return "object"; }
+}
+public class Oracle {
+ static Object external() { return null; }
+ public static void main(String[] args) {
+ AccessFilter value = new AccessFilter();
+ Object argument = external();
+ System.out.println(String.join(",", value.accessFiltered(external()),
+ value.accessFiltered(argument), value.accessFiltered("x")));
+ }
+}`,
+ );
+ execFileSync('javac', [...compilerLocale, '-d', root, source]);
+ expect(execFileSync('java', ['-cp', root, 'Oracle'], { encoding: 'utf8' }).trim()).toBe(
+ 'object,object,object',
+ );
+ } finally {
+ rmSync(root, { recursive: true, force: true });
+ }
+ },
+ );
+
+ it.runIf(javacAvailable)(
+ 'confirms enclosing-subclass access and protected receiver restrictions',
+ () => {
+ const root = mkdtempSync(path.join(tmpdir(), 'gitnexus-javac-protected-'));
+ try {
+ const sources = path.join(FIXTURES, 'java-protected-overloads');
+ execFileSync('javac', [
+ ...compilerLocale,
+ '-d',
+ root,
+ path.join(sources, 'a/A.java'),
+ path.join(sources, 'b/B.java'),
+ ]);
+ expect(execFileSync('java', ['-cp', root, 'b.B'], { encoding: 'utf8' }).trim()).toBe(
+ 'string,object,object,string,string,string,static-string',
+ );
+ } finally {
+ rmSync(root, { recursive: true, force: true });
+ }
+ },
+ );
+
+ it.runIf(javacAvailable)(
+ 'compiles the exact issue fixture and confirms its six target descriptors',
+ () => {
+ const root = mkdtempSync(path.join(tmpdir(), 'gitnexus-javac-overloads-'));
+ try {
+ const sources = path.join(FIXTURES, 'java-overload-types/src/main/java/demo');
+ execFileSync('javac', [
+ ...compilerLocale,
+ '-d',
+ root,
+ ...readdirSync(sources)
+ .filter((name) => name.endsWith('.java'))
+ .map((name) => path.join(sources, name)),
+ ]);
+ const bytecode = execFileSync(
+ 'javap',
+ [
+ '-classpath',
+ root,
+ '-c',
+ '-p',
+ 'demo.LookupService',
+ 'demo.OrderService',
+ 'demo.BillingService',
+ ],
+ { encoding: 'utf8' },
+ );
+ for (const [descriptor, count] of [
+ ['find:(Ljava/util/Collection;)Ljava/util/Map;', 3],
+ ['find:(Ljava/lang/Long;)Ldemo/Perm;', 1],
+ ['resolve:(Ljava/lang/String;)Ldemo/Perm;', 1],
+ ['resolve:(Ljava/lang/Long;)Ldemo/Perm;', 1],
+ ['count:(Ldemo/Req;)I', 2],
+ ['count:(Ljava/lang/Long;Ldemo/Req;)I', 1],
+ ] as const) {
+ expect(
+ bytecode
+ .split('\n')
+ .filter((line) => line.includes('invoke') && line.includes(descriptor)),
+ descriptor,
+ ).toHaveLength(count);
+ }
+ } finally {
+ rmSync(root, { recursive: true, force: true });
+ }
+ },
+ );
+
+ it.runIf(javacAvailable)(
+ 'confirms phase ordering, arrays, invariance, target typing and zero-argument specificity',
+ () => {
+ const root = mkdtempSync(path.join(tmpdir(), 'gitnexus-javac-phases-'));
+ try {
+ const file = path.join(root, 'Oracle.java');
+ writeFileSync(
+ file,
+ `import java.util.*;
+class Oracle {
+ static String phase(Integer x) { return "box"; } static String phase(long x) { return "wide"; }
+ static String boxed(long x) { return "unbox"; } static String boxed(Number x) { return "reference"; }
+ static String variable(String... x) { return "varargs"; } static String variable(Object x) { return "fixed"; }
+ static String empty(Object... x) { return "object"; } static String empty(String... x) { return "string"; }
+ static String generic(Collection x) { return "collection"; } static String generic(Object x) { return "object"; }
+ static String primitive(List x) { return "list"; } static String primitive(Object x) { return "object"; }
+ static String array(Object[] x) { return "array"; } static String array(Object x) { return "object"; }
+ public static void main(String[] args) {
+ List ids = Arrays.asList(1L);
+ System.out.println(String.join(",", phase(1), boxed(Integer.valueOf(1)), variable("x"), empty(),
+ generic(ids), generic(Arrays.asList(1L)), generic(Collections.singletonList(1L)),
+ primitive(Arrays.asList(new long[]{1L})), array(new int[1][1])));
+ }
+}`,
+ );
+ execFileSync('javac', [...compilerLocale, '-d', root, file]);
+ expect(execFileSync('java', ['-cp', root, 'Oracle'], { encoding: 'utf8' }).trim()).toBe(
+ 'wide,reference,fixed,string,object,collection,collection,object,array',
+ );
+ } finally {
+ rmSync(root, { recursive: true, force: true });
+ }
+ },
+ );
+
+ it.runIf(javacAvailable)(
+ 'confirms inherited field precedence and enhanced-for variable scope',
+ () => {
+ const root = mkdtempSync(path.join(tmpdir(), 'gitnexus-javac-lexical-'));
+ try {
+ const source = path.join(root, 'Lexical.java');
+ writeFileSync(
+ source,
+ `class Lexical {
+ static class Base { String value = "base"; }
+ static class Outer { Long value = 1L; class Inner extends Base {
+ String pick(String value) { return "string"; } String pick(Long value) { return "long"; }
+ String byThis() { return pick(this.value); } String byImplicit() { return pick(value); }
+ } }
+ static class Loop { Long item = 1L;
+ String[] source(String value) { return new String[]{"wrong"}; }
+ String[] source(Long value) { return new String[]{"right"}; }
+ String body(String value) { return "string"; } String body(Long value) { return "long"; }
+ String run() { String result = ""; for (String item : source(item)) result += item + ":" + body(item); return result; }
+ }
+ public static void main(String[] args) {
+ Outer.Inner value = new Outer().new Inner();
+ System.out.println(value.byThis() + "," + value.byImplicit() + "," + new Loop().run());
+ }
+}`,
+ );
+ execFileSync('javac', [...compilerLocale, '-d', root, source]);
+ expect(execFileSync('java', ['-cp', root, 'Lexical'], { encoding: 'utf8' }).trim()).toBe(
+ 'string,string,right:string',
+ );
+ } finally {
+ rmSync(root, { recursive: true, force: true });
+ }
+ },
+ );
+
+ it.runIf(javacAvailable).each([
+ [
+ 'null tie',
+ 'void choose(String x) {} void choose(Integer x) {} void run() { choose(null); }',
+ 'ambiguous',
+ ],
+ [
+ 'crossed parameters',
+ 'void choose(String x,Object y) {} void choose(Object x,String y) {} void run() { choose("a","b"); }',
+ 'ambiguous',
+ ],
+ [
+ 'poly target tie',
+ 'void choose(java.util.List x) {} void choose(java.util.Collection x) {} void run() { choose(java.util.Arrays.asList(1)); }',
+ 'ambiguous',
+ ],
+ ['widen then box', 'void choose(Long x) {} void run() { choose(1); }', 'incompatible'],
+ [
+ 'static invocation after selection',
+ 'static void choose(Object x) {} void choose(String x) {} static void run() { choose("x"); }',
+ 'non-static method choose(String)',
+ ],
+ [
+ 'type-qualified invocation after selection',
+ 'static void choose(Object x) {} void choose(String x) {} static void run() { Invalid.choose("x"); }',
+ 'non-static method choose(String)',
+ ],
+ ])('rejects %s', (_name, body, diagnostic) => {
+ const root = mkdtempSync(path.join(tmpdir(), 'gitnexus-javac-invalid-'));
+ try {
+ const source = path.join(root, 'Invalid.java');
+ mkdirSync(path.join(root, 'classes'));
+ writeFileSync(source, `class Invalid { ${body} }`);
+ const compilation = spawnSync(
+ 'javac',
+ [...compilerLocale, '-d', path.join(root, 'classes'), source],
+ {
+ encoding: 'utf8',
+ },
+ );
+ expect(compilation.status).not.toBe(0);
+ expect(compilation.stderr).toContain(diagnostic);
+ } finally {
+ rmSync(root, { recursive: true, force: true });
+ }
+ });
+});
diff --git a/gitnexus/test/integration/resolvers/java-lombok.test.ts b/gitnexus/test/integration/resolvers/java-lombok.test.ts
index 34a3a27f1..fcf5bcc0f 100644
--- a/gitnexus/test/integration/resolvers/java-lombok.test.ts
+++ b/gitnexus/test/integration/resolvers/java-lombok.test.ts
@@ -12,6 +12,7 @@ import {
runPipelineFromRepo,
getRelationships,
getNodesByLabelFull,
+ getResolutionOutcomes,
} from './helpers.js';
import {
loadParseCache,
@@ -44,6 +45,39 @@ function projectCallsToGetName(result: Awaited {
+ it('keeps an inherited synthetic setter in the overload family when its formal types are unavailable', async () => {
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-java-lombok-overload-'));
+ try {
+ writeFixtureRepo(root, {
+ 'Inheritance.java': `import lombok.Data;
+@Data class GeneratedBase { private String value; }
+class GeneratedChild extends GeneratedBase { public void setValue(Object value) {} }
+class Consumer { void choose(GeneratedChild target) { target.setValue("x"); } }
+`,
+ });
+ const linked = await runPipelineFromRepo(root, () => {});
+ expect(
+ getRelationships(linked, 'CALLS').filter(
+ (edge) => edge.source === 'choose' && edge.target === 'setValue',
+ ),
+ ).toEqual([]);
+ expect(getResolutionOutcomes(linked)).toContainEqual(
+ expect.objectContaining({
+ kind: 'suppressed',
+ reason: 'overload-unknown',
+ name: 'setValue',
+ candidateGraphIds: expect.arrayContaining([
+ 'Method:Inheritance.java:GeneratedBase.setValue#1',
+ 'Method:Inheritance.java:GeneratedChild.setValue#1',
+ ]),
+ candidateSetComplete: true,
+ }),
+ );
+ } finally {
+ fs.rmSync(root, { recursive: true, force: true });
+ }
+ }, 60000);
+
it.each(['same-file', 'cross-file'] as const)(
'resolves all getter and setter calls to their owner in a %s fixture',
async (layout) => {
diff --git a/gitnexus/test/integration/resolvers/java-overload-jls.test.ts b/gitnexus/test/integration/resolvers/java-overload-jls.test.ts
new file mode 100644
index 000000000..1442028f5
--- /dev/null
+++ b/gitnexus/test/integration/resolvers/java-overload-jls.test.ts
@@ -0,0 +1,542 @@
+import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
+import { tmpdir } from 'node:os';
+import path from 'node:path';
+import { afterAll, beforeAll, describe, expect, it } from 'vitest';
+import { runPipelineFromRepo } from '../../../src/core/ingestion/pipeline.js';
+import type { PipelineResult } from '../../../src/types/pipeline.js';
+import { FIXTURES, getRelationships, getResolutionOutcomes, writeFixtureRepo } from './helpers.js';
+
+function targets(result: PipelineResult, caller: string, method: string): string[] {
+ return getRelationships(result, 'CALLS')
+ .filter((edge) => edge.source === caller && edge.target === method)
+ .map((edge) => edge.rel.targetId)
+ .sort();
+}
+
+it('records unresolved receivers once across replay without merging distinct sites', async () => {
+ const root = mkdtempSync(path.join(tmpdir(), 'gitnexus-java-receiver-replay-'));
+ try {
+ writeFixtureRepo(root, {
+ 'Replay.java': `class Replay {
+ void unresolved() {
+ System.out.println("first");
+ System.out.println("second");
+ External.value.value().value();
+ }
+ }`,
+ });
+ const result = await runPipelineFromRepo(root, () => {});
+ const drops = getResolutionOutcomes(result)
+ .filter((outcome) => outcome.kind === 'suppressed')
+ .filter((outcome) => outcome.reason === 'receiver-unresolved');
+ expect(drops.filter((outcome) => outcome.name === 'println')).toHaveLength(2);
+ expect(
+ drops.filter((outcome) => outcome.name === 'value' && outcome.siteKind === 'call'),
+ ).toHaveLength(2);
+ const identities = drops.map((outcome) =>
+ JSON.stringify([outcome.filePath, outcome.range, outcome.siteKind, outcome.name]),
+ );
+ expect(new Set(identities).size).toBe(drops.length);
+ } finally {
+ rmSync(root, { recursive: true, force: true });
+ }
+});
+
+describe('Java protected overloads in enclosing subclasses', () => {
+ let result: PipelineResult;
+ beforeAll(async () => {
+ result = await runPipelineFromRepo(path.join(FIXTURES, 'java-protected-overloads'), () => {});
+ }, 120_000);
+
+ it.each(['nested', 'deeplyNested', 'enclosingSubclass', 'staticNested'])(
+ 'permits protected access from %s through the enclosing subclass',
+ (caller) => {
+ expect(targets(result, caller, 'pick')).toEqual(['Method:a/A.java:A.pick#1~String']);
+ },
+ );
+
+ it.each(['baseReceiver', 'siblingReceiver'])(
+ 'keeps the protected receiver restriction for %s',
+ (caller) => {
+ expect(targets(result, caller, 'pick')).toEqual(['Method:a/A.java:A.pick#1~Object']);
+ },
+ );
+
+ it('allows a protected static method through the declaring type', () => {
+ expect(targets(result, 'protectedStatic', 'staticPick')).toEqual([
+ 'Method:a/A.java:A.staticPick#1~String',
+ ]);
+ });
+});
+
+describe('Java imported overloads and declared dispatch bounds', () => {
+ const root = mkdtempSync(path.join(tmpdir(), 'gitnexus-java-imported-overloads-'));
+ let result: PipelineResult;
+ beforeAll(async () => {
+ writeFixtureRepo(root, {
+ 'api/Explicit.java': `package api; public class Explicit {
+ public static void pick(Object x) {} public static void shadow(Object x) {}
+ }`,
+ 'api/Demand.java': `package api; public class Demand {
+ public static void pick(String x) {} public static void shadow(Object x) {}
+ }`,
+ 'client/Imports.java': `package client; import static api.Explicit.pick;
+ import static api.Explicit.shadow; import static api.Demand.*;
+ class Imports { void importedPick() { pick("x"); }
+ void importedShadow() { shadow("x"); } }`,
+ 'api/Hidden.java': `package api; class Hidden { public static void hidden() {} }`,
+ 'client/HiddenUse.java': `package client; import static api.Hidden.hidden;
+ class HiddenUse { void inaccessibleOwner() { hidden(); } }`,
+ 'api/PrivateHost.java': `package api; import static api.PrivateHost.Holder.secret;
+ public class PrivateHost { public static class Holder { private static void secret() {} }
+ void privateImport() { secret(); } }`,
+ 'Dispatch.java': `interface Parent { void task(); } interface Child extends Parent {}
+ interface StaticAPI {
+ static void staticRun() {} private void privateRun() {}
+ default void privateBridge() { this.privateRun(); }
+ }
+ class RuntimeDecoy implements StaticAPI { public void staticRun() {} public void privateRun() {} }
+ class ChildImpl implements Child { public void task() {} }
+ class OtherImpl implements Parent { public void task() {} }
+ class Dispatch {
+ void staticDispatch() { StaticAPI.staticRun(); }
+ void childDispatch(Child child) { child.task(); }
+ void anonymousDispatch() { Child child = new Child() { public void task() {} }; child.task(); }
+ void runnableGood() { Runnable r = new Runnable() { public void run() {} public void run(String x) {} }; r.run(); }
+ void runnableBad() { Runnable r = new Runnable() { public void run() {} public void run(String x) {} }; r.run("x"); }
+ }`,
+ });
+ result = await runPipelineFromRepo(root, () => {});
+ }, 120_000);
+ afterAll(() => rmSync(root, { recursive: true, force: true }));
+
+ it('keeps different-signature demand imports in the overload family', () => {
+ expect(targets(result, 'importedPick', 'pick')).toEqual([
+ 'Method:api/Demand.java:Demand.pick#1',
+ ]);
+ });
+ it('lets a single static import shadow only an identical demand-import signature', () => {
+ expect(targets(result, 'importedShadow', 'shadow')).toEqual([
+ 'Method:api/Explicit.java:Explicit.shadow#1',
+ ]);
+ });
+ it('does not import a public method through an inaccessible owner type', () => {
+ expect(targets(result, 'inaccessibleOwner', 'hidden')).toEqual([]);
+ });
+ it('does not grant nestmate access at a static-import declaration', () => {
+ expect(targets(result, 'privateImport', 'secret')).toEqual([]);
+ });
+ it('retains the original child-interface bound during runtime dispatch', () => {
+ const ids = targets(result, 'childDispatch', 'task');
+ expect(ids).toContain('Method:Dispatch.java:ChildImpl.task#0');
+ expect(ids).not.toContain('Method:Dispatch.java:OtherImpl.task#0');
+ });
+ it('keeps a known anonymous receiver on its own implementation branch', () => {
+ const ids = targets(result, 'anonymousDispatch', 'task');
+ expect(ids).toContain('Method:Dispatch.java:Dispatch$1.task#0');
+ expect(ids).not.toContain('Method:Dispatch.java:ChildImpl.task#0');
+ expect(ids).not.toContain('Method:Dispatch.java:OtherImpl.task#0');
+ });
+ it('restricts a Runnable receiver to the platform run() signature', () => {
+ expect(targets(result, 'runnableGood', 'run')).toEqual([
+ 'Method:Dispatch.java:Dispatch$2.run#0',
+ ]);
+ expect(targets(result, 'runnableBad', 'run')).toEqual([]);
+ });
+ it('does not dispatch static or private interface methods to same-named instance methods', () => {
+ expect(targets(result, 'staticDispatch', 'staticRun')).toEqual([
+ 'Method:Dispatch.java:StaticAPI.staticRun#0',
+ ]);
+ expect(targets(result, 'privateBridge', 'privateRun')).toEqual([
+ 'Method:Dispatch.java:StaticAPI.privateRun#0',
+ ]);
+ });
+});
+
+describe('Java JLS overload resolution — issue #3789', () => {
+ let original: PipelineResult;
+ beforeAll(async () => {
+ original = await runPipelineFromRepo(path.join(FIXTURES, 'java-overload-types'), () => {});
+ }, 120_000);
+
+ it('retains the original six signatures and their exact direct caller counts', () => {
+ const expected: Record = {
+ 'find#1~Collection': 3,
+ 'find#1~Long': 1,
+ 'resolve#1~String': 1,
+ 'resolve#1~Long': 1,
+ 'count#1': 2,
+ 'count#2': 1,
+ };
+ const calls = getRelationships(original, 'CALLS');
+ for (const [signature, count] of Object.entries(expected)) {
+ const id = `Method:src/main/java/demo/LookupService.java:LookupService.${signature}`;
+ expect(original.graph.getNode(id), signature).toBeDefined();
+ expect(
+ new Set(calls.filter((edge) => edge.rel.targetId === id).map((edge) => edge.rel.sourceId))
+ .size,
+ signature,
+ ).toBe(count);
+ }
+ });
+
+ it('selects collection factories without attributing them to the scalar overload', () => {
+ for (const caller of ['listOrders', 'settle', 'find']) {
+ expect(targets(original, caller, 'find')).toEqual([
+ 'Method:src/main/java/demo/LookupService.java:LookupService.find#1~Collection',
+ ]);
+ }
+ expect(
+ getResolutionOutcomes(original).filter(
+ (outcome) => outcome.kind === 'suppressed' && outcome.reason.startsWith('overload-'),
+ ),
+ ).toEqual([]);
+ });
+});
+
+describe('Java compile-time overload families', () => {
+ const root = mkdtempSync(path.join(tmpdir(), 'gitnexus-java-overload-jls-'));
+ let result: PipelineResult;
+ beforeAll(async () => {
+ writeFixtureRepo(root, {
+ 'Cases.java': `
+import java.util.*;
+class Base { public void inherited(long value) {} public void declared(Object value) {} }
+class Derived extends Base {
+ public void inherited(Integer value) {}
+ public void declared(String value) {}
+}
+class Intermediate extends Base {}
+class Leaf extends Intermediate { public void inherited(Integer value) {} }
+class Nest {
+ private void nest(Integer value) {} private void nest(long value) {}
+ class Inner { void nestedPrivate(Nest outer) { outer.nest(1); } }
+}
+class StaticInvocation {
+ static void staticChoice(Object value) {} void staticChoice(String value) {}
+ static void staticUnqualified() { staticChoice("x"); }
+ static void staticQualified() { StaticInvocation.staticChoice("x"); }
+}
+class AccessFilter {
+ private void accessFiltered(String value) {} public void accessFiltered(Object value) {}
+}
+class BaseShadow { String value; }
+class OuterShadow { Long value; class InnerShadow extends BaseShadow {
+ void shadowPick(String value) {} void shadowPick(Long value) {}
+ void inheritedByThis() { shadowPick(this.value); }
+ void inheritedImplicit() { shadowPick(value); }
+} }
+class ForScope { Long item;
+ String[] loopSource(String value) { return null; } String[] loopSource(Long value) { return null; }
+ void loopBody(String value) {} void loopBody(Long value) {}
+ void enhancedForScope() { for (String item : loopSource(item)) { loopBody(item); } }
+}
+interface API { void dispatch(long value); void dispatch(Integer value); }
+interface Generic { void genericDispatch(T value); }
+class StringGeneric implements Generic {
+ public void genericDispatch(String value) {} public void genericDispatch(Integer value) {}
+}
+class IntegerGeneric implements Generic { public void genericDispatch(Integer value) {} }
+interface Left { void preferred(String value); }
+interface Right { void preferred(String value); }
+abstract class Both implements Left, Right { void preferred(Object value) {} }
+class Implementation implements API {
+ public void dispatch(long value) {} public void dispatch(Integer value) {}
+}
+class A { void run() {} A next() { return this; } }
+class B { void run() {} B next() { return this; } }
+class SubA extends A { void run() {} }
+interface Maker { A create(long value); B create(Integer value); }
+class MakerImpl implements Maker { public SubA create(long value) { return new SubA(); } public B create(Integer value) { return new B(); } }
+class Cases {
+ void phase(Integer value) {} void phase(long value) {}
+ void boxed(long value) {} void boxed(Number value) {}
+ void variable(String... values) {} void variable(Object value) {}
+ void empty(Object... values) {} void empty(String... values) {}
+ void nullable(String value) {} void nullable(Integer value) {}
+ void crossed(String a, Object b) {} void crossed(Object a, String b) {}
+ void opaque(String value) {} void opaque(Long value) {}
+ void forbidden(Long value) {}
+ void array(Object value) {} void array(String[] value) {}
+ void dimensions(Object[] value) {} void dimensions(Object value) {}
+ void concrete(Collection value) {} void concrete(Object value) {}
+ void poly(Collection value) {} void poly(Object value) {}
+ void polyAmbiguous(List value) {} void polyAmbiguous(Collection value) {}
+ void primitiveArray(List value) {} void primitiveArray(Object value) {}
+ B make(Integer value) { return new B(); } A make(long value) { return new A(); }
+ A uncertain(String value) { return new A(); } B uncertain(Long value) { return new B(); }
+ void dimension(String value) {} void dimension(Object value) {}
+ void widening() { phase(1); }
+ void refBeforeUnbox(Integer value) { boxed(value); }
+ void fixedBeforeVarargs() { variable("x"); }
+ void emptyVarargs() { empty(); }
+ void nullAmbiguous() { nullable(null); }
+ void crossedAmbiguous() { crossed("a", "b"); }
+ Object external() { return null; }
+ void unknownArgument() { opaque(external()); }
+ void knownInapplicable() { forbidden(1); }
+ void arraySpecific(String[] value) { array(value); }
+ void primitiveDimensions(int[][] value) { dimensions(value); }
+ void invariant(List value) { concrete(value); }
+ void targetTyped(Long value) { poly(Arrays.asList(value)); }
+ void targetTypedSingleton(Long value) { poly(Collections.singletonList(value)); }
+ void polyTie() { polyAmbiguous(Arrays.asList(1)); }
+ void primitiveFactory(long[] values) { primitiveArray(Arrays.asList(values)); }
+ void inheritedPhase(Derived value) { value.inherited(1); }
+ void emptyIntermediate(Leaf value) { value.inherited(1); }
+ void declaredReceiver() { Base value = new Derived(); value.declared("x"); }
+ void virtualDispatch(API value) { value.dispatch(1); }
+ void chain() { make(1).run(); }
+ void assignedChain() { var value = make(1); value.run(); }
+ void nestedChain() { make(1).next().run(); }
+ void unknownAssignment() { var value = uncertain(external()); value.run(); }
+ void ambiguousAssignment() { var value = uncertain(null); value.run(); }
+ void interfaceReturn(Maker maker) { maker.create(1).run(); }
+ void trailingDimension(String value[]) { dimension(value); }
+ void equivalentPreferred(Both value) { value.preferred("x"); }
+ void genericCall(Generic value) { value.genericDispatch("x"); }
+ void inaccessibleAlternate(AccessFilter value) { value.accessFiltered(external()); }
+ void accessibleKnown(AccessFilter value) { value.accessFiltered("x"); }
+ void accessibleObject(AccessFilter value, Object argument) { value.accessFiltered(argument); }
+ void genericAny(Generic value, T item) { value.genericDispatch(item); }
+}`,
+ 'api/Service.java': `package api; public interface Service { void select(long value); void select(Integer value); }`,
+ 'client/Client.java': `package client; class Client { void acrossPackages(api.Service service) { service.select(1); } }`,
+ 'names/Names.java': `package names; public class Names { public static class Long {} }`,
+ 'names/Other.java': `package names; public class Other { public static class Long {} }`,
+ 'client/StaticType.java': `package client; import static names.Names.Long;
+class StaticType {
+ void select(Long value) {} void select(Number value) {}
+ void staticTypeImport() { select(1L); }
+ void staticSourceType(Long value) { select(value); }
+}`,
+ 'client/MissingStaticType.java': `package client; import static external.Names.Long;
+class MissingStaticType { void select(Long value) {} void select(Number value) {} void missingStaticType() { select(1L); } }`,
+ 'client/ConflictingStaticType.java': `package client; import static names.Names.Long; import static names.Other.Long;
+class ConflictingStaticType { void select(Long value) {} void select(Number value) {} void conflictingStaticTypes() { select(1L); } }`,
+ 'first/Base.java': `package first; public class Base { void access(String value) {} protected void protectedChoice(Integer value) {} protected void protectedChoice(long value) {} }`,
+ 'second/Middle.java': `package second; public class Middle extends first.Base {}`,
+ 'second/ProtectedChild.java': `package second; import first.Base; class ProtectedChild extends Base { void superProtected() { super.protectedChoice(1); } }`,
+ 'first/Child.java': `package first; class Child extends second.Middle {
+ void access(Object value) {} void interruptedPackage() { access("x"); }
+ void castBase() { ((Base)this).access("x"); }
+}`,
+ 'shadow/Shadow.java': `package shadow;
+class Arrays { static Object asList(long value) { return null; } }
+class Long {}
+class Shadow {
+ void select(java.util.Collection value) {} void select(java.lang.Long value) {}
+ void fakeFactory() { select(Arrays.asList(1L)); }
+ void scalar(java.lang.Long value) {} void scalar(Long value) {}
+ void shadowedBox(Long value) { scalar(value); }
+ void generic(java.lang.Long value) {} void generic(Object value) {}
+ void typeParameter(Long value) { generic(value); }
+}
+class FactoryField {
+ Object Arrays;
+ void select(java.util.Collection value) {} void select(java.lang.Long value) {}
+ void fieldFactory() { select(Arrays.asList(1L)); }
+}
+class Incomplete extends external.Base {
+ void select(java.lang.String value) {} void select(java.lang.Long value) {}
+ void inheritedUnknown() { select("x"); }
+}
+class ObjectOverload {
+ void wait(Integer value) {}
+ void implicitObject() throws InterruptedException { wait(1); }
+}`,
+ });
+ result = await runPipelineFromRepo(root, () => {});
+ }, 120_000);
+ afterAll(() => rmSync(root, { recursive: true, force: true }));
+
+ it.each([
+ ['widening', 'phase', 'Cases.phase#1~long'],
+ ['refBeforeUnbox', 'boxed', 'Cases.boxed#1~Number'],
+ ['fixedBeforeVarargs', 'variable', 'Cases.variable#1'],
+ ['emptyVarargs', 'empty', 'Cases.empty'],
+ ['arraySpecific', 'array', 'Cases.array#1~String[]'],
+ ['primitiveDimensions', 'dimensions', 'Cases.dimensions#1~Object[]'],
+ ['invariant', 'concrete', 'Cases.concrete#1~Object'],
+ ['targetTyped', 'poly', 'Cases.poly#1~Collection'],
+ ['targetTypedSingleton', 'poly', 'Cases.poly#1~Collection'],
+ ['primitiveFactory', 'primitiveArray', 'Cases.primitiveArray#1~Object'],
+ ['inheritedPhase', 'inherited', 'Base.inherited#1'],
+ ['emptyIntermediate', 'inherited', 'Base.inherited#1'],
+ ['trailingDimension', 'dimension', 'Cases.dimension#1~Object'],
+ ['nestedPrivate', 'nest', 'Nest.nest#1~long'],
+ ['accessibleKnown', 'accessFiltered', 'AccessFilter.accessFiltered#1~Object'],
+ ['accessibleObject', 'accessFiltered', 'AccessFilter.accessFiltered#1~Object'],
+ ['declaredReceiver', 'declared', 'Base.declared#1'],
+ ])('%s selects %s by the earliest applicable JLS phase', (caller, method, signature) => {
+ expect(targets(result, caller, method)).toEqual([`Method:Cases.java:${signature}`]);
+ });
+
+ it.each([
+ ['nullAmbiguous', 'nullable', 'ambiguous'],
+ ['crossedAmbiguous', 'crossed', 'ambiguous'],
+ ['unknownArgument', 'opaque', 'unknown'],
+ ['knownInapplicable', 'forbidden', 'inapplicable'],
+ ['polyTie', 'polyAmbiguous', 'ambiguous'],
+ ['staticUnqualified', 'staticChoice', 'inapplicable'],
+ ['staticQualified', 'staticChoice', 'inapplicable'],
+ ])('%s preserves a terminal %s outcome', (caller, method, status) => {
+ expect(targets(result, caller, method)).toEqual([]);
+ expect(getResolutionOutcomes(result)).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ kind: 'suppressed',
+ name: method,
+ reason: status === 'ambiguous' ? 'member-lookup-ambiguous' : `overload-${status}`,
+ }),
+ ]),
+ );
+ });
+
+ it('fans out only the selected interface signature', () => {
+ expect(targets(result, 'virtualDispatch', 'dispatch')).toEqual([
+ 'Method:Cases.java:API.dispatch#1~long',
+ 'Method:Cases.java:Implementation.dispatch#1~long',
+ ]);
+ });
+
+ it.each(['inheritedByThis', 'inheritedImplicit'])(
+ 'uses the inherited field before the enclosing field for %s',
+ (caller) => {
+ const calls = targets(result, caller, 'shadowPick');
+ expect(calls).toHaveLength(1);
+ expect(result.graph.getNode(calls[0]!)?.properties.parameterTypes).toEqual(['String']);
+ },
+ );
+
+ it('uses the field in an enhanced-for iterable and the loop variable in its body', () => {
+ expect(targets(result, 'enhancedForScope', 'loopSource')).toEqual([
+ 'Method:Cases.java:ForScope.loopSource#1~Long',
+ ]);
+ expect(targets(result, 'enhancedForScope', 'loopBody')).toEqual([
+ 'Method:Cases.java:ForScope.loopBody#1~String',
+ ]);
+ });
+
+ it('preserves generic dispatch filtering and matches the grounded signature across implementation overloads', () => {
+ expect(targets(result, 'genericCall', 'genericDispatch')).toEqual([
+ 'Method:Cases.java:Generic.genericDispatch#1',
+ 'Method:Cases.java:StringGeneric.genericDispatch#1~String',
+ ]);
+ expect(targets(result, 'genericAny', 'genericDispatch')).toEqual([
+ 'Method:Cases.java:Generic.genericDispatch#1',
+ 'Method:Cases.java:IntegerGeneric.genericDispatch#1',
+ 'Method:Cases.java:StringGeneric.genericDispatch#1~String',
+ ]);
+ });
+
+ it('selects the String varargs declaration even when legacy variadic graph IDs coincide', () => {
+ const lines = readFileSync(path.join(root, 'Cases.java'), 'utf8').split('\n');
+ const line = lines.findIndex((text) => text.includes('void empty(String...'));
+ const column = lines[line]!.indexOf('void empty(String...');
+ expect(getResolutionOutcomes(result)).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ kind: 'resolved',
+ name: 'empty',
+ targetId: `def:Cases.java#${line + 1}:${column}:Method:empty`,
+ }),
+ ]),
+ );
+ });
+
+ it('treats interface methods as public across package boundaries', () => {
+ expect(targets(result, 'acrossPackages', 'select')).toEqual([
+ 'Method:api/Service.java:Service.select#1~long',
+ ]);
+ });
+
+ it('resolves a source type named Long by identity without assigning wrapper semantics', () => {
+ expect(targets(result, 'shadowedBox', 'scalar')).toEqual([
+ 'Method:shadow/Shadow.java:Shadow.scalar#1~Long',
+ ]);
+ });
+
+ it.each([
+ ['staticTypeImport', 'Number'],
+ ['staticSourceType', 'Long'],
+ ])(
+ 'resolves %s with the statically imported source type distinct from java.lang.Long',
+ (caller, parameter) => {
+ expect(targets(result, caller, 'select')).toEqual([
+ `Method:client/StaticType.java:StaticType.select#1~${parameter}`,
+ ]);
+ },
+ );
+
+ it('does not re-inherit a package member after an intermediate class in another package', () => {
+ expect(targets(result, 'interruptedPackage', 'access')).toEqual([
+ 'Method:first/Child.java:Child.access#1',
+ ]);
+ expect(targets(result, 'castBase', 'access')).toEqual(['Method:first/Base.java:Base.access#1']);
+ });
+
+ it('permits a protected superclass call from a subclass in another package', () => {
+ expect(targets(result, 'superProtected', 'protectedChoice')).toEqual([
+ 'Method:first/Base.java:Base.protectedChoice#1~long',
+ ]);
+ });
+
+ it.each([
+ ['fakeFactory', 'select'],
+ ['fieldFactory', 'select'],
+ ['typeParameter', 'generic'],
+ ['inheritedUnknown', 'select'],
+ ['implicitObject', 'wait'],
+ ['missingStaticType', 'select'],
+ ['conflictingStaticTypes', 'select'],
+ ['equivalentPreferred', 'preferred'],
+ ])('records uncertainty for %s without a guessed overload edge', (caller, method) => {
+ expect(targets(result, caller, method)).toEqual([]);
+ expect(
+ getResolutionOutcomes(result).some(
+ (outcome) =>
+ outcome.kind === 'suppressed' &&
+ outcome.name === method &&
+ outcome.reason === 'overload-unknown' &&
+ outcome.callerId?.includes(`.${caller}#`),
+ ),
+ ).toBe(true);
+ });
+
+ it('reports unmodeled method-call arguments even when javac can select an overload', () => {
+ expect(targets(result, 'inaccessibleAlternate', 'accessFiltered')).toEqual([]);
+ expect(getResolutionOutcomes(result)).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ kind: 'suppressed',
+ name: 'accessFiltered',
+ reason: 'overload-unknown',
+ callerId: expect.stringContaining('.inaccessibleAlternate#'),
+ detail: 'Unsupported Java argument, generic constraint, or subtype evidence',
+ }),
+ ]),
+ );
+ });
+
+ it.each(['chain', 'assignedChain', 'nestedChain'])(
+ 'uses the selected compile-time return for %s',
+ (caller) => {
+ expect(targets(result, caller, 'make')).toEqual(['Method:Cases.java:Cases.make#1~long']);
+ expect(targets(result, caller, 'run')).toEqual(['Method:Cases.java:A.run#0']);
+ },
+ );
+
+ it.each(['unknownAssignment', 'ambiguousAssignment'])(
+ 'does not infer an assignment receiver before selection for %s',
+ (caller) => {
+ expect(targets(result, caller, 'uncertain')).toEqual([]);
+ expect(targets(result, caller, 'run')).toEqual([]);
+ },
+ );
+
+ it('uses the selected interface declaration return despite a covariant implementation return', () => {
+ expect(targets(result, 'interfaceReturn', 'run')).toContain('Method:Cases.java:A.run#0');
+ expect(targets(result, 'interfaceReturn', 'run')).not.toContain('Method:Cases.java:B.run#0');
+ });
+});
diff --git a/gitnexus/test/integration/resolvers/java.test.ts b/gitnexus/test/integration/resolvers/java.test.ts
index 1c8720877..c174baf7f 100644
--- a/gitnexus/test/integration/resolvers/java.test.ts
+++ b/gitnexus/test/integration/resolvers/java.test.ts
@@ -658,13 +658,11 @@ describe('Java variadic call resolution', () => {
expect(fmtCall!.targetFilePath).toBe('com/example/util/Formatter.java');
});
- it('0-arg call to format(int, String...) still resolves in legacy mode (arity rejection is registry-only)', () => {
- // When `requiredParameterCount = 1`, `javaArityCompatibility` returns
- // 'incompatible' for 0-arg calls, which would prevent the CALLS edge.
- // This test documents the current resolution behavior for this shape.
+ it('rejects a 0-arg call to format(int, String...) with a required prefix', () => {
+ // JLS 15.12.2.4 still requires every fixed-prefix parameter.
const calls = getRelationships(result, 'CALLS');
const zeroArgFmtCall = calls.find((c) => c.target === 'format' && c.source === 'badCall');
- expect(zeroArgFmtCall).toBeDefined();
+ expect(zeroArgFmtCall).toBeUndefined();
});
});
@@ -2905,22 +2903,12 @@ describe('Java cast receiver resolution', () => {
);
});
- // ((String) obj).act(): `String` is a resolvable-SHAPE cast type (simple
- // identifier) that is not locally indexed, so resolution deliberately falls
- // back to obj's OWN declared type (Fallback). This is intentionally kept,
- // unlike the unparseable-cast case (#2353 review F1), whose criterion is:
- // a paren group that is type-shaped but UNPARSEABLE (generic / array / FQN)
- // must resolve to nothing, because falling through to the pre-cast
- // expression's declared type emits a confident wrong edge. Here the cast IS
- // parseable — it just names a type we didn't index — so no better
- // information exists, and upcast casts make the declared type a plausible
- // dispatch target. Residual risk kept visible: a cross-cast to an unindexed
- // sibling type would still emit this declared-type fallback edge.
- it('falls back to the declared type for a cast to an unindexed simple type (String)', () => {
+ // The cast's type governs member lookup. String has no act() method;
+ // falling back to the pre-cast Fallback type invents a call javac rejects.
+ it('does not fall back to the pre-cast type for an unindexed String receiver', () => {
const calls = getRelationships(result, 'CALLS');
const actCall = calls.find((c) => c.target === 'act' && c.source === 'castUnindexedType');
- expect(actCall).toBeDefined();
- expect(actCall!.targetFilePath).toBe('models/Fallback.java');
+ expect(actCall).toBeUndefined();
});
});
@@ -3100,16 +3088,9 @@ describe('Java bare-this dispatch (Case 4 pinning)', () => {
expect(getNodesByLabel(result, 'Interface')).toEqual(['Task']);
});
- // Characterization, not idealization: `this.greet("world")` (arity 1)
- // inside Derived binds to Derived.greet(String, int) — Case 4's
- // `pickFirstNonStaticOnly` short-circuits on a single-overload owner
- // without arity narrowing, so the inherited Base.greet(String) never gets
- // a look. The ideal Base.greet target is the deferred Case 4 arity item
- // (docs/plans/2026-07-02-001 § Deferred). Under PR #2353's Case 0.5, the
- // `hiddenByName` C++ name-hiding rule dropped this member site entirely
- // and a free-call fallback edge (reason 'local-call') to the same target
- // masked the drop.
- it('pins this.greet("world") in Derived to Derived.greet(String,int) — arity-blind shortcut', () => {
+ // The inherited one-parameter overload participates alongside the local
+ // two-parameter declaration (JLS 15.12.2); javac selects Base.greet(String).
+ it('resolves this.greet("world") to the inherited Base.greet(String)', () => {
const calls = getRelationships(result, 'CALLS');
const greetEdges = [
...new Set(
@@ -3118,7 +3099,7 @@ describe('Java bare-this dispatch (Case 4 pinning)', () => {
.map((c) => `${c.targetFilePath} reason=${c.rel.reason}`),
),
].sort();
- expect(greetEdges).toEqual(['models/Derived.java reason=global']);
+ expect(greetEdges).toEqual(['models/Base.java reason=import-resolved']);
});
// Characterization, not idealization: with field `size` AND method
diff --git a/gitnexus/test/unit/callable-id-lockstep.test.ts b/gitnexus/test/unit/callable-id-lockstep.test.ts
index 5555286e6..37d286af0 100644
--- a/gitnexus/test/unit/callable-id-lockstep.test.ts
+++ b/gitnexus/test/unit/callable-id-lockstep.test.ts
@@ -69,6 +69,26 @@ const semanticDef = (
});
describe('boundCallableStartRow - #2735 semantic position join', () => {
+ it('reads unrelated definition names only while building the per-file index', () => {
+ let reads = 0;
+ const defs = Array.from({ length: 128 }, (_, i) => {
+ const def = semanticDef('src/file.ext', 'Method', `Owner.method${i}`, i + 1, 0);
+ const qualifiedName = def.qualifiedName;
+ Object.defineProperty(def, 'qualifiedName', {
+ get() {
+ reads++;
+ return qualifiedName;
+ },
+ });
+ return def;
+ });
+ for (let i = 0; i < defs.length; i++) {
+ const wrapper = stubNode({ type: 'binding_wrapper', id: i, row: i, endColumn: 20 });
+ expect(boundCallableStartRow(wrapper, `method${i}`, 'Method', defs)).toBe(i);
+ }
+ expect(reads).toBeLessThanOrEqual(defs.length * 2);
+ });
+
it('keeps the wrapper row when no semantic definition is available', () => {
const wrapper = stubNode({
type: 'binding_wrapper',
diff --git a/gitnexus/test/unit/java-export-detection.test.ts b/gitnexus/test/unit/java-export-detection.test.ts
new file mode 100644
index 000000000..ac04e8e87
--- /dev/null
+++ b/gitnexus/test/unit/java-export-detection.test.ts
@@ -0,0 +1,29 @@
+import { describe, expect, it, vi } from 'vitest';
+import { javaExportChecker } from '../../src/core/ingestion/export-detection.js';
+import { getJavaParser } from '../../src/core/ingestion/languages/java/query.js';
+
+describe('Java export detection', () => {
+ it.each([
+ ['public class A { void run() {} }', true],
+ ['class A { public void run() {} }', true],
+ ['class A { /* comment */ @Deprecated public void run() {} }', true],
+ ['class A { private void run() {} }', false],
+ ['class A { void run() {} }', false],
+ ])('retains modifier and ancestor visibility: %s', (source, exported) => {
+ const tree = getJavaParser().parse(source);
+ const method = tree.rootNode.descendantsOfType('method_declaration')[0]!;
+ expect(javaExportChecker(method.childForFieldName('name')!, 'run')).toBe(exported);
+ });
+
+ it('does not scan sibling methods while checking a declaration', () => {
+ const tree = getJavaParser().parse(
+ `class Wide { ${Array.from({ length: 128 }, (_, i) => `void method${i}() {}`).join('\n')} }`,
+ );
+ const body = tree.rootNode.namedChildren[0]!.childForFieldName('body')!;
+ const name = body.namedChildren[0]!.childForFieldName('name')!;
+ const child = vi.spyOn(body, 'child');
+ const namedChild = vi.spyOn(body, 'namedChild');
+ expect(javaExportChecker(name, 'method0')).toBe(false);
+ expect(child.mock.calls.length + namedChild.mock.calls.length).toBeLessThanOrEqual(1);
+ });
+});
diff --git a/gitnexus/test/unit/mcp-response-filter.test.ts b/gitnexus/test/unit/mcp-response-filter.test.ts
index 75d1ce641..cc34ad23d 100644
--- a/gitnexus/test/unit/mcp-response-filter.test.ts
+++ b/gitnexus/test/unit/mcp-response-filter.test.ts
@@ -19,7 +19,7 @@ const context = {
typed_properties: [{ uid: 'Property:src/pay.ts:currency' }],
epistemic: 'lower-bound',
boundaries: ['Dynamic dispatch is unresolved'],
- causes: { callableValueReferences: 1 },
+ causes: { callableValueReferences: 1, overloadResolution: 2 },
warning: 'Source extraction incomplete',
partial: true,
truncated: true,
diff --git a/gitnexus/test/unit/scope-resolution/callable-value-target-index.test.ts b/gitnexus/test/unit/scope-resolution/callable-value-target-index.test.ts
index 6f301fe70..98512086e 100644
--- a/gitnexus/test/unit/scope-resolution/callable-value-target-index.test.ts
+++ b/gitnexus/test/unit/scope-resolution/callable-value-target-index.test.ts
@@ -64,6 +64,34 @@ const targetsFor = (nodes: readonly StubNode[], defs: readonly ReturnType {
+ it.each([128, 512])(
+ 'rejects %i unrelated value names without reading definition positions',
+ (count) => {
+ let positionReads = 0;
+ const definitions = Array.from({ length: count }, (_, index) => {
+ const definition = def('Variable', 'a.ts', `config${index}`, index + 1);
+ return {
+ ...definition,
+ get nodeId() {
+ positionReads++;
+ return definition.nodeId;
+ },
+ };
+ });
+ const scopes = scopesOf(definitions);
+ positionReads = 0;
+ expect(
+ buildGraphTargetIndex(
+ scopes,
+ new Map() as GraphNodeLookup,
+ undefined,
+ graphOf([node('Function', 'a.ts', 'handler', 3)]),
+ ).size,
+ ).toBe(0);
+ expect(positionReads).toBe(0);
+ },
+ );
+
it('admits a value binding whose callable node sits at its own line', () => {
// The #2687 closure-binding shape: the value def and the Function node are
// the same construct, so they share file, line and name.
@@ -80,6 +108,21 @@ describe('buildGraphTargetIndex — value bindings join by position', () => {
).toEqual([]);
});
+ it('rejects a same-name callable at the same line in another file', () => {
+ expect(
+ targetsFor(
+ [node('Function', 'other.ts', 'handler', 3)],
+ [def('Variable', 'a.ts', 'handler', 3)],
+ ),
+ ).toEqual([]);
+ });
+
+ it('normalises the value binding sigil before looking for callable names', () => {
+ expect(
+ targetsFor([node('Function', 'a.php', 'save', 5)], [def('Variable', 'a.php', '$save', 5)]),
+ ).toEqual(['def:a.php#5:0:Variable:$save => Function:a.php:save']);
+ });
+
it('REJECTS a value binding whose node at that position is NOT callable', () => {
expect(
targetsFor([node('Const', 'a.ts', 'CONFIG', 4)], [def('Const', 'a.ts', 'CONFIG', 4)]),
diff --git a/gitnexus/test/unit/scope-resolution/java/java-overload-argument-facts.test.ts b/gitnexus/test/unit/scope-resolution/java/java-overload-argument-facts.test.ts
new file mode 100644
index 000000000..faad45eb0
--- /dev/null
+++ b/gitnexus/test/unit/scope-resolution/java/java-overload-argument-facts.test.ts
@@ -0,0 +1,380 @@
+import { afterEach, describe, expect, it } from 'vitest';
+import { getJavaParser } from '../../../../src/core/ingestion/languages/java/query.js';
+import {
+ captureJavaOverloadFacts,
+ clearJavaOverloadFacts,
+ getJavaOverloadCall,
+ getJavaOverloadFacts,
+ isJavaOverloadFileFacts,
+ setJavaOverloadFacts,
+} from '../../../../src/core/ingestion/languages/java/overload-argument-facts.js';
+
+function capture(source: string) {
+ const tree = getJavaParser().parse(source);
+ return captureJavaOverloadFacts(tree.rootNode)!;
+}
+afterEach(clearJavaOverloadFacts);
+
+describe('Java lossless overload argument facts', () => {
+ it('does not compare bindings from unrelated methods with the same parameter name', () => {
+ const size = 128;
+ const tree = getJavaParser().parse(
+ `class Wide { ${Array.from(
+ { length: size },
+ (_, i) => `void method${i}(String value) { use(value); }`,
+ ).join('\n')} }`,
+ );
+ const owner = tree.rootNode.namedChildren[0]!;
+ const id = owner.id;
+ let reads = 0;
+ Object.defineProperty(owner, 'id', {
+ get() {
+ reads++;
+ return id;
+ },
+ });
+ const facts = captureJavaOverloadFacts(tree.rootNode)!;
+ expect(facts.calls.map((call) => call.arguments[0])).toEqual(
+ Array.from({ length: size }, () =>
+ expect.objectContaining({ kind: 'typed', spelling: 'String' }),
+ ),
+ );
+ expect(reads).toBeLessThanOrEqual(size * 16);
+ });
+
+ it('materializes a wide class body once during traversal', () => {
+ const source = `class Wide { ${Array.from(
+ { length: 128 },
+ (_, i) => `void method${i}() { use("text"); }`,
+ ).join('\n')} }`;
+ const tree = getJavaParser().parse(source);
+ const body = tree.rootNode.namedChildren[0]!.childForFieldName('body')!;
+ const children = body.namedChildren;
+ let reads = 0;
+ Object.defineProperty(body, 'namedChildren', {
+ get() {
+ reads++;
+ return children;
+ },
+ });
+ const facts = captureJavaOverloadFacts(tree.rootNode)!;
+ expect(facts.methods).toHaveLength(128);
+ expect(facts.calls).toHaveLength(128);
+ expect(reads).toBe(1);
+ });
+
+ it('keeps formal parameters in their own methods and respects field/block shadowing', () => {
+ const facts = capture(`class A {
+ Long value;
+ void first(String value) { use(value); }
+ void second(Long value) { use(value); }
+ void blocks() { { String value = "x"; use(value); } use(value); }
+ }`);
+ expect(
+ facts.calls.filter((call) => call.name === 'use').map((call) => call.arguments[0]),
+ ).toMatchObject([
+ { kind: 'typed', spelling: 'String' },
+ { kind: 'typed', spelling: 'Long' },
+ { kind: 'typed', spelling: 'String' },
+ { kind: 'typed', spelling: 'Long' },
+ ]);
+ });
+
+ it('does not let a later local declaration hide the field at an earlier call', () => {
+ const facts = capture(
+ 'class A { Long value; void run() { use(value); String value = "x"; use(value); } }',
+ );
+ expect(facts.calls.map((call) => call.arguments[0])).toMatchObject([
+ { kind: 'typed', spelling: 'Long' },
+ { kind: 'typed', spelling: 'String' },
+ ]);
+ });
+
+ it('uses an explicit this field declaration despite a local with the same name', () => {
+ const facts = capture(
+ 'class A { Long value; void run(String value) { use(value); use(this.value); } }',
+ );
+ expect(facts.calls.map((call) => call.arguments[0])).toMatchObject([
+ { kind: 'typed', spelling: 'String' },
+ { kind: 'typed', spelling: 'Long' },
+ ]);
+ });
+
+ it('looks up inherited fields before enclosing fields and retains declaration context', () => {
+ const facts = capture(`class Base { String value; }
+ class Outer { Long value; class Inner extends Base {
+ void run(Long value) { use(this.value); use(value); }
+ void implicit() { use(value); }
+ } }`);
+ const arguments_ = facts.calls
+ .filter((call) => call.name === 'use')
+ .map((call) => call.arguments[0]);
+ expect(arguments_).toMatchObject([
+ { kind: 'typed', spelling: 'String', at: { startLine: 1 } },
+ { kind: 'typed', spelling: 'Long', at: { startLine: 3 } },
+ { kind: 'typed', spelling: 'String', at: { startLine: 1 } },
+ ]);
+ });
+
+ it('keeps enhanced-for bindings out of the iterable expression', () => {
+ const facts = capture(
+ 'class A { Long item; void run() { for (String item : source(item)) use(item); } }',
+ );
+ expect(facts.calls.map((call) => call.arguments[0])).toMatchObject([
+ { kind: 'typed', spelling: 'Long' },
+ { kind: 'typed', spelling: 'String' },
+ ]);
+ });
+
+ it.each([
+ ['unknown superclass', '', 'extends External'],
+ ['generic field substitution', 'class Base { T value; }', 'extends Base'],
+ [
+ 'ambiguous interface fields',
+ 'interface Left { String value = "x"; } interface Right { Long value = 1L; }',
+ 'implements Left, Right',
+ ],
+ ])('does not infer an outer field across %s', (_description, declarations, heritage) => {
+ const facts = capture(`${declarations} class Outer { Long value; class Inner ${heritage} {
+ void run() { use(value); use(this.value); }
+ } }`);
+ expect(
+ facts.calls.filter((call) => call.name === 'use').map((call) => call.arguments[0]),
+ ).toEqual([{ kind: 'unknown' }, { kind: 'unknown' }]);
+ });
+
+ it('does not inherit private fields or use an outer field for explicit this', () => {
+ const facts = capture(
+ 'class Base { private String value; } class Outer { Long value; class Inner extends Base { void run() { use(value); use(this.value); } } }',
+ );
+ expect(facts.calls.map((call) => call.arguments[0])).toMatchObject([
+ { kind: 'typed', spelling: 'Long' },
+ { kind: 'unknown' },
+ ]);
+ });
+
+ it('resolves an enclosing field through a deep empty inheritance diamond', () => {
+ const declarations = ['interface A0 {}', 'interface B0 {}'];
+ // Without memoization this small hierarchy expands into millions of paths.
+ for (let depth = 1; depth <= 24; depth++) {
+ declarations.push(
+ `interface A${depth} extends A${depth - 1}, B${depth - 1} {}`,
+ `interface B${depth} extends A${depth - 1}, B${depth - 1} {}`,
+ );
+ }
+ const facts = capture(`${declarations.join('\n')}
+ class Outer { Long value; class Inner implements A24, B24 {
+ void run() { use(value); use(value); }
+ } }`);
+ expect(facts.calls.map((call) => call.arguments[0])).toMatchObject([
+ { kind: 'typed', spelling: 'Long' },
+ { kind: 'typed', spelling: 'Long' },
+ ]);
+ });
+
+ it('does not reuse a shallow field proof beyond the inheritance depth bound', () => {
+ const declarations = ['interface A0 {}'];
+ for (let depth = 1; depth <= 63; depth++)
+ declarations.push(`interface A${depth} extends A${depth - 1} {}`);
+ const facts = capture(`${declarations.join('\n')}
+ class Outer { Long value; class Inner implements A0, A63 {
+ void run() { use(value); use(value); }
+ } }`);
+ expect(facts.calls.map((call) => call.arguments[0])).toEqual([
+ { kind: 'unknown' },
+ { kind: 'unknown' },
+ ]);
+ });
+
+ it('keeps cyclic field inheritance unknown across repeated lookups', () => {
+ const facts = capture(`interface Left extends Right {} interface Right extends Left {}
+ class Outer { Long value; class Inner implements Left, Right {
+ void run() { use(value); use(value); }
+ } }`);
+ expect(facts.calls.map((call) => call.arguments[0])).toEqual([
+ { kind: 'unknown' },
+ { kind: 'unknown' },
+ ]);
+ });
+
+ it('retains numeric suffixes, declared containers, dimensions, casts, and null separately', () => {
+ const facts = capture(
+ 'class A { void run(java.util.List ids, int[][] grid) { use(1L, 1f, ids, grid, (String)null, null); } }',
+ );
+ expect(facts.calls[0]!.arguments).toMatchObject([
+ { kind: 'typed', spelling: 'long' },
+ { kind: 'typed', spelling: 'float' },
+ { kind: 'typed', spelling: 'java.util.List' },
+ { kind: 'typed', spelling: 'int[][]' },
+ { kind: 'typed', spelling: 'String' },
+ { kind: 'null' },
+ ]);
+ });
+
+ it('retains qualified generic formal types and array dimensions in candidate declarations', () => {
+ const facts = capture(
+ 'class A { void use(java.util.Collection ids, String[] names, int value[], Long... rest) {} }',
+ );
+ expect(facts.methods[0]!.parameterTypes).toEqual([
+ 'java.util.Collection',
+ 'String[]',
+ 'int[]',
+ 'Long',
+ 'varargs',
+ ]);
+ });
+
+ it('preserves trailing dimensions on method and explicitly typed lambda formals', () => {
+ const facts = capture(
+ 'class A { void run(String value[]) { use(value); java.util.function.Consumer lambda = (String item[]) -> use(item); } }',
+ );
+ expect(
+ facts.calls.filter((call) => call.name === 'use').map((call) => call.arguments[0]),
+ ).toMatchObject([
+ { kind: 'typed', spelling: 'String[]' },
+ { kind: 'typed', spelling: 'String[]' },
+ ]);
+ });
+
+ it('retains poly factories and explicit type arguments rather than freezing List', () => {
+ const facts = capture(
+ 'import java.util.Arrays; class A { void run(Long id) { use(Arrays.asList(id, 1L)); } }',
+ );
+ expect(facts.calls.find((call) => call.name === 'use')!.arguments[0]).toMatchObject({
+ kind: 'factory',
+ owner: 'Arrays',
+ name: 'asList',
+ typeArguments: ['Number'],
+ shadowed: false,
+ arguments: [
+ { kind: 'typed', spelling: 'Long' },
+ { kind: 'typed', spelling: 'long' },
+ ],
+ });
+ });
+
+ it('records value shadowing of a factory qualifier and type-parameter declarations', () => {
+ const facts = capture(
+ 'class A { void run(Object Arrays) { use(Arrays.asList(1L)); } }',
+ );
+ expect(facts.types[0]!.typeParameters).toEqual(['Long']);
+ expect(facts.methods[0]!.typeParameters).toEqual(['String']);
+ expect(facts.calls.find((call) => call.name === 'use')!.arguments[0]).toMatchObject({
+ kind: 'factory',
+ shadowed: true,
+ });
+ });
+
+ it('keeps same-start chained call sites distinct through a plain-data round trip', () => {
+ const facts = capture('class A { void run(Long id) { lookup.find(id).get(id); } }');
+ const restored: unknown = JSON.parse(JSON.stringify(facts));
+ expect(isJavaOverloadFileFacts(restored)).toBe(true);
+ setJavaOverloadFacts('A.java', restored as typeof facts);
+ for (const call of facts.calls)
+ expect(getJavaOverloadCall('A.java', call.range, call.name)).toEqual(call);
+ setJavaOverloadFacts('A.java', undefined);
+ expect(getJavaOverloadFacts('A.java')).toBeUndefined();
+ expect(
+ getJavaOverloadCall('A.java', facts.calls[0]!.range, facts.calls[0]!.name),
+ ).toBeUndefined();
+ });
+
+ it('assigns enum constant methods to their synthetic owners and retains the host base', () => {
+ const facts = capture(`class Host { enum Mode {
+ FIRST { void hook() { helper(); } }, SECOND { void hook() {} }, PLAIN;
+ void helper() {}
+ } }`);
+ expect(facts.types.map(({ name, simpleName, bases }) => ({ name, simpleName, bases }))).toEqual(
+ [
+ { name: 'Host', simpleName: 'Host', bases: [] },
+ { name: 'Mode', simpleName: 'Mode', bases: [] },
+ { name: 'Host$Mode$1', simpleName: 'Host$Mode$1', bases: ['Mode'] },
+ { name: 'Host$Mode$2', simpleName: 'Host$Mode$2', bases: ['Mode'] },
+ ],
+ );
+ expect(facts.methods.map(({ owner, name }) => `${owner}.${name}`)).toEqual([
+ 'Host$Mode$1.hook',
+ 'Host$Mode$2.hook',
+ 'Mode.helper',
+ ]);
+ });
+
+ it('captures implicit record accessors without duplicating explicit zero-argument accessors', () => {
+ const facts = capture(`class Host { void run() {
+ record Data(String name, int count, String... tags) {
+ public String name() { return name; }
+ int count(int factor) { return count * factor; }
+ }
+ } }`);
+ const accessors = facts.methods.filter((method) => method.owner === 'Host$1Data');
+ expect(
+ accessors.map(({ name, parameterTypes, modifiers }) => ({
+ name,
+ parameterTypes,
+ modifiers,
+ })),
+ ).toEqual([
+ { name: 'count', parameterTypes: [], modifiers: ['public'] },
+ { name: 'tags', parameterTypes: [], modifiers: ['public'] },
+ { name: 'name', parameterTypes: [], modifiers: ['public'] },
+ { name: 'count', parameterTypes: ['int'], modifiers: [] },
+ ]);
+ expect(isJavaOverloadFileFacts(JSON.parse(JSON.stringify(facts)))).toBe(true);
+ });
+
+ it('does not synthesize accessor facts for malformed record component names', () => {
+ const facts = capture('record Missing(int x, y) {} record Unnamed(int _) {}');
+ expect(facts.methods.map(({ owner, name }) => `${owner}.${name}`)).toEqual(['Missing.x']);
+ });
+
+ it('retains type visibility and gives only direct interface members implicit public access', () => {
+ const facts = capture(`public class Outer {
+ private static class Hidden {}
+ protected class Protected {}
+ class PackageMember {}
+ }
+ interface API {
+ class Member {}
+ enum Mode { VALUE }
+ default void run() { class Local {} }
+ }
+ @interface Annotation { class Member {} }`);
+ expect(facts.types.map(({ name, modifiers }) => ({ name, modifiers }))).toEqual([
+ { name: 'Outer', modifiers: ['public'] },
+ { name: 'Hidden', modifiers: ['private', 'static'] },
+ { name: 'Protected', modifiers: ['protected'] },
+ { name: 'PackageMember', modifiers: [] },
+ { name: 'API', modifiers: [] },
+ { name: 'Member', modifiers: ['public'] },
+ { name: 'Mode', modifiers: ['public'] },
+ { name: 'API$1Local', modifiers: [] },
+ { name: 'Annotation', modifiers: [] },
+ { name: 'Member', modifiers: ['public'] },
+ ]);
+ });
+
+ it('validates optional persisted type modifiers without rejecting older facts', () => {
+ const facts = capture('public class A {}');
+ const { modifiers: _modifiers, ...olderType } = facts.types[0]!;
+ expect(isJavaOverloadFileFacts({ ...facts, types: [olderType] })).toBe(true);
+ expect(isJavaOverloadFileFacts(JSON.parse(JSON.stringify(facts)))).toBe(true);
+ expect(isJavaOverloadFileFacts({ ...facts, types: [{ ...olderType, modifiers: null }] })).toBe(
+ false,
+ );
+ expect(isJavaOverloadFileFacts({ ...facts, types: [{ ...olderType, modifiers: [1] }] })).toBe(
+ false,
+ );
+ });
+
+ it('rejects malformed persisted facts and retains empty types for inherited inventories', () => {
+ expect(capture('class Empty {}').types).toMatchObject([{ name: 'Empty', bases: [] }]);
+ const facts = capture('class A { void run() { use(null); } }');
+ expect(
+ isJavaOverloadFileFacts({
+ ...facts,
+ calls: [{ ...facts.calls[0], arguments: [{ kind: 'typed', spelling: 1 }] }],
+ }),
+ ).toBe(false);
+ expect(isJavaOverloadFileFacts({ ...facts, methods: [null] })).toBe(false);
+ });
+});
diff --git a/gitnexus/test/unit/scope-resolution/java/java-overload-assignment-capture.test.ts b/gitnexus/test/unit/scope-resolution/java/java-overload-assignment-capture.test.ts
new file mode 100644
index 000000000..27915b068
--- /dev/null
+++ b/gitnexus/test/unit/scope-resolution/java/java-overload-assignment-capture.test.ts
@@ -0,0 +1,45 @@
+import { describe, expect, it } from 'vitest';
+import { javaProvider } from '../../../../src/core/ingestion/languages/java.js';
+import { emitJavaScopeCaptures } from '../../../../src/core/ingestion/languages/java/captures.js';
+import { extract } from '../../../../src/core/ingestion/scope-extractor.js';
+
+describe('Java inferred call-result assignment captures', () => {
+ it('keeps overload return types on their own declarations with complete written syntax', () => {
+ const source = `class Case {
+ java.util.List[] make(Integer value) { return null; }
+ p.Result make(long value)[] { return null; }
+ }`;
+ const parsed = extract(emitJavaScopeCaptures(source, 'Case.java'), 'Case.java', javaProvider);
+ expect(
+ parsed.localDefs
+ .filter((definition) => definition.type === 'Method')
+ .map((definition) => ({
+ parameters: definition.parameterTypes,
+ returnType: definition.returnType,
+ })),
+ ).toEqual([
+ { parameters: ['Integer'], returnType: 'java.util.List[]' },
+ { parameters: ['long'], returnType: 'p.Result[]' },
+ ]);
+ });
+ it('preserves the exact initializer call identity only for var declarations', () => {
+ const source = `class Case {
+ Object make(int value) { return null; }
+ void run(Case other) {
+ var first = make(1);
+ var wrapped = (make(2));
+ var member = other.make(3);
+ Object typed = make(4);
+ var constructed = new Case();
+ }
+ }`;
+ const parsed = extract(emitJavaScopeCaptures(source, 'Case.java'), 'Case.java', javaProvider);
+ const sites = parsed.callResultAssignmentSites ?? [];
+ expect(sites.map((site) => site.lhs)).toEqual(['first', 'wrapped', 'member']);
+ for (const site of sites) {
+ expect(parsed.referenceSites).toContainEqual(
+ expect.objectContaining({ name: 'make', atRange: site.callSite, inScope: site.inScope }),
+ );
+ }
+ });
+});
diff --git a/gitnexus/test/unit/scope-resolution/java/java-overload-resolution.test.ts b/gitnexus/test/unit/scope-resolution/java/java-overload-resolution.test.ts
new file mode 100644
index 000000000..8b6cb86f2
--- /dev/null
+++ b/gitnexus/test/unit/scope-resolution/java/java-overload-resolution.test.ts
@@ -0,0 +1,437 @@
+import { afterEach, describe, expect, it } from 'vitest';
+import {
+ buildDefIndex,
+ buildMethodDispatchIndex,
+ buildQualifiedNameIndex,
+ buildScopeTree,
+ type Range,
+ type Scope,
+ type SymbolDefinition,
+} from 'gitnexus-shared';
+import { createSemanticModel } from '../../../../src/core/ingestion/model/semantic-model.js';
+import type { ScopeResolutionIndexes } from '../../../../src/core/ingestion/model/scope-resolution-indexes.js';
+import {
+ captureJavaOverloadFacts,
+ clearJavaOverloadFacts,
+ setJavaOverloadFacts,
+} from '../../../../src/core/ingestion/languages/java/overload-argument-facts.js';
+import { resolveJavaReceiverMember } from '../../../../src/core/ingestion/languages/java/overload-resolution.js';
+import { getJavaParser } from '../../../../src/core/ingestion/languages/java/query.js';
+
+function resolveSynthetic(
+ signatures: readonly { count: number; types?: readonly string[] }[],
+ arity = 1,
+) {
+ const filePath = 'Synthetic.java';
+ const range: Range = { startLine: 1, startCol: 0, endLine: 10, endCol: 0 };
+ const atRange: Range = { startLine: 5, startCol: 0, endLine: 5, endCol: 12 };
+ const owner: SymbolDefinition = {
+ nodeId: 'Class:Synthetic.java:Pair',
+ filePath,
+ type: 'Class',
+ qualifiedName: 'Pair',
+ declarationRange: range,
+ };
+ const methods: SymbolDefinition[] = signatures.map((signature, index) => ({
+ nodeId: `Method:Synthetic.java:Pair.setValue#${index}`,
+ filePath,
+ type: 'Method',
+ ownerId: owner.nodeId,
+ qualifiedName: 'Pair.setValue',
+ parameterCount: signature.count,
+ parameterTypes: signature.types === undefined ? undefined : [...signature.types],
+ isSynthetic: true,
+ }));
+ const classScope: Scope = {
+ id: 'class',
+ kind: 'Class',
+ parent: 'module',
+ filePath,
+ range,
+ ownedDefs: [owner, ...methods],
+ imports: [],
+ bindings: new Map(),
+ typeBindings: new Map(),
+ };
+ const moduleScope: Scope = {
+ ...classScope,
+ id: 'module',
+ kind: 'Module',
+ parent: null,
+ ownedDefs: [],
+ };
+ const definitions = [owner, ...methods];
+ const scopes = {
+ scopeTree: buildScopeTree([moduleScope, classScope]),
+ defs: buildDefIndex(definitions),
+ qualifiedNames: buildQualifiedNameIndex(definitions),
+ methodDispatch: buildMethodDispatchIndex({
+ owners: [owner.nodeId],
+ computeMro: () => [],
+ implementsOf: () => [],
+ }),
+ } as ScopeResolutionIndexes;
+ const model = createSemanticModel();
+ for (const method of methods) model.methods.register(owner.nodeId, 'setValue', method);
+ setJavaOverloadFacts(filePath, {
+ version: 1,
+ packageName: '',
+ imports: [],
+ types: [
+ {
+ name: 'Pair',
+ simpleName: 'Pair',
+ kind: 'class_declaration',
+ range,
+ visible: range,
+ bases: [],
+ typeParameters: [],
+ },
+ ],
+ methods: [],
+ calls: [
+ {
+ name: 'setValue',
+ range: atRange,
+ arguments: Array.from({ length: arity }, () => ({
+ kind: 'typed',
+ spelling: 'int',
+ at: atRange,
+ })),
+ receiverIsValue: true,
+ staticContext: false,
+ },
+ ],
+ });
+ return resolveJavaReceiverMember(
+ owner,
+ 'setValue',
+ { inScope: classScope.id, kind: 'call', callForm: 'member', name: 'setValue', atRange, arity },
+ scopes,
+ model,
+ );
+}
+
+function hierarchyFixture(
+ declarations: readonly string[],
+ bases: readonly string[],
+ conversion?: {
+ argumentType: string;
+ parameterTypes: readonly [string, string];
+ },
+) {
+ const filePath = 'Hierarchy.java';
+ const parameterTypes = conversion?.parameterTypes ?? ['Long', 'String'];
+ const source = `${declarations.join('\n')}
+ class Owner ${bases.length === 0 ? '' : `implements ${bases.join(', ')}`} {
+ public void pick(${parameterTypes[0]} value) {}
+ public void pick(${parameterTypes[1]} value) {}
+ void run(${conversion === undefined ? '' : `${conversion.argumentType} value`}) {
+ pick(${conversion === undefined ? '1L' : 'value'});
+ }
+ }`;
+ const facts = captureJavaOverloadFacts(getJavaParser().parse(source).rootNode)!;
+ let baseReads = 0;
+ for (const type of facts.types) {
+ const declaredBases = type.bases;
+ Object.defineProperty(type, 'bases', {
+ get() {
+ baseReads++;
+ return declaredBases;
+ },
+ });
+ }
+ const owners: SymbolDefinition[] = facts.types.map((type) => ({
+ nodeId: `${type.kind === 'interface_declaration' ? 'Interface' : 'Class'}:${filePath}:${type.name}`,
+ filePath,
+ type: type.kind === 'interface_declaration' ? 'Interface' : 'Class',
+ qualifiedName: type.name,
+ declarationRange: type.range,
+ }));
+ const owner = owners.find((definition) => definition.qualifiedName === 'Owner')!;
+ const methods: SymbolDefinition[] = facts.methods
+ .filter((method) => method.name === 'pick')
+ .map((method, index) => ({
+ nodeId: `Method:${filePath}:Owner.pick#${index}`,
+ filePath,
+ type: 'Method',
+ ownerId: owner.nodeId,
+ qualifiedName: 'Owner.pick',
+ declarationRange: method.range,
+ parameterCount: method.parameterTypes.length,
+ parameterTypes: [...method.parameterTypes],
+ }));
+ const classScope: Scope = {
+ id: 'class',
+ kind: 'Class',
+ parent: 'module',
+ filePath,
+ range: owner.declarationRange!,
+ ownedDefs: [owner, ...methods],
+ imports: [],
+ bindings: new Map(),
+ typeBindings: new Map(),
+ };
+ const moduleScope: Scope = { ...classScope, id: 'module', kind: 'Module', parent: null };
+ const definitions = [...owners, ...methods];
+ const scopes = {
+ scopeTree: buildScopeTree([moduleScope, classScope]),
+ defs: buildDefIndex(definitions),
+ qualifiedNames: buildQualifiedNameIndex(definitions),
+ // Only Owner declares methods. The captured bases supply the independent
+ // hierarchy-completeness proof; no inherited-member lookup is needed.
+ methodDispatch: buildMethodDispatchIndex({
+ owners: owners.map((definition) => definition.nodeId),
+ computeMro: () => [],
+ implementsOf: () => [],
+ }),
+ } as ScopeResolutionIndexes;
+ const model = createSemanticModel();
+ for (const method of methods) model.methods.register(owner.nodeId, 'pick', method);
+ setJavaOverloadFacts(filePath, facts);
+ const call = facts.calls.find((site) => site.name === 'pick')!;
+ return {
+ baseReads: () => baseReads,
+ resolve: () =>
+ resolveJavaReceiverMember(
+ owner,
+ 'pick',
+ {
+ inScope: classScope.id,
+ kind: 'call',
+ callForm: 'member',
+ name: 'pick',
+ atRange: call.range,
+ arity: 1,
+ },
+ scopes,
+ model,
+ ),
+ };
+}
+
+afterEach(clearJavaOverloadFacts);
+
+describe('Java overload fact lookup work', () => {
+ it.each(['none', 'method', 'nested'])(
+ 'bounds declaration reads across independent families (generics=%s)',
+ (generics) => {
+ const size = 64;
+ const generic = generics !== 'none';
+ const filePath = 'Wide.java';
+ const source = `class Wide${generics === 'nested' ? '' : ''} { ${Array.from(
+ { length: size },
+ (_, i) => `
+ void pick${i}(Object value) {}
+ void pick${i}(String value) {}
+ ${generic ? '' : ''} void run${i}(${generic ? 'String value' : ''}) { pick${i}(${generic ? 'value' : '"text"'}); }
+ `,
+ ).join('\n')} }`;
+ const facts = captureJavaOverloadFacts(getJavaParser().parse(source).rootNode)!;
+ const owner: SymbolDefinition = {
+ nodeId: `Class:${filePath}:Wide`,
+ filePath,
+ type: 'Class',
+ qualifiedName: 'Wide',
+ declarationRange: facts.types[0]!.range,
+ };
+ const methods: SymbolDefinition[] = facts.methods
+ .filter((fact) => fact.name.startsWith('pick'))
+ .map((fact) => ({
+ nodeId: `Method:${filePath}:Wide.${fact.name}#1~${fact.parameterTypes[0]}`,
+ filePath,
+ type: 'Method',
+ ownerId: owner.nodeId,
+ qualifiedName: `Wide.${fact.name}`,
+ declarationRange: fact.range,
+ parameterCount: 1,
+ parameterTypes: [...fact.parameterTypes],
+ }));
+ const classScope: Scope = {
+ id: 'class',
+ kind: 'Class',
+ parent: 'module',
+ filePath,
+ range: owner.declarationRange!,
+ ownedDefs: [owner, ...methods],
+ imports: [],
+ bindings: new Map(),
+ typeBindings: new Map(),
+ };
+ const definitions = [owner, ...methods];
+ const scopes = {
+ scopeTree: buildScopeTree([
+ { ...classScope, id: 'module', kind: 'Module', parent: null },
+ classScope,
+ ]),
+ defs: buildDefIndex(definitions),
+ qualifiedNames: buildQualifiedNameIndex(definitions),
+ methodDispatch: buildMethodDispatchIndex({
+ owners: [owner.nodeId],
+ computeMro: () => [],
+ implementsOf: () => [],
+ }),
+ } as ScopeResolutionIndexes;
+ const model = createSemanticModel();
+ for (const method of methods)
+ model.methods.register(owner.nodeId, method.qualifiedName!.split('.')[1]!, method);
+ setJavaOverloadFacts(filePath, facts);
+ let reads = 0;
+ for (const fact of facts.methods)
+ for (const key of ['name', 'range', 'typeParameters'] as const) {
+ const value = fact[key];
+ Object.defineProperty(fact, key, {
+ get() {
+ reads++;
+ return value;
+ },
+ });
+ }
+ for (const call of facts.calls) {
+ expect(
+ resolveJavaReceiverMember(
+ owner,
+ call.name,
+ {
+ inScope: 'class',
+ kind: 'call',
+ callForm: 'member',
+ name: call.name,
+ atRange: call.range,
+ arity: 1,
+ },
+ scopes,
+ model,
+ ),
+ ).toMatchObject(
+ generic
+ ? { kind: 'unknown' }
+ : { kind: 'resolved', definition: { parameterTypes: ['String'] } },
+ );
+ }
+ expect(reads).toBeLessThanOrEqual(facts.methods.length * 20);
+ },
+ );
+});
+
+describe('Java overload families with synthetic signature metadata', () => {
+ it.each([
+ { label: 'missing', types: undefined },
+ { label: 'empty', types: [] },
+ { label: 'inconsistent', types: ['int', 'int'] },
+ ])(
+ 'retains singleton compatibility with $label types for one declared parameter',
+ ({ types }) => {
+ expect(resolveSynthetic([{ count: 1, types }])).toBeUndefined();
+ },
+ );
+
+ it('keeps an overloaded family unknown when one formal signature is unavailable', () => {
+ expect(resolveSynthetic([{ count: 1 }, { count: 1, types: ['int'] }])).toMatchObject({
+ kind: 'unknown',
+ candidateSetComplete: true,
+ });
+ });
+
+ it('still rejects a call whose arity differs from the known declaration arity', () => {
+ expect(resolveSynthetic([{ count: 1 }], 0)).toMatchObject({ kind: 'inapplicable' });
+ });
+});
+
+describe('Java inherited overload inventory bounds', () => {
+ it.each([12, 16])(
+ 'checks a depth-%i diamond without expanding every inheritance path',
+ (depth) => {
+ const declarations = ['interface A0 {}', 'interface B0 {}'];
+ for (let level = 1; level <= depth; level++) {
+ declarations.push(
+ `interface A${level} extends A${level - 1}, B${level - 1} {}`,
+ `interface B${level} extends A${level - 1}, B${level - 1} {}`,
+ );
+ }
+ const fixture = hierarchyFixture(declarations, [`A${depth}`, `B${depth}`]);
+ expect(fixture.resolve()).toMatchObject({
+ kind: 'resolved',
+ definition: { parameterTypes: ['Long'] },
+ });
+ expect(fixture.baseReads()).toBeLessThanOrEqual((declarations.length + 1) * 2);
+ },
+ );
+
+ it.each([
+ ['C1', 'C63'],
+ ['C63', 'C1'],
+ ])('does not reuse a shallow proof past the depth bound (%s, %s)', (first, second) => {
+ const declarations = ['interface C0 {}'];
+ for (let level = 1; level <= 63; level++)
+ declarations.push(`interface C${level} extends C${level - 1} {}`);
+ const fixture = hierarchyFixture(declarations, [first, second]);
+ expect(fixture.resolve()).toMatchObject({
+ kind: 'unknown',
+ candidateSetComplete: false,
+ });
+ });
+
+ it('retains cyclic-hierarchy uncertainty across repeated inventory requests', () => {
+ const fixture = hierarchyFixture(
+ ['interface Left extends Right {}', 'interface Right extends Left {}'],
+ ['Left'],
+ );
+ for (let attempt = 0; attempt < 2; attempt++)
+ expect(fixture.resolve()).toMatchObject({ kind: 'unknown', candidateSetComplete: false });
+ });
+});
+
+describe('Java source subtype conversion bounds', () => {
+ const conversion = { argumentType: 'Argument', parameterTypes: ['Unrelated', 'Object'] as const };
+
+ it.each([12, 16])(
+ 'checks an unrelated formal through a depth-%i diamond once per type',
+ (depth) => {
+ const declarations = ['interface A0 {}', 'interface B0 {}'];
+ for (let level = 1; level <= depth; level++) {
+ declarations.push(
+ `interface A${level} extends A${level - 1}, B${level - 1} {}`,
+ `interface B${level} extends A${level - 1}, B${level - 1} {}`,
+ );
+ }
+ declarations.push(`class Argument implements A${depth}, B${depth} {}`, 'class Unrelated {}');
+ const fixture = hierarchyFixture(declarations, [], conversion);
+ expect(fixture.resolve()).toMatchObject({
+ kind: 'resolved',
+ definition: { parameterTypes: ['Object'] },
+ });
+ expect(fixture.baseReads()).toBeLessThanOrEqual((declarations.length + 1) * 2);
+ },
+ );
+
+ it.each([
+ ['C1', 'C63'],
+ ['C63', 'C1'],
+ ])(
+ 'does not reuse a shallow subtype rejection beyond the depth bound (%s, %s)',
+ (first, second) => {
+ const declarations = ['interface C0 {}'];
+ for (let level = 1; level <= 63; level++)
+ declarations.push(`interface C${level} extends C${level - 1} {}`);
+ declarations.push(`class Argument implements ${first}, ${second} {}`, 'class Unrelated {}');
+ const fixture = hierarchyFixture(declarations, [], conversion);
+ expect(fixture.resolve()).toMatchObject({ kind: 'unknown', candidateSetComplete: true });
+ },
+ );
+
+ it('retains cyclic subtype uncertainty instead of selecting the Object fallback', () => {
+ const fixture = hierarchyFixture(
+ [
+ 'interface Left extends Right {}',
+ 'interface Right extends Left {}',
+ 'class Argument implements Left {}',
+ 'class Unrelated {}',
+ ],
+ [],
+ conversion,
+ );
+ for (let attempt = 0; attempt < 2; attempt++)
+ expect(fixture.resolve()).toMatchObject({ kind: 'unknown', candidateSetComplete: true });
+ });
+});
diff --git a/gitnexus/test/unit/scope-resolution/java/java-overload-types.test.ts b/gitnexus/test/unit/scope-resolution/java/java-overload-types.test.ts
new file mode 100644
index 000000000..a5f3648db
--- /dev/null
+++ b/gitnexus/test/unit/scope-resolution/java/java-overload-types.test.ts
@@ -0,0 +1,155 @@
+import { describe, expect, it } from 'vitest';
+import {
+ javaInvocationConversion,
+ javaSubtype,
+ selectJavaOverload,
+ type JavaType,
+ type JavaPrimitive,
+ type JavaOverloadCandidate,
+ type JavaTypeRelations,
+} from '../../../../src/core/ingestion/languages/java/overload-types.js';
+
+const relations: JavaTypeRelations = { sourceSubtype: () => 'unknown' };
+function type(name: string): JavaType {
+ if (name === '?') return { kind: 'unknown' };
+ if (name === 'null') return { kind: 'null' };
+ if (name.endsWith('[]')) return { kind: 'array', component: type(name.slice(0, -2)) };
+ if (['boolean', 'byte', 'short', 'char', 'int', 'long', 'float', 'double'].includes(name))
+ return { kind: 'primitive', name: name as JavaPrimitive };
+ return {
+ kind: 'reference',
+ name: name.includes('.') ? name : `java.lang.${name}`,
+ arguments: [],
+ };
+}
+function select(signatures: readonly (readonly string[])[], arguments_: readonly string[]) {
+ const candidates: JavaOverloadCandidate[] = signatures.map((parameters) => ({
+ definition: parameters.join(','),
+ parameters: parameters.map((parameter) => type(parameter.replace('...', '[]'))),
+ variadic: parameters.at(-1)?.endsWith('...') ?? false,
+ }));
+ return selectJavaOverload(
+ candidates,
+ arguments_.length,
+ (index, target, phase) =>
+ javaInvocationConversion(type(arguments_[index]!), target, phase, relations),
+ relations,
+ );
+}
+
+describe('Java overload invocation phases and specificity (JLS 15.12.2)', () => {
+ it.each(['constructor', '__proto__', 'toString'])(
+ 'treats external name %s as unknown instead of consulting object prototypes',
+ (name) => {
+ expect(
+ javaSubtype({ kind: 'reference', name, arguments: [] }, type('Number'), relations),
+ ).toBe('unknown');
+ },
+ );
+ it('returns unknown for adversarial type depth instead of overflowing the stack', () => {
+ let left: JavaType = type('int');
+ let right: JavaType = type('int');
+ for (let index = 0; index < 12_000; index++) {
+ left = { kind: 'array', component: left };
+ right = { kind: 'array', component: right };
+ }
+ expect(javaSubtype(left, right, relations)).toBe('unknown');
+ });
+ it('keeps override-equivalent preferred declaration selection unknown rather than ambiguous', () => {
+ expect(select([['String'], ['String'], ['Object']], ['String']).kind).toBe('unknown');
+ expect(select([['int'], ['int']], ['int']).kind).toBe('unknown');
+ });
+ it.each([
+ [[['String'], ['Long']], ['String'], 'String', 'strict'],
+ [[['long'], ['Integer']], ['int'], 'long', 'strict'],
+ [[['Object'], ['int']], ['Integer'], 'Object', 'strict'],
+ [[['long']], ['Integer'], 'long', 'loose'],
+ [[['String'], ['Object']], ['null'], 'String', 'strict'],
+ [[['Object'], ['String...']], ['String'], 'Object', 'strict'],
+ [[['Object'], ['String...']], ['String[]'], 'String...', 'strict'],
+ [[['Integer'], ['int...']], ['int'], 'Integer', 'loose'],
+ [[['Object...'], ['String...']], [], 'String...', 'variable'],
+ [[['Object'], ['Object[]']], ['int[][]'], 'Object[]', 'strict'],
+ ] as const)(
+ 'selects %s for %s without registration-order dependence',
+ (signatures, arguments_, winner, phase) => {
+ for (const order of [signatures, [...signatures].reverse()]) {
+ expect(select(order, arguments_)).toMatchObject({
+ kind: 'resolved',
+ definition: winner,
+ phase,
+ });
+ }
+ },
+ );
+
+ it.each([
+ [[['Long']], ['int']],
+ [[['byte']], ['int']],
+ [[['long[]']], ['int[]']],
+ [[['Object[]']], ['int[]']],
+ [[['boolean']], ['int']],
+ ] as const)('rejects invocation conversion %s for %s', (signatures, arguments_) => {
+ expect(select(signatures, arguments_).kind).toBe('inapplicable');
+ });
+
+ it.each([
+ [[['String'], ['Long']], ['null']],
+ [
+ [
+ ['int', 'Object'],
+ ['long', 'String'],
+ ],
+ ['int', 'String'],
+ ],
+ ] as const)(
+ 'preserves incomparable applicable signatures %s for %s',
+ (signatures, arguments_) => {
+ expect(select(signatures, arguments_).kind).toBe('ambiguous');
+ },
+ );
+
+ it('does not treat an unknown argument as null or a compatible wildcard', () => {
+ expect(select([['String'], ['Long']], ['?']).kind).toBe('unknown');
+ });
+
+ it('does not jump past an uncertain strict-phase candidate', () => {
+ const candidates: JavaOverloadCandidate[] = [
+ { definition: 'unknown reference', parameters: [{ kind: 'unknown' }], variadic: false },
+ { definition: 'boxed', parameters: [type('Integer')], variadic: false },
+ ];
+ expect(
+ selectJavaOverload(
+ candidates,
+ 1,
+ (_index, target, phase) => javaInvocationConversion(type('int'), target, phase, relations),
+ relations,
+ ).kind,
+ ).toBe('unknown');
+ });
+
+ it('preserves invariant generic arguments across List to Collection widening', () => {
+ const list = { kind: 'reference' as const, name: 'java.util.List', arguments: [type('Long')] };
+ expect(
+ javaSubtype(
+ list,
+ { kind: 'reference', name: 'java.util.Collection', arguments: [type('Long')] },
+ relations,
+ ),
+ ).toBe('yes');
+ expect(
+ javaSubtype(
+ list,
+ { kind: 'reference', name: 'java.util.Collection', arguments: [type('Number')] },
+ relations,
+ ),
+ ).toBe('no');
+ expect(
+ javaSubtype(
+ list,
+ { kind: 'reference', name: 'java.util.Collection', arguments: [{ kind: 'unknown' }] },
+ relations,
+ ),
+ ).toBe('unknown');
+ });
+});
diff --git a/gitnexus/test/unit/scope-resolution/provider-call-outcome.test.ts b/gitnexus/test/unit/scope-resolution/provider-call-outcome.test.ts
new file mode 100644
index 000000000..216e952ef
--- /dev/null
+++ b/gitnexus/test/unit/scope-resolution/provider-call-outcome.test.ts
@@ -0,0 +1,596 @@
+import { describe, expect, it } from 'vitest';
+import {
+ buildDefIndex,
+ buildMethodDispatchIndex,
+ buildModuleScopeIndex,
+ buildQualifiedNameIndex,
+ buildScopeTree,
+ type ParsedFile,
+ type ReferenceSite,
+ type Scope,
+ type SymbolDefinition,
+ type TypeRef,
+} from 'gitnexus-shared';
+import { createKnowledgeGraph } from '../../../src/core/graph/graph.js';
+import { createSemanticModel } from '../../../src/core/ingestion/model/semantic-model.js';
+import type { ScopeResolutionIndexes } from '../../../src/core/ingestion/model/scope-resolution-indexes.js';
+import type {
+ ReceiverMemberResolution,
+ ScopeResolver,
+} from '../../../src/core/ingestion/scope-resolution/contract/scope-resolver.js';
+import { buildGraphNodeLookup } from '../../../src/core/ingestion/scope-resolution/graph-bridge/node-lookup.js';
+import { emitFreeCallFallback } from '../../../src/core/ingestion/scope-resolution/passes/free-call-fallback.js';
+import { emitReceiverBoundCalls } from '../../../src/core/ingestion/scope-resolution/passes/receiver-bound-calls.js';
+import { applyPreciseCallResultBindings } from '../../../src/core/ingestion/scope-resolution/pipeline/run.js';
+import { createCalleeIdAccumulator } from '../../../src/core/ingestion/scope-resolution/graph-bridge/callee-id-sink.js';
+import {
+ callSuppressionIdentities,
+ providerCallOutcome,
+ selectedCallKey,
+} from '../../../src/core/ingestion/scope-resolution/passes/provider-call-outcome.js';
+import type { ResolutionOutcome } from '../../../src/core/ingestion/scope-resolution/resolution-outcome.js';
+import { buildWorkspaceResolutionIndex } from '../../../src/core/ingestion/scope-resolution/workspace-index.js';
+
+function fixture(
+ extraDefinitions: readonly SymbolDefinition[] = [],
+ ownerType: 'Class' | 'Interface' = 'Class',
+ ownerFilePath = 'owner.ts',
+) {
+ const caller: SymbolDefinition = {
+ nodeId: 'def:caller',
+ filePath: 'caller.ts',
+ type: 'Function',
+ qualifiedName: 'main',
+ };
+ const first: SymbolDefinition = {
+ nodeId: 'def:first',
+ filePath: 'first.ts',
+ type: 'Function',
+ qualifiedName: 'choose',
+ };
+ const second: SymbolDefinition = {
+ nodeId: 'def:second',
+ filePath: 'second.ts',
+ type: 'Function',
+ qualifiedName: 'choose',
+ };
+ const owner: SymbolDefinition = {
+ nodeId: 'def:owner',
+ filePath: ownerFilePath,
+ type: ownerType,
+ qualifiedName: 'Owner',
+ };
+ const member: SymbolDefinition = {
+ nodeId: 'def:member',
+ filePath: ownerFilePath,
+ type: 'Method',
+ qualifiedName: 'Owner.choose',
+ ownerId: owner.nodeId,
+ parameterCount: 1,
+ parameterTypes: ['int'],
+ };
+ const defs = [caller, first, second, owner, member, ...extraDefinitions];
+ const moduleScope: Scope = {
+ id: 'scope:module',
+ parent: null,
+ kind: 'Module',
+ filePath: 'caller.ts',
+ range: { startLine: 0, startCol: 0, endLine: 30, endCol: 0 },
+ ownedDefs: [],
+ imports: [],
+ typeBindings: new Map(),
+ bindings: new Map(),
+ };
+ const scope: Scope = {
+ id: 'scope:caller',
+ parent: moduleScope.id,
+ kind: 'Function',
+ filePath: 'caller.ts',
+ range: { startLine: 1, startCol: 0, endLine: 20, endCol: 0 },
+ ownedDefs: [caller],
+ imports: [],
+ typeBindings: new Map(),
+ bindings: new Map([['choose', [{ def: first, origin: 'local' }]]]),
+ };
+ const site: ReferenceSite = {
+ inScope: scope.id,
+ name: 'choose',
+ kind: 'call',
+ callForm: 'free',
+ arity: 1,
+ atRange: { startLine: 3, startCol: 2, endLine: 3, endCol: 10 },
+ };
+ const parsed: ParsedFile = {
+ filePath: scope.filePath,
+ moduleScope: moduleScope.id,
+ scopes: [moduleScope, scope],
+ localDefs: defs,
+ parsedImports: [],
+ referenceSites: [site],
+ };
+ const scopes = {
+ scopeTree: buildScopeTree([moduleScope, scope]),
+ defs: buildDefIndex(defs),
+ qualifiedNames: buildQualifiedNameIndex(defs),
+ moduleScopes: buildModuleScopeIndex([
+ { filePath: scope.filePath, moduleScopeId: moduleScope.id },
+ ]),
+ methodDispatch: buildMethodDispatchIndex({
+ owners: [],
+ computeMro: () => [],
+ implementsOf: () => [],
+ }),
+ imports: new Map(),
+ bindings: new Map(),
+ bindingAugmentations: new Map(),
+ workspaceFqnBindings: new Map(),
+ workspaceTypeBindings: new Map(),
+ namespaceFqnBindings: new Map(),
+ namespaceTypeBindings: new Map(),
+ accessibleNamespacesByScope: new Map(),
+ referenceSites: [site],
+ sccs: [],
+ } as unknown as ScopeResolutionIndexes;
+ const graph = createKnowledgeGraph();
+ for (const [definition, id] of [
+ [caller, 'Function:caller.ts:main'],
+ [first, 'Function:first.ts:choose'],
+ [second, 'Function:second.ts:choose'],
+ [owner, 'Class:owner.ts:Owner'],
+ [member, 'Method:owner.ts:Owner.choose#1'],
+ ...extraDefinitions.map((definition) => [definition, `graph:${definition.nodeId}`] as const),
+ ] as const) {
+ graph.addNode({
+ id,
+ label: definition.type,
+ properties: {
+ name: definition.qualifiedName!.split('.').at(-1)!,
+ qualifiedName: definition.qualifiedName,
+ filePath: definition.filePath,
+ parameterCount: definition.parameterCount,
+ parameterTypes: definition.parameterTypes,
+ },
+ });
+ }
+ const nodeLookup = buildGraphNodeLookup(graph);
+ const model = createSemanticModel();
+ const outcomes: ResolutionOutcome[] = [];
+ const handled = new Set();
+ const context = { graph, scopes, nodeLookup, model };
+ const run = (
+ decision: ReceiverMemberResolution | undefined,
+ selectedCalls?: Map,
+ ) => {
+ emitFreeCallFallback(
+ graph,
+ scopes,
+ [parsed],
+ nodeLookup,
+ { bySourceScope: new Map() },
+ handled,
+ model,
+ buildWorkspaceResolutionIndex([parsed]),
+ {
+ resolveUnqualifiedCall: () => decision,
+ recordResolutionOutcome: (o) => outcomes.push(o),
+ selectedCalls,
+ },
+ );
+ return graph.relationships.filter((relation) => relation.type === 'CALLS');
+ };
+ return {
+ context,
+ site,
+ member,
+ owner,
+ first,
+ second,
+ outcomes,
+ handled,
+ run,
+ parsed,
+ scope,
+ moduleScope,
+ };
+}
+
+function receiverFixture() {
+ const owners: SymbolDefinition[] = ['First', 'Second'].map((name) => ({
+ nodeId: `def:${name}`,
+ filePath: 'caller.ts',
+ type: 'Class',
+ qualifiedName: name,
+ }));
+ const implementations = owners.map(
+ (owner): SymbolDefinition => ({
+ nodeId: `${owner.nodeId}.choose`,
+ filePath: owner.filePath,
+ type: 'Method',
+ qualifiedName: `${owner.qualifiedName}.choose`,
+ ownerId: owner.nodeId,
+ parameterCount: 1,
+ parameterTypes: ['int'],
+ }),
+ );
+ const f = fixture([...owners, ...implementations], 'Interface', 'caller.ts');
+ const site: ReferenceSite = {
+ ...f.site,
+ callForm: 'member',
+ explicitReceiver: { name: 'service' },
+ };
+ const scope: Scope = {
+ ...f.scope,
+ bindings: new Map([['Owner', [{ def: f.owner, origin: 'local' }]]]),
+ typeBindings: new Map([
+ [
+ 'service',
+ {
+ rawName: 'Owner',
+ declaredAtScope: f.scope.id,
+ source: 'parameter-annotation',
+ },
+ ],
+ ]),
+ };
+ const parsed = {
+ ...f.parsed,
+ scopes: [f.moduleScope, scope],
+ referenceSites: [site],
+ };
+ const scopes = {
+ ...f.context.scopes,
+ scopeTree: buildScopeTree(parsed.scopes),
+ referenceSites: [site],
+ };
+ const { graph, model, nodeLookup } = f.context;
+ model.methods.register(f.owner.nodeId, 'choose', f.member);
+ for (const implementation of implementations) {
+ model.methods.register(implementation.ownerId!, 'choose', implementation);
+ }
+ for (const owner of owners) {
+ graph.addRelationship({
+ id: `implements:${owner.nodeId}`,
+ sourceId: `graph:${owner.nodeId}`,
+ targetId: 'Class:owner.ts:Owner',
+ type: 'IMPLEMENTS',
+ confidence: 1,
+ reason: 'test',
+ });
+ }
+ const run = (
+ resolution: ReceiverMemberResolution,
+ dispatch?: ScopeResolver['resolveSelectedMethodImplementation'],
+ ) => {
+ emitReceiverBoundCalls(
+ graph,
+ scopes,
+ [parsed],
+ nodeLookup,
+ f.handled,
+ {
+ isSuperReceiver: () => false,
+ arityCompatibility: () => 'compatible',
+ resolveReceiverMember: () => resolution,
+ resolveSelectedMethodImplementation: dispatch,
+ },
+ buildWorkspaceResolutionIndex([parsed]),
+ model,
+ { recordResolutionOutcome: (outcome) => f.outcomes.push(outcome) },
+ );
+ return graph.relationships.filter((edge) => edge.type === 'CALLS');
+ };
+ return { ...f, owners, implementations, run };
+}
+
+describe('provider call outcomes', () => {
+ it('distinguishes nested calls that share their starting position', () => {
+ const f = fixture();
+ expect(selectedCallKey('caller.ts', f.site.atRange)).not.toBe(
+ selectedCallKey('caller.ts', {
+ ...f.site.atRange,
+ endCol: f.site.atRange.endCol + 4,
+ }),
+ );
+ });
+
+ it('uses the selected primary return even when runtime dispatch has several targets', () => {
+ const f = fixture();
+ const parsed = {
+ ...f.parsed,
+ callResultAssignmentSites: [{ callSite: f.site.atRange, inScope: f.scope.id, lhs: 'result' }],
+ };
+ const returned: TypeRef = {
+ rawName: 'Owner',
+ declaredAtScope: 'scope:declaration',
+ source: 'return-annotation',
+ };
+ const index = {
+ ...buildWorkspaceResolutionIndex([parsed]),
+ declaredReturnTypeByCallableId: new Map([[f.first.nodeId, returned]]),
+ };
+ const calleeIds = createCalleeIdAccumulator();
+ calleeIds.add('caller.ts', 3, 2, 'Function:first.ts:choose');
+ calleeIds.add('caller.ts', 3, 2, 'Function:second.ts:choose');
+ const selected = new Map([[selectedCallKey('caller.ts', f.site.atRange), f.first]]);
+ const update = () =>
+ applyPreciseCallResultBindings(
+ [parsed],
+ f.context.scopes,
+ index,
+ calleeIds,
+ f.context.nodeLookup,
+ f.context.model,
+ true,
+ selected,
+ );
+ expect(update()).toBe(1);
+ expect(f.scope.typeBindings.get('result')).toMatchObject({
+ rawName: 'Owner',
+ declaredAtScope: 'scope:declaration',
+ });
+ expect(update()).toBe(0);
+ });
+
+ it.each([undefined, null])(
+ 'clears an inferred assignment before an unselected producer can leak a target (%s)',
+ (primary) => {
+ const f = fixture();
+ const parsed = {
+ ...f.parsed,
+ callResultAssignmentSites: [
+ { callSite: f.site.atRange, inScope: f.scope.id, lhs: 'result' },
+ ],
+ };
+ (f.scope.typeBindings as Map).set('result', {
+ rawName: 'WrongReturn',
+ declaredAtScope: f.scope.id,
+ source: 'assignment-inferred',
+ });
+ const selected = new Map();
+ if (primary === null) selected.set(selectedCallKey('caller.ts', f.site.atRange), null);
+ const update = () =>
+ applyPreciseCallResultBindings(
+ [parsed],
+ f.context.scopes,
+ buildWorkspaceResolutionIndex([parsed]),
+ createCalleeIdAccumulator(),
+ f.context.nodeLookup,
+ f.context.model,
+ true,
+ selected,
+ );
+ expect(update()).toBe(1);
+ expect(f.scope.typeBindings.has('result')).toBe(false);
+ expect(update()).toBe(0);
+ },
+ );
+
+ it('keeps scope identities separate from graph method, owner and caller identities', () => {
+ const f = fixture();
+ const result = providerCallOutcome(
+ {
+ kind: 'unknown',
+ candidateIds: [f.member.nodeId],
+ candidateSetComplete: true,
+ },
+ f.site,
+ 'caller.ts',
+ 'receiver-bound-calls',
+ f.context,
+ );
+ expect(result).toMatchObject({
+ kind: 'suppressed',
+ reason: 'overload-unknown',
+ siteKind: 'call',
+ candidateIds: ['def:member'],
+ candidateGraphIds: ['Method:owner.ts:Owner.choose#1'],
+ candidateOwnerIds: ['Class:owner.ts:Owner'],
+ callerId: 'Function:caller.ts:main',
+ candidateSetComplete: true,
+ });
+ });
+
+ it('retains known candidates and reports incomplete identity coverage', () => {
+ const f = fixture();
+ const result = callSuppressionIdentities([f.member.nodeId, 'def:missing'], f.site, f.context);
+ expect(result.candidateGraphIds).toEqual(['Method:owner.ts:Owner.choose#1']);
+ expect(result.candidateSetComplete).toBe(false);
+ });
+
+ it('prefers scope definitions, then the first supplied definition, then the model', () => {
+ const f = fixture();
+ f.context.model.symbols.add('second.ts', 'choose', 'def:supplied', 'Function', {
+ qualifiedName: 'choose',
+ });
+ f.context.model.symbols.add('second.ts', 'choose', 'def:model', 'Function', {
+ qualifiedName: 'choose',
+ });
+ const result = callSuppressionIdentities(
+ [f.member.nodeId, 'def:supplied', 'def:model', 'def:supplied'],
+ f.site,
+ f.context,
+ true,
+ [
+ { ...f.first, nodeId: f.member.nodeId },
+ { ...f.first, nodeId: 'def:supplied' },
+ { ...f.second, nodeId: 'def:supplied' },
+ ],
+ );
+ expect(result).toMatchObject({
+ candidateGraphIds: [
+ 'Method:owner.ts:Owner.choose#1',
+ 'Function:first.ts:choose',
+ 'Function:second.ts:choose',
+ ],
+ candidateOwnerIds: ['Class:owner.ts:Owner'],
+ callerId: 'Function:caller.ts:main',
+ candidateSetComplete: true,
+ });
+ });
+
+ it.each([256, 512])('bounds identity lookup work for %i supplied candidates', (size) => {
+ let reads = 0;
+ const ids = Array.from({ length: size }, (_, i) => `def:candidate:${i}`);
+ const definitions = ids.map(
+ (id, i): SymbolDefinition => ({
+ get nodeId() {
+ reads++;
+ return id;
+ },
+ filePath: `candidate${i}.ts`,
+ type: 'Function',
+ qualifiedName: 'choose',
+ }),
+ );
+ const f = fixture(definitions);
+ const suppliedIds = new Set(ids);
+ const context = {
+ ...f.context,
+ scopes: {
+ ...f.context.scopes,
+ defs: buildDefIndex(f.parsed.localDefs.filter((def) => !suppliedIds.has(def.nodeId))),
+ },
+ };
+ reads = 0;
+ const result = callSuppressionIdentities(ids, f.site, context, true, definitions);
+ expect(reads).toBeLessThanOrEqual(size * 16);
+ expect(result.candidateGraphIds).toEqual(ids.map((id) => `graph:${id}`));
+ expect(result.candidateSetComplete).toBe(true);
+ });
+
+ it.each(['unknown', 'ambiguous', 'inapplicable'] as const)(
+ 'does not resume lexical fallback after a provider %s decision',
+ (kind) => {
+ const f = fixture();
+ expect(
+ f.run({
+ kind,
+ candidateIds: [f.first.nodeId],
+ candidateSetComplete: true,
+ }),
+ ).toEqual([]);
+ expect(f.handled.has('caller.ts:3:2')).toBe(true);
+ expect(f.outcomes).toContainEqual(
+ expect.objectContaining({
+ kind: 'suppressed',
+ candidateGraphIds: ['Function:first.ts:choose'],
+ callerId: 'Function:caller.ts:main',
+ candidateSetComplete: true,
+ reason: kind === 'ambiguous' ? 'member-lookup-ambiguous' : `overload-${kind}`,
+ }),
+ );
+ },
+ );
+
+ it('honors the selected declaration before a different lexical first match', () => {
+ const f = fixture();
+ expect(f.run({ kind: 'resolved', definition: f.second }).map((edge) => edge.targetId)).toEqual([
+ 'Function:second.ts:choose',
+ ]);
+ expect(f.outcomes).toContainEqual(
+ expect.objectContaining({
+ kind: 'resolved',
+ phase: 'free-call-fallback',
+ targetId: f.second.nodeId,
+ }),
+ );
+ });
+
+ it('preserves existing lookup when the provider declines handling', () => {
+ const f = fixture();
+ const selected = new Map();
+ expect(f.run(undefined, selected).map((edge) => edge.targetId)).toEqual([
+ 'Function:first.ts:choose',
+ ]);
+ expect(selected.get(selectedCallKey('caller.ts', f.site.atRange))).toBe(f.first);
+ });
+
+ it('retains a partial boundary when a selected declaration has no graph identity', () => {
+ const f = fixture();
+ expect(
+ f.run({
+ kind: 'resolved',
+ definition: {
+ ...f.second,
+ nodeId: 'def:missing',
+ qualifiedName: 'missing',
+ },
+ }),
+ ).toEqual([]);
+ expect(f.handled.has('caller.ts:3:2')).toBe(true);
+ expect(f.outcomes).toContainEqual(
+ expect.objectContaining({
+ kind: 'suppressed',
+ reason: 'overload-unknown',
+ candidateSetComplete: false,
+ }),
+ );
+ });
+
+ it.each(['unknown', 'ambiguous', 'inapplicable'] as const)(
+ 'does not emit a first member after a receiver %s decision',
+ (kind) => {
+ const f = receiverFixture();
+ expect(
+ f.run({
+ kind,
+ candidateIds: [f.member.nodeId],
+ candidateSetComplete: true,
+ }),
+ ).toEqual([]);
+ expect(f.handled.has('caller.ts:3:2')).toBe(true);
+ expect(f.outcomes).toContainEqual(
+ expect.objectContaining({
+ kind: 'suppressed',
+ phase: 'receiver-bound-calls',
+ candidateSetComplete: true,
+ }),
+ );
+ },
+ );
+
+ it('preserves the selected declaration while dispatching its signature', () => {
+ const f = receiverFixture();
+ const calls = f.run({ kind: 'resolved', definition: f.member }, (selected, owner) => {
+ expect(selected).toBe(f.member);
+ const implementation = f.implementations.find((item) => item.ownerId === owner.nodeId);
+ return implementation === undefined
+ ? undefined
+ : { kind: 'resolved', definition: implementation };
+ });
+ expect(calls.map((edge) => edge.targetId).sort()).toEqual(
+ [
+ 'Method:owner.ts:Owner.choose#1',
+ ...f.implementations.map((item) => `graph:${item.nodeId}`),
+ ].sort(),
+ );
+ expect(f.outcomes).toContainEqual(
+ expect.objectContaining({
+ kind: 'resolved',
+ phase: 'receiver-bound-calls',
+ targetId: f.member.nodeId,
+ }),
+ );
+ });
+
+ it('aggregates unresolved implementations separately from a resolved primary', () => {
+ const f = receiverFixture();
+ const calls = f.run({ kind: 'resolved', definition: f.member }, (_selected, owner) => ({
+ kind: 'unknown',
+ candidateSetComplete: true,
+ candidateIds: f.implementations
+ .filter((item) => item.ownerId === owner.nodeId)
+ .map((item) => item.nodeId),
+ }));
+ expect(calls.map((edge) => edge.targetId)).toEqual(['Method:owner.ts:Owner.choose#1']);
+ const dispatch = f.outcomes.filter((outcome) => outcome.phase === 'receiver-dispatch');
+ expect(dispatch).toHaveLength(1);
+ expect(dispatch[0]).toMatchObject({
+ kind: 'suppressed',
+ reason: 'overload-unknown',
+ candidateSetComplete: true,
+ candidateIds: f.implementations.map((item) => item.nodeId),
+ candidateOwnerIds: f.owners.map((owner) => `graph:${owner.nodeId}`),
+ });
+ });
+});
diff --git a/gitnexus/test/unit/scope-resolution/selected-call-propagation.test.ts b/gitnexus/test/unit/scope-resolution/selected-call-propagation.test.ts
new file mode 100644
index 000000000..70de849c3
--- /dev/null
+++ b/gitnexus/test/unit/scope-resolution/selected-call-propagation.test.ts
@@ -0,0 +1,277 @@
+import { describe, expect, it } from 'vitest';
+import {
+ buildDefIndex,
+ buildMethodDispatchIndex,
+ buildModuleScopeIndex,
+ buildQualifiedNameIndex,
+ buildScopeTree,
+ type BindingRef,
+ type CallResultAssignmentSite,
+ type ParsedFile,
+ type ReferenceSite,
+ type Scope,
+ type SymbolDefinition,
+ type TypeRef,
+} from 'gitnexus-shared';
+import { createKnowledgeGraph } from '../../../src/core/graph/graph.js';
+import { createSemanticModel } from '../../../src/core/ingestion/model/semantic-model.js';
+import type { ScopeResolutionIndexes } from '../../../src/core/ingestion/model/scope-resolution-indexes.js';
+import { createCalleeIdAccumulator } from '../../../src/core/ingestion/scope-resolution/graph-bridge/callee-id-sink.js';
+import { buildGraphNodeLookup } from '../../../src/core/ingestion/scope-resolution/graph-bridge/node-lookup.js';
+import { emitReceiverBoundCalls } from '../../../src/core/ingestion/scope-resolution/passes/receiver-bound-calls.js';
+import {
+ callResultBindingKey,
+ selectedCallKey,
+} from '../../../src/core/ingestion/scope-resolution/passes/provider-call-outcome.js';
+import { applyPreciseCallResultBindings } from '../../../src/core/ingestion/scope-resolution/pipeline/run.js';
+import type { ResolutionOutcome } from '../../../src/core/ingestion/scope-resolution/resolution-outcome.js';
+import { buildWorkspaceResolutionIndex } from '../../../src/core/ingestion/scope-resolution/workspace-index.js';
+
+function chainFixture(reverse: boolean) {
+ const count = 24;
+ const filePath = 'chain.code';
+ const caller: SymbolDefinition = {
+ nodeId: 'caller',
+ filePath,
+ type: 'Function',
+ qualifiedName: 'main',
+ };
+ const owners: SymbolDefinition[] = Array.from({ length: count }, (_, i) => ({
+ nodeId: `owner:${i}`,
+ filePath,
+ type: 'Class',
+ qualifiedName: `Owner${i}`,
+ }));
+ const methods: SymbolDefinition[] = owners.map((owner, i) => ({
+ nodeId: `method:${i}`,
+ filePath,
+ type: 'Method',
+ ownerId: owner.nodeId,
+ qualifiedName: `${owner.qualifiedName}.${i + 1 < count ? `next${i}` : 'finish'}`,
+ parameterCount: 0,
+ parameterTypes: [],
+ }));
+ const module: Scope = {
+ id: 'module',
+ parent: null,
+ kind: 'Module',
+ filePath,
+ range: { startLine: 0, startCol: 0, endLine: 100, endCol: 0 },
+ ownedDefs: owners,
+ imports: [],
+ typeBindings: new Map(),
+ bindings: new Map(
+ owners.map((owner) => [owner.qualifiedName!, [{ def: owner, origin: 'local' }]]),
+ ),
+ };
+ const bindings = new Map();
+ const typeBindings = new Map();
+ for (let i = 0; i < count; i++) {
+ // The producer join must clear every speculative inferred binding except
+ // the explicitly typed chain root before reference resolution.
+ typeBindings.set(`value${i}`, {
+ rawName: `Owner${i}`,
+ declaredAtScope: module.id,
+ source: i === 0 ? 'parameter-annotation' : 'assignment-inferred',
+ });
+ bindings.set(`value${i}`, [
+ {
+ def: { nodeId: `value:${i}`, filePath, type: 'Variable', qualifiedName: `value${i}` },
+ origin: 'local',
+ },
+ ]);
+ }
+ const scope: Scope = {
+ id: 'function',
+ parent: module.id,
+ kind: 'Function',
+ filePath,
+ range: { startLine: 1, startCol: 0, endLine: 90, endCol: 0 },
+ ownedDefs: [caller],
+ imports: [],
+ typeBindings,
+ bindings,
+ };
+ const sites: ReferenceSite[] = methods.map((method, i) => ({
+ inScope: scope.id,
+ kind: 'call',
+ callForm: 'member',
+ arity: 0,
+ name: method.qualifiedName!.split('.').at(-1)!,
+ explicitReceiver: { name: `value${i}` },
+ atRange: { startLine: i + 2, startCol: 0, endLine: i + 2, endCol: 15 },
+ }));
+ const assignments: CallResultAssignmentSite[] = sites.slice(0, -1).map((site, i) => ({
+ callSite: site.atRange,
+ inScope: scope.id,
+ lhs: `value${i + 1}`,
+ }));
+ const defs = [caller, ...owners, ...methods];
+ const parsed: ParsedFile = {
+ filePath,
+ moduleScope: module.id,
+ scopes: [module, scope],
+ localDefs: defs,
+ parsedImports: [],
+ referenceSites: reverse ? [...sites].reverse() : sites,
+ callResultAssignmentSites: assignments,
+ };
+ const scopes = {
+ scopeTree: buildScopeTree(parsed.scopes),
+ defs: buildDefIndex(defs),
+ qualifiedNames: buildQualifiedNameIndex(defs),
+ moduleScopes: buildModuleScopeIndex([{ filePath, moduleScopeId: module.id }]),
+ methodDispatch: buildMethodDispatchIndex({
+ owners: [],
+ computeMro: () => [],
+ implementsOf: () => [],
+ }),
+ imports: new Map(),
+ bindings: new Map(),
+ bindingAugmentations: new Map(),
+ workspaceFqnBindings: new Map(),
+ workspaceTypeBindings: new Map(),
+ namespaceFqnBindings: new Map(),
+ namespaceTypeBindings: new Map(),
+ accessibleNamespacesByScope: new Map(),
+ referenceSites: sites,
+ sccs: [],
+ } as unknown as ScopeResolutionIndexes;
+ const graph = createKnowledgeGraph();
+ for (const def of defs)
+ graph.addNode({
+ id: def.nodeId,
+ label: def.type,
+ properties: {
+ name: def.qualifiedName!.split('.').at(-1)!,
+ qualifiedName: def.qualifiedName,
+ filePath,
+ parameterCount: def.parameterCount,
+ parameterTypes: def.parameterTypes,
+ },
+ });
+ const nodeLookup = buildGraphNodeLookup(graph);
+ const model = createSemanticModel();
+ for (const [i, method] of methods.entries())
+ model.methods.register(owners[i]!.nodeId, sites[i]!.name, method);
+ const workspace = {
+ ...buildWorkspaceResolutionIndex([parsed]),
+ declaredReturnTypeByCallableId: new Map(
+ methods.slice(0, -1).map((method, i) => [
+ method.nodeId,
+ {
+ rawName: `Owner${i + 1}`,
+ declaredAtScope: module.id,
+ source: 'return-annotation' as const,
+ },
+ ]),
+ ),
+ };
+ const selected = new Map();
+ const pending = new Map();
+ const outcomes: ResolutionOutcome[] = [];
+ const calleeIds = createCalleeIdAccumulator();
+ const byProducer = new Map(
+ assignments.map((assignment) => [selectedCallKey(filePath, assignment.callSite), assignment]),
+ );
+ const clearCount = applyPreciseCallResultBindings(
+ [parsed],
+ scopes,
+ workspace,
+ calleeIds,
+ nodeLookup,
+ model,
+ true,
+ selected,
+ );
+ let selections = 0;
+ const run = (unknownRoot: boolean) =>
+ emitReceiverBoundCalls(
+ graph,
+ scopes,
+ [parsed],
+ nodeLookup,
+ new Set(),
+ {
+ isSuperReceiver: () => false,
+ arityCompatibility: () => 'compatible',
+ resolveReceiverMember: (owner) => {
+ selections++;
+ const definition = methods.find((method) => method.ownerId === owner.nodeId)!;
+ return unknownRoot && owner.nodeId === owners[0]!.nodeId
+ ? { kind: 'unknown', candidateIds: [definition.nodeId], candidateSetComplete: true }
+ : { kind: 'resolved', definition };
+ },
+ },
+ workspace,
+ model,
+ {
+ selectedCalls: selected,
+ pendingCallReceivers: pending,
+ callResultProducers: new Map(
+ assignments.map((assignment) => [
+ callResultBindingKey(scope.id, assignment.lhs),
+ assignment,
+ ]),
+ ),
+ onSelectedCall: (key) => {
+ const assignment = byProducer.get(key);
+ if (assignment === undefined) return;
+ // Deliver the exact dependency slice to the same return-binding join
+ // used by the orchestrator; no workspace scan per selected producer.
+ applyPreciseCallResultBindings(
+ [{ ...parsed, callResultAssignmentSites: [assignment] }],
+ scopes,
+ workspace,
+ calleeIds,
+ nodeLookup,
+ model,
+ true,
+ selected,
+ );
+ },
+ recordResolutionOutcome: (outcome) => outcomes.push(outcome),
+ },
+ );
+ return {
+ count,
+ clearCount,
+ graph,
+ methods,
+ outcomes,
+ pending,
+ selected,
+ typeBindings,
+ run,
+ selections: () => selections,
+ };
+}
+
+describe('selected call dependency propagation', () => {
+ it.each([false, true])(
+ 'resolves a long assignment chain in one receiver pass (reverse=%s)',
+ (reverse) => {
+ const f = chainFixture(reverse);
+ expect(f.clearCount).toBe(f.count - 1);
+ f.run(false);
+ expect(
+ [...f.graph.iterRelationshipsByType('CALLS')].map((edge) => edge.targetId).sort(),
+ ).toEqual(f.methods.map((method) => method.nodeId).sort());
+ expect(f.selections()).toBe(f.count);
+ expect(f.pending.size).toBe(0);
+ expect(f.typeBindings.get(`value${f.count - 1}`)?.declaredAtScope).toBe('module');
+ },
+ );
+
+ it.each([false, true])('keeps a long unknown-origin chain suppressed (reverse=%s)', (reverse) => {
+ const f = chainFixture(reverse);
+ f.run(true);
+ expect([...f.graph.iterRelationshipsByType('CALLS')]).toEqual([]);
+ expect(f.selections()).toBe(1);
+ expect(f.pending.size).toBe(f.count - 1);
+ expect(f.typeBindings.size).toBe(1);
+ expect(f.outcomes).toContainEqual(
+ expect.objectContaining({ reason: 'receiver-unresolved', name: 'finish' }),
+ );
+ });
+});
diff --git a/gitnexus/test/unit/scope-resolution/unresolved-overloads.test.ts b/gitnexus/test/unit/scope-resolution/unresolved-overloads.test.ts
new file mode 100644
index 000000000..f976837c7
--- /dev/null
+++ b/gitnexus/test/unit/scope-resolution/unresolved-overloads.test.ts
@@ -0,0 +1,279 @@
+import { describe, expect, it } from 'vitest';
+import type { ResolutionOutcome } from '../../../src/core/ingestion/scope-resolution/resolution-outcome.js';
+import {
+ MAX_OVERLOAD_IDENTITIES_PER_SITE,
+ MAX_OVERLOAD_SUMMARY_BYTES,
+ MAX_UNRESOLVED_OVERLOAD_SITES,
+ probeOverloadBoundaries,
+ summarizeUnresolvedOverloads,
+} from '../../../src/core/ingestion/scope-resolution/unresolved-overloads.js';
+
+type Suppressed = Extract;
+const target = 'Method:src/Host.java:Host.find#1~String';
+const otherTarget = 'Method:src/Host.java:Host.find#1~Long';
+const owner = 'Class:src/Host.java:Host';
+const caller = 'Method:src/Caller.java:Caller.run#0';
+
+function suppressed(overrides: Partial = {}): Suppressed {
+ return {
+ kind: 'suppressed',
+ phase: 'receiver-bound-calls',
+ filePath: 'src/Caller.java',
+ name: 'find',
+ range: { startLine: 5, startCol: 2, endLine: 5, endCol: 20 },
+ reason: 'overload-ambiguous',
+ candidateIds: ['def:Host.find.String', 'def:Host.find.Long'],
+ candidateGraphIds: [target, otherTarget],
+ candidateOwnerIds: [owner],
+ callerId: caller,
+ candidateSetComplete: true,
+ siteKind: 'call',
+ ...overrides,
+ };
+}
+
+function resolved(outcome: Suppressed): Extract {
+ return {
+ kind: 'resolved',
+ phase: outcome.phase,
+ filePath: outcome.filePath,
+ name: outcome.name,
+ range: outcome.range,
+ targetId: target,
+ };
+}
+
+const probe = (
+ summary: unknown,
+ ids: readonly string[],
+ direction: 'upstream' | 'downstream' | 'both' = 'upstream',
+) => probeOverloadBoundaries(summary, new Set(ids), direction);
+
+describe('persisted unresolved overload coverage', () => {
+ it('records a measured-clean receipt, distinct from an absent receipt', () => {
+ const clean = summarizeUnresolvedOverloads([]);
+ expect(clean).toEqual({
+ schemaVersion: 1,
+ complete: true,
+ totalSites: 0,
+ omittedSites: 0,
+ sites: [],
+ });
+ expect(probe(clean, [target])).toEqual({ notes: [], sites: 0 });
+ expect(probe(undefined, [target])).toMatchObject({
+ sites: 0,
+ notes: [expect.stringContaining('not recorded')],
+ });
+ expect(probe(summarizeUnresolvedOverloads([], false), [target]).notes).toEqual([
+ expect.stringContaining('partial'),
+ ]);
+ });
+
+ it('matches public candidate, caller and declaring-owner IDs in their own directions', () => {
+ const summary = summarizeUnresolvedOverloads([suppressed()]);
+ expect(probe(summary, [target]).sites).toBe(1);
+ expect(probe(summary, [owner]).sites).toBe(1);
+ expect(probe(summary, [caller], 'downstream').sites).toBe(1);
+ expect(probe(summary, [caller], 'both').sites).toBe(1);
+ expect(probe(summary, [caller])).toEqual({ sites: 0, notes: [] });
+ expect(probe(summary, [target], 'downstream')).toEqual({ sites: 0, notes: [] });
+ expect(probe(summary, ['def:Host.find.String'])).toEqual({ sites: 0, notes: [] });
+ expect(probe(summary, ['Class:src/Host.java:Unrelated'])).toEqual({ sites: 0, notes: [] });
+ });
+
+ it('counts one site once when several candidates, owners or phases match', () => {
+ const outcome = suppressed();
+ const summary = summarizeUnresolvedOverloads([
+ outcome,
+ outcome,
+ suppressed({
+ phase: 'receiver-dispatch',
+ reason: 'overload-unknown',
+ name: 'qualified.find',
+ }),
+ ]);
+ expect(summary.totalSites).toBe(1);
+ expect(probe(summary, [target, otherTarget, owner, caller], 'both').sites).toBe(1);
+ expect(summary.sites[0].reasons).toEqual(['overload-ambiguous', 'overload-unknown']);
+ });
+
+ it('reconciles resolved-after-suppressed only within the same decision phase', () => {
+ const primary = suppressed();
+ expect(summarizeUnresolvedOverloads([primary, resolved(primary)]).sites).toEqual([]);
+ const dispatch = suppressed({ phase: 'receiver-dispatch', reason: 'overload-unknown' });
+ const summary = summarizeUnresolvedOverloads([primary, dispatch, resolved(primary)]);
+ expect(summary.totalSites).toBe(1);
+ expect(summary.sites[0].reasons).toEqual(['overload-unknown']);
+ expect(probe(summary, [target]).sites).toBe(1);
+ });
+
+ it('uses the last decision in a phase and excludes proven inapplicable calls', () => {
+ const first = suppressed();
+ const last = suppressed({ candidateGraphIds: [otherTarget], candidateIds: ['def:other'] });
+ expect(probe(summarizeUnresolvedOverloads([first, last]), [target])).toEqual({
+ sites: 0,
+ notes: [],
+ });
+ expect(
+ summarizeUnresolvedOverloads([first, suppressed({ reason: 'overload-inapplicable' })]).sites,
+ ).toEqual([]);
+ expect(summarizeUnresolvedOverloads([resolved(first), first]).totalSites).toBe(1);
+ });
+
+ it('does not label property accesses or unrelated suppressions as missed calls', () => {
+ const summary = summarizeUnresolvedOverloads([
+ suppressed({ reason: 'member-lookup-ambiguous', siteKind: 'read' }),
+ suppressed({ reason: 'member-lookup-ambiguous', siteKind: 'write' }),
+ suppressed({ reason: 'receiver-unresolved' }),
+ suppressed({ reason: 'selected-callable-deleted' }),
+ { ...resolved(suppressed()), kind: 'fallback-guessed', targetId: target },
+ {
+ kind: 'reexport-ambiguous',
+ phase: 'exports',
+ filePath: 'src/barrel.ts',
+ name: 'find',
+ candidateIds: ['def:one', 'def:two'],
+ },
+ ]);
+ expect(summary).toMatchObject({ complete: true, totalSites: 0, sites: [] });
+ const oldMember = summarizeUnresolvedOverloads([
+ suppressed({ reason: 'member-lookup-ambiguous', siteKind: undefined }),
+ ]);
+ expect(oldMember).toMatchObject({ complete: false, totalSites: 0 });
+ expect(probe(oldMember, [target]).notes.join(' ')).toContain('partial');
+ });
+
+ it.each([
+ ['missing candidate identities', { candidateGraphIds: undefined }],
+ ['empty candidate identities', { candidateGraphIds: [] }],
+ ['missing caller identity', { callerId: undefined }],
+ ['missing candidate completeness', { candidateSetComplete: undefined }],
+ ['incomplete candidates', { candidateSetComplete: false }],
+ ])('does not certify absent keys with %s', (_description, overrides) => {
+ const summary = summarizeUnresolvedOverloads([suppressed(overrides)]);
+ expect(summary.complete).toBe(false);
+ expect(probe(summary, ['Method:elsewhere:unseen'])).toEqual({
+ sites: 0,
+ notes: [expect.stringContaining('partial')],
+ });
+ });
+
+ it('keeps known matches as measured floors in a partial summary', () => {
+ const summary = summarizeUnresolvedOverloads([suppressed({ candidateSetComplete: false })]);
+ expect(probe(summary, [target])).toEqual({
+ sites: 1,
+ notes: [expect.stringContaining('1 call site'), expect.stringContaining('partial')],
+ });
+ });
+
+ it('bounds site count and never treats an omitted candidate as measured zero', () => {
+ const outcomes = Array.from({ length: MAX_UNRESOLVED_OVERLOAD_SITES + 1 }, (_, index) =>
+ suppressed({
+ range: { startLine: index + 1, startCol: 0, endLine: index + 1, endCol: 1 },
+ candidateIds: [`def:${index}`],
+ candidateGraphIds: [`Method:file:${index}`],
+ }),
+ );
+ const summary = summarizeUnresolvedOverloads(outcomes);
+ expect(summary).toMatchObject({
+ complete: false,
+ totalSites: outcomes.length,
+ omittedSites: 1,
+ });
+ expect(summary.sites).toHaveLength(MAX_UNRESOLVED_OVERLOAD_SITES);
+ const retained = new Set(summary.sites.flatMap((site) => site.candidateIds));
+ const omitted = outcomes.find((outcome) => !retained.has(outcome.candidateGraphIds![0]))!;
+ expect(probe(summary, omitted.candidateGraphIds!)).toEqual({
+ sites: 0,
+ notes: [expect.stringContaining('partial')],
+ });
+ });
+
+ it('bounds per-site identities and serialized bytes with explicit partial coverage', () => {
+ const many = Array.from(
+ { length: MAX_OVERLOAD_IDENTITIES_PER_SITE + 1 },
+ (_, index) => `Method:file:${index}`,
+ );
+ const refs = summarizeUnresolvedOverloads([suppressed({ candidateGraphIds: many })]);
+ expect(refs.complete).toBe(false);
+ expect(refs.sites[0].candidateIds).toHaveLength(MAX_OVERLOAD_IDENTITIES_PER_SITE);
+ const huge = summarizeUnresolvedOverloads([
+ suppressed({ name: 'x'.repeat(MAX_OVERLOAD_SUMMARY_BYTES) }),
+ ]);
+ expect(huge).toMatchObject({ complete: false, omittedSites: 1, sites: [] });
+ expect(Buffer.byteLength(JSON.stringify(huge))).toBeLessThan(MAX_OVERLOAD_SUMMARY_BYTES);
+ });
+
+ it('is deterministic and preserves identities through JSON including prototype-shaped strings', () => {
+ const one = suppressed({ candidateGraphIds: ['constructor', '__proto__', 'toString'] });
+ const two = suppressed({ range: { startLine: 10, startCol: 2, endLine: 10, endCol: 20 } });
+ const summary = summarizeUnresolvedOverloads([one, two]);
+ expect(summarizeUnresolvedOverloads([two, one])).toEqual(summary);
+ const restored = JSON.parse(JSON.stringify(summary));
+ expect(probe(restored, ['__proto__', 'constructor'])).toEqual({
+ sites: 1,
+ notes: [expect.stringContaining('1 call site')],
+ });
+ });
+
+ it('supports caller-file filtering without counting excluded sites', () => {
+ const summary = summarizeUnresolvedOverloads([suppressed({ filePath: 'test/Caller.java' })]);
+ expect(probeOverloadBoundaries(summary, new Set([target]), 'upstream', () => false)).toEqual({
+ notes: [],
+ sites: 0,
+ });
+ });
+
+ it('hedges a capped traversal without inventing counts for unexamined targets', () => {
+ const summary = summarizeUnresolvedOverloads([suppressed()]);
+ expect(
+ probeOverloadBoundaries(summary, new Set(['unexamined']), 'upstream', () => true, false),
+ ).toEqual({
+ sites: 0,
+ notes: [expect.stringContaining('traversal was incomplete')],
+ });
+ expect(
+ probeOverloadBoundaries(
+ summarizeUnresolvedOverloads([]),
+ new Set([target]),
+ 'upstream',
+ () => true,
+ false,
+ ),
+ ).toEqual({
+ sites: 0,
+ notes: [],
+ });
+ });
+
+ it.each([
+ null,
+ [],
+ { schemaVersion: 2, complete: true, totalSites: 0, omittedSites: 0, sites: [] },
+ { schemaVersion: 1, complete: true, totalSites: -1, omittedSites: 0, sites: [] },
+ { schemaVersion: 1, complete: true, totalSites: 1, omittedSites: 0, sites: [] },
+ { schemaVersion: 1, complete: true, totalSites: 1, omittedSites: 1, sites: [] },
+ { schemaVersion: 1, complete: 'true', totalSites: 0, omittedSites: 0, sites: [] },
+ Object.create({ schemaVersion: 1, complete: true, totalSites: 0, omittedSites: 0, sites: [] }),
+ ])('treats malformed receipts as unverified: %j', (summary) => {
+ expect(probe(summary, [target])).toEqual({
+ sites: 0,
+ notes: [expect.stringContaining('metadata is invalid')],
+ });
+ });
+
+ it('rejects malformed, duplicated or forged site identities', () => {
+ const summary = summarizeUnresolvedOverloads([suppressed()]);
+ for (const sites of [
+ [{ ...summary.sites[0], candidateIds: ['same', 'same'] }],
+ [{ ...summary.sites[0], id: 'invented' }],
+ [{ ...summary.sites[0], range: { startLine: 1, startCol: 0, endLine: 0, endCol: 0 } }],
+ [{ ...summary.sites[0], reasons: ['receiver-unresolved'] }],
+ [summary.sites[0], summary.sites[0]],
+ ]) {
+ expect(
+ probe({ ...summary, totalSites: sites.length, sites }, [target]).notes.join(' '),
+ ).toContain('metadata is invalid');
+ }
+ });
+});
diff --git a/gitnexus/vitest.config.ts b/gitnexus/vitest.config.ts
index 8315e6e6f..e70c94929 100644
--- a/gitnexus/vitest.config.ts
+++ b/gitnexus/vitest.config.ts
@@ -115,6 +115,7 @@ export default defineConfig({
'test/integration/api-impact-e2e.test.ts',
'test/integration/shape-check-regression.test.ts',
'test/integration/java-class-impact.test.ts',
+ 'test/integration/java-overload-impact-persistence.test.ts',
'test/integration/class-impact-all-languages.test.ts',
'test/integration/lbug-orphan-sidecar-recovery.test.ts',
'test/integration/lbug-interrupted-checkpoint-recovery.test.ts',
@@ -227,6 +228,7 @@ export default defineConfig({
'test/integration/api-impact-e2e.test.ts',
'test/integration/shape-check-regression.test.ts',
'test/integration/java-class-impact.test.ts',
+ 'test/integration/java-overload-impact-persistence.test.ts',
'test/integration/class-impact-all-languages.test.ts',
'test/integration/lbug-orphan-sidecar-recovery.test.ts',
'test/integration/lbug-interrupted-checkpoint-recovery.test.ts',