GitNexus/gitnexus/test/unit/scope-resolution/java/java-captures.test.ts
Abhinav Pandey 281ce2600c
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>
2026-06-05 06:39:12 +01:00

119 lines
4.9 KiB
TypeScript

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