diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 93bb2b8b5d..3e97c12bd8 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -1450,7 +1450,7 @@ public function processStmtNode( $originalStorage = $storage; $unrolledEndScope = null; - if ($context->isTopLevel()) { + if ($context->shouldRunLoopConvergence()) { $storage = $originalStorage->duplicate(); $originalScope = $this->polluteScopeWithAlwaysIterableForeach ? $scope->filterByTruthyValue($arrayComparisonExpr) : $scope; @@ -1673,7 +1673,7 @@ public function processStmtNode( } $bodyScope = $condResult->getTruthyScope(); - if ($context->isTopLevel()) { + if ($context->shouldRunLoopConvergence()) { $count = 0; do { $prevScope = $bodyScope; @@ -1766,7 +1766,7 @@ public function processStmtNode( $impurePoints = []; $originalStorage = $storage; - if ($context->isTopLevel()) { + if ($context->shouldRunLoopConvergence()) { do { $prevScope = $bodyScope; $bodyScope = $bodyScope->mergeWith($scope); @@ -1886,7 +1886,7 @@ public function processStmtNode( } } - if ($context->isTopLevel()) { + if ($context->shouldRunLoopConvergence()) { $count = 0; do { $prevScope = $bodyScope; diff --git a/src/Analyser/StatementContext.php b/src/Analyser/StatementContext.php index fb2893b584..27495eec64 100644 --- a/src/Analyser/StatementContext.php +++ b/src/Analyser/StatementContext.php @@ -13,8 +13,10 @@ final class StatementContext { + private const LOOP_CONVERGENCE_DEPTH_LIMIT = 2; + private function __construct( - private bool $isTopLevel, + private int $depth, private int $foreachUnrollFactor = 1, ) { @@ -25,7 +27,7 @@ private function __construct( */ public static function createTopLevel(): self { - return new self(true); + return new self(0); } /** @@ -33,12 +35,17 @@ public static function createTopLevel(): self */ public static function createDeep(): self { - return new self(false); + return new self(self::LOOP_CONVERGENCE_DEPTH_LIMIT); } public function isTopLevel(): bool { - return $this->isTopLevel; + return $this->depth === 0; + } + + public function shouldRunLoopConvergence(): bool + { + return $this->depth < self::LOOP_CONVERGENCE_DEPTH_LIMIT; } public function getForeachUnrollFactor(): int @@ -48,16 +55,16 @@ public function getForeachUnrollFactor(): int public function enterDeep(): self { - if ($this->isTopLevel) { - return new self(false, $this->foreachUnrollFactor); + if ($this->depth >= self::LOOP_CONVERGENCE_DEPTH_LIMIT) { + return $this; } - return $this; + return new self($this->depth + 1, $this->foreachUnrollFactor); } public function enterUnrolledForeach(int $totalKeys): self { - return new self($this->isTopLevel, $this->foreachUnrollFactor * $totalKeys); + return new self($this->depth, $this->foreachUnrollFactor * $totalKeys); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-10438.php b/tests/PHPStan/Analyser/nsrt/bug-10438.php index 89fbd0762c..19ee224936 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-10438.php +++ b/tests/PHPStan/Analyser/nsrt/bug-10438.php @@ -21,7 +21,7 @@ public function extract(SimpleXMLElement $data, string $type = 'Meta'): array $meta[$key] = (string)$tag->{$valueName}; continue; } - assertType('array', $meta); + assertType('array|string>', $meta); $meta[$key] = []; assertType('array{}', $meta[$key]); foreach ($tag->{$valueName} as $value) { diff --git a/tests/PHPStan/Analyser/nsrt/bug-10666.php b/tests/PHPStan/Analyser/nsrt/bug-10666.php new file mode 100644 index 0000000000..805dcfc857 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10666.php @@ -0,0 +1,37 @@ +", $text); + array_pop($text); + assertType("list<'x'>", $text); + } + assertType("non-empty-list<'x'>", $text); + return implode('', $text); +} + +function shiftInNestedLoop(int $n): string +{ + $text = []; + while (empty($text)) { + $i = 0; + while ($i < $n) { + $text[] = 'x'; + $i++; + } + array_shift($text); + } + assertType("non-empty-list<'x'>", $text); + return implode('', $text); +} diff --git a/tests/PHPStan/Analyser/nsrt/generalize-scope-recursive.php b/tests/PHPStan/Analyser/nsrt/generalize-scope-recursive.php index d4a82c8dcb..0b03bacddb 100644 --- a/tests/PHPStan/Analyser/nsrt/generalize-scope-recursive.php +++ b/tests/PHPStan/Analyser/nsrt/generalize-scope-recursive.php @@ -16,7 +16,7 @@ public function doFoo(array $array, array $values) } } - assertType('array{}|array{foo?: array}', $data); + assertType('array{}|array{foo: array}', $data); } /** diff --git a/tests/PHPStan/Rules/Variables/EmptyRuleTest.php b/tests/PHPStan/Rules/Variables/EmptyRuleTest.php index 582fdeb107..a3ca451895 100644 --- a/tests/PHPStan/Rules/Variables/EmptyRuleTest.php +++ b/tests/PHPStan/Rules/Variables/EmptyRuleTest.php @@ -223,6 +223,13 @@ public function testBug7806(): void $this->analyse([__DIR__ . '/data/bug-7806.php'], []); } + public function testBug10666(): void + { + $this->treatPhpDocTypesAsCertain = true; + + $this->analyse([__DIR__ . '/data/bug-10666.php'], []); + } + public function testBug14393(): void { $this->treatPhpDocTypesAsCertain = true; diff --git a/tests/PHPStan/Rules/Variables/data/bug-10666.php b/tests/PHPStan/Rules/Variables/data/bug-10666.php new file mode 100644 index 0000000000..dd1ed8eb8f --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-10666.php @@ -0,0 +1,60 @@ + $items + */ + public function fiveLevelWhile(array $items): int + { + $count = 0; + while ($items !== []) { + $item = array_shift($items); + $parts = explode('.', $item); + while ($parts !== []) { + $part = array_shift($parts); + $chars = str_split($part); + while ($chars !== []) { + $char = array_shift($chars); + $codes = [ord($char)]; + while ($codes !== []) { + $code = array_shift($codes); + $bits = []; + while ($code > 0) { + $bits[] = $code & 1; + $code >>= 1; + } + $count += count($bits); + } + } + } + } + return $count; + } + + /** + * @param list $items + */ + public function fiveLevelFor(array $items): int + { + $count = 0; + for ($a = 0; $a < count($items); $a++) { + $parts = explode('.', $items[$a]); + for ($b = 0; $b < count($parts); $b++) { + $chars = str_split($parts[$b]); + for ($c = 0; $c < count($chars); $c++) { + $codes = [ord($chars[$c])]; + for ($d = 0; $d < count($codes); $d++) { + $code = $codes[$d]; + for ($e = 0; $e < 8; $e++) { + $count += ($code >> $e) & 1; + } + } + } + } + } + return $count; + } + + /** + * @param list $items + */ + public function fiveLevelForeach(array $items): int + { + $count = 0; + foreach ($items as $item) { + $parts = explode('.', $item); + foreach ($parts as $part) { + $chars = str_split($part); + foreach ($chars as $char) { + $codes = [ord($char)]; + foreach ($codes as $code) { + $bits = []; + foreach (range(0, 7) as $i) { + $bits[] = ($code >> $i) & 1; + } + $count += array_sum($bits); + } + } + } + } + return $count; + } + + /** + * @param array> $data + */ + public function mixedLoopTypes(array $data): int + { + $total = 0; + foreach ($data as $key => $values) { + $i = 0; + while ($i < count($values)) { + $n = $values[$i]; + for ($j = 0; $j < $n; $j++) { + $k = $j; + do { + $total += $k; + $k--; + } while ($k > 0); + } + $i++; + } + } + return $total; + } +}