fix(php): resolve static calls through import aliases (#1545) (#1795)

Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
This commit is contained in:
Colby Mchenry
2026-09-08 15:11:20 -05:00
committed by GitHub
co-authored by Colby McHenry
parent a961491cea
commit 040ba388da
7 changed files with 210 additions and 1 deletions
+41
View File
@@ -1373,6 +1373,47 @@ function pickClosestJvmCandidate(candidates: Node[], fromPath: string): Node {
return best;
}
/**
* PHP scoped calls are encoded as "Alias.method" by both extractors. A use
* mapping names a namespace, not a filesystem path, so resolve the receiver
* through its localName and look up the method on that exact imported type.
* undefined means this is not an imported static call; null means the import
* owns the call but its method is unavailable, so name fallbacks must not guess.
*/
export function resolvePhpImportedStaticCall(
ref: UnresolvedRef,
context: ResolutionContext,
): ResolvedRef | null | undefined {
if (ref.language !== 'php' || ref.referenceKind !== 'calls') return undefined;
const call = /^(\w+)\.(\w+)$/.exec(ref.referenceName);
if (!call) return undefined;
const [, receiver, member] = call;
const imp = context.getImportMappings(ref.filePath, ref.language)
.find((i) => i.localName === receiver);
if (!imp) return undefined;
// PHP variables occupy a different namespace from class imports. Extraction
// strips the leading "$" from "$Alias->method()" too; leave that receiver to
// local type inference even when a class import has the same local name.
const lines = context.getFileLines?.(ref.filePath) ?? context.readFile(ref.filePath)?.split('\n');
const line = lines?.[ref.line - 1];
if (line?.slice(ref.column).startsWith('$')) return undefined;
const fqn = imp.source.replace(/^\\/, '');
const separator = fqn.lastIndexOf('\\');
const typeName = separator < 0
? fqn
: `${fqn.slice(0, separator)}::${fqn.slice(separator + 1)}`;
const owners = context.getNodesByQualifiedName(typeName)
.filter((n) => n.language === 'php' && STATIC_MEMBER_CONTAINERS.has(n.kind));
if (owners.length !== 1) return null;
const owner = owners[0]!;
const methods = context.getNodesByQualifiedName(`${owner.qualifiedName}::${member}`)
.filter((n) => n.language === 'php' && n.kind === 'method' && n.filePath === owner.filePath);
if (methods.length !== 1) return null;
return { original: ref, targetNodeId: methods[0]!.id, confidence: 0.95, resolvedBy: 'import' };
}
export function resolveViaImport(
ref: UnresolvedRef,
context: ResolutionContext
+7 -1
View File
@@ -17,7 +17,7 @@ import {
ImportMapping,
} from './types';
import { isVisibleAcrossFiles, matchReference, matchFunctionRef, matchDottedCallChain, matchScopedCallChain, matchMethodCall, sameLanguageFamily, crossesKnownFamily, dumpNameMatcherProfile, clearNameMatcherMemos } from './name-matcher';
import { resolveViaImport, resolveJvmImport, extractImportMappings, extractReExports, loadCppIncludeDirs, isPhpIncludePathRef, isCobolCopybookRef, isNixPathImportRef, clearImportResolverMemos, resolveImportPath } from './import-resolver';
import { resolveViaImport, resolvePhpImportedStaticCall, resolveJvmImport, extractImportMappings, extractReExports, loadCppIncludeDirs, isPhpIncludePathRef, isCobolCopybookRef, isNixPathImportRef, clearImportResolverMemos, resolveImportPath } from './import-resolver';
import { ResolverPool, minRefsForPool } from './resolver-pool';
import { detectFrameworks } from './frameworks';
import { synthesizeCallbackEdges } from './callback-synthesizer';
@@ -958,6 +958,12 @@ export class ReferenceResolver {
if (razorResult) return razorResult;
}
// An explicit PHP class import owns its static calls, including an
// unavailable method. Do not let same-name fallbacks change the receiver
// to an unrelated Service/Repository type (#1545).
const phpStaticImport = resolvePhpImportedStaticCall(ref, this.context);
if (phpStaticImport !== undefined) return this.gateLanguage(phpStaticImport, ref);
const candidates: ResolvedRef[] = [];
// Strategy 1: Try framework-specific resolution. Cross-language bridges