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
@@ -2349,6 +2349,33 @@ export class TreeSitterExtractor {
|
||||
// single-dot receiver regex fails. Pull out the immediate field after `this.`
|
||||
// so the receiver is the field name (`userbo`), which the resolver can then
|
||||
// look up in the enclosing class's field declarations.
|
||||
// PHP static-factory fluent chain: `Cls::for($x)->method()` — the receiver
|
||||
// is itself a static call, so resolution must infer the method's class
|
||||
// from what `Cls::for` RETURNS (its `: self` / `: static` / `: Type`),
|
||||
// #608 (mirrors the C++ chain fix in #645). Encode `<Cls::factory>().<method>`;
|
||||
// the `().` marker lets the PHP resolver split it. The receiver text
|
||||
// (`Cls::for('x')`) carries the args, so without this it degrades to an
|
||||
// unresolvable string and the call edge is dropped.
|
||||
if (methodName && this.language === 'php' && objectField.type === 'scoped_call_expression') {
|
||||
const innerScope = getChildByField(objectField, 'scope');
|
||||
const innerName = getChildByField(objectField, 'name');
|
||||
if (innerScope && innerName) {
|
||||
calleeName = `${getNodeText(innerScope, this.source)}::${getNodeText(innerName, this.source)}().${methodName}`;
|
||||
} else {
|
||||
calleeName = methodName;
|
||||
}
|
||||
if (calleeName) {
|
||||
this.unresolvedReferences.push({
|
||||
fromNodeId: callerId,
|
||||
referenceName: calleeName,
|
||||
referenceKind: 'calls',
|
||||
line: node.startPosition.row + 1,
|
||||
column: node.startPosition.column,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let receiverName: string;
|
||||
if (objectField.type === 'field_access') {
|
||||
const inner = getChildByField(objectField, 'object');
|
||||
|
||||
Reference in New Issue
Block a user