Skip to content

Commit 764e9a6

Browse files
committed
Improve closure with by-ref uses analysis
1 parent 83281d2 commit 764e9a6

File tree

9 files changed

+157
-28
lines changed

9 files changed

+157
-28
lines changed

build/PHPStan/Build/GeneratorYieldSendTypeExtension.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
use PHPStan\Analyser\Generator\AttrGroupsAnalysisRequest;
77
use PHPStan\Analyser\Generator\ExprAnalysisRequest;
88
use PHPStan\Analyser\Generator\ExprAnalysisResult;
9+
use PHPStan\Analyser\Generator\ExprAnalysisResultStorage;
910
use PHPStan\Analyser\Generator\NodeCallbackRequest;
11+
use PHPStan\Analyser\Generator\PersistStorageRequest;
12+
use PHPStan\Analyser\Generator\RestoreStorageRequest;
1013
use PHPStan\Analyser\Generator\StmtAnalysisRequest;
1114
use PHPStan\Analyser\Generator\StmtAnalysisResult;
1215
use PHPStan\Analyser\Generator\StmtsAnalysisRequest;
@@ -57,6 +60,12 @@ public function getType(Expr $expr, Scope $scope): ?Type
5760
if ((new ObjectType(AttrGroupsAnalysisRequest::class))->isSuperTypeOf($valueType)->yes()) {
5861
return new NullType();
5962
}
63+
if ((new ObjectType(PersistStorageRequest::class))->isSuperTypeOf($valueType)->yes()) {
64+
return new ObjectType(ExprAnalysisResultStorage::class);
65+
}
66+
if ((new ObjectType(RestoreStorageRequest::class))->isSuperTypeOf($valueType)->yes()) {
67+
return new NullType();
68+
}
6069
if ((new ObjectType(TypeExprRequest::class))->isSuperTypeOf($valueType)->yes()) {
6170
return new ObjectType(TypeExprResult::class);
6271
}

src/Analyser/Generator/ExprAnalysisResultStorage.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@ public function __construct()
1919
$this->expressionAnalysisResults = new SplObjectStorage();
2020
}
2121

22+
public function duplicate(): self
23+
{
24+
$new = new self();
25+
$new->expressionAnalysisResults->addAll($this->expressionAnalysisResults);
26+
return $new;
27+
}
28+
2229
public function storeExprAnalysisResult(Expr $expr, ExprAnalysisResult $result): void
2330
{
2431
$this->expressionAnalysisResults[$expr] = $result;

src/Analyser/Generator/ExprHandler/ClosureHandler.php

Lines changed: 46 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
use PHPStan\Analyser\Generator\GeneratorScope;
1616
use PHPStan\Analyser\Generator\NodeCallbackRequest;
1717
use PHPStan\Analyser\Generator\NodeHandler\ParamHandler;
18+
use PHPStan\Analyser\Generator\NoopNodeCallback;
19+
use PHPStan\Analyser\Generator\PersistStorageRequest;
20+
use PHPStan\Analyser\Generator\RestoreStorageRequest;
1821
use PHPStan\Analyser\Generator\StmtAnalysisResult;
1922
use PHPStan\Analyser\Generator\StmtsAnalysisRequest;
2023
use PHPStan\Analyser\Generator\TypeExprRequest;
@@ -29,6 +32,7 @@
2932
use PHPStan\Node\InvalidateExprNode;
3033
use PHPStan\Node\PropertyAssignNode;
3134
use PHPStan\Node\ReturnStatement;
35+
use PHPStan\Parser\ImmediatelyInvokedClosureVisitor;
3236
use PHPStan\ShouldNotHappenException;
3337
use PHPStan\TrinaryLogic;
3438
use PHPStan\Type\ClosureType;
@@ -85,7 +89,7 @@ public function analyseExpr(
8589
return new ExprAnalysisResult(
8690
$result->type,
8791
$result->nativeType,
88-
$result->scope,
92+
$closureScope,
8993
hasYield: false,
9094
isAlwaysTerminating: false,
9195
throwPoints: [],
@@ -170,14 +174,15 @@ public function processClosureNode(
170174
}
171175

172176
$closureScope = $scope->enterAnonymousFunctionWithoutReflection($expr, $callableParameters);
177+
$closureScope = $closureScope->processClosureScope($scope, null, $byRefUses);
173178
$gatheredReturnStatements = [];
174179
$gatheredYieldStatements = [];
175180
$onlyNeverExecutionEnds = null;
176181
$closureImpurePoints = [];
177182
$invalidateExpressions = [];
178183
$executionEnds = [];
179184

180-
$closureStmtsCallback = static function (Node $node, Scope $scope, callable $nodeCallback) use ($closureScope, &$gatheredReturnStatements, &$gatheredYieldStatements, &$onlyNeverExecutionEnds, &$closureImpurePoints, &$invalidateExpressions, &$executionEnds): void {
185+
$closureStmtsCallback = static function (Node $node, Scope $scope, callable $nodeCallback) use (&$closureScope, &$gatheredReturnStatements, &$gatheredYieldStatements, &$onlyNeverExecutionEnds, &$closureImpurePoints, &$invalidateExpressions, &$executionEnds): void {
181186
$nodeCallback($node, $scope);
182187
if ($scope->getAnonymousFunctionReflection() !== $closureScope->getAnonymousFunctionReflection()) {
183188
return;
@@ -241,15 +246,44 @@ public function processClosureNode(
241246
if (count($byRefUses) === 0) {
242247
$closureStatementResult = yield new StmtsAnalysisRequest($expr->stmts, $closureScope, StatementContext::createTopLevel(), $closureStmtsCallback);
243248
} else {
244-
// todo
245-
$closureStatementResultGen = $this->processClosureNodeAndStabilizeScope(
246-
$expr,
247-
$closureScope,
248-
$closureStmtsCallback,
249-
);
250-
yield from $closureStatementResultGen;
251-
$closureStatementResult = $closureStatementResultGen->getReturn();
252-
$scope = $closureScope->processClosureScope($closureStatementResult->scope, $scope, $byRefUses);
249+
$count = 0;
250+
$closureResultScope = null;
251+
252+
$storage = yield new PersistStorageRequest();
253+
do {
254+
yield new RestoreStorageRequest($storage->duplicate());
255+
$prevScope = $closureScope;
256+
257+
$intermediaryClosureScopeResult = yield new StmtsAnalysisRequest($expr->stmts, $closureScope, StatementContext::createTopLevel(), new NoopNodeCallback());
258+
$intermediaryClosureScope = $intermediaryClosureScopeResult->scope;
259+
foreach ($intermediaryClosureScopeResult->exitPoints as $exitPoint) {
260+
$intermediaryClosureScope = $intermediaryClosureScope->mergeWith($exitPoint->getScope());
261+
}
262+
263+
if ($expr->getAttribute(ImmediatelyInvokedClosureVisitor::ATTRIBUTE_NAME) === true) {
264+
$closureResultScope = $intermediaryClosureScope;
265+
break;
266+
}
267+
268+
$closureScope = $scope->enterAnonymousFunctionWithoutReflection($expr, $callableParameters);
269+
$closureScope = $closureScope->processClosureScope($intermediaryClosureScope, $prevScope, $byRefUses);
270+
271+
if ($closureScope->equals($prevScope)) {
272+
break;
273+
}
274+
if ($count >= 1) {
275+
$closureScope = $prevScope->generalizeWith($closureScope);
276+
}
277+
$count++;
278+
} while ($count < 3);
279+
280+
if ($closureResultScope === null) {
281+
$closureResultScope = $closureScope;
282+
}
283+
284+
yield new RestoreStorageRequest($storage->duplicate());
285+
$closureStatementResult = yield new StmtsAnalysisRequest($expr->stmts, $closureScope, StatementContext::createTopLevel(), $closureStmtsCallback);
286+
$closureScope = $scope->processClosureScope($closureResultScope, null, $byRefUses);
253287
}
254288

255289
$resultGen = $this->processClosureStatementResult(
@@ -289,7 +323,7 @@ public function processClosureNode(
289323
$alternativeNodeCallback,
290324
);
291325

292-
return $resultGen->getReturn();
326+
return [$result, $closureScope];
293327
}
294328

295329
/**
@@ -430,17 +464,4 @@ private function processClosureStatementResult(
430464
];
431465
}
432466

433-
/**
434-
* @param callable(Node, Scope, callable(Node, Scope): void): void $closureStmtsCallback
435-
* @return Generator<int, GeneratorTValueType, GeneratorTSendType, StmtAnalysisResult>
436-
*/
437-
private function processClosureNodeAndStabilizeScope(
438-
Closure $expr,
439-
GeneratorScope $closureScope,
440-
callable $closureStmtsCallback,
441-
): Generator
442-
{
443-
return yield new StmtsAnalysisRequest($expr->stmts, $closureScope, StatementContext::createTopLevel(), $closureStmtsCallback);
444-
}
445-
446467
}

src/Analyser/Generator/GeneratorNodeScopeResolver.php

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,8 @@
6262
* a hypothetical method call) are analyzed on-demand when requested, with the Fiber
6363
* suspending until analysis completes
6464
*
65-
* @phpstan-type GeneratorTValueType = ExprAnalysisRequest|StmtAnalysisRequest|StmtsAnalysisRequest|NodeCallbackRequest|AttrGroupsAnalysisRequest|TypeExprRequest
66-
* @phpstan-type GeneratorTSendType = ExprAnalysisResult|StmtAnalysisResult|TypeExprResult|null
65+
* @phpstan-type GeneratorTValueType = ExprAnalysisRequest|StmtAnalysisRequest|StmtsAnalysisRequest|NodeCallbackRequest|AttrGroupsAnalysisRequest|TypeExprRequest|PersistStorageRequest|RestoreStorageRequest
66+
* @phpstan-type GeneratorTSendType = ExprAnalysisResult|StmtAnalysisResult|TypeExprResult|ExprAnalysisResultStorage|null
6767
*/
6868
final class GeneratorNodeScopeResolver
6969
{
@@ -80,6 +80,7 @@ public function __construct(
8080
*/
8181
public function setAnalysedFiles(array $files): void
8282
{
83+
// todo
8384
}
8485

8586
/**
@@ -246,6 +247,20 @@ private function runTrampoline(
246247
);
247248
$gen->generator->current();
248249
continue;
250+
} elseif ($yielded instanceof PersistStorageRequest) {
251+
$stack[] = $gen;
252+
$gen = new IdentifiedGeneratorInStack(
253+
$this->persistStorage($exprAnalysisResultStorage),
254+
new Stmt\Expression(new Node\Scalar\String_('fake')),
255+
$yielded->originFile,
256+
$yielded->originLine,
257+
);
258+
$gen->generator->current();
259+
continue;
260+
} elseif ($yielded instanceof RestoreStorageRequest) {
261+
$exprAnalysisResultStorage = $yielded->storage;
262+
$gen->generator->next();
263+
continue;
249264
} else { // phpcs:ignore
250265
throw new NeverException($yielded);
251266
}
@@ -359,6 +374,15 @@ private function analyseAttrGroups(Stmt $stmt, array $attrGroups, GeneratorScope
359374
yield from $handler->processAttributeGroups($stmt, $attrGroups, $scope, $alternativeNodeCallback);
360375
}
361376

377+
/**
378+
* @return Generator<int, GeneratorTValueType, GeneratorTSendType, ExprAnalysisResultStorage>
379+
*/
380+
private function persistStorage(ExprAnalysisResultStorage $storage): Generator
381+
{
382+
yield from [];
383+
return $storage;
384+
}
385+
362386
/**
363387
* @param (callable(Node, Scope, callable(Node, Scope): void): void)|null $alternativeNodeCallback
364388
* @return Generator<int, GeneratorTValueType, GeneratorTSendType, ExprAnalysisResult>

src/Analyser/Generator/IdentifiedGeneratorInStack.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ final class IdentifiedGeneratorInStack
1818
* Generator<int, GeneratorTValueType, GeneratorTSendType, StmtAnalysisResult>| // analyseStmts
1919
* Generator<int, GeneratorTValueType, GeneratorTSendType, void>| // analyseAttrGroups
2020
* Generator<int, GeneratorTValueType, GeneratorTSendType, ExprAnalysisResult>| // analyseExpr
21-
* Generator<int, GeneratorTValueType, GeneratorTSendType, TypeExprResult> // analyseExprForType
21+
* Generator<int, GeneratorTValueType, GeneratorTSendType, TypeExprResult>| // analyseExprForType
22+
* Generator<int, GeneratorTValueType, GeneratorTSendType, ExprAnalysisResultStorage> // persistStorage
2223
* ) $generator
2324
* @param Node|Node[] $node
2425
*/
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Analyser\Generator;
4+
5+
use function debug_backtrace;
6+
use const DEBUG_BACKTRACE_IGNORE_ARGS;
7+
8+
final class PersistStorageRequest
9+
{
10+
11+
public ?string $originFile = null;
12+
13+
public ?int $originLine = null;
14+
15+
public function __construct()
16+
{
17+
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
18+
$this->originFile = $trace[0]['file'] ?? null;
19+
$this->originLine = $trace[0]['line'] ?? null;
20+
}
21+
22+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Analyser\Generator;
4+
5+
final class RestoreStorageRequest
6+
{
7+
8+
public function __construct(
9+
public readonly ExprAnalysisResultStorage $storage,
10+
)
11+
{
12+
}
13+
14+
}

tests/PHPStan/Analyser/Generator/data/gnsr.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,12 @@ function (): void {
6767
};
6868
assertType('Closure(): 1', $cb);
6969

70+
$a = 1;
71+
$cb = function () use (&$a) {
72+
return 1;
73+
};
74+
assertType('Closure(): 1', $cb);
75+
7076
$cb = function (string $s) {
7177
return $s;
7278
};
@@ -76,7 +82,30 @@ function (): void {
7682
function (): void {
7783
$a = 0;
7884
$cb = function () use (&$a): void {
85+
assertType('0|\'s\'', $a);
7986
$a = 's';
8087
};
8188
assertType('0|\'s\'', $a);
8289
};
90+
91+
function (): void {
92+
$a = 0;
93+
$b = 0;
94+
$cb = function () use (&$a, $b): void {
95+
assertType('int<0, max>', $a);
96+
assertType('0', $b);
97+
$a = $a + 1;
98+
$b = 1;
99+
};
100+
assertType('int<0, max>', $a);
101+
assertType('0', $b);
102+
};
103+
104+
function (): void {
105+
$a = 0;
106+
$cb = function () use (&$a): void {
107+
assertType('0|1', $a);
108+
$a = 1;
109+
};
110+
assertType('0|1', $a);
111+
};

tests/PHPStan/Analyser/nsrt/gnsr-yield-in-generator-ns.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ public function doFoo(): Generator
1919
assertType(StmtAnalysisResult::class, yield new StmtsAnalysisRequest());
2020
assertType('null', yield new NodeCallbackRequest());
2121
assertType('null', yield new AttrGroupsAnalysisRequest());
22+
assertType(ExprAnalysisResultStorage::class, yield new PersistStorageRequest());
23+
assertType('null', yield new RestoreStorageRequest());
2224
}
2325

2426
}

0 commit comments

Comments
 (0)