Skip to content

Commit f2009eb

Browse files
SanderMullerclaude
andcommitted
fix: capture dependencies for dynamic function calls
PHPStan's DependencyResolver resolves non-Name callees ($callable()) via the scope type and records classes referenced by the callable's signature. Skipping them lost those dependency edges, so a change in a file referenced only through a dynamic call signature could leave a stale cache entry. New tripwire pins the PHPStan behavior. Found by codex review. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 2347f37 commit f2009eb

2 files changed

Lines changed: 20 additions & 6 deletions

File tree

‎src/NodeTypeResolver/PHPStan/Scope/PHPStanNodeScopeResolver.php‎

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -805,7 +805,6 @@ private function captureNodeDependencies(Node $node, MutatingScope $mutatingScop
805805
* flows from body expressions that are captured by their own nodes
806806
* - foreach over a literal array: a literal is never an Iterator object,
807807
* element classes are captured by the element nodes
808-
* - dynamic function calls: PHPStan's branch only handles Name
809808
* - native PHP function calls: their reflections carry no file and the first
810809
* resolution makes PHPStan load its entire function signature map (~100ms,
811810
* otherwise never loaded), see NativeFunctionDependencyAssumptionTest
@@ -830,11 +829,7 @@ private function isDependencyFreeNode(Node $node): bool
830829
return $node->expr instanceof Array_;
831830
}
832831

833-
if ($node instanceof FuncCall) {
834-
if (! $node->name instanceof Name) {
835-
return true;
836-
}
837-
832+
if ($node instanceof FuncCall && $node->name instanceof Name) {
838833
return $this->nativeFunctionCallAnalyzer->isNativeFunctionCall($node->name);
839834
}
840835

‎tests/NodeTypeResolver/PHPStan/Scope/DependencyResolverNodeTypesTest.php‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,4 +88,23 @@ static function (string $className): string {
8888
' PHPStanNodeScopeResolver::isDependencyResolvingNode() to match, then this list.'
8989
);
9090
}
91+
92+
public function testFuncCallBranchResolvesDynamicCallees(): void
93+
{
94+
$reflectionClass = new ReflectionClass(DependencyResolver::class);
95+
96+
/** @var string $fileName */
97+
$fileName = $reflectionClass->getFileName();
98+
$source = file_get_contents($fileName);
99+
$this->assertIsString($source);
100+
101+
// the FuncCall branch resolves dynamic callees ($callable()) via the scope type,
102+
// so isDependencyFreeNode() must not skip non-Name function calls
103+
$this->assertStringContainsString(
104+
'getCallableParametersAcceptors',
105+
$source,
106+
'PHPStan DependencyResolver no longer resolves dynamic callables, review' .
107+
' whether PHPStanNodeScopeResolver::isDependencyFreeNode() can skip non-Name FuncCalls.'
108+
);
109+
}
91110
}

0 commit comments

Comments
 (0)