diff --git a/gitnexus/src/core/ingestion/languages/java/captures.ts b/gitnexus/src/core/ingestion/languages/java/captures.ts index 3adb036c8..2212c4355 100644 --- a/gitnexus/src/core/ingestion/languages/java/captures.ts +++ b/gitnexus/src/core/ingestion/languages/java/captures.ts @@ -372,23 +372,42 @@ function synthesizeJavaAnonymousClassDeclarations(rootNode: SyntaxNode): Capture // constant's class extends its HOST ENUM (javac semantics), so the // inherits reference names the enum — giving `mroFor(E$N) ∋ E` and // keeping bare calls from the body to the enum's own helpers alive - // through the ownership gate's MRO arm. No receiver typeBinding piece: - // constants are not variable initializers; `E.A.hook()` dispatch rides - // the existing enum receiver machinery. + // through the ownership gate's MRO arm. for (const constant of rootNode.descendantsOfType('enum_constant')) { - const name = synthesizeJavaAnonymousClassName(constant); - if (name === undefined) continue; - const body = constant.childForFieldName?.('body'); - if (body === null || body === undefined || body.type !== 'class_body') continue; - out.push({ - '@declaration.class': nodeToCapture('@declaration.class', body), - '@declaration.name': syntheticCapture('@declaration.name', body, name), - }); const hostEnum = javaEnclosingEnumNameOf(constant); - if (hostEnum !== undefined) { + const bodiedName = synthesizeJavaAnonymousClassName(constant); + if (bodiedName !== undefined) { + const body = constant.childForFieldName?.('body'); + if (body !== null && body !== undefined && body.type === 'class_body') { + out.push({ + '@declaration.class': nodeToCapture('@declaration.class', body), + '@declaration.name': syntheticCapture('@declaration.name', body, bodiedName), + }); + if (hostEnum !== undefined) { + out.push({ + '@reference.inherits': nodeToCapture('@reference.inherits', body), + '@reference.name': syntheticCapture('@reference.name', body, hostEnum), + }); + } + } + } + + // Receiver dispatch (#2561): `E.CONST.method()` resolves through the + // generic compound-receiver chain walk, which looks up each dotted + // segment via the owning class scope's `typeBindings` map — the same + // mechanism a field declaration uses (`private User user;` binds + // `user` on the class scope). Binding the constant's own simple name + // there — to its synthesized `E$N` class when bodied (MRO includes E, + // so members inherited from the enum still resolve), or to the host + // enum itself when body-less — makes `E.CONST.method()` resolve with + // no changes to the shared receiver-binding machinery. + const constantNameNode = constant.childForFieldName?.('name'); + const constantType = bodiedName ?? hostEnum; + if (constantNameNode !== null && constantNameNode !== undefined && constantType !== undefined) { out.push({ - '@reference.inherits': nodeToCapture('@reference.inherits', body), - '@reference.name': syntheticCapture('@reference.name', body, hostEnum), + '@type-binding.annotation': nodeToCapture('@type-binding.annotation', constant), + '@type-binding.name': nodeToCapture('@type-binding.name', constantNameNode), + '@type-binding.type': syntheticCapture('@type-binding.type', constant, constantType), }); } } diff --git a/gitnexus/test/fixtures/lang-resolution/java-enum-constant-body/src/EnumConst.java b/gitnexus/test/fixtures/lang-resolution/java-enum-constant-body/src/EnumConst.java index 65292b0c5..6342aa576 100644 --- a/gitnexus/test/fixtures/lang-resolution/java-enum-constant-body/src/EnumConst.java +++ b/gitnexus/test/fixtures/lang-resolution/java-enum-constant-body/src/EnumConst.java @@ -21,4 +21,8 @@ class Unrelated { public void caller() { hook(); } + + public void dispatchToConstant() { + EnumConst.A.hook(); + } } diff --git a/gitnexus/test/fixtures/lang-resolution/java-enum-constant-body/src/Plain.java b/gitnexus/test/fixtures/lang-resolution/java-enum-constant-body/src/Plain.java new file mode 100644 index 000000000..87c750f61 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/java-enum-constant-body/src/Plain.java @@ -0,0 +1,13 @@ +public enum Plain { + A; + + public void m() { + System.out.println("plain m"); + } +} + +class PlainCaller { + public void callPlain() { + Plain.A.m(); + } +} diff --git a/gitnexus/test/integration/resolvers/java.test.ts b/gitnexus/test/integration/resolvers/java.test.ts index c7e0545c0..1eeb35e04 100644 --- a/gitnexus/test/integration/resolvers/java.test.ts +++ b/gitnexus/test/integration/resolvers/java.test.ts @@ -3055,3 +3055,32 @@ describe('Java enum constant bodies (#2555)', () => { expect(misattributed).toBeUndefined(); }, 60000); }); + +// --------------------------------------------------------------------------- +// #2561: E.CONST.method() emits no CALLS edge — the receiver-side follow-up +// to #2555. A bodied constant's receiver must resolve to its synthesized +// E$N class; a body-less constant's receiver must resolve to the host enum +// itself. +// --------------------------------------------------------------------------- + +describe('Java enum-constant receiver dispatch (#2561)', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo(path.join(FIXTURES, 'java-enum-constant-body'), () => {}); + }, 60000); + + it('resolves EnumConst.A.hook() to the bodied constant override (EnumConst$1.hook#0)', () => { + const calls = getRelationships(result, 'CALLS'); + const dispatch = calls.find((c) => c.source === 'dispatchToConstant' && c.target === 'hook'); + expect(dispatch).toBeDefined(); + expect(dispatch!.rel.targetId).toBe('Method:src/EnumConst.java:EnumConst$1.hook#0'); + }); + + it("resolves Plain.A.m() to the body-less constant's inherited enum method (Plain.m#0)", () => { + const calls = getRelationships(result, 'CALLS'); + const dispatch = calls.find((c) => c.source === 'callPlain' && c.target === 'm'); + expect(dispatch).toBeDefined(); + expect(dispatch!.rel.targetId).toBe('Method:src/Plain.java:Plain.m#0'); + }); +});