Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -1453,6 +1453,11 @@ public function processStmtNode(
$stmt->expr,
new Array_([]),
);
// Inside the loop body the iterated expression is provably non-empty, so
// narrow it (list -> non-empty-list, array -> non-empty-array) at body entry
// from the pre-loop scope. This is independent of polluteScopeWithAlwaysIterableForeach,
// which only controls what leaks into the after-loop scope.
$nonEmptyIterateeScope = $scope->filterByTruthyValue($arrayComparisonExpr);
$this->callNodeCallback($nodeCallback, new InForeachNode($stmt), $scope, $storage);
$originalScope = $scope;
$bodyScope = $scope;
Expand All @@ -1475,7 +1480,7 @@ public function processStmtNode(
if ($context->isTopLevel()) {
$storage = $originalStorage->duplicate();

$originalScope = $this->polluteScopeWithAlwaysIterableForeach ? $scope->filterByTruthyValue($arrayComparisonExpr) : $scope;
$originalScope = $nonEmptyIterateeScope;
$unrolledResult = $this->tryProcessUnrolledConstantArrayForeach($stmt, $originalScope, $originalStorage, $context);
if ($unrolledResult !== null) {
$bodyScope = $unrolledResult['bodyScope'];
Expand All @@ -1486,7 +1491,7 @@ public function processStmtNode(
$count = 0;
do {
$prevScope = $bodyScope;
$bodyScope = $bodyScope->mergeWith($this->polluteScopeWithAlwaysIterableForeach ? $scope->filterByTruthyValue($arrayComparisonExpr) : $scope);
$bodyScope = $bodyScope->mergeWith($nonEmptyIterateeScope);
$storage = $originalStorage->duplicate();
$bodyScope = $this->enterForeach($bodyScope, $storage, $originalScope, $stmt, $nodeCallback);
$bodyScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, new NoopNodeCallback(), $context->enterDeep())->filterOutLoopExitPoints();
Expand All @@ -1506,7 +1511,7 @@ public function processStmtNode(
}
}

$bodyScope = $bodyScope->mergeWith($this->polluteScopeWithAlwaysIterableForeach ? $scope->filterByTruthyValue($arrayComparisonExpr) : $scope);
$bodyScope = $bodyScope->mergeWith($nonEmptyIterateeScope);
$storage = $originalStorage;
$bodyScope = $this->enterForeach($bodyScope, $storage, $originalScope, $stmt, $nodeCallback);
$finalPassContext = $unrolledTotalKeys !== null ? $context->enterUnrolledForeach($unrolledTotalKeys) : $context;
Expand Down
4 changes: 2 additions & 2 deletions src/Command/AnalyseCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -477,8 +477,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int
if ($generateBaselineFile !== null) {
$this->runDiagnoseExtensions($container, $inceptionResult->getErrorOutput(), $analysisResult->getProcessedFiles());
if (count($internalErrorsTuples) > 0) {
foreach ($internalErrorsTuples as [$internalError]) {
$inceptionResult->getStdOutput()->writeLineFormatted($internalError->getMessage());
foreach ($internalErrorsTuples as [$error]) {

@staabm staabm Jul 25, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are this changes necessary?
(Did your change broke the polluteScopeWithAlwaysIterableForeach use-case?)

$inceptionResult->getStdOutput()->writeLineFormatted($error->getMessage());
$inceptionResult->getStdOutput()->writeLineFormatted('');
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,16 +134,16 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier):
if ($identifier->isClass()) {
$fetchedClassNode = null;
$fetchedFile = null;
foreach ($files as $file) {
$fetchedClassNodes = $this->fileNodesFetcher->fetchNodes($file)->getClassNodes();
foreach ($files as $sourceFile) {
$fetchedClassNodes = $this->fileNodesFetcher->fetchNodes($sourceFile)->getClassNodes();

if (!array_key_exists($identifierName, $fetchedClassNodes)) {
return null;
}

/** @var FetchedNode<Node\Stmt\ClassLike> $fetchedClassNode */
$fetchedClassNode = current($fetchedClassNodes[$identifierName]);
$fetchedFile = $file;
$fetchedFile = $sourceFile;
}

[$reflectionCacheKey, $variableCacheKey] = $this->getCacheKeys($fetchedFile, $identifier);
Expand Down
15 changes: 12 additions & 3 deletions src/Type/Constant/ConstantArrayType.php
Original file line number Diff line number Diff line change
Expand Up @@ -811,7 +811,11 @@ public function isSuperTypeOf(Type $type): IsSuperTypeOfResult
continue;
}

[$thisUnsealedKey, $thisUnsealedValue] = $this->unsealed;
$thisUnsealed = $this->unsealed;
if ($thisUnsealed === null) {
throw new ShouldNotHappenException();
}
[$thisUnsealedKey, $thisUnsealedValue] = $thisUnsealed;
$keyCheck = $thisUnsealedKey->isSuperTypeOf($typeKey);
if ($keyCheck->no()) {
if ($type->isOptionalKey($i)) {
Expand All @@ -835,8 +839,13 @@ public function isSuperTypeOf(Type $type): IsSuperTypeOfResult
if ($thisUnsealedness->no()) {
$results[] = IsSuperTypeOfResult::createMaybe();
} else {
[$thisUnsealedKey, $thisUnsealedValue] = $this->unsealed;
[$typeUnsealedKey, $typeUnsealedValue] = $type->unsealed;
$thisUnsealed = $this->unsealed;
$typeUnsealed = $type->unsealed;
if ($thisUnsealed === null || $typeUnsealed === null) {
throw new ShouldNotHappenException();
}
[$thisUnsealedKey, $thisUnsealedValue] = $thisUnsealed;
[$typeUnsealedKey, $typeUnsealedValue] = $typeUnsealed;
$results[] = $thisUnsealedKey->isSuperTypeOf($typeUnsealedKey);
$results[] = $thisUnsealedValue->isSuperTypeOf($typeUnsealedValue);
}
Expand Down
4 changes: 2 additions & 2 deletions src/Type/UnionType.php
Original file line number Diff line number Diff line change
Expand Up @@ -239,9 +239,9 @@ public function accepts(Type $type, bool $strictTypes): AcceptsResult
}
if ($commonReasons !== null && count($commonReasons) > 0) {
$decorated = [];
foreach (array_keys($innerAccepts) as $i) {
foreach (array_keys($innerAccepts) as $j) {
foreach ($commonReasons as $reason) {
$decorated[] = sprintf('Type #%d from the union: %s', $i + 1, $reason);
$decorated[] = sprintf('Type #%d from the union: %s', $j + 1, $reason);
}
}
$result = new AcceptsResult($result->result, $decorated);
Expand Down
36 changes: 36 additions & 0 deletions tests/PHPStan/Analyser/Bug13312NoPolluteTest.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 Bug13312NoPolluteTest extends TypeInferenceTestCase
{

public static function dataFileAsserts(): iterable
{
yield from self::gatherAssertTypes(__DIR__ . '/data/bug-13312-no-pollute.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-13312-no-pollute.neon',
];
}

}
2 changes: 2 additions & 0 deletions tests/PHPStan/Analyser/bug-13312-no-pollute.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
parameters:
polluteScopeWithAlwaysIterableForeach: false
37 changes: 37 additions & 0 deletions tests/PHPStan/Analyser/data/bug-13312-no-pollute.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php declare(strict_types = 1);

namespace Bug13312NoPollute;

use function PHPStan\Testing\assertType;

/** @param list<mixed> $arr */
function foo(array $arr): void {
assertType('list<mixed>', $arr);
foreach ($arr as $v) {
assertType('non-empty-list<mixed>', $arr);
}
assertType('list<mixed>', $arr);

for ($i = 0; $i < count($arr); ++$i) {
assertType('non-empty-list<mixed>', $arr);
}
assertType('list<mixed>', $arr);
}

/** @param array<string, int> $arr */
function fooStringKeyed(array $arr): void {
assertType('array<string, int>', $arr);
foreach ($arr as $v) {
assertType('non-empty-array<string, int>', $arr);
}
assertType('array<string, int>', $arr);
}

/** @param list<mixed> $arr */
function fooReassign(array $arr): void {
foreach ($arr as $v) {
$arr = [];
assertType('array{}', $arr);
}
assertType('array{}', $arr);
}
18 changes: 18 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-13312.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,24 @@ function foo(array $arr): void {
}


/** @param array<string, int> $arr */
function fooStringKeyed(array $arr): void {
assertType('array<string, int>', $arr);
foreach ($arr as $v) {
assertType('non-empty-array<string, int>', $arr);
}
assertType('array<string, int>', $arr);
}

/** @param list<mixed> $arr */
function fooReassign(array $arr): void {
foreach ($arr as $v) {
$arr = [];
assertType('array{}', $arr);
}
assertType('array{}', $arr);
}

function fooBar(mixed $mixed): void {
assertType('mixed', $mixed);
foreach ($mixed as $v) {
Expand Down
4 changes: 0 additions & 4 deletions tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -961,10 +961,6 @@ public function testBug8467c(): void
'Variable $v might not be defined.',
16,
],
[
'Variable $v might not be defined.',
18,
],
]);
}

Expand Down
Loading