Skip to content

Commit 7d5cdfc

Browse files
VincentLangletphpstan-bot
authored andcommitted
Fix phpstan/phpstan#14096: By-reference variables in closures lose type after deferred application
- When multiple closures capture the same by-ref variable, deferred application caused later closures to overwrite earlier modifications with their stale types - Pass current scope as prevScope when applying deferred by-ref results so types are unioned instead of replaced - Guard processClosureScope against undefined variables in prevScope - New regression test in tests/PHPStan/Analyser/nsrt/bug-14096.php
1 parent dab4101 commit 7d5cdfc

File tree

4 files changed

+57
-4
lines changed

4 files changed

+57
-4
lines changed

src/Analyser/MutatingScope.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3808,7 +3808,7 @@ public function processClosureScope(
38083808

38093809
$variableType = $closureScope->getVariableType($variableName);
38103810

3811-
if ($prevScope !== null) {
3811+
if ($prevScope !== null && $prevScope->hasVariableType($variableName)->yes()) {
38123812
$prevVariableType = $prevScope->getVariableType($variableName);
38133813
if (!$variableType->equals($prevVariableType)) {
38143814
$variableType = TypeCombinator::union($variableType, $prevVariableType);

src/Analyser/NodeScopeResolver.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3455,7 +3455,7 @@ public function processArgs(
34553455
}
34563456

34573457
foreach ($deferredByRefClosureResults as $deferredClosureResult) {
3458-
$scope = $deferredClosureResult->applyByRefUseScope($scope);
3458+
$scope = $deferredClosureResult->applyByRefUseScope($scope, $scope);
34593459
}
34603460

34613461
if ($parameters !== null) {

src/Analyser/ProcessClosureResult.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,13 @@ public function getScope(): MutatingScope
3131
return $this->scope;
3232
}
3333

34-
public function applyByRefUseScope(MutatingScope $scope): MutatingScope
34+
public function applyByRefUseScope(MutatingScope $scope, ?MutatingScope $prevScope = null): MutatingScope
3535
{
3636
if ($this->byRefClosureResultScope === null) {
3737
return $scope;
3838
}
3939

40-
return $scope->processClosureScope($this->byRefClosureResultScope, null, $this->byRefUses);
40+
return $scope->processClosureScope($this->byRefClosureResultScope, $prevScope, $this->byRefUses);
4141
}
4242

4343
/**
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug14096;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class AbstractView {}
8+
class App {}
9+
interface ServerRequestInterface {}
10+
11+
class Test
12+
{
13+
/**
14+
* @template T of App
15+
*
16+
* @param \Closure(ServerRequestInterface): T $createAppFx
17+
* @param \Closure(T): ServerRequestInterface $simulateRequestFx
18+
*
19+
* @return T
20+
*/
21+
protected function simulateAppCallback(\Closure $createAppFx, \Closure $simulateRequestFx): App
22+
{
23+
$appBase = $createAppFx(new class() implements ServerRequestInterface {});
24+
$request = $simulateRequestFx($appBase);
25+
26+
$app = $createAppFx($request);
27+
28+
return $app;
29+
}
30+
31+
/**
32+
* @template T of AbstractView
33+
*
34+
* @param \Closure(ServerRequestInterface): T $createViewFx
35+
* @param \Closure(T): ServerRequestInterface $simulateRequestFx
36+
*
37+
* @return T
38+
*/
39+
protected function simulateViewCallback(\Closure $createViewFx, \Closure $simulateRequestFx): AbstractView
40+
{
41+
$view = null;
42+
$this->simulateAppCallback(static function (ServerRequestInterface $request) use ($createViewFx, &$view) {
43+
$view = $createViewFx($request);
44+
45+
return new App();
46+
}, static function () use ($simulateRequestFx, &$view) {
47+
return $simulateRequestFx($view);
48+
});
49+
50+
assertType('T of Bug14096\AbstractView (method Bug14096\Test::simulateViewCallback(), argument)|null', $view);
51+
return $view;
52+
}
53+
}

0 commit comments

Comments
 (0)