mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-03 02:21:44 +00:00
feat(scope-resolution): add emitUnresolvedReceiverEdges hook for dynamic languages
Adds an optional post-resolution pass on the ScopeResolver contract: when a member-call receiver cannot be typed by the scope chain (no TypeRef), the language may emit CALLS edges via a workspace-wide unique-name lookup. Runs after emitReceiverBoundCalls and before emitFreeCallFallback, gated per-language. PHP wires the hook to recover member calls on mixed/untyped parameters (e.g. save(mixed $entity) calling $entity->getId()), restoring parity with the legacy DAG. Re-enables the previously skipped save → getId test; PHP suite is now 160/160 with no skips in both flag states.
This commit is contained in:
parent
4594f5d36a
commit
a8201cd9c9
4 changed files with 48 additions and 16 deletions
|
|
@ -295,6 +295,10 @@ const phpScopeResolver: ScopeResolver = {
|
|||
// PHP hoists method return-type bindings to the Module scope so
|
||||
// `propagateImportedReturnTypes` can pick them up across files.
|
||||
hoistTypeBindingsToModule: true,
|
||||
|
||||
// PHP recovers member calls on `mixed`/untyped receivers via a
|
||||
// workspace-wide unique-method-name lookup, mirroring the legacy DAG.
|
||||
emitUnresolvedReceiverEdges: phpEmitUnresolvedReceiverEdges,
|
||||
};
|
||||
|
||||
export { phpScopeResolver };
|
||||
|
|
|
|||
|
|
@ -576,4 +576,32 @@ export interface ScopeResolver {
|
|||
readonly treeCache?: { get(filePath: string): unknown };
|
||||
},
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* Optional post-resolution pass: emit CALLS edges for member-call sites
|
||||
* whose receiver cannot be typed by the scope chain (no `TypeRef`).
|
||||
* Dynamically-typed languages with untyped/`mixed`/`Any` parameters use
|
||||
* this hook to recover the call edge via workspace-wide method-name
|
||||
* lookup, mirroring what their legacy resolvers did.
|
||||
*
|
||||
* Runs AFTER `emitReceiverBoundCalls` and BEFORE `emitFreeCallFallback`.
|
||||
* Implementations MUST:
|
||||
* - Skip sites already in `handledSites` (Invariant I2).
|
||||
* - Add resolved site keys to `handledSites` before returning.
|
||||
* - Stay narrow: a unique workspace-wide match is the safe baseline.
|
||||
* Multi-candidate fallbacks should narrow by arity / argument types
|
||||
* before emitting to keep false-positive rate bounded.
|
||||
*
|
||||
* Returns the number of edges emitted (for telemetry).
|
||||
*
|
||||
* Default: undefined (no unresolved-receiver fallback).
|
||||
*/
|
||||
readonly emitUnresolvedReceiverEdges?: (
|
||||
graph: KnowledgeGraph,
|
||||
scopes: ScopeResolutionIndexes,
|
||||
parsedFiles: readonly ParsedFile[],
|
||||
nodeLookup: GraphNodeLookup,
|
||||
handledSites: Set<string>,
|
||||
model: SemanticModel,
|
||||
) => number;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -252,6 +252,17 @@ export function runScopeResolution(
|
|||
workspaceIndex,
|
||||
readonlyModel,
|
||||
);
|
||||
const unresolvedReceiverExtras =
|
||||
provider.emitUnresolvedReceiverEdges !== undefined
|
||||
? provider.emitUnresolvedReceiverEdges(
|
||||
graph,
|
||||
indexes,
|
||||
parsedFiles,
|
||||
nodeLookup,
|
||||
handledSites,
|
||||
readonlyModel,
|
||||
)
|
||||
: 0;
|
||||
const freeCallExtras = emitFreeCallFallback(
|
||||
graph,
|
||||
indexes,
|
||||
|
|
@ -299,7 +310,7 @@ export function runScopeResolution(
|
|||
filesSkipped,
|
||||
importsEmitted,
|
||||
resolve: resolveStats,
|
||||
referenceEdgesEmitted: emitted + receiverExtras + freeCallExtras,
|
||||
referenceEdgesEmitted: emitted + receiverExtras + unresolvedReceiverExtras + freeCallExtras,
|
||||
referenceSkipped: skipped,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,18 +11,9 @@ import {
|
|||
getNodesByLabelFull,
|
||||
edgeSet,
|
||||
runPipelineFromRepo,
|
||||
isLegacyResolverParityRun,
|
||||
type PipelineResult,
|
||||
} from './helpers.js';
|
||||
|
||||
/**
|
||||
* Whether the current run uses the scope-based (registry-primary) PHP resolver.
|
||||
* When true, some tests that rely on unresolvable-receiver fallback (mixed-typed
|
||||
* parameters) are skipped — the contract needs a postResolutionFallback hook to
|
||||
* support this pattern (tracked as follow-up to RFC #909 Ring 3).
|
||||
*/
|
||||
const isRegistryPrimaryRun = !isLegacyResolverParityRun('php');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Heritage: PSR-4 imports, extends, implements, trait use, enums, calls
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -101,12 +92,10 @@ describe('PHP heritage & import resolution', () => {
|
|||
});
|
||||
|
||||
// save($entity: mixed) calls $entity->getId() — the receiver is typed `mixed`
|
||||
// so there is no TypeRef in scope. The scope-resolver contract does not yet
|
||||
// have a postResolutionFallback hook for unresolvable-receiver member calls
|
||||
// (follow-up to RFC #909 Ring 3). The legacy DAG resolves this via its own
|
||||
// workspace-wide method-name lookup. Skip in registry-primary mode until the
|
||||
// contract grows the necessary hook.
|
||||
it.skipIf(isRegistryPrimaryRun)('emits CALLS edge: save → getId', () => {
|
||||
// so there is no TypeRef in scope. The scope-resolver `emitUnresolvedReceiverEdges`
|
||||
// hook (PHP-wired) recovers this case via workspace-wide unique-name lookup,
|
||||
// matching the legacy DAG behavior.
|
||||
it('emits CALLS edge: save → getId', () => {
|
||||
const calls = getRelationships(result, 'CALLS').filter(
|
||||
(e) => e.source === 'save' && e.target === 'getId',
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue