fix(java): resolve E.CONST.method() enum-constant receiver dispatch (#2561)

Calling a method on an enum-constant receiver (E.CONST.method()) emitted
no CALLS edge. The receiver "E.CONST" is a two-segment compound receiver;
resolveCompoundReceiverClass walks each dotted segment via the owning
class scope's typeBindings map, but enum constants had no typeBinding, so
the constant segment dead-ended and no target was ever resolved.

#2555/#2558 gave bodied constants a first-class synthesized E$N class with
an MRO that includes the host enum; this is the receiver-side follow-up.
synthesizeJavaAnonymousClassDeclarations now emits a class-scope
typeBinding for every enum constant's simple name -> its E$N class (bodied)
or the host enum itself (body-less), reusing the exact mechanism a field
declaration uses. The generic compound-receiver chain walk then resolves
E.CONST.method() with no change to any shared scope-resolution code.

Bodied dispatch (EnumConst.A.hook() -> EnumConst$1.hook#0) and body-less
inherited dispatch (Plain.A.m() -> Plain.m#0) are covered by new tests in
the existing java-enum-constant-body fixture; both were verified to fail
against the pre-fix tree.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude 2026-07-21 11:04:37 +00:00
parent 73dbd9e640
commit caf117cf98
4 changed files with 79 additions and 14 deletions

View file

@ -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),
});
}
}

View file

@ -21,4 +21,8 @@ class Unrelated {
public void caller() {
hook();
}
public void dispatchToConstant() {
EnumConst.A.hook();
}
}

View file

@ -0,0 +1,13 @@
public enum Plain {
A;
public void m() {
System.out.println("plain m");
}
}
class PlainCaller {
public void callPlain() {
Plain.A.m();
}
}

View file

@ -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');
});
});