* 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:
co-authored by
Claude Opus 4.8
parent
aa07dc59d4
commit
48d4654e8d
@@ -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([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user