Skip to content

Commit c28092e

Browse files
committed
Dependent variables in foreach when iterating over constant array
1 parent 8208ca6 commit c28092e

File tree

4 files changed

+88
-0
lines changed

4 files changed

+88
-0
lines changed

src/Analyser/MutatingScope.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3464,6 +3464,36 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self
34643464
return $scope;
34653465
}
34663466

3467+
/**
3468+
* @param string $exprString
3469+
* @param ConditionalExpressionHolder[] $conditionalExpressionHolders
3470+
* @return self
3471+
*/
3472+
public function addConditionalExpressions(string $exprString, array $conditionalExpressionHolders): self
3473+
{
3474+
$conditionalExpressions = $this->conditionalExpressions;
3475+
$conditionalExpressions[$exprString] = $conditionalExpressionHolders;
3476+
return $this->scopeFactory->create(
3477+
$this->context,
3478+
$this->isDeclareStrictTypes(),
3479+
$this->constantTypes,
3480+
$this->getFunction(),
3481+
$this->getNamespace(),
3482+
$this->variableTypes,
3483+
$this->moreSpecificTypes,
3484+
$this->typeGuards,
3485+
$conditionalExpressions,
3486+
$this->inClosureBindScopeClass,
3487+
$this->anonymousFunctionReflection,
3488+
$this->inFirstLevelStatement,
3489+
$this->currentlyAssignedExpressions,
3490+
$this->nativeExpressionTypes,
3491+
$this->inFunctionCallsStack,
3492+
$this->afterExtractCall,
3493+
$this->parentScope
3494+
);
3495+
}
3496+
34673497
public function exitFirstLevelStatements(): self
34683498
{
34693499
return $this->scopeFactory->create(

src/Analyser/NodeScopeResolver.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2753,6 +2753,7 @@ private function processVarAnnotation(MutatingScope $scope, string $variableName
27532753
private function enterForeach(MutatingScope $scope, Foreach_ $stmt): MutatingScope
27542754
{
27552755
$comment = CommentHelper::getDocComment($stmt);
2756+
$iterateeType = $scope->getType($stmt->expr);
27562757
if ($stmt->valueVar instanceof Variable && is_string($stmt->valueVar->name)) {
27572758
$scope = $scope->enterForeach(
27582759
$stmt->expr,
@@ -2778,6 +2779,26 @@ private function enterForeach(MutatingScope $scope, Foreach_ $stmt): MutatingSco
27782779
}
27792780
}
27802781

2782+
if (
2783+
$comment === null
2784+
&& $iterateeType instanceof ConstantArrayType
2785+
&& $stmt->valueVar instanceof Variable && is_string($stmt->valueVar->name)
2786+
&& $stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name)
2787+
) {
2788+
$conditionalHolders = [];
2789+
foreach ($iterateeType->getKeyTypes() as $i => $keyType) {
2790+
$valueType = $iterateeType->getValueTypes()[$i];
2791+
$conditionalHolders[] = new ConditionalExpressionHolder([
2792+
'$' . $stmt->keyVar->name => $keyType,
2793+
], new VariableTypeHolder($valueType, TrinaryLogic::createYes()));
2794+
}
2795+
2796+
$scope = $scope->addConditionalExpressions(
2797+
'$' . $stmt->valueVar->name,
2798+
$conditionalHolders
2799+
);
2800+
}
2801+
27812802
if ($stmt->valueVar instanceof List_ || $stmt->valueVar instanceof Array_) {
27822803
$exprType = $scope->getType($stmt->expr);
27832804
$itemType = $exprType->getIterableValueType();

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10546,6 +10546,11 @@ public function dataConditionalTypeWithNonEmptyArray(): array
1054610546
return $this->gatherAssertTypes(__DIR__ . '/data/conditional-non-empty-array.php');
1054710547
}
1054810548

10549+
public function dataForeachDependentKeyValue(): array
10550+
{
10551+
return $this->gatherAssertTypes(__DIR__ . '/data/foreach-dependent-key-value.php');
10552+
}
10553+
1054910554
/**
1055010555
* @param string $file
1055110556
* @return array<string, mixed[]>
@@ -10741,6 +10746,7 @@ private function gatherAssertTypes(string $file): array
1074110746
* @dataProvider dataDependentVariableCertainty
1074210747
* @dataProvider dataBug1865
1074310748
* @dataProvider dataConditionalTypeWithNonEmptyArray
10749+
* @dataProvider dataForeachDependentKeyValue
1074410750
* @param string $assertType
1074510751
* @param string $file
1074610752
* @param mixed ...$args
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
namespace ForeachDependentKeyValue;
4+
5+
use function PHPStan\Analyser\assertType;
6+
7+
class Foo
8+
{
9+
10+
/**
11+
* @param array{foo: int, bar: string} $a
12+
*/
13+
public function doFoo(array $a): void
14+
{
15+
foreach ($a as $key => $val) {
16+
assertType('int|string', $val);
17+
if ($key === 'foo') {
18+
assertType('int', $val);
19+
} else {
20+
assertType('string', $val);
21+
}
22+
23+
if ($key === 'bar') {
24+
assertType('string', $val);
25+
} else {
26+
assertType('int', $val);
27+
}
28+
}
29+
}
30+
31+
}

0 commit comments

Comments
 (0)