feat(resolution): conformance-aware chained-method resolution (#750) (#754)

* feat(resolution): conformance-aware chained-method resolution (#750)

A chained static-factory/fluent call whose method lives on a SUPERTYPE the
receiver conforms to — a protocol-extension method (Swift), an interface default
method, or an inherited superclass method — now resolves. resolveMethodOnType
falls back to walking the return type's implements/extends edges (via the new
context.getSupertypes) when the method isn't a direct member. Because those edges
don't exist during the single-pass resolution, a second pass
(resolveChainedCallsViaConformance) re-resolves the deferred chained refs after
edges are built. Still validated, so a wrong inference yields no edge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(changelog): conformance-aware chained-method resolution (#750)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-06-09 01:38:37 -04:00
committed by GitHub
co-authored by Claude Opus 4.8
parent aa07dc59d4
commit 48d4654e8d
7 changed files with 196 additions and 4 deletions
+66
View File
@@ -2402,4 +2402,70 @@ class Caller {
expect(callerNamesOf('Other::OnlyOther')).toEqual([]);
});
});
describe('Chained call resolves a method on a supertype (conformance, #750)', () => {
function callerNamesOf(qualifiedName: string): string[] {
const target = cg.getNodesByKind('method').find((n) => n.qualifiedName === qualifiedName);
if (!target) return [];
const names = cg
.getIncomingEdges(target.id)
.filter((e) => e.kind === 'calls')
.map((e) => cg.getNode(e.source)?.name)
.filter((n): n is string => !!n);
return [...new Set(names)].sort();
}
it('resolves a chained method defined only on a SUPERCLASS the return type extends', async () => {
// draw() lives on Base; Widget (the factory's return type) has no draw() of
// its own. Decoy.draw must never win. Needs the conformance second pass.
fs.writeFileSync(
path.join(tempDir, 'Main.java'),
`class Base { void draw() {} }
class Widget extends Base {}
class Decoy { void draw() {} }
class Factory { static Widget create() { return new Widget(); } }
class Caller {
void run() { Factory.create().draw(); }
}
`
);
cg = await CodeGraph.init(tempDir, { index: true });
expect(callerNamesOf('Base::draw')).toEqual(['run']);
expect(callerNamesOf('Decoy::draw')).toEqual([]);
});
it('resolves a chained method defined on an INTERFACE the return type implements (default method)', async () => {
fs.writeFileSync(
path.join(tempDir, 'Main.java'),
`interface Drawable { default void draw() {} }
class Widget implements Drawable {}
class Decoy { void draw() {} }
class Factory { static Widget create() { return new Widget(); } }
class Caller {
void run() { Factory.create().draw(); }
}
`
);
cg = await CodeGraph.init(tempDir, { index: true });
expect(callerNamesOf('Drawable::draw')).toEqual(['run']);
expect(callerNamesOf('Decoy::draw')).toEqual([]);
});
it('still creates NO edge when no supertype has the method (safety preserved)', async () => {
fs.writeFileSync(
path.join(tempDir, 'Main.java'),
`class Base {}
class Widget extends Base {}
class Other { void onlyOther() {} }
class Factory { static Widget create() { return new Widget(); } }
class Caller {
void run() { Factory.create().onlyOther(); }
}
`
);
cg = await CodeGraph.init(tempDir, { index: true });
// Neither Widget nor Base has onlyOther() — must not attach to Other::onlyOther.
expect(callerNamesOf('Other::onlyOther')).toEqual([]);
});
});
});