From 0f9415c60bcc315b56a12355aed58050f7502d38 Mon Sep 17 00:00:00 2001 From: Abhinav Pandey Date: Fri, 5 Jun 2026 02:24:19 +0530 Subject: [PATCH 1/3] fix(java): close parsing-layer coverage gaps F35/F38/F41 (#1928) Registry-primary scope-resolution path (the live one post-#942/#943): - F35 [HIGH]: qualified / qualified-generic constructor calls. `new pkg.Foo()` parses as a `scoped_type_identifier` that the query bound only as `@reference.call.constructor.qualified` with no `@reference.name`, so the scope extractor fell back to the whole-expression anchor and the reference name became the raw `new pkg.Foo()` text (never resolved). Bind the simple -name tail (end-anchored last child) and add an arm for the previously uncaptured `new pkg.Box()` (qualified + generic) shape. - F38 [MEDIUM]: `super(...)` / `this(...)` explicit constructor invocations, modeled as `explicit_constructor_invocation` and never matched by the scope query, dropped the chained-constructor CALLS edges. Synthesize them with the target resolved structurally (this -> enclosing type name; super -> superclass tail via the shared javaBaseLookupNameNode, skipping implicit Object) plus arity for overload disambiguation. - F41 [LOW]: interpretJavaTypeBinding stripped the qualifier before generics, so a qualified generic type arg (`Map`) was cut inside the generic into `User>`. Strip generics first, then the qualifier; make the erasure fallback qualifier-tolerant. F36/F37 already landed upstream (#1940/#1956); F39/F40 are legacy-bank remnants that are no longer consumed (legacy @import skipped in parse-worker; legacy @call never read in parse-impl) so they are intentionally left untouched. Tests: low-level capture unit tests (constructor shapes incl. double-match guard; super/this/enum/implicit-Object), interpretJavaTypeBinding unit tests (qualified generic args + the corruption case), and end-to-end resolver tests with new fixtures asserting the CALLS edges resolve to the correct constructors. Co-authored-by: Cursor --- .../core/ingestion/languages/java/captures.ts | 99 ++++++++++++++- .../ingestion/languages/java/interpret.ts | 18 ++- .../core/ingestion/languages/java/query.ts | 17 ++- .../models/Base.java | 5 + .../models/Child.java | 11 ++ .../java-qualified-constructor/App.java | 6 + .../java-qualified-constructor/pkg/Box.java | 5 + .../java-qualified-constructor/pkg/Foo.java | 7 ++ .../integration/resolvers/java-1928.test.ts | 66 ++++++++++ .../java/java-captures.test.ts | 119 ++++++++++++++++++ .../java/java-interpret.test.ts | 77 ++++++++++++ 11 files changed, 422 insertions(+), 8 deletions(-) create mode 100644 gitnexus/test/fixtures/lang-resolution/java-explicit-constructor/models/Base.java create mode 100644 gitnexus/test/fixtures/lang-resolution/java-explicit-constructor/models/Child.java create mode 100644 gitnexus/test/fixtures/lang-resolution/java-qualified-constructor/App.java create mode 100644 gitnexus/test/fixtures/lang-resolution/java-qualified-constructor/pkg/Box.java create mode 100644 gitnexus/test/fixtures/lang-resolution/java-qualified-constructor/pkg/Foo.java create mode 100644 gitnexus/test/integration/resolvers/java-1928.test.ts create mode 100644 gitnexus/test/unit/scope-resolution/java/java-captures.test.ts create mode 100644 gitnexus/test/unit/scope-resolution/java/java-interpret.test.ts diff --git a/gitnexus/src/core/ingestion/languages/java/captures.ts b/gitnexus/src/core/ingestion/languages/java/captures.ts index 85c5a9106e..cd44634598 100644 --- a/gitnexus/src/core/ingestion/languages/java/captures.ts +++ b/gitnexus/src/core/ingestion/languages/java/captures.ts @@ -216,7 +216,104 @@ export function emitJavaScopeCaptures( out.push(grouped); } - return [...resolveVarTypeBindings(out), ...synthesizeJavaInheritanceReferences(tree.rootNode)]; + return [ + ...resolveVarTypeBindings(out), + ...synthesizeJavaInheritanceReferences(tree.rootNode), + ...synthesizeJavaExplicitConstructorReferences(tree.rootNode), + ]; +} + +/** + * Synthesize `@reference.call.constructor` captures for explicit constructor + * invocations — `super(...)` and `this(...)` (F38 #1928). tree-sitter-java + * models these as `explicit_constructor_invocation` nodes, which the scope + * query does not match, so the chained-constructor CALLS edges (subclass ctor → + * superclass ctor; ctor → sibling overload) were silently dropped. + * + * The grammar gives no constructor *name* at the call site (the child is a bare + * `(super)` / `(this)` token), so the target name is resolved structurally: + * - `this(...)` → the enclosing type's own simple name (constructor symbols + * are keyed by the declaring class name). + * - `super(...)` → the enclosing class's superclass simple-name tail (reusing + * `javaBaseLookupNameNode` so qualified/generic supers reduce + * to the bare class name, matching the EXTENDS synth). An + * implicit `Object` super (no `superclass` field) has no + * in-graph symbol, so it is skipped rather than emitting a + * dangling reference. + * Arity is attached so overloaded constructors disambiguate downstream, mirroring + * the call-site arity synthesized for `new X(...)`. + */ +function synthesizeJavaExplicitConstructorReferences(root: SyntaxNode): CaptureMatch[] { + const out: CaptureMatch[] = []; + const stack: SyntaxNode[] = [root]; + while (stack.length > 0) { + const node = stack.pop()!; + if (node.type === 'explicit_constructor_invocation') { + emitJavaExplicitConstructorRef(out, node); + } + for (let i = 0; i < node.namedChildCount; i++) { + const child = node.namedChild(i); + if (child !== null) stack.push(child); + } + } + return out; +} + +const TYPE_DECL_NODE_TYPES = new Set([ + 'class_declaration', + 'enum_declaration', + 'record_declaration', +]); + +function emitJavaExplicitConstructorRef(out: CaptureMatch[], node: SyntaxNode): void { + const ctor = node.childForFieldName('constructor'); + if (ctor === null) return; + + const enclosingType = findEnclosingTypeDeclaration(node); + if (enclosingType === null) return; + + let targetNameNode: SyntaxNode | null = null; + if (ctor.type === 'this') { + targetNameNode = enclosingType.childForFieldName('name'); + } else if (ctor.type === 'super') { + // Only class_declaration carries a `superclass` field; enum/record cannot + // declare an explicit superclass, so `super(...)` there has no resolvable + // target symbol. + const superclass = enclosingType.childForFieldName('superclass'); + if (superclass === null) return; + for (const base of superclass.namedChildren) { + if (base === null) continue; + const nameNode = javaBaseLookupNameNode(base); + if (nameNode !== null) { + targetNameNode = nameNode; + break; + } + } + } + if (targetNameNode === null) return; + + const argList = node.childForFieldName('arguments'); + const args = + argList === null + ? [] + : argList.namedChildren.filter( + (c) => c !== null && c.type !== 'block_comment' && c.type !== 'line_comment', + ); + + out.push({ + '@reference.call.constructor': nodeToCapture('@reference.call.constructor', node), + '@reference.name': nodeToCapture('@reference.name', targetNameNode), + '@reference.arity': syntheticCapture('@reference.arity', node, String(args.length)), + }); +} + +function findEnclosingTypeDeclaration(node: SyntaxNode): SyntaxNode | null { + let cur: SyntaxNode | null = node.parent; + while (cur !== null) { + if (TYPE_DECL_NODE_TYPES.has(cur.type)) return cur; + cur = cur.parent; + } + return null; } /** diff --git a/gitnexus/src/core/ingestion/languages/java/interpret.ts b/gitnexus/src/core/ingestion/languages/java/interpret.ts index 87da580b6f..09c911aa97 100644 --- a/gitnexus/src/core/ingestion/languages/java/interpret.ts +++ b/gitnexus/src/core/ingestion/languages/java/interpret.ts @@ -74,10 +74,14 @@ export function interpretJavaTypeBinding(captures: CaptureMatch): ParsedTypeBind const typeCap = captures['@type-binding.type']; if (nameCap === undefined || typeCap === undefined) return null; - // Strip qualifier first so that `com.example.BaseModel` becomes - // `BaseModel` before stripGeneric — the JVM-erasure fallback pattern - // requires an unqualified identifier at the start of the string. - const rawType = stripGeneric(stripQualifier(typeCap.text.trim())); + // Strip generics BEFORE the qualifier (F41 #1928). Stripping the qualifier + // first uses `lastIndexOf('.')`, which for a qualified *type argument* + // (`Map`) cuts inside the generic and yields a + // corrupted `User>`. Unwrapping generics first reduces the string to a single + // (possibly qualified) class name, then the qualifier strip leaves the bare + // simple name. `stripGeneric`'s erasure fallback is qualifier-tolerant so a + // qualified generic base (`com.example.BaseModel`) still reduces correctly. + const rawType = stripQualifier(stripGeneric(typeCap.text.trim())); // Skip `var` — tree-sitter-java parses `var` as type_identifier with // text "var". When used without a constructor initializer, there's no @@ -127,8 +131,10 @@ function stripGeneric(text: string): string { // `BaseModel` → `BaseModel`, `Builder` → `Builder`. // This mirrors JVM type erasure — the raw class name is the resolvable symbol. // The pattern matches up to the first `<` to handle nested generics safely - // (e.g. `BaseModel>` → `BaseModel`). - const fallback = text.match(/^([A-Za-z_$][A-Za-z0-9_$]*)<.+>$/s); + // (e.g. `BaseModel>` → `BaseModel`). The base is allowed to be + // qualified (`com.example.BaseModel` → `com.example.BaseModel`) since the + // caller strips the qualifier afterwards (F41 #1928). + const fallback = text.match(/^((?:[A-Za-z_$][A-Za-z0-9_$]*\.)*[A-Za-z_$][A-Za-z0-9_$]*)<.+>$/s); if (fallback !== null) return fallback[1].trim(); return text; diff --git a/gitnexus/src/core/ingestion/languages/java/query.ts b/gitnexus/src/core/ingestion/languages/java/query.ts index e1e581ad52..4821e9703b 100644 --- a/gitnexus/src/core/ingestion/languages/java/query.ts +++ b/gitnexus/src/core/ingestion/languages/java/query.ts @@ -214,8 +214,23 @@ const JAVA_SCOPE_QUERY = ` type: (generic_type (type_identifier) @reference.name)) @reference.call.constructor +;; References — qualified constructor calls: new pkg.Foo(), new a.b.Foo() (F35 #1928) +;; tree-sitter-java parses \`pkg.Foo\` as a scoped_type_identifier whose final +;; child is the simple type. Bind that tail as @reference.name (trailing \`.\` +;; anchor = last child) so resolution targets \`Foo\`, not the raw \`pkg.Foo\` text. +;; Mirrors the TS/JS new-expression qualified-constructor capture. (object_creation_expression - type: (scoped_type_identifier) @reference.call.constructor.qualified) @reference.call.constructor + type: (scoped_type_identifier + (type_identifier) @reference.name .) @reference.call.constructor.qualified) @reference.call.constructor + +;; References — qualified + generic constructor calls: new pkg.Box() (F35 #1928) +;; The base is a generic_type whose first child is a scoped_type_identifier, so +;; neither the simple-generic nor the plain-scoped arm above matches it. Bind the +;; scoped tail as @reference.name. +(object_creation_expression + type: (generic_type + (scoped_type_identifier + (type_identifier) @reference.name .) @reference.call.constructor.qualified)) @reference.call.constructor ;; References — method references: User::getName, obj::method (method_reference diff --git a/gitnexus/test/fixtures/lang-resolution/java-explicit-constructor/models/Base.java b/gitnexus/test/fixtures/lang-resolution/java-explicit-constructor/models/Base.java new file mode 100644 index 0000000000..2c9334f2e7 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/java-explicit-constructor/models/Base.java @@ -0,0 +1,5 @@ +package models; + +public class Base { + public Base(int x) {} +} diff --git a/gitnexus/test/fixtures/lang-resolution/java-explicit-constructor/models/Child.java b/gitnexus/test/fixtures/lang-resolution/java-explicit-constructor/models/Child.java new file mode 100644 index 0000000000..14de7ea12b --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/java-explicit-constructor/models/Child.java @@ -0,0 +1,11 @@ +package models; + +public class Child extends Base { + public Child() { + super(1); + } + + public Child(int x) { + this(); + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/java-qualified-constructor/App.java b/gitnexus/test/fixtures/lang-resolution/java-qualified-constructor/App.java new file mode 100644 index 0000000000..c6147a221f --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/java-qualified-constructor/App.java @@ -0,0 +1,6 @@ +public class App { + public void make() { + pkg.Foo f = new pkg.Foo(); + new pkg.Box(); + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/java-qualified-constructor/pkg/Box.java b/gitnexus/test/fixtures/lang-resolution/java-qualified-constructor/pkg/Box.java new file mode 100644 index 0000000000..05c940e714 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/java-qualified-constructor/pkg/Box.java @@ -0,0 +1,5 @@ +package pkg; + +public class Box { + public Box() {} +} diff --git a/gitnexus/test/fixtures/lang-resolution/java-qualified-constructor/pkg/Foo.java b/gitnexus/test/fixtures/lang-resolution/java-qualified-constructor/pkg/Foo.java new file mode 100644 index 0000000000..87bcc7cd09 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/java-qualified-constructor/pkg/Foo.java @@ -0,0 +1,7 @@ +package pkg; + +public class Foo { + public Foo() {} + + public void run() {} +} diff --git a/gitnexus/test/integration/resolvers/java-1928.test.ts b/gitnexus/test/integration/resolvers/java-1928.test.ts new file mode 100644 index 0000000000..55e53f7089 --- /dev/null +++ b/gitnexus/test/integration/resolvers/java-1928.test.ts @@ -0,0 +1,66 @@ +/** + * Java parsing-layer coverage gaps (#1928) — end-to-end resolution. + * + * - F35: qualified / qualified-generic constructor calls (`new pkg.Foo()`, + * `new pkg.Box()`) resolve to the target constructor instead of + * dropping the edge on a corrupted `pkg.Foo` reference name. + * - F38: `super(...)` / `this(...)` explicit constructor invocations emit CALLS + * edges to the superclass / sibling constructor. + */ +import { describe, it, expect, beforeAll } from 'vitest'; +import path from 'path'; +import { FIXTURES, getRelationships, runPipelineFromRepo, type PipelineResult } from './helpers.js'; + +describe('Java qualified constructor resolution (F35 #1928)', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo(path.join(FIXTURES, 'java-qualified-constructor'), () => {}); + }, 60000); + + it('resolves `new pkg.Foo()` to the Foo constructor', () => { + const calls = getRelationships(result, 'CALLS'); + const fooCtor = calls.find((c) => c.target === 'Foo' && c.source === 'make'); + expect(fooCtor).toBeDefined(); + expect(fooCtor!.targetLabel).toBe('Constructor'); + expect(fooCtor!.targetFilePath).toBe('pkg/Foo.java'); + }); + + it('resolves `new pkg.Box()` to the Box constructor', () => { + const calls = getRelationships(result, 'CALLS'); + const boxCtor = calls.find((c) => c.target === 'Box' && c.source === 'make'); + expect(boxCtor).toBeDefined(); + expect(boxCtor!.targetLabel).toBe('Constructor'); + expect(boxCtor!.targetFilePath).toBe('pkg/Box.java'); + }); + + it('does not emit a CALLS edge to a corrupted `pkg.Foo` / `pkg.Box` name', () => { + const calls = getRelationships(result, 'CALLS'); + expect(calls.some((c) => c.target === 'pkg.Foo' || c.target === 'pkg.Box')).toBe(false); + }); +}); + +describe('Java explicit constructor invocation resolution (F38 #1928)', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo(path.join(FIXTURES, 'java-explicit-constructor'), () => {}); + }, 60000); + + it('resolves `super(1)` in Child() to the Base constructor', () => { + const calls = getRelationships(result, 'CALLS'); + const superCall = calls.find((c) => c.target === 'Base' && c.targetLabel === 'Constructor'); + expect(superCall).toBeDefined(); + expect(superCall!.source).toBe('Child'); + expect(superCall!.targetFilePath).toBe('models/Base.java'); + }); + + it('resolves `this()` in Child(int) to the Child constructor', () => { + const calls = getRelationships(result, 'CALLS'); + const thisCall = calls.find( + (c) => c.target === 'Child' && c.targetLabel === 'Constructor' && c.source === 'Child', + ); + expect(thisCall).toBeDefined(); + expect(thisCall!.targetFilePath).toBe('models/Child.java'); + }); +}); diff --git a/gitnexus/test/unit/scope-resolution/java/java-captures.test.ts b/gitnexus/test/unit/scope-resolution/java/java-captures.test.ts new file mode 100644 index 0000000000..f4d5d0251b --- /dev/null +++ b/gitnexus/test/unit/scope-resolution/java/java-captures.test.ts @@ -0,0 +1,119 @@ +/** + * Low-level coverage for the Java scope-captures orchestrator + * (`emitJavaScopeCaptures`), focused on the #1928 parsing-layer fixes: + * + * - F35: qualified / qualified-generic constructor calls bind the simple-name + * tail as @reference.name (not the raw `pkg.Foo` text). + * - F38: `super(...)` / `this(...)` explicit constructor invocations are + * captured as @reference.call.constructor references with arity. + * + * Runs against the installed tree-sitter-java grammar so it catches grammar + * drift before the integration parity gate. + */ + +import { describe, it, expect } from 'vitest'; +import { emitJavaScopeCaptures } from '../../../../src/core/ingestion/languages/java/captures.js'; + +function wrapExpr(expr: string): string { + return `class C { void m() { ${expr}; } }`; +} + +/** All constructor-call matches in `src`, as `{ name, qualified, arity }`. */ +function ctorRefs(src: string) { + return emitJavaScopeCaptures(src, 'C.java') + .filter((m) => m['@reference.call.constructor'] !== undefined) + .map((m) => ({ + name: m['@reference.name']?.text, + qualified: m['@reference.call.constructor.qualified']?.text, + arity: m['@reference.arity']?.text, + })); +} + +describe('emitJavaScopeCaptures — constructor reference names (F35 #1928)', () => { + it('binds the simple name for an unqualified `new User()`', () => { + const refs = ctorRefs(wrapExpr('new User()')); + expect(refs).toContainEqual({ name: 'User', qualified: undefined, arity: '0' }); + }); + + it('binds the simple-name tail for a qualified `new pkg.Foo()`', () => { + const refs = ctorRefs(wrapExpr('new pkg.Foo()')); + const foo = refs.find((r) => r.name === 'Foo'); + expect(foo).toBeDefined(); + expect(foo!.qualified).toBe('pkg.Foo'); + // The name must be the bare tail, never the raw scoped text. + expect(refs.some((r) => r.name === 'pkg.Foo')).toBe(false); + }); + + it('binds the simple-name tail for a deeply-nested `new a.b.Foo()`', () => { + const refs = ctorRefs(wrapExpr('new a.b.Foo()')); + const foo = refs.find((r) => r.name === 'Foo'); + expect(foo).toBeDefined(); + expect(foo!.qualified).toBe('a.b.Foo'); + expect(refs.some((r) => r.name === 'a' || r.name === 'b')).toBe(false); + }); + + it('binds the simple name for a simple-generic `new Box()`', () => { + const refs = ctorRefs(wrapExpr('new Box()')); + const box = refs.find((r) => r.name === 'Box'); + expect(box).toBeDefined(); + expect(box!.qualified).toBeUndefined(); + }); + + it('binds the simple-name tail for a qualified-generic `new pkg.Box()`', () => { + const refs = ctorRefs(wrapExpr('new pkg.Box()')); + const box = refs.find((r) => r.name === 'Box'); + expect(box).toBeDefined(); + expect(box!.qualified).toBe('pkg.Box'); + expect(refs.some((r) => r.name === 'pkg.Box' || r.name === 'String')).toBe(false); + }); + + it('carries the argument arity on a qualified constructor call', () => { + const refs = ctorRefs(wrapExpr('new pkg.Foo(1, 2, 3)')); + const foo = refs.find((r) => r.name === 'Foo'); + expect(foo!.arity).toBe('3'); + }); + + it('emits exactly one constructor reference per `new` expression', () => { + // Regression guard: the qualified + qualified-generic arms must not + // double-match the plain/generic arms. + expect(ctorRefs(wrapExpr('new pkg.Foo()')).length).toBe(1); + expect(ctorRefs(wrapExpr('new pkg.Box()')).length).toBe(1); + expect(ctorRefs(wrapExpr('new a.b.Foo()')).length).toBe(1); + }); +}); + +describe('emitJavaScopeCaptures — explicit constructor invocations (F38 #1928)', () => { + it('captures `super(...)` as a constructor ref to the superclass simple name', () => { + const src = 'class C extends pkg.Base { C() { super(1, 2); } }'; + const refs = ctorRefs(src); + const sup = refs.find((r) => r.name === 'Base'); + expect(sup).toBeDefined(); + expect(sup!.arity).toBe('2'); + }); + + it('reduces a generic superclass `super(...)` target to the bare name', () => { + const src = 'class C extends Box { C() { super(); } }'; + const refs = ctorRefs(src); + expect(refs.some((r) => r.name === 'Box' && r.arity === '0')).toBe(true); + }); + + it('captures `this(...)` as a constructor ref to the enclosing class name', () => { + const src = 'class C { C() { this(1); } C(int x) {} }'; + const refs = ctorRefs(src); + const self = refs.find((r) => r.name === 'C' && r.arity === '1'); + expect(self).toBeDefined(); + }); + + it('does NOT synthesize a super ref when there is no explicit superclass', () => { + // Implicit `Object` super — no in-graph symbol, so no reference is emitted. + const src = 'class C { C() { super(); } }'; + const refs = ctorRefs(src); + expect(refs.length).toBe(0); + }); + + it('captures `this(...)` inside an enum constructor', () => { + const src = 'enum E { A; E() { this(1); } E(int x) {} }'; + const refs = ctorRefs(src); + expect(refs.some((r) => r.name === 'E' && r.arity === '1')).toBe(true); + }); +}); diff --git a/gitnexus/test/unit/scope-resolution/java/java-interpret.test.ts b/gitnexus/test/unit/scope-resolution/java/java-interpret.test.ts new file mode 100644 index 0000000000..8099b4fe93 --- /dev/null +++ b/gitnexus/test/unit/scope-resolution/java/java-interpret.test.ts @@ -0,0 +1,77 @@ +/** + * Coverage for `interpretJavaTypeBinding` type-name normalization, focused on + * F41 (#1928): generics must be stripped BEFORE the qualifier so a qualified + * generic *type argument* (`Map`) is not corrupted + * into `User>` by an early `lastIndexOf('.')` cut. + */ + +import { describe, it, expect } from 'vitest'; +import type { Capture, CaptureMatch } from 'gitnexus-shared'; +import { interpretJavaTypeBinding } from '../../../../src/core/ingestion/languages/java/interpret.js'; + +const ZERO_RANGE = { startLine: 0, startCol: 0, endLine: 0, endCol: 0 } as const; +const cap = (name: string, text: string): Capture => ({ name, text, range: ZERO_RANGE }); + +/** Build an annotation-source type binding (`Type name;`). */ +function binding(typeText: string, sourceTag = '@type-binding.annotation'): CaptureMatch { + return { + '@type-binding.name': cap('@type-binding.name', 'x'), + '@type-binding.type': cap('@type-binding.type', typeText), + [sourceTag]: cap(sourceTag, typeText), + }; +} + +/** The normalized `rawTypeName` for a given raw type string. */ +function raw(typeText: string): string | undefined { + return interpretJavaTypeBinding(binding(typeText))?.rawTypeName; +} + +describe('interpretJavaTypeBinding — type normalization (F41 #1928)', () => { + it('strips a qualifier from a plain qualified type', () => { + expect(raw('com.example.User')).toBe('User'); + }); + + it('strips generics from an unqualified generic base', () => { + expect(raw('BaseModel')).toBe('BaseModel'); + }); + + it('strips generics AND qualifier from a qualified generic base', () => { + expect(raw('com.example.BaseModel')).toBe('BaseModel'); + }); + + it('does not corrupt a qualified generic TYPE ARGUMENT (the F41 bug)', () => { + // Before the fix: stripQualifier ran first → `User>` (trailing bracket). + expect(raw('Map')).toBe('User'); + }); + + it('extracts the element type from a single-arg container with qualified arg', () => { + expect(raw('List')).toBe('User'); + }); + + it('extracts the element type from a qualified container', () => { + expect(raw('java.util.List')).toBe('User'); + }); + + it('extracts the element type from a simple container', () => { + expect(raw('List')).toBe('User'); + expect(raw('Optional')).toBe('User'); + }); + + it('extracts the value type from a two-arg map', () => { + expect(raw('Map')).toBe('User'); + }); + + it('passes through a plain simple type', () => { + expect(raw('User')).toBe('User'); + }); + + it('falls back to the raw class name for an unrecognized nested generic', () => { + // Nested generic args defeat the single/two-arg element extraction; the + // erasure fallback keeps the outer class name (pre-existing behavior). + expect(raw('List>')).toBe('List'); + }); + + it('returns null for a bare `var` with no concrete type', () => { + expect(interpretJavaTypeBinding(binding('var'))).toBeNull(); + }); +}); From 816321dee29930164529905fc67a34bc27fdb6d8 Mon Sep 17 00:00:00 2001 From: Abhinav Pandey Date: Fri, 5 Jun 2026 04:02:44 +0530 Subject: [PATCH 2/3] fix(scope-resolution): register Constructor overload keys so this()/super() chains don't self-loop (#1928 F38 review) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review of #2045 caught two gaps; both confirmed by reproduction. P2 — F38 this() emitted a self-loop. On the java-explicit-constructor fixture, Child(int){ this(); } produced CALLS Child()#0 -> Child()#0 instead of Child(int)#1 -> Child()#0. Root cause is the language-agnostic graph-bridge: the parse phase mints distinct Constructor nodes (Child#0, Child#1) carrying parameterTypes, but node-lookup.ts registered the parameter-types / shape overload keys only for Function/Method, never Constructor, so both ctors collapsed onto the first-wins qualified/simple key and the caller Child(int) resolved to Child#0 (the this() target). Extend the overload keys to Constructor in both node-lookup.ts (registration) and ids.ts (lookup) via a shared isOverloadableCallable predicate. Verified the edge now connects distinct nodes (Child#1 -> Child#0); super(1)->Base#1 still correct. No cross-language regressions (the 9 worker-path failures reproduce identically on clean HEAD). Also harden the integration test: it matched the this() edge on name only, which a self-loop satisfies; now assert the endpoints are DISTINCT constructors. P3 — F41 order-regression guard was inert (List> normalizes to List under both strip orders). Add List> -> List, which is corrupted to Foo> under the old order and only correct generics-first. Co-authored-by: Cursor --- .../scope-resolution/graph-bridge/ids.ts | 19 +++++++++++++++--- .../graph-bridge/node-lookup.ts | 20 +++++++++++++------ .../integration/resolvers/java-1928.test.ts | 12 ++++++++++- .../java/java-interpret.test.ts | 9 +++++++++ 4 files changed, 50 insertions(+), 10 deletions(-) diff --git a/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/ids.ts b/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/ids.ts index 92f3f2d8b7..441bb3e4b0 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/ids.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/ids.ts @@ -54,6 +54,16 @@ function isCallerAnchorLabel(label: NodeLabel): boolean { ); } +/** + * Callables whose same-name overloads occupy distinct graph nodes keyed by + * parameter types / shape. Must mirror `isOverloadableCallable` in + * `node-lookup.ts` so registration and lookup agree (Constructor included — + * #1928 F38). + */ +function isOverloadableCallable(label: NodeLabel | undefined): boolean { + return label === 'Function' || label === 'Method' || label === 'Constructor'; +} + /** * Look up a `SymbolDefinition` in the graph node lookup. * @@ -107,7 +117,7 @@ export function resolveDefGraphId( if (cHit !== undefined) return cHit; } if ( - (def.type === 'Function' || def.type === 'Method') && + isOverloadableCallable(def.type) && def.parameterTypes !== undefined && def.parameterTypeClasses !== undefined ) { @@ -120,9 +130,12 @@ export function resolveDefGraphId( } // Overload disambiguation: when the def carries parameter types, // try the parameter-typed key first so same-name same-arity - // overloads route to their distinct graph nodes. + // overloads route to their distinct graph nodes. Constructors are + // included so a `this(int)`/`super(int)` chain or `new Foo(int)` + // resolves to the matching ctor overload instead of first-wins + // collapsing onto another `Foo` ctor (a self-loop) — #1928 F38. if ( - (def.type === 'Function' || def.type === 'Method') && + isOverloadableCallable(def.type) && def.parameterTypes !== undefined && def.parameterTypes.length > 0 ) { diff --git a/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/node-lookup.ts b/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/node-lookup.ts index 80bc5dedd0..d7da8a56fa 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/node-lookup.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/node-lookup.ts @@ -99,11 +99,7 @@ export function buildGraphNodeLookup(graph: KnowledgeGraph): GraphNodeLookup { // a parameter-types-suffixed key so resolveDefGraphId can find // the right overload by matching its def's parameterTypes. const pTypes = (props as { parameterTypes?: readonly string[] }).parameterTypes; - if ( - pTypes !== undefined && - pTypes.length > 0 && - (node.label === 'Function' || node.label === 'Method') - ) { + if (pTypes !== undefined && pTypes.length > 0 && isOverloadableCallable(node.label)) { const pKey = qualifiedKey( props.filePath, node.label, @@ -115,7 +111,7 @@ export function buildGraphNodeLookup(graph: KnowledgeGraph): GraphNodeLookup { const pClasses = (props as { parameterTypeClasses?: readonly ParameterTypeClass[] }) .parameterTypeClasses; const shapeTag = parameterShapeIdTag(pTypes, pClasses); - if (shapeTag !== '' && (node.label === 'Function' || node.label === 'Method')) { + if (shapeTag !== '' && isOverloadableCallable(node.label)) { const shapeKey = qualifiedKey(props.filePath, node.label, `${keyQualified}${shapeTag}`); if (!lookup.has(shapeKey)) lookup.set(shapeKey, node.id); } @@ -162,6 +158,18 @@ export function buildGraphNodeLookup(graph: KnowledgeGraph): GraphNodeLookup { return lookup; } +/** + * Callables whose same-name overloads must route to distinct graph nodes via + * the parameter-types / shape key. Constructors belong here too: a class with + * `Foo()` and `Foo(int)` mints distinct `#0`/`#1` Constructor nodes, and a + * `this(...)`/`super(...)` edge (or any `new Foo(args)`) must reach the right + * one. Without the overload key both ctor nodes collapse onto the first-wins + * qualified/simple key, turning a `this()` chain into a self-loop (#1928 F38). + */ +function isOverloadableCallable(label: NodeLabel): boolean { + return label === 'Function' || label === 'Method' || label === 'Constructor'; +} + export function isLinkableLabel(label: NodeLabel): boolean { return ( label === 'Function' || diff --git a/gitnexus/test/integration/resolvers/java-1928.test.ts b/gitnexus/test/integration/resolvers/java-1928.test.ts index 55e53f7089..ce739cc420 100644 --- a/gitnexus/test/integration/resolvers/java-1928.test.ts +++ b/gitnexus/test/integration/resolvers/java-1928.test.ts @@ -53,14 +53,24 @@ describe('Java explicit constructor invocation resolution (F38 #1928)', () => { expect(superCall).toBeDefined(); expect(superCall!.source).toBe('Child'); expect(superCall!.targetFilePath).toBe('models/Base.java'); + // Source is the arity-0 `Child()`, where `super(1)` lives. + expect(superCall!.rel.sourceId).toContain('Child.Child#0'); + expect(superCall!.rel.targetId).toContain('Base.Base#1'); }); - it('resolves `this()` in Child(int) to the Child constructor', () => { + it('resolves `this()` in Child(int) to a DISTINCT Child constructor (no self-loop)', () => { const calls = getRelationships(result, 'CALLS'); const thisCall = calls.find( (c) => c.target === 'Child' && c.targetLabel === 'Constructor' && c.source === 'Child', ); expect(thisCall).toBeDefined(); expect(thisCall!.targetFilePath).toBe('models/Child.java'); + // The edge must connect DISTINCT constructors: the caller `Child(int)` (#1) + // chains to `Child()` (#0). A self-loop (`#0 → #0`) — the bug this PR's + // review caught (#1928 F38: ctor overload keys missing in node-lookup) — + // satisfies the name-only match above but must NOT pass here. + expect(thisCall!.rel.sourceId).not.toBe(thisCall!.rel.targetId); + expect(thisCall!.rel.sourceId).toContain('Child.Child#1'); + expect(thisCall!.rel.targetId).toContain('Child.Child#0'); }); }); diff --git a/gitnexus/test/unit/scope-resolution/java/java-interpret.test.ts b/gitnexus/test/unit/scope-resolution/java/java-interpret.test.ts index 8099b4fe93..989a94edd3 100644 --- a/gitnexus/test/unit/scope-resolution/java/java-interpret.test.ts +++ b/gitnexus/test/unit/scope-resolution/java/java-interpret.test.ts @@ -71,6 +71,15 @@ describe('interpretJavaTypeBinding — type normalization (F41 #1928)', () => { expect(raw('List>')).toBe('List'); }); + it('keeps the outer class for a QUALIFIED nested generic element (guards the strip order)', () => { + // Unlike `List>` (no dot inside the args, so it yields + // `List` under both strip orders), this input has a qualified nested + // element: the OLD order (stripQualifier first) cut inside the generic and + // produced a corrupted `Foo>`; only generics-first yields `List`. + // This is the case that actually fails if the F41 reorder regresses. + expect(raw('List>')).toBe('List'); + }); + it('returns null for a bare `var` with no concrete type', () => { expect(interpretJavaTypeBinding(binding('var'))).toBeNull(); }); From aaea9ee05dffe90b663a3f6821abba2f716b773b Mon Sep 17 00:00:00 2001 From: Abhinav Pandey Date: Fri, 5 Jun 2026 10:26:53 +0530 Subject: [PATCH 3/3] fix(java): update fingerprint and add notes for constructor query captures in baselines.json Updated the fingerprint for the Java section and added detailed notes regarding the enhancements in constructor query captures, including qualified and qualified-generic constructor queries. This change reflects ongoing improvements in the parsing layer coverage and fixture updates. --- 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 85b2efd580..e896c23832 100644 --- a/gitnexus/bench/scope-capture/baselines.json +++ b/gitnexus/bench/scope-capture/baselines.json @@ -57,9 +57,10 @@ "_rebaselined": "#1970 review + tri-review follow-ups: constructor-call retag, cascade calls, built-in suppression, enum scope, #1926 F24/F25, named-ctor dedup (crash fix), container-name binding suppression; heritage file-affinity resolution. Fixtures: member-call-contexts, constructor-body, named-constructor-body, heritage-name-collision, construct-cascade." }, "java": { - "fingerprint": "d5cf68e9faf92fffd928c1ee6e584c72cc65918d1f1b5078abb3bfe09ac699bf", + "fingerprint": "9b29cafe32873b4902bda311bd089ffc04efe08f13557b966d29544be514080a", "scaling_budget": 1.5, - "_rebaselined": "#1956 synth-widening: + java-iface-extends fixture; synthesizeJavaInheritanceReferences now ALSO walks interface_declaration extends_interfaces (interface IA extends IB, IC), matching the #1940 legacy leg. (Earlier U2+review: java-qualified-base fixture covers 2- AND 3-segment qualified bases guarding the legacy end-anchor; synth tail-resolves scoped bases.) Linear (~1.03). (Earliest: java added to bench, exposed+fixed the O(n^2) findNodeAtRange root-walk; 3.09 -> ~0.99.) | #942: scope-resolution-only cleanup reworded fixture comments; capture byte-positions shift, capture LOGIC unchanged." + "_rebaselined": "#1956 synth-widening: + java-iface-extends fixture; synthesizeJavaInheritanceReferences now ALSO walks interface_declaration extends_interfaces (interface IA extends IB, IC), matching the #1940 legacy leg. (Earlier U2+review: java-qualified-base fixture covers 2- AND 3-segment qualified bases guarding the legacy end-anchor; synth tail-resolves scoped bases.) Linear (~1.03). (Earliest: java added to bench, exposed+fixed the O(n^2) findNodeAtRange root-walk; 3.09 -> ~0.99.) | #942: scope-resolution-only cleanup reworded fixture comments; capture byte-positions shift, capture LOGIC unchanged.", + "_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)." }, "typescript": { "fingerprint": "3f44a4a6892698df2d145c8ff2812c3b318807648983c88aca28fbd694f172f9",