mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-03 02:21:44 +00:00
fix(php): tighten unresolved-receiver fallback to exact-required arity (U4)
phpEmitUnresolvedReceiverEdges emits 0.6-confidence CALLS edges for
member-call sites whose receiver has no type binding (PHP 'mixed'-typed
parameters, untyped variables) when the called method name has a single
workspace-wide candidate. The existing first-stage gate accepted any
argCount in 'min..max' (or '>= min' with variadic), which over-emits
when the lone candidate has optional / defaulted parameters - the
fallback fires for calls that happen to fit the wider range but
weren't meant for that target.
Add an EXACT-required-arity gate for the 0.6-confidence path only:
for fixed-arity candidates, require argCount === requiredParameterCount.
Variadic candidates ('...' marker in parameterTypes) keep the relaxed
'>= required' semantics already enforced by the first-stage check.
Adds php-unresolved-receiver-arity fixture with seven scenarios covering
happy path, default-param exact match, default-param over-arity (the
narrowed case), variadic at/above/below required, and the class-
detection sanity check. Pre-fix the over-arity scenario emits a stray
0.6 edge; post-fix it does not. All existing PHP integration tests
continue to pass.
This commit is contained in:
parent
14a57b5226
commit
e90f77cb2a
5 changed files with 170 additions and 0 deletions
|
|
@ -303,6 +303,29 @@ function phpEmitUnresolvedReceiverEdges(
|
|||
continue;
|
||||
}
|
||||
|
||||
// Tighten the fallback further with an EXACT-required-arity gate
|
||||
// (Finding 8 / U4): the first-stage `narrowOverloadCandidates`
|
||||
// accepts any argCount in `min..max` (or `>= min` when variadic),
|
||||
// which over-emits 0.6-confidence edges for common method names
|
||||
// whose only workspace candidate has optional / defaulted params.
|
||||
// For the fallback path only, require argCount === required for
|
||||
// fixed-arity candidates. Variadic candidates keep the relaxed
|
||||
// `argCount >= required` semantics (already enforced by the first-
|
||||
// stage check, so no extra work here).
|
||||
const min = fnDef.requiredParameterCount;
|
||||
const hasVarArgs =
|
||||
fnDef.parameterTypes !== undefined &&
|
||||
fnDef.parameterTypes.some((t) => t === '...' || t.startsWith('...'));
|
||||
if (
|
||||
min !== undefined &&
|
||||
Number.isFinite(site.arity) &&
|
||||
site.arity >= 0 &&
|
||||
!hasVarArgs &&
|
||||
site.arity !== min
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const callerGraphId = resolveCallerGraphId(site.inScope, scopes, nodeLookup);
|
||||
if (callerGraphId === undefined) continue;
|
||||
const tgtGraphId = resolveDefGraphId(fnDef.filePath, fnDef, nodeLookup);
|
||||
|
|
|
|||
26
gitnexus/test/fixtures/lang-resolution/php-unresolved-receiver-arity/app/Models/Handler.php
vendored
Normal file
26
gitnexus/test/fixtures/lang-resolution/php-unresolved-receiver-arity/app/Models/Handler.php
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
/**
|
||||
* Single workspace-unique candidate for each fallback-method name so the
|
||||
* unresolved-receiver fallback in phpEmitUnresolvedReceiverEdges fires
|
||||
* for untyped receivers. The names are deliberately chosen to NOT collide
|
||||
* with other fixtures so cross-fixture test interference cannot occur.
|
||||
*
|
||||
* Method-arity matrix:
|
||||
* - happyPath(): min=0, max=0
|
||||
* - withDefault(string $a, int $b = 0): min=1, max=2
|
||||
* - variadicLog(string $level, ...$args): min=1, max=undefined, hasVarArgs
|
||||
* - variadicLogTwoRequired(string $a, string $b, ...$rest): min=2, max=undefined, hasVarArgs
|
||||
*/
|
||||
class Handler
|
||||
{
|
||||
public function happyPath(): void {}
|
||||
|
||||
public function withDefault(string $a, int $b = 0): void {}
|
||||
|
||||
public function variadicLog(string $level, ...$args): void {}
|
||||
|
||||
public function variadicLogTwoRequired(string $a, string $b, ...$rest): void {}
|
||||
}
|
||||
58
gitnexus/test/fixtures/lang-resolution/php-unresolved-receiver-arity/app/Services/Caller.php
vendored
Normal file
58
gitnexus/test/fixtures/lang-resolution/php-unresolved-receiver-arity/app/Services/Caller.php
vendored
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
/**
|
||||
* Each method's receiver is an untyped parameter so the high-confidence
|
||||
* receiver-bound passes drop the site, leaving it for
|
||||
* phpEmitUnresolvedReceiverEdges (the 0.6-confidence fallback).
|
||||
*
|
||||
* The fallback's gate is EXACT-required-arity post-fix:
|
||||
* - call argCount === fnDef.requiredParameterCount → edge emitted
|
||||
* - call argCount !== required, non-variadic → NO edge (post-fix; pre-fix may have emitted)
|
||||
* - variadic candidate, argCount >= required → edge emitted
|
||||
* - variadic candidate, argCount < required → NO edge
|
||||
*/
|
||||
class Caller
|
||||
{
|
||||
public function callHappyPath($h): void
|
||||
{
|
||||
// happyPath(): min=0. argCount=0 → exact match. Edge.
|
||||
$h->happyPath();
|
||||
}
|
||||
|
||||
public function callDefaultExactRequired($h): void
|
||||
{
|
||||
// withDefault($a, $b=0): min=1. argCount=1 === min → exact match. Edge.
|
||||
$h->withDefault('a');
|
||||
}
|
||||
|
||||
public function callDefaultBeyondRequired($h): void
|
||||
{
|
||||
// withDefault($a, $b=0): min=1, max=2. argCount=2 > min.
|
||||
// Pre-fix: first-stage narrow accepts (2 <= 2), edge emitted.
|
||||
// Post-fix: exact-required gate rejects (2 !== 1), no edge.
|
||||
$h->withDefault('a', 99);
|
||||
}
|
||||
|
||||
public function callVariadicAtRequired($h): void
|
||||
{
|
||||
// variadicLog($level, ...$args): min=1, hasVarArgs.
|
||||
// argCount=1 === min → edge emitted (variadic relaxed path).
|
||||
$h->variadicLog('info');
|
||||
}
|
||||
|
||||
public function callVariadicBeyondRequired($h): void
|
||||
{
|
||||
// variadicLog($level, ...$args): min=1, hasVarArgs.
|
||||
// argCount=2 > min, variadic → edge emitted.
|
||||
$h->variadicLog('info', 'arg1');
|
||||
}
|
||||
|
||||
public function callVariadicBelowRequired($h): void
|
||||
{
|
||||
// variadicLogTwoRequired($a, $b, ...$rest): min=2, hasVarArgs.
|
||||
// argCount=1 < min → no edge (first-stage rejects). Both pre/post-fix.
|
||||
$h->variadicLogTwoRequired('only-one');
|
||||
}
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/php-unresolved-receiver-arity/composer.json
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/php-unresolved-receiver-arity/composer.json
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"autoload": {
|
||||
"psr-4": { "App\\": "app/" }
|
||||
}
|
||||
}
|
||||
|
|
@ -2237,3 +2237,61 @@ describe('PHP dynamic dispatch — negative regression suite', () => {
|
|||
expect(reads.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// phpEmitUnresolvedReceiverEdges exact-required-arity gate (Finding 8 / U4).
|
||||
// The 0.6-confidence fallback for untyped receivers now requires argCount
|
||||
// to exactly match the candidate's required parameter count for fixed-
|
||||
// arity candidates. Variadic candidates keep the relaxed argCount >=
|
||||
// required semantics.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('PHP unresolved-receiver fallback exact-required-arity gate', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'php-unresolved-receiver-arity'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
const fallbackEdgeFromTo = (source: string, target: string) =>
|
||||
getRelationships(result, 'CALLS').filter(
|
||||
(c) =>
|
||||
c.source === source && c.target === target && c.targetFilePath === 'app/Models/Handler.php',
|
||||
);
|
||||
|
||||
it('detects Handler and Caller classes', () => {
|
||||
expect(getNodesByLabel(result, 'Class')).toContain('Handler');
|
||||
expect(getNodesByLabel(result, 'Class')).toContain('Caller');
|
||||
});
|
||||
|
||||
it('happy path: argCount === required (0===0) emits 0.6 fallback edge', () => {
|
||||
expect(fallbackEdgeFromTo('callHappyPath', 'happyPath').length).toBe(1);
|
||||
});
|
||||
|
||||
it('argCount === required (1===1) on candidate with default param still emits edge', () => {
|
||||
expect(fallbackEdgeFromTo('callDefaultExactRequired', 'withDefault').length).toBe(1);
|
||||
});
|
||||
|
||||
it('argCount > required (2>1) on candidate with default param emits NO edge post-fix', () => {
|
||||
// Pre-fix: first-stage narrowOverloadCandidates accepted (1 <= 2 <= 2).
|
||||
// Post-fix: exact-required gate rejects (2 !== 1).
|
||||
expect(fallbackEdgeFromTo('callDefaultBeyondRequired', 'withDefault').length).toBe(0);
|
||||
});
|
||||
|
||||
it('variadic candidate, argCount === required (1===1) emits edge', () => {
|
||||
expect(fallbackEdgeFromTo('callVariadicAtRequired', 'variadicLog').length).toBe(1);
|
||||
});
|
||||
|
||||
it('variadic candidate, argCount > required (2>1) emits edge (relaxed)', () => {
|
||||
expect(fallbackEdgeFromTo('callVariadicBeyondRequired', 'variadicLog').length).toBe(1);
|
||||
});
|
||||
|
||||
it('variadic candidate, argCount < required (1<2) emits NO edge', () => {
|
||||
expect(fallbackEdgeFromTo('callVariadicBelowRequired', 'variadicLogTwoRequired').length).toBe(
|
||||
0,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue