Skip to content

Commit d3e7b9c

Browse files
committed
Scope - any variable after extract() call might exist
1 parent af60a50 commit d3e7b9c

File tree

7 files changed

+88
-4
lines changed

7 files changed

+88
-4
lines changed

src/Analyser/DirectScopeFactory.php

+3
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ public function __construct(
7676
* @param array<string, true> $currentlyAssignedExpressions
7777
* @param array<string, Type> $nativeExpressionTypes
7878
* @param array<\PHPStan\Reflection\FunctionReflection|\PHPStan\Reflection\MethodReflection> $inFunctionCallsStack
79+
* @param bool $afterExtractCall
7980
* @param Scope|null $parentScope
8081
*
8182
* @return MutatingScope
@@ -94,6 +95,7 @@ public function create(
9495
array $currentlyAssignedExpressions = [],
9596
array $nativeExpressionTypes = [],
9697
array $inFunctionCallsStack = [],
98+
bool $afterExtractCall = false,
9799
?Scope $parentScope = null
98100
): MutatingScope
99101
{
@@ -126,6 +128,7 @@ public function create(
126128
$inFunctionCallsStack,
127129
$this->dynamicConstantNames,
128130
$this->treatPhpDocTypesAsCertain,
131+
$afterExtractCall,
129132
$parentScope
130133
);
131134
}

src/Analyser/LazyScopeFactory.php

+3
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ public function __construct(
4848
* @param array<string, true> $currentlyAssignedExpressions
4949
* @param array<string, Type> $nativeExpressionTypes
5050
* @param array<\PHPStan\Reflection\FunctionReflection|\PHPStan\Reflection\MethodReflection> $inFunctionCallsStack
51+
* @param bool $afterExtractCall
5152
* @param Scope|null $parentScope
5253
*
5354
* @return MutatingScope
@@ -66,6 +67,7 @@ public function create(
6667
array $currentlyAssignedExpressions = [],
6768
array $nativeExpressionTypes = [],
6869
array $inFunctionCallsStack = [],
70+
bool $afterExtractCall = false,
6971
?Scope $parentScope = null
7072
): MutatingScope
7173
{
@@ -98,6 +100,7 @@ public function create(
98100
$inFunctionCallsStack,
99101
$this->dynamicConstantNames,
100102
$this->treatPhpDocTypesAsCertain,
103+
$afterExtractCall,
101104
$parentScope
102105
);
103106
}

src/Analyser/MutatingScope.php

+49-4
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,8 @@ class MutatingScope implements Scope
155155

156156
private bool $treatPhpDocTypesAsCertain;
157157

158+
private bool $afterExtractCall;
159+
158160
private ?Scope $parentScope;
159161

160162
/**
@@ -181,6 +183,7 @@ class MutatingScope implements Scope
181183
* @param array<MethodReflection|FunctionReflection> $inFunctionCallsStack
182184
* @param string[] $dynamicConstantNames
183185
* @param bool $treatPhpDocTypesAsCertain
186+
* @param bool $afterExtractCall
184187
* @param Scope|null $parentScope
185188
*/
186189
public function __construct(
@@ -207,6 +210,7 @@ public function __construct(
207210
array $inFunctionCallsStack = [],
208211
array $dynamicConstantNames = [],
209212
bool $treatPhpDocTypesAsCertain = true,
213+
bool $afterExtractCall = false,
210214
?Scope $parentScope = null
211215
)
212216
{
@@ -237,6 +241,7 @@ public function __construct(
237241
$this->inFunctionCallsStack = $inFunctionCallsStack;
238242
$this->dynamicConstantNames = $dynamicConstantNames;
239243
$this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain;
244+
$this->afterExtractCall = $afterExtractCall;
240245
$this->parentScope = $parentScope;
241246
}
242247

@@ -335,9 +340,30 @@ private function getVariableTypes(): array
335340
return $this->variableTypes;
336341
}
337342

338-
private function isRootScope(): bool
343+
private function canAnyVariableExist(): bool
344+
{
345+
return ($this->function === null && !$this->isInAnonymousFunction()) || $this->afterExtractCall;
346+
}
347+
348+
public function afterExtractCall(): self
339349
{
340-
return $this->function === null && !$this->isInAnonymousFunction();
350+
return $this->scopeFactory->create(
351+
$this->context,
352+
$this->isDeclareStrictTypes(),
353+
$this->constantTypes,
354+
$this->getFunction(),
355+
$this->getNamespace(),
356+
$this->getVariableTypes(),
357+
$this->moreSpecificTypes,
358+
$this->inClosureBindScopeClass,
359+
$this->anonymousFunctionReflection,
360+
$this->isInFirstLevelStatement(),
361+
$this->currentlyAssignedExpressions,
362+
$this->nativeExpressionTypes,
363+
$this->inFunctionCallsStack,
364+
true,
365+
$this->parentScope
366+
);
341367
}
342368

343369
public function hasVariableType(string $variableName): TrinaryLogic
@@ -347,7 +373,7 @@ public function hasVariableType(string $variableName): TrinaryLogic
347373
}
348374

349375
if (!isset($this->variableTypes[$variableName])) {
350-
if ($this->isRootScope()) {
376+
if ($this->canAnyVariableExist()) {
351377
return TrinaryLogic::createMaybe();
352378
}
353379

@@ -1957,6 +1983,7 @@ public function doNotTreatPhpDocTypesAsCertain(): Scope
19571983
$this->inFunctionCallsStack,
19581984
$this->dynamicConstantNames,
19591985
false,
1986+
$this->afterExtractCall,
19601987
$this->parentScope
19611988
);
19621989
}
@@ -2213,6 +2240,7 @@ public function pushInFunctionCall($reflection): self
22132240
$this->currentlyAssignedExpressions,
22142241
$this->nativeExpressionTypes,
22152242
$stack,
2243+
$this->afterExtractCall,
22162244
$this->parentScope
22172245
);
22182246
}
@@ -2236,6 +2264,7 @@ public function popInFunctionCall(): self
22362264
$this->currentlyAssignedExpressions,
22372265
$this->nativeExpressionTypes,
22382266
$stack,
2267+
$this->afterExtractCall,
22392268
$this->parentScope
22402269
);
22412270
}
@@ -2622,6 +2651,7 @@ public function enterAnonymousFunction(
26222651
[],
26232652
$nativeTypes,
26242653
[],
2654+
false,
26252655
$this
26262656
);
26272657
}
@@ -2668,6 +2698,7 @@ public function enterArrowFunction(Expr\ArrowFunction $arrowFunction): self
26682698
[],
26692699
[],
26702700
[],
2701+
$this->afterExtractCall,
26712702
$this->parentScope
26722703
);
26732704
}
@@ -2782,6 +2813,7 @@ public function enterExpressionAssign(Expr $expr): self
27822813
$currentlyAssignedExpressions,
27832814
$this->nativeExpressionTypes,
27842815
[],
2816+
$this->afterExtractCall,
27852817
$this->parentScope
27862818
);
27872819
}
@@ -2806,6 +2838,7 @@ public function exitExpressionAssign(Expr $expr): self
28062838
$currentlyAssignedExpressions,
28072839
$this->nativeExpressionTypes,
28082840
[],
2841+
$this->afterExtractCall,
28092842
$this->parentScope
28102843
);
28112844
}
@@ -2861,6 +2894,7 @@ public function assignVariable(string $variableName, Type $type, ?TrinaryLogic $
28612894
$this->currentlyAssignedExpressions,
28622895
$nativeTypes,
28632896
$this->inFunctionCallsStack,
2897+
$this->afterExtractCall,
28642898
$this->parentScope
28652899
);
28662900
}
@@ -2890,6 +2924,7 @@ public function unsetExpression(Expr $expr): self
28902924
[],
28912925
$nativeTypes,
28922926
[],
2927+
$this->afterExtractCall,
28932928
$this->parentScope
28942929
);
28952930
} elseif ($expr instanceof Expr\ArrayDimFetch && $expr->dim !== null) {
@@ -2948,6 +2983,7 @@ public function specifyExpressionType(Expr $expr, Type $type): self
29482983
$this->currentlyAssignedExpressions,
29492984
$this->nativeExpressionTypes,
29502985
$this->inFunctionCallsStack,
2986+
$this->afterExtractCall,
29512987
$this->parentScope
29522988
);
29532989
}
@@ -2979,6 +3015,7 @@ public function specifyExpressionType(Expr $expr, Type $type): self
29793015
$this->currentlyAssignedExpressions,
29803016
$nativeTypes,
29813017
$this->inFunctionCallsStack,
3018+
$this->afterExtractCall,
29823019
$this->parentScope
29833020
);
29843021
} elseif ($expr instanceof Expr\ArrayDimFetch && $expr->dim !== null) {
@@ -3055,6 +3092,7 @@ public function invalidateExpression(Expr $expressionToInvalidate, bool $require
30553092
$this->currentlyAssignedExpressions,
30563093
[],
30573094
[],
3095+
$this->afterExtractCall,
30583096
$this->parentScope
30593097
);
30603098
}
@@ -3167,6 +3205,7 @@ public function exitFirstLevelStatements(): self
31673205
$this->currentlyAssignedExpressions,
31683206
$this->nativeExpressionTypes,
31693207
$this->inFunctionCallsStack,
3208+
$this->afterExtractCall,
31703209
$this->parentScope
31713210
);
31723211
}
@@ -3202,6 +3241,7 @@ private function addMoreSpecificTypes(array $types): self
32023241
$this->currentlyAssignedExpressions,
32033242
$this->nativeExpressionTypes,
32043243
[],
3244+
$this->afterExtractCall,
32053245
$this->parentScope
32063246
);
32073247
}
@@ -3221,7 +3261,7 @@ public function mergeWith(?self $otherScope): self
32213261

32223262
$ourVariableTypes = $this->getVariableTypes();
32233263
$theirVariableTypes = $otherScope->getVariableTypes();
3224-
if ($this->isRootScope()) {
3264+
if ($this->canAnyVariableExist()) {
32253265
foreach (array_keys($theirVariableTypes) as $name) {
32263266
if (array_key_exists($name, $ourVariableTypes)) {
32273267
continue;
@@ -3253,6 +3293,7 @@ public function mergeWith(?self $otherScope): self
32533293
return $holder->getCertainty()->yes();
32543294
})),
32553295
[],
3296+
$this->afterExtractCall && $otherScope->afterExtractCall,
32563297
$this->parentScope
32573298
);
32583299
}
@@ -3323,6 +3364,7 @@ public function processFinallyScope(self $finallyScope, self $originalFinallySco
33233364
array_map($typeToVariableHolder, $originalFinallyScope->nativeExpressionTypes)
33243365
)),
33253366
[],
3367+
$this->afterExtractCall,
33263368
$this->parentScope
33273369
);
33283370
}
@@ -3414,6 +3456,7 @@ public function processClosureScope(
34143456
[],
34153457
$this->nativeExpressionTypes,
34163458
$this->inFunctionCallsStack,
3459+
$this->afterExtractCall,
34173460
$this->parentScope
34183461
);
34193462
}
@@ -3462,6 +3505,7 @@ public function processAlwaysIterableForeachScopeWithoutPollute(self $finalScope
34623505
[],
34633506
$nativeTypes,
34643507
[],
3508+
$this->afterExtractCall,
34653509
$this->parentScope
34663510
);
34673511
}
@@ -3506,6 +3550,7 @@ public function generalizeWith(self $otherScope): self
35063550
[],
35073551
$nativeTypes,
35083552
[],
3553+
$this->afterExtractCall,
35093554
$this->parentScope
35103555
);
35113556
}

src/Analyser/NodeScopeResolver.php

+4
Original file line numberDiff line numberDiff line change
@@ -1562,6 +1562,10 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression
15621562
) {
15631563
$scope = $scope->assignVariable('http_response_header', new ArrayType(new IntegerType(), new StringType()));
15641564
}
1565+
1566+
if (isset($functionReflection) && $functionReflection->getName() === 'extract') {
1567+
$scope = $scope->afterExtractCall();
1568+
}
15651569
} elseif ($expr instanceof MethodCall) {
15661570
$originalScope = $scope;
15671571
if (

src/Analyser/ScopeFactory.php

+2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ interface ScopeFactory
2424
* @param array<string, true> $currentlyAssignedExpressions
2525
* @param array<string, Type> $nativeExpressionTypes
2626
* @param array<MethodReflection|FunctionReflection> $inFunctionCallsStack
27+
* @param bool $afterExtractCall
2728
* @param Scope|null $parentScope
2829
*
2930
* @return MutatingScope
@@ -42,6 +43,7 @@ public function create(
4243
array $currentlyAssignedExpressions = [],
4344
array $nativeExpressionTypes = [],
4445
array $inFunctionCallsStack = [],
46+
bool $afterExtractCall = false,
4547
?Scope $parentScope = null
4648
): MutatingScope;
4749

tests/PHPStan/Analyser/NodeScopeResolverTest.php

+6
Original file line numberDiff line numberDiff line change
@@ -10230,6 +10230,11 @@ public function dataArraySliceNonEmpty(): array
1023010230
return $this->gatherAssertTypes(__DIR__ . '/data/array-slice-non-empty.php');
1023110231
}
1023210232

10233+
public function dataBug3990(): array
10234+
{
10235+
return $this->gatherAssertTypes(__DIR__ . '/data/bug-3990.php');
10236+
}
10237+
1023310238
/**
1023410239
* @dataProvider dataBug2574
1023510240
* @dataProvider dataBug2577
@@ -10318,6 +10323,7 @@ public function dataArraySliceNonEmpty(): array
1031810323
* @dataProvider dataBug2816Two
1031910324
* @dataProvider dataBug3985
1032010325
* @dataProvider dataArraySliceNonEmpty
10326+
* @dataProvider dataBug3990
1032110327
* @param string $assertType
1032210328
* @param string $file
1032310329
* @param mixed ...$args
+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace Bug3990;
4+
5+
use PHPStan\TrinaryLogic;
6+
use function PHPStan\Analyser\assertVariableCertainty;
7+
8+
function doFoo(array $config): void
9+
{
10+
extract($config);
11+
12+
assertVariableCertainty(TrinaryLogic::createMaybe(), $a);
13+
14+
if (isset($a)) {
15+
assertVariableCertainty(TrinaryLogic::createYes(), $a);
16+
} else {
17+
assertVariableCertainty(TrinaryLogic::createMaybe(), $a);
18+
}
19+
20+
assertVariableCertainty(TrinaryLogic::createMaybe(), $a);
21+
}

0 commit comments

Comments
 (0)