A method called through a PHP fluent static factory — `ApiClient::for($c)->createOrder()`, the canonical Laravel per-credential/per-tenant client idiom — produced no `calls` edge: the receiver of `->createOrder` is the `Cls::for(...)` static call, whose result type was never recovered, so the edge was dropped and `codegraph_callers` returned nothing. Same shape as the C++ singleton/factory fix (#645), reusing its return_type column + the chained-call mechanism: - Capture PHP return types (getReturnType): `: self` / `: static` / `$this` stored as the `self` marker, a concrete `: Type` as its short name, primitives/unions dropped. - Encode the chained scoped-call receiver as `Cls::for().method` so the resolver can split it (PHP-gated, in extractCall). - New matchPhpCallChain: look up the factory's return type (`self` → the factory's own class; concrete → that class), then resolve AND validate the method on it — a wrong inference yields no edge, never a wrong one. EXTRACTION_VERSION 4->5 (re-index to populate PHP return types + chained edges). Validated on koel (1383 PHP files): node count identical (no explosion), 0 edges lost, +80 chained-call edges recovered; synthetic tests cover the self-factory, concrete-return, namespace, decoy, and absent-method cases. 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
75ae1e8bd9
commit
eb5960b535
@@ -2158,4 +2158,41 @@ void wrong() { WidgetFactory::create().onlyOther(); }
|
||||
expect(callerNamesOf('Other::onlyOther')).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PHP chained static-factory call resolution (#608)', () => {
|
||||
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 Cls::for($x)->method() via the factory\'s `: self` return (#608)', async () => {
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, 'ApiClient.php'),
|
||||
`<?php\nclass ApiClient {\n public static function for(string $c): self { return new self; }\n public function createOrder(array $p): array { return []; }\n}\n`
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, 'DispatchOrder.php'),
|
||||
`<?php\nclass DispatchOrder {\n public function handle(): void {\n ApiClient::for('cred')->createOrder([]);\n }\n}\n`
|
||||
);
|
||||
cg = await CodeGraph.init(tempDir, { index: true });
|
||||
// The chained call's edge attaches to the factory result's method.
|
||||
expect(callerNamesOf('ApiClient::createOrder')).toContain('handle');
|
||||
});
|
||||
|
||||
it('creates NO edge when the factory result lacks the method (#608)', async () => {
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, 'lib.php'),
|
||||
`<?php\nclass ApiClient { public static function for(string $c): self { return new self; } }\nclass Other { public function onlyOther(): void {} }\nclass Caller { public function go(): void { ApiClient::for('x')->onlyOther(); } }\n`
|
||||
);
|
||||
cg = await CodeGraph.init(tempDir, { index: true });
|
||||
// ApiClient has no onlyOther — must not mis-attach to the same-named Other::onlyOther.
|
||||
expect(callerNamesOf('Other::onlyOther')).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user