Skip to content
2 changes: 1 addition & 1 deletion src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -3877,7 +3877,7 @@ public function processAlwaysIterableForeachScopeWithoutPollute(self $finalScope
$this->getNamespace(),
$expressionTypes,
$nativeTypes,
$this->conditionalExpressions,
$this->intersectConditionalExpressions($finalScope->conditionalExpressions),
$this->inClosureBindScopeClasses,
$this->anonymousFunctionReflection,
$this->inFirstLevelStatement,
Expand Down
36 changes: 36 additions & 0 deletions tests/PHPStan/Analyser/Bug14446Test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php declare(strict_types = 1);

namespace PHPStan\Analyser;

use PHPStan\Testing\TypeInferenceTestCase;
use PHPUnit\Framework\Attributes\DataProvider;

class Bug14446Test extends TypeInferenceTestCase
{

public static function dataFileAsserts(): iterable
{
yield from self::gatherAssertTypes(__DIR__ . '/data/bug-14446.php');
}

/**
* @param mixed ...$args
*/
#[DataProvider('dataFileAsserts')]
public function testFileAsserts(
string $assertType,
string $file,
...$args,
): void
{
$this->assertFileAsserts($assertType, $file, ...$args);
}

public static function getAdditionalConfigFiles(): array
{
return [
__DIR__ . '/bug-14446.neon',
];
}

}
8 changes: 8 additions & 0 deletions tests/PHPStan/Analyser/bug-14446.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
parameters:
polluteScopeWithAlwaysIterableForeach: false

services:
-
class: PHPStan\Rules\ForeachLoop\OverwriteVariablesWithForeachRule
tags:
- phpstan.rules.rule
70 changes: 70 additions & 0 deletions tests/PHPStan/Analyser/data/bug-14446.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php declare(strict_types = 1);

namespace Bug14446;

use function PHPStan\Testing\assertType;

function test(bool $initial): void {
$current = $initial;

while (true) {
assertType('bool', $initial);
if (!$current) {
assertType('bool', $initial);
break;
}

$items = [1];
foreach ($items as $item) {
$current = false;
}
}

assertType('bool', $initial);
var_dump($initial === true);
}

function testMaybeIterable(bool $initial): void {
$current = $initial;

while (true) {
assertType('bool', $initial);
if (!$current) {
assertType('bool', $initial);
break;
}

$items = rand() > 0 ? [1] : [];
foreach ($items as $item) {
$current = false;
}
}

assertType('bool', $initial);
var_dump($initial === true);
}

/**
* @param mixed $value
*/
function testForeachKeyOverwrite($value): void {
if (is_array($value) && $value !== []) {
$hasOnlyStringKey = true;
foreach (array_keys($value) as $key) {
if (is_int($key)) {
$hasOnlyStringKey = false;
break;
}
}

assertType('bool', $hasOnlyStringKey);

if ($hasOnlyStringKey) {
// $key should not be in scope here with polluteScopeWithAlwaysIterableForeach: false
// Second foreach should not report "Foreach overwrites $key with its key variable"
foreach ($value as $key => $element) {
assertType('(int|string)', $key);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ class StrictComparisonOfDifferentTypesRuleTest extends RuleTestCase

private bool $treatPhpDocTypesAsCertain = true;

private bool $polluteScopeWithAlwaysIterableForeach = true;

protected function getRule(): Rule
{
return new StrictComparisonOfDifferentTypesRule(
Expand All @@ -36,6 +38,11 @@ protected function shouldTreatPhpDocTypesAsCertain(): bool
return $this->treatPhpDocTypesAsCertain;
}

protected function shouldPolluteScopeWithAlwaysIterableForeach(): bool
{
return $this->polluteScopeWithAlwaysIterableForeach;
}

public function testStrictComparison(): void
{
$tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting <fg=cyan>treatPhpDocTypesAsCertain: false</> in your <fg=cyan>%configurationFile%</>.';
Expand Down Expand Up @@ -1184,4 +1191,10 @@ public function testBug13421(): void
$this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-13421.php'], []);
}

public function testBug14446(): void
{
$this->polluteScopeWithAlwaysIterableForeach = false;
$this->analyse([__DIR__ . '/../../Analyser/data/bug-14446.php'], []);
}

}
Loading