diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 72f087b400..f3904f0274 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -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; @@ -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']; @@ -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(); @@ -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; diff --git a/src/Command/AnalyseCommand.php b/src/Command/AnalyseCommand.php index 5b3be09c6b..64d55f8da4 100644 --- a/src/Command/AnalyseCommand.php +++ b/src/Command/AnalyseCommand.php @@ -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]) { + $inceptionResult->getStdOutput()->writeLineFormatted($error->getMessage()); $inceptionResult->getStdOutput()->writeLineFormatted(''); } diff --git a/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php b/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php index b83b5a35a1..8c4b70d805 100644 --- a/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php +++ b/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php @@ -134,8 +134,8 @@ 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; @@ -143,7 +143,7 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier): /** @var FetchedNode $fetchedClassNode */ $fetchedClassNode = current($fetchedClassNodes[$identifierName]); - $fetchedFile = $file; + $fetchedFile = $sourceFile; } [$reflectionCacheKey, $variableCacheKey] = $this->getCacheKeys($fetchedFile, $identifier); diff --git a/src/Type/Constant/ConstantArrayType.php b/src/Type/Constant/ConstantArrayType.php index 7eca2eaa58..60372869ae 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -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)) { @@ -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); } diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index 011cdec938..b8dca23375 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -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); diff --git a/tests/PHPStan/Analyser/Bug13312NoPolluteTest.php b/tests/PHPStan/Analyser/Bug13312NoPolluteTest.php new file mode 100644 index 0000000000..43f89c0d88 --- /dev/null +++ b/tests/PHPStan/Analyser/Bug13312NoPolluteTest.php @@ -0,0 +1,36 @@ +assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/bug-13312-no-pollute.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/bug-13312-no-pollute.neon b/tests/PHPStan/Analyser/bug-13312-no-pollute.neon new file mode 100644 index 0000000000..3ee516d3be --- /dev/null +++ b/tests/PHPStan/Analyser/bug-13312-no-pollute.neon @@ -0,0 +1,2 @@ +parameters: + polluteScopeWithAlwaysIterableForeach: false diff --git a/tests/PHPStan/Analyser/data/bug-13312-no-pollute.php b/tests/PHPStan/Analyser/data/bug-13312-no-pollute.php new file mode 100644 index 0000000000..2926f97297 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-13312-no-pollute.php @@ -0,0 +1,37 @@ + $arr */ +function foo(array $arr): void { + assertType('list', $arr); + foreach ($arr as $v) { + assertType('non-empty-list', $arr); + } + assertType('list', $arr); + + for ($i = 0; $i < count($arr); ++$i) { + assertType('non-empty-list', $arr); + } + assertType('list', $arr); +} + +/** @param array $arr */ +function fooStringKeyed(array $arr): void { + assertType('array', $arr); + foreach ($arr as $v) { + assertType('non-empty-array', $arr); + } + assertType('array', $arr); +} + +/** @param list $arr */ +function fooReassign(array $arr): void { + foreach ($arr as $v) { + $arr = []; + assertType('array{}', $arr); + } + assertType('array{}', $arr); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-13312.php b/tests/PHPStan/Analyser/nsrt/bug-13312.php index 3a7ab22873..9a47594a06 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-13312.php +++ b/tests/PHPStan/Analyser/nsrt/bug-13312.php @@ -32,6 +32,24 @@ function foo(array $arr): void { } +/** @param array $arr */ +function fooStringKeyed(array $arr): void { + assertType('array', $arr); + foreach ($arr as $v) { + assertType('non-empty-array', $arr); + } + assertType('array', $arr); +} + +/** @param list $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) { diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php index ca23663401..a4add283d7 100644 --- a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php @@ -961,10 +961,6 @@ public function testBug8467c(): void 'Variable $v might not be defined.', 16, ], - [ - 'Variable $v might not be defined.', - 18, - ], ]); }