From 9766cb7abfd255f5e6b941d26aca4e60b4da86cd Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Tue, 21 Jul 2026 06:16:32 +0000 Subject: [PATCH 1/4] fix(java): emit a graph node for record_declaration (#2564) JAVA_QUERIES had no @definition.record capture, unlike its class_declaration/interface_declaration/enum_declaration siblings and unlike CSHARP_QUERIES' own record_declaration pattern. A Java record's container node was never created, so its HAS_METHOD edges were dropped at persistence even though ownership resolution computed a valid ownerId for its methods. Downstream label mapping, the class-extractor config, the dispatch table, and ownership reconciliation already treated 'Record' correctly - this was purely a missing structure-phase capture. --- .../src/core/ingestion/tree-sitter-queries.ts | 3 +- .../java-record-methods/Point.java | 11 ++++++ .../test/integration/resolvers/java.test.ts | 34 +++++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 gitnexus/test/fixtures/lang-resolution/java-record-methods/Point.java diff --git a/gitnexus/src/core/ingestion/tree-sitter-queries.ts b/gitnexus/src/core/ingestion/tree-sitter-queries.ts index d1b0359aa..3261913ba 100644 --- a/gitnexus/src/core/ingestion/tree-sitter-queries.ts +++ b/gitnexus/src/core/ingestion/tree-sitter-queries.ts @@ -743,10 +743,11 @@ export const PYTHON_QUERIES = ` // Java queries - works with tree-sitter-java export const JAVA_QUERIES = ` -; Classes, Interfaces, Enums, Annotations +; Classes, Interfaces, Enums, Records, Annotations (class_declaration name: (identifier) @name) @definition.class (interface_declaration name: (identifier) @name) @definition.interface (enum_declaration name: (identifier) @name) @definition.enum +(record_declaration name: (identifier) @name) @definition.record (annotation_type_declaration name: (identifier) @name) @definition.annotation ; Anonymous class bodies: new Runnable() { ... } — no @name capture; the diff --git a/gitnexus/test/fixtures/lang-resolution/java-record-methods/Point.java b/gitnexus/test/fixtures/lang-resolution/java-record-methods/Point.java new file mode 100644 index 000000000..f4471a1e1 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/java-record-methods/Point.java @@ -0,0 +1,11 @@ +package probe; + +public record Point(int x, int y) { + public int sum() { + return x + y; + } + + public int scaled(int factor) { + return sum() * factor; + } +} diff --git a/gitnexus/test/integration/resolvers/java.test.ts b/gitnexus/test/integration/resolvers/java.test.ts index 66bc820dd..0d53ce0eb 100644 --- a/gitnexus/test/integration/resolvers/java.test.ts +++ b/gitnexus/test/integration/resolvers/java.test.ts @@ -1174,6 +1174,40 @@ describe('Java chained method call resolution', () => { }); }); +// --------------------------------------------------------------------------- +// Java record: container node + HAS_METHOD edges +// Regression test for #2564: JAVA_QUERIES previously had no @definition.record +// capture, so a record never got a Class/Record graph node — its methods +// existed as ownerless orphans with no HAS_METHOD edge. +// --------------------------------------------------------------------------- + +describe('Java record method resolution (#2564)', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo(path.join(FIXTURES, 'java-record-methods'), () => {}); + }, 60000); + + it('detects a Record node for Point', () => { + const records = getNodesByLabel(result, 'Record'); + expect(records).toContain('Point'); + }); + + it('emits HAS_METHOD edges linking sum and scaled to Point', () => { + const hasMethod = getRelationships(result, 'HAS_METHOD'); + const sumEdge = hasMethod.find((e) => e.source === 'Point' && e.target === 'sum'); + const scaledEdge = hasMethod.find((e) => e.source === 'Point' && e.target === 'scaled'); + expect(sumEdge).toBeDefined(); + expect(scaledEdge).toBeDefined(); + }); + + it('resolves scaled() calling sum() via a CALLS edge', () => { + const calls = getRelationships(result, 'CALLS'); + const sumCall = calls.find((c) => c.target === 'sum' && c.source === 'scaled'); + expect(sumCall).toBeDefined(); + }); +}); + // --------------------------------------------------------------------------- // Java 16+ instanceof pattern variable: `if (obj instanceof User user)` // Phase 5.2: extractPatternBinding on instanceof_expression binds user → User. From 61ed3421fa08ee0a0f8aa20ad7b5839a89904fe8 Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Tue, 21 Jul 2026 06:21:45 +0000 Subject: [PATCH 2/4] fix(java): treat a new-expression as a typed receiver for its chained call (#2564) new Local().inner() bound the whole object_creation_expression as @reference.receiver, so its raw source text ("new Local()") became the receiver name. That text can never match a scope binding, so the call silently fell through to name-only fallback resolution and could resolve to an unrelated same-named method on a collision. Normalize the receiver to the constructed type's simple name (reusing javaBaseSimpleNameOf, already used for the anonymous-class inheritance edge) so Case 2 (class-name / static receiver) in receiver-bound-calls.ts resolves it via its normal MRO walk. Mirrors the existing normalizePhpReceiver precedent in php/captures.ts - a language-local capture rewrite, no shared-pipeline change. --- .../core/ingestion/languages/java/captures.ts | 23 ++++++++++++ .../java-new-expr-chain-call/LocalChain.java | 12 +++++++ .../java-new-expr-chain-call/Other.java | 7 ++++ .../test/integration/resolvers/java.test.ts | 35 +++++++++++++++++++ 4 files changed, 77 insertions(+) create mode 100644 gitnexus/test/fixtures/lang-resolution/java-new-expr-chain-call/LocalChain.java create mode 100644 gitnexus/test/fixtures/lang-resolution/java-new-expr-chain-call/Other.java diff --git a/gitnexus/src/core/ingestion/languages/java/captures.ts b/gitnexus/src/core/ingestion/languages/java/captures.ts index 542d09d38..9864d0763 100644 --- a/gitnexus/src/core/ingestion/languages/java/captures.ts +++ b/gitnexus/src/core/ingestion/languages/java/captures.ts @@ -150,6 +150,29 @@ export function emitJavaScopeCaptures( continue; } + // Normalize a `new`-expression receiver to its constructed type's simple + // name: `new Local().inner()` binds the WHOLE `object_creation_expression` + // as `@reference.receiver`, so its raw text is `"new Local()"` — a string + // that can never match a scope binding, so the compound-receiver resolver + // silently falls through to name-only fallback resolution and picks the + // wrong same-named method on a collision (#2564). Rewriting the text to + // just `Local` lets Case 2 (class-name / static receiver) in + // receiver-bound-calls.ts resolve it via its normal MRO walk. Mirrors the + // established `normalizePhpReceiver` precedent (php/captures.ts) — a + // language-local capture rewrite, no shared-pipeline change. + if (grouped['@reference.receiver'] !== undefined) { + const receiverNode = nodeIfType(nodeMap['@reference.receiver'], 'object_creation_expression'); + const typeNode = receiverNode?.childForFieldName('type'); + const simpleName = typeNode ? javaBaseSimpleNameOf(typeNode) : undefined; + if (simpleName !== undefined) { + grouped['@reference.receiver'] = syntheticCapture( + '@reference.receiver', + receiverNode!, + simpleName, + ); + } + } + // Filter read.member when it's a child of method_invocation or assignment. // `@reference.read.member` is captured directly on the `field_access` node. if (grouped['@reference.read.member'] !== undefined) { diff --git a/gitnexus/test/fixtures/lang-resolution/java-new-expr-chain-call/LocalChain.java b/gitnexus/test/fixtures/lang-resolution/java-new-expr-chain-call/LocalChain.java new file mode 100644 index 000000000..599f8d5ed --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/java-new-expr-chain-call/LocalChain.java @@ -0,0 +1,12 @@ +package probe; + +public class LocalChain { + void m() { + class Local { + void inner() { + System.out.println("right target"); + } + } + new Local().inner(); + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/java-new-expr-chain-call/Other.java b/gitnexus/test/fixtures/lang-resolution/java-new-expr-chain-call/Other.java new file mode 100644 index 000000000..2b722fc3d --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/java-new-expr-chain-call/Other.java @@ -0,0 +1,7 @@ +package probe; + +class Other { + void inner() { + System.out.println("wrong target"); + } +} diff --git a/gitnexus/test/integration/resolvers/java.test.ts b/gitnexus/test/integration/resolvers/java.test.ts index 0d53ce0eb..c7e0545c0 100644 --- a/gitnexus/test/integration/resolvers/java.test.ts +++ b/gitnexus/test/integration/resolvers/java.test.ts @@ -1174,6 +1174,41 @@ describe('Java chained method call resolution', () => { }); }); +// --------------------------------------------------------------------------- +// Chained call on a new-expression receiver: new Local().inner() +// The receiver of inner() is an object_creation_expression, not a variable. +// Regression test for #2564: without treating `new Local()` as a typed +// receiver, the call falls back to name-only resolution and can pick an +// unrelated same-named method (Other.inner) instead of Local.inner. +// --------------------------------------------------------------------------- + +describe('Java chained call on a new-expression receiver (#2564)', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo(path.join(FIXTURES, 'java-new-expr-chain-call'), () => {}); + }, 60000); + + it('detects LocalChain and Other classes', () => { + const classes = getNodesByLabel(result, 'Class'); + expect(classes).toContain('LocalChain'); + expect(classes).toContain('Other'); + }); + + it('resolves new Local().inner() to the local Local#inner, NOT Other#inner', () => { + const calls = getRelationships(result, 'CALLS'); + const localInner = calls.find( + (c) => + c.target === 'inner' && c.source === 'm' && c.targetFilePath.includes('LocalChain.java'), + ); + const otherInner = calls.find( + (c) => c.target === 'inner' && c.source === 'm' && c.targetFilePath.includes('Other.java'), + ); + expect(localInner).toBeDefined(); + expect(otherInner).toBeUndefined(); + }); +}); + // --------------------------------------------------------------------------- // Java record: container node + HAS_METHOD edges // Regression test for #2564: JAVA_QUERIES previously had no @definition.record From 9c062197c94c088af866fdcd87c68aa4c42e1dae Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Tue, 21 Jul 2026 06:54:10 +0000 Subject: [PATCH 3/4] fix(storage): bump INCREMENTAL_SCHEMA_VERSION for the Java record fix (#2564) Review finding: the record_declaration container-node fix (894110bf) makes previously-uncaptured Record nodes and HAS_METHOD edges appear for the first time, but the incremental write set only covers changed files. Without this bump, an existing index would silently keep omitting the Record node and its HAS_METHOD edges for unchanged record files after an ordinary incremental analyze. Same contract as v7 (#2437/#2522) and the two closest precedents, v8 (#2550) and v9 (#2555), which bumped this constant for the identical "model X as first-class node" class of change. --- gitnexus/src/storage/repo-manager.ts | 10 +++++++++- gitnexus/test/unit/call-summary-schema-version.test.ts | 10 +++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/gitnexus/src/storage/repo-manager.ts b/gitnexus/src/storage/repo-manager.ts index 568d288bd..e34432a7a 100644 --- a/gitnexus/src/storage/repo-manager.ts +++ b/gitnexus/src/storage/repo-manager.ts @@ -429,8 +429,16 @@ export interface RepoMeta { * `E.hook` to `E$1.hook`, and nested-host anonymous names re-key * (`EnumWrap$1` → `EnumWrap$Mode$1`). Same contract as v8: identities move * on unchanged files; force a full re-analyze. + * v10: Java `record_declaration` now emits a first-class `Record` graph node + * (#2564): a record's container node was previously never created (JAVA_QUERIES + * had no capture for it), so its methods existed as ownerless Method nodes + * with no `HAS_METHOD` edge. The incremental write set only covers changed + * files — a top-up against a pre-v10 index would keep silently omitting the + * `Record` node and its `HAS_METHOD` edges for every unchanged record file + * (same v7 contract: new nodes/edges the incremental path would otherwise + * never backfill); force a full re-analyze instead. */ -export const INCREMENTAL_SCHEMA_VERSION = 9; +export const INCREMENTAL_SCHEMA_VERSION = 10; export interface IndexedRepo { repoPath: string; diff --git a/gitnexus/test/unit/call-summary-schema-version.test.ts b/gitnexus/test/unit/call-summary-schema-version.test.ts index 8e7e3b5ce..a8240fa0f 100644 --- a/gitnexus/test/unit/call-summary-schema-version.test.ts +++ b/gitnexus/test/unit/call-summary-schema-version.test.ts @@ -73,8 +73,8 @@ describe('CALL_SUMMARY relation-type exclusion (U-C1)', () => { }); describe('CALL_SUMMARY incremental reuse gate (U-C5)', () => { - it('INCREMENTAL_SCHEMA_VERSION is bumped to 9 (Java enum-constant-body + JLS-naming re-index window)', () => { - expect(INCREMENTAL_SCHEMA_VERSION).toBe(9); + it('INCREMENTAL_SCHEMA_VERSION is bumped to 10 (Java record container-node re-index window)', () => { + expect(INCREMENTAL_SCHEMA_VERSION).toBe(10); }); it('a pre-current stamp fails the `=== INCREMENTAL_SCHEMA_VERSION` reuse gate → forces full re-analyze', () => { @@ -108,7 +108,11 @@ describe('CALL_SUMMARY incremental reuse gate (U-C5)', () => { // topmost-anchored `EnumWrap$1`-style ids would be stranded alongside // the re-keyed ones on unchanged files → must NOT reuse. expect(passesReuseGate(8)).toBe(false); + // A pre-v10 (v9) index predates the Java record container-node fix + // (#2564) — a record's methods would keep being ownerless Method nodes + // with no HAS_METHOD edge on unchanged files → must NOT reuse. + expect(passesReuseGate(9)).toBe(false); // A current-version stamp passes the gate (incremental top-up eligible). - expect(passesReuseGate(9)).toBe(true); + expect(passesReuseGate(10)).toBe(true); }); }); From 5fb8aef59f34af45e313aae43006ce70fac2d43f Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Tue, 21 Jul 2026 07:18:38 +0000 Subject: [PATCH 4/4] test(bench): rebaseline java scope-capture fingerprint for record support (#2564) CI caught this: adding the record_declaration capture legitimately changes the pinned java capture fingerprint, same as every prior capture-behavior change to this language (#2550, #2555). Rebaselined following the established _rebaselined_* precedent; scaling ratio 1.059 stays well within the 1.5 budget. --- gitnexus/bench/scope-capture/baselines.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/gitnexus/bench/scope-capture/baselines.json b/gitnexus/bench/scope-capture/baselines.json index d4e86debb..d1413db56 100644 --- a/gitnexus/bench/scope-capture/baselines.json +++ b/gitnexus/bench/scope-capture/baselines.json @@ -89,7 +89,7 @@ "_rebaselined": "#1919 review CF3 fix: extended kotlin-local-property-owner (init/accessor destructuring) + new dart-accessor-owner fixture (getter/setter ownership). Fingerprint-only corpus drift; scaling ~1.0." }, "java": { - "fingerprint": "975b68aaac6d06094260fb0c67f9b1bc03692ba7220669d192aca9dccd5fc0ca", + "fingerprint": "85fc7af9c3c1bceac76cb4f27214410b04967682a2eaa7e468e26efd1f4e2537", "scaling_budget": 1.5, "_rebaselined_callable_flow_2522_review": "PR #2522 review hardening: callable operands retain expression/qualified identity and formals retain signature metadata; same-name lexical regions use an O(ancestor-depth) ID-set lookup. Prior d5c59d7dc9e206637515d5aea1163f7c1cdd76410c38c5fe6143d13d19677d6a -> 004a3592998dca1193bd1429a8284513725de7764f2a3eceedaaa984cfd763b4; scaling 0.992 < 1.5.", "_rebaselined_callable_flow_2522_followup": "PR #2522 follow-up: Java method-reference/SAM callable flow facts with invocation-result suppression. Prior 062d754764aaa8a6772fb90875c710502a63e3e7a300e633942381ed914faada -> d5c59d7dc9e206637515d5aea1163f7c1cdd76410c38c5fe6143d13d19677d6a; scaling 1.074 < 1.5.", @@ -97,7 +97,8 @@ "_note": "#1928 / #2045: F35 adds qualified + qualified-generic constructor query captures (`new pkg.Foo()`, `new a.b.Foo()`, `new pkg.Box()`); F38 synthesizes `@reference.call.constructor` on `super(...)`/`this(...)` explicit_constructor_invocation nodes; F41 generic-aware stripQualifier in interpret (type-binding normalization). + java-qualified-constructor and java-explicit-constructor fixtures. Pure capture-additive + fixture-corpus drift; scaling stays linear (~1.06).", "_rebaselined_2522_review_fixes": "PR #2522 review fixes: get/test dropped from callableProtocolMethods. Prior 004a3592998dca1193bd1429a8284513725de7764f2a3eceedaaa984cfd763b4 -> f3b4f4b6610e07c3ac90deb1c53d3572b6ad55a36e5d7134984876d30031ff67; scaling ratio re-verified within budget.", "_rebaselined_2550_instance_model": "PR #2549 (#2550): anonymous class bodies emit synthesized @declaration.class/@declaration.name (Worker$N), an @reference.inherits to the constructed type, and receiver @type-binding.* captures; six new java-* fixtures joined the corpus. Prior f3b4f4b6610e07c3ac90deb1c53d3572b6ad55a36e5d7134984876d30031ff67 -> d79c3b92acfc866094981499b977388ca14f90839bca0c040342ab1cec00aa90; scaling 1.058 < 1.5.", - "_rebaselined_2555_enum_constant_bodies": "PR for #2555: enum constant bodies emit synthesized E$N classes + @reference.inherits to the host enum; anonymous naming follows JLS 13.1 immediately-enclosing-type chains INCLUDING anonymous enclosing types (NestHost$1$1, N$1$1); six new java-* fixtures joined the corpus. Prior d79c3b92acfc866094981499b977388ca14f90839bca0c040342ab1cec00aa90 -> 975b68aaac6d06094260fb0c67f9b1bc03692ba7220669d192aca9dccd5fc0ca; scaling 1.05 < 1.5." + "_rebaselined_2555_enum_constant_bodies": "PR for #2555: enum constant bodies emit synthesized E$N classes + @reference.inherits to the host enum; anonymous naming follows JLS 13.1 immediately-enclosing-type chains INCLUDING anonymous enclosing types (NestHost$1$1, N$1$1); six new java-* fixtures joined the corpus. Prior d79c3b92acfc866094981499b977388ca14f90839bca0c040342ab1cec00aa90 -> 975b68aaac6d06094260fb0c67f9b1bc03692ba7220669d192aca9dccd5fc0ca; scaling 1.05 < 1.5.", + "_rebaselined_2564_record_capture": "PR for #2564: JAVA_QUERIES gained a (record_declaration name: (identifier) @name) @definition.record capture, previously entirely missing (record_declaration had no structure-phase capture at all, unlike class/interface/enum) - a record's methods existed as ownerless Method nodes with no HAS_METHOD edge. Two new java-* fixtures (java-record-methods, java-new-expr-chain-call) joined the corpus. Prior 975b68aaac6d06094260fb0c67f9b1bc03692ba7220669d192aca9dccd5fc0ca -> 85fc7af9c3c1bceac76cb4f27214410b04967682a2eaa7e468e26efd1f4e2537; scaling 1.059 < 1.5." }, "typescript": { "fingerprint": "3280b13d3f9378ab23eee31c2edc779b5a9ae1e7bb510c23a24855b44406d2f4",