fix(java): close parsing-layer coverage gaps F35/F38/F41 (#1928) (#2045)

* 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<String>()` (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<String, com.example.User>`) 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 <cursoragent@cursor.com>

* fix(scope-resolution): register Constructor overload keys so this()/super() chains don't self-loop (#1928 F38 review)

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<Map<String,User>> normalizes to
List under both strip orders). Add List<com.x.Foo<String>> -> List, which is
corrupted to Foo<String>> under the old order and only correct generics-first.

Co-authored-by: Cursor <cursoragent@cursor.com>

* 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.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
This commit is contained in:
Abhinav Pandey 2026-06-05 11:09:12 +05:30 committed by GitHub
parent 93e04b46d6
commit 281ce2600c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 474 additions and 19 deletions

View file

@ -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<T>), 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<T>), 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<T>()`); 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",

View file

@ -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;
}
/**

View file

@ -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<T>` becomes
// `BaseModel<T>` 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<String, com.example.User>`) 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<T>`) 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<T>` → `BaseModel`, `Builder<Self>` → `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<List<String>>` → `BaseModel`).
const fallback = text.match(/^([A-Za-z_$][A-Za-z0-9_$]*)<.+>$/s);
// (e.g. `BaseModel<List<String>>` → `BaseModel`). The base is allowed to be
// qualified (`com.example.BaseModel<T>` → `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;

View file

@ -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<T>() (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

View file

@ -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
) {

View file

@ -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' ||

View file

@ -0,0 +1,5 @@
package models;
public class Base {
public Base(int x) {}
}

View file

@ -0,0 +1,11 @@
package models;
public class Child extends Base {
public Child() {
super(1);
}
public Child(int x) {
this();
}
}

View file

@ -0,0 +1,6 @@
public class App {
public void make() {
pkg.Foo f = new pkg.Foo();
new pkg.Box<String>();
}
}

View file

@ -0,0 +1,5 @@
package pkg;
public class Box<T> {
public Box() {}
}

View file

@ -0,0 +1,7 @@
package pkg;
public class Foo {
public Foo() {}
public void run() {}
}

View file

@ -0,0 +1,76 @@
/**
* Java parsing-layer coverage gaps (#1928) end-to-end resolution.
*
* - F35: qualified / qualified-generic constructor calls (`new pkg.Foo()`,
* `new pkg.Box<String>()`) 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<String>()` 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');
// 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 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');
});
});

View file

@ -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<String>()`', () => {
const refs = ctorRefs(wrapExpr('new Box<String>()'));
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<String>()`', () => {
const refs = ctorRefs(wrapExpr('new pkg.Box<String>()'));
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<String>()')).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<String> { 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);
});
});

View file

@ -0,0 +1,86 @@
/**
* Coverage for `interpretJavaTypeBinding` type-name normalization, focused on
* F41 (#1928): generics must be stripped BEFORE the qualifier so a qualified
* generic *type argument* (`Map<String, com.example.User>`) 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<T>')).toBe('BaseModel');
});
it('strips generics AND qualifier from a qualified generic base', () => {
expect(raw('com.example.BaseModel<T>')).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<String, com.example.User>')).toBe('User');
});
it('extracts the element type from a single-arg container with qualified arg', () => {
expect(raw('List<com.example.User>')).toBe('User');
});
it('extracts the element type from a qualified container', () => {
expect(raw('java.util.List<User>')).toBe('User');
});
it('extracts the element type from a simple container', () => {
expect(raw('List<User>')).toBe('User');
expect(raw('Optional<User>')).toBe('User');
});
it('extracts the value type from a two-arg map', () => {
expect(raw('Map<String, User>')).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<Map<String, User>>')).toBe('List');
});
it('keeps the outer class for a QUALIFIED nested generic element (guards the strip order)', () => {
// Unlike `List<Map<String, User>>` (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<String>>`; only generics-first yields `List`.
// This is the case that actually fails if the F41 reorder regresses.
expect(raw('List<com.x.Foo<String>>')).toBe('List');
});
it('returns null for a bare `var` with no concrete type', () => {
expect(interpretJavaTypeBinding(binding('var'))).toBeNull();
});
});