From 0367cfc9db52c7928b7e0ad8bac81f61b1c4cb36 Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Sat, 24 Oct 2026 15:58:24 +0000 Subject: [PATCH 1/2] Converge loops nested in a top-level loop's convergence passes instead of walking them once - StatementContext: add shouldConvergeLoops() and enterLoopPass(). A top-level loop's passes now run in a deep context whose loops still converge; the passes of those nested loops run in a plain deep context, where loops are walked once, so the cost stays polynomial in the nesting depth. - ForeachHandler, WhileHandler, DoWhileHandler, ForHandler: converge when shouldConvergeLoops() (was isTopLevel()) and walk passes with enterLoopPass() (was enterDeep()). Foreach unrolling over constant arrays stays top-level only. - Previously a nested loop inside an outer pass ran a single iteration, so a counter it increments stayed 0|1. A condition such as `$n > 1` was then never true in the outer loop's passes, and an array appended under it converged to array{}|array{string} instead of list. - Analogous cases fixed by the same change and covered by tests: nested while/for/do-while inner loops, while/for/do-while outer loops, a flag set inside the inner body, and three levels of nesting. - turbo-ext: mirror StatementContext (new slot, methods, direct entries) and the four loop handlers; regenerate generated/StatementContext.h. - bug-10438: the expectation recorded the same bug; an inner foreach appends any number of strings, so list is correct. Co-Authored-By: Claude Opus 5.5 --- src/Analyser/StatementContext.php | 36 +++- src/Analyser/StmtHandler/DoWhileHandler.php | 4 +- src/Analyser/StmtHandler/ForHandler.php | 4 +- src/Analyser/StmtHandler/ForeachHandler.php | 6 +- src/Analyser/StmtHandler/WhileHandler.php | 4 +- tests/PHPStan/Analyser/nsrt/bug-10438.php | 2 +- tests/PHPStan/Analyser/nsrt/bug-15435.php | 195 ++++++++++++++++++ .../Functions/SortWithoutEffectRuleTest.php | 5 + .../Rules/Functions/data/bug-15435.php | 24 +++ turbo-ext/src/DoWhileHandler.cpp | 10 +- turbo-ext/src/ForHandler.cpp | 10 +- turbo-ext/src/ForeachHandler.cpp | 12 +- turbo-ext/src/StatementContext.cpp | 63 ++++-- turbo-ext/src/WhileHandler.cpp | 10 +- turbo-ext/src/generated/StatementContext.h | 87 ++++---- turbo-ext/src/support.h | 2 + 16 files changed, 394 insertions(+), 80 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-15435.php create mode 100644 tests/PHPStan/Rules/Functions/data/bug-15435.php diff --git a/src/Analyser/StatementContext.php b/src/Analyser/StatementContext.php index 178cce93d2d..ce303e284a4 100644 --- a/src/Analyser/StatementContext.php +++ b/src/Analyser/StatementContext.php @@ -23,6 +23,7 @@ private function __construct( private bool $resolveTemplateArguments = true, private ?Type $expectedReturnType = null, private ?Type $nativeExpectedReturnType = null, + private bool $convergesLoops = false, ) { } @@ -48,6 +49,35 @@ public function isTopLevel(): bool return $this->isTopLevel; } + /** + * Whether loops walk their body until the types stabilize. Besides the + * top level, the loops nested in a convergence pass of a top-level loop + * do, and so do the loops nested in their final walks - otherwise a + * variable a nested loop changes over several iterations keeps the type + * of its first iteration in the outer loop's passes. + */ + public function shouldConvergeLoops(): bool + { + return $this->isTopLevel || $this->convergesLoops; + } + + /** + * The context of a loop's convergence pass. The loops nested in a pass of + * a top-level loop converge themselves, their passes' nested loops are + * walked once. + */ + public function enterLoopPass(): self + { + if ($this->isTopLevel) { + return new self(false, $this->foreachUnrollFactor, $this->resolveTemplateArguments, $this->expectedReturnType, $this->nativeExpectedReturnType, true); + } + if ($this->convergesLoops) { + return new self(false, $this->foreachUnrollFactor, $this->resolveTemplateArguments, $this->expectedReturnType, $this->nativeExpectedReturnType); + } + + return $this; + } + public function getForeachUnrollFactor(): int { return $this->foreachUnrollFactor; @@ -64,7 +94,7 @@ public function withoutTemplateArgumentResolution(): self return $this; } - return new self($this->isTopLevel, $this->foreachUnrollFactor, false, $this->expectedReturnType, $this->nativeExpectedReturnType); + return new self($this->isTopLevel, $this->foreachUnrollFactor, false, $this->expectedReturnType, $this->nativeExpectedReturnType, $this->convergesLoops); } /** @@ -78,7 +108,7 @@ public function withExpectedReturnType(?Type $expectedReturnType, ?Type $nativeE return $this; } - return new self($this->isTopLevel, $this->foreachUnrollFactor, $this->resolveTemplateArguments, $expectedReturnType, $nativeExpectedReturnType); + return new self($this->isTopLevel, $this->foreachUnrollFactor, $this->resolveTemplateArguments, $expectedReturnType, $nativeExpectedReturnType, $this->convergesLoops); } public function getExpectedReturnType(): ?Type @@ -102,7 +132,7 @@ public function enterDeep(): self public function enterUnrolledForeach(int $totalKeys): self { - return new self($this->isTopLevel, $this->foreachUnrollFactor * $totalKeys, $this->resolveTemplateArguments, $this->expectedReturnType, $this->nativeExpectedReturnType); + return new self($this->isTopLevel, $this->foreachUnrollFactor * $totalKeys, $this->resolveTemplateArguments, $this->expectedReturnType, $this->nativeExpectedReturnType, $this->convergesLoops); } } diff --git a/src/Analyser/StmtHandler/DoWhileHandler.php b/src/Analyser/StmtHandler/DoWhileHandler.php index f05e8967912..dc2a237cbb5 100644 --- a/src/Analyser/StmtHandler/DoWhileHandler.php +++ b/src/Analyser/StmtHandler/DoWhileHandler.php @@ -67,7 +67,7 @@ public function processStmt( $prevEntryScope = null; $previousPass = null; $previousPassStorage = null; - if ($context->isTopLevel()) { + if ($context->shouldConvergeLoops()) { $bodyIsReplayable = $nodeScopeResolver->isReplayableConvergenceBody($stmt, $stmt->stmts); // each pass after the first re-walks only the statements whose // entry state differs from the previous pass's (StatementsHandler::processLoopPassStmtNodes()) @@ -91,7 +91,7 @@ public function processStmt( $scope->pushExpressionResultStorage($storage); try { $passRecord = null; - $bodyScopeResult = $nodeScopeResolver->processLoopPassStmtNodes($stmt, $stmt->stmts, $bodyScope, $storage, $bodyRecording, $context->enterDeep()->withoutTemplateArgumentResolution(), $previousPass, $passRecord)->filterOutLoopExitPoints(); + $bodyScopeResult = $nodeScopeResolver->processLoopPassStmtNodes($stmt, $stmt->stmts, $bodyScope, $storage, $bodyRecording, $context->enterLoopPass()->withoutTemplateArgumentResolution(), $previousPass, $passRecord)->filterOutLoopExitPoints(); $previousPass = $passRecord; $previousPassStorage = $storage; $backEdgeScope = $bodyScopeResult->getLoopBackEdgeScope(); diff --git a/src/Analyser/StmtHandler/ForHandler.php b/src/Analyser/StmtHandler/ForHandler.php index 7a95e2c5710..bdcb5d385cc 100644 --- a/src/Analyser/StmtHandler/ForHandler.php +++ b/src/Analyser/StmtHandler/ForHandler.php @@ -195,7 +195,7 @@ public function processStmt( } } - if ($context->isTopLevel()) { + if ($context->shouldConvergeLoops()) { $count = 0; $prevEntryScope = null; // each pass after the first re-walks only the statements whose @@ -226,7 +226,7 @@ public function processStmt( $passFlows[] = $passCondResult->getVariableFlow(); } $passRecord = null; - $bodyScopeResult = $nodeScopeResolver->processLoopPassStmtNodes($stmt, $stmt->stmts, $bodyScope, $storage, new NoopNodeCallback(), $context->enterDeep()->withoutTemplateArgumentResolution(), $previousPass, $passRecord)->filterOutLoopExitPoints(); + $bodyScopeResult = $nodeScopeResolver->processLoopPassStmtNodes($stmt, $stmt->stmts, $bodyScope, $storage, new NoopNodeCallback(), $context->enterLoopPass()->withoutTemplateArgumentResolution(), $previousPass, $passRecord)->filterOutLoopExitPoints(); $previousPass = $passRecord; $previousPassStorage = $storage; $passFlows[] = $bodyScopeResult->getVariableFlow(); diff --git a/src/Analyser/StmtHandler/ForeachHandler.php b/src/Analyser/StmtHandler/ForeachHandler.php index 2ba5323bf7b..813aee98cfa 100644 --- a/src/Analyser/StmtHandler/ForeachHandler.php +++ b/src/Analyser/StmtHandler/ForeachHandler.php @@ -195,7 +195,7 @@ static function () use ($condResult, $emptyArrayType): Type { $replayEntryScope = null; $unrolledEndScope = null; $unrolledTotalKeys = null; - if ($context->isTopLevel()) { + if ($context->shouldConvergeLoops()) { $storage = $originalStorage->duplicate(); $originalScope = $nonEmptyIterateeScope; @@ -213,7 +213,7 @@ static function () use ($condResult, $emptyArrayType): Type { $foreachIterateeType = $iterateeResult->getType(); $foreachNativeIterateeType = $iterateeResult->getNativeType(); } - $unrolledResult = $this->tryProcessUnrolledConstantArrayForeach($nodeScopeResolver, $stmt, $originalScope, $originalStorage, $context, $foreachIterateeType, $foreachNativeIterateeType); + $unrolledResult = $context->isTopLevel() ? $this->tryProcessUnrolledConstantArrayForeach($nodeScopeResolver, $stmt, $originalScope, $originalStorage, $context, $foreachIterateeType, $foreachNativeIterateeType) : null; if ($unrolledResult !== null) { $bodyScope = $unrolledResult['bodyScope']; $unrolledEndScope = $unrolledResult['endScope']; @@ -250,7 +250,7 @@ static function () use ($condResult, $emptyArrayType): Type { try { $bodyScope = $this->enterForeach($nodeScopeResolver, $bodyScope, $storage, $originalScope, $stmt, $foreachIterateeType, $foreachNativeIterateeType, $nodeCallback); $passRecord = null; - $bodyScopeResult = $nodeScopeResolver->processLoopPassStmtNodes($stmt, $stmt->stmts, $bodyScope, $storage, $bodyRecording, $context->enterDeep()->withoutTemplateArgumentResolution(), $previousPass, $passRecord)->filterOutLoopExitPoints(); + $bodyScopeResult = $nodeScopeResolver->processLoopPassStmtNodes($stmt, $stmt->stmts, $bodyScope, $storage, $bodyRecording, $context->enterLoopPass()->withoutTemplateArgumentResolution(), $previousPass, $passRecord)->filterOutLoopExitPoints(); $previousPass = $passRecord; $previousPassStorage = $storage; $backEdgeScope = $bodyScopeResult->getLoopBackEdgeScope(); diff --git a/src/Analyser/StmtHandler/WhileHandler.php b/src/Analyser/StmtHandler/WhileHandler.php index aa198450901..b4536cb4fa2 100644 --- a/src/Analyser/StmtHandler/WhileHandler.php +++ b/src/Analyser/StmtHandler/WhileHandler.php @@ -94,7 +94,7 @@ public function processStmt( $prevEntryScope = null; $previousPass = null; $previousPassStorage = null; - if ($context->isTopLevel()) { + if ($context->shouldConvergeLoops()) { $count = 0; $bodyIsReplayable = $nodeScopeResolver->isReplayableConvergenceBody($stmt, $stmt->stmts); do { @@ -119,7 +119,7 @@ public function processStmt( $passCondResult = $nodeScopeResolver->processExprNode($stmt, $stmt->cond, $bodyScope, $storage, $condRecording, ExpressionContext::createDeep(resolveTemplateArguments: false)); $bodyScope = $passCondResult->getTruthyScope(); $passRecord = null; - $bodyScopeResult = $nodeScopeResolver->processLoopPassStmtNodes($stmt, $stmt->stmts, $bodyScope, $storage, $bodyRecording, $context->enterDeep()->withoutTemplateArgumentResolution(), $previousPass, $passRecord)->filterOutLoopExitPoints(); + $bodyScopeResult = $nodeScopeResolver->processLoopPassStmtNodes($stmt, $stmt->stmts, $bodyScope, $storage, $bodyRecording, $context->enterLoopPass()->withoutTemplateArgumentResolution(), $previousPass, $passRecord)->filterOutLoopExitPoints(); $previousPass = $passRecord; $previousPassStorage = $storage; $backEdgeScope = $bodyScopeResult->getLoopBackEdgeScope(); diff --git a/tests/PHPStan/Analyser/nsrt/bug-10438.php b/tests/PHPStan/Analyser/nsrt/bug-10438.php index 89fbd0762c9..19ee2249366 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-15435.php b/tests/PHPStan/Analyser/nsrt/bug-15435.php new file mode 100644 index 00000000000..019631cb631 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-15435.php @@ -0,0 +1,195 @@ + $facts + * @param list $months + */ +function nestedLoop(array $facts, array $months): void +{ + $keys = []; + foreach ($facts as $fact) { + $n = 0; + foreach ($months as $m) { + if ('' !== $m) { + ++$n; + } + } + if ($n > 1) { + $keys[] = $fact['key']; + } + } + assertType('list', $keys); + sort($keys); +} + +/** + * @param list $facts + * @param list $months + */ +function flagInInner(array $facts, array $months): void +{ + $keys = []; + foreach ($facts as $fact) { + $n = 0; + $found = false; + foreach ($months as $m) { + $n++; + if ($n > 1) { + $found = true; + } + } + if ($found) { + $keys[] = $fact['key']; + } + } + assertType('list', $keys); +} + +/** + * @param list $facts + * @param list $months + */ +function whileInner(array $facts, array $months): void +{ + $keys = []; + foreach ($facts as $fact) { + $n = 0; + while ($n < count($months)) { + $n++; + } + if ($n > 1) { + $keys[] = $fact['key']; + } + } + assertType('list', $keys); +} + +/** + * @param list $facts + * @param list $months + */ +function forInner(array $facts, array $months): void +{ + $keys = []; + foreach ($facts as $fact) { + $n = 0; + for ($i = 0; $i < count($months); $i++) { + $n++; + } + if ($n > 1) { + $keys[] = $fact['key']; + } + } + assertType('list', $keys); +} + +/** + * @param list $facts + * @param list $months + */ +function doWhileInner(array $facts, array $months): void +{ + $keys = []; + foreach ($facts as $fact) { + $n = 0; + do { + $n++; + } while ($n < count($months)); + if ($n > 1) { + $keys[] = $fact['key']; + } + } + assertType('list', $keys); +} + +/** + * @param list $facts + * @param list $months + */ +function whileOuter(array $facts, array $months): void +{ + $keys = []; + $j = 0; + while ($j < count($facts)) { + $n = 0; + foreach ($months as $m) { + if ('' !== $m) { + ++$n; + } + } + if ($n > 1) { + $keys[] = $facts[$j]['key']; + } + $j++; + } + assertType('list', $keys); +} + +/** + * @param list $facts + * @param list $months + */ +function forOuter(array $facts, array $months): void +{ + $keys = []; + for ($j = 0; $j < count($facts); $j++) { + $n = 0; + foreach ($months as $m) { + if ('' !== $m) { + ++$n; + } + } + if ($n > 1) { + $keys[] = $facts[$j]['key']; + } + } + assertType('list', $keys); +} + +/** + * @param list $facts + * @param list $months + */ +function doWhileOuter(array $facts, array $months): void +{ + $keys = []; + $j = 0; + do { + $n = 0; + foreach ($months as $m) { + if ('' !== $m) { + ++$n; + } + } + if ($n > 1) { + $keys[] = 'x'; + } + $j++; + } while ($j < count($facts)); + assertType("list<'x'>", $keys); +} + +/** + * @param list> $groups + * @param list $months + */ +function threeLevels(array $groups, array $months): void +{ + $keys = []; + foreach ($groups as $group) { + foreach ($group as $item) { + $n = 0; + foreach ($months as $m) { + ++$n; + } + if ($n > 1) { + $keys[] = $item; + } + } + } + assertType('list', $keys); +} diff --git a/tests/PHPStan/Rules/Functions/SortWithoutEffectRuleTest.php b/tests/PHPStan/Rules/Functions/SortWithoutEffectRuleTest.php index 28bebc71eb6..8c334307c5d 100644 --- a/tests/PHPStan/Rules/Functions/SortWithoutEffectRuleTest.php +++ b/tests/PHPStan/Rules/Functions/SortWithoutEffectRuleTest.php @@ -185,4 +185,9 @@ public function testNamedArguments(): void ]); } + public function testBug15435(): void + { + $this->analyse([__DIR__ . '/data/bug-15435.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Functions/data/bug-15435.php b/tests/PHPStan/Rules/Functions/data/bug-15435.php new file mode 100644 index 00000000000..9364b670ae2 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-15435.php @@ -0,0 +1,24 @@ + $facts + * @param list $months + */ +function nestedLoop(array $facts, array $months): void +{ + $keys = []; + foreach ($facts as $fact) { + $n = 0; + foreach ($months as $m) { + if ('' !== $m) { + ++$n; + } + } + if ($n > 1) { + $keys[] = $fact['key']; + } + } + sort($keys); +} diff --git a/turbo-ext/src/DoWhileHandler.cpp b/turbo-ext/src/DoWhileHandler.cpp index 8bd3377df21..29c4683b6ee 100644 --- a/turbo-ext/src/DoWhileHandler.cpp +++ b/turbo-ext/src/DoWhileHandler.cpp @@ -76,12 +76,14 @@ class DoWhileHandler zend_long count = 0; bool isTopLevel; if (UNEXPECTED(!pt_statement_context_is_top_level(context, isTopLevel))) return zv::Val(); + bool shouldConvergeLoops; + if (UNEXPECTED(!pt_statement_context_should_converge_loops(context, shouldConvergeLoops))) return zv::Val(); zv::Val replayBodyRecording, replayPassStorage, replayPassResult; zv::Val prevEntryScope; zv::Val previousPass = zv::Val::null(); zv::Val previousPassStorage = zv::Val::null(); - if (isTopLevel) { + if (shouldConvergeLoops) { zval *stmts = ptsh::readNodeProperty(pt_dwh_stmts_site, stmt, PT_LC("stmts")); if (UNEXPECTED(stmts == NULL)) return zv::Val(); bool bodyIsReplayable; @@ -117,9 +119,9 @@ class DoWhileHandler zval *passStmts = ptsh::readNodeProperty(pt_dwh_stmts_site, stmt, PT_LC("stmts")); if (UNEXPECTED(passStmts == NULL)) return; zv::Val stmtsHold = zv::Val::copyOf(zv::Ref(passStmts)); - zv::Val deepContext = pt_statement_context_enter_deep(context); - if (UNEXPECTED(deepContext.isUndef())) return; - zv::Val passContext = pt_statement_context_without_template_argument_resolution(deepContext.raw()); + zv::Val loopPassContext = pt_statement_context_enter_loop_pass(context); + if (UNEXPECTED(loopPassContext.isUndef())) return; + zv::Val passContext = pt_statement_context_without_template_argument_resolution(loopPassContext.raw()); if (UNEXPECTED(passContext.isUndef())) return; zv::Val passRecord = zv::Val::null(); zv::Val walked = pt_node_scope_resolver_process_loop_pass_stmt_nodes(nodeScopeResolver, stmt, stmtsHold.raw(), bodyScope.raw(), storage.raw(), bodyRecording.raw(), passContext.raw(), previousPass.raw(), passRecord); diff --git a/turbo-ext/src/ForHandler.cpp b/turbo-ext/src/ForHandler.cpp index f187def3240..9488566557d 100644 --- a/turbo-ext/src/ForHandler.cpp +++ b/turbo-ext/src/ForHandler.cpp @@ -212,7 +212,9 @@ class ForHandler bool isTopLevel; if (UNEXPECTED(!pt_statement_context_is_top_level(context, isTopLevel))) return zv::Val(); - if (isTopLevel) { + bool shouldConvergeLoops; + if (UNEXPECTED(!pt_statement_context_should_converge_loops(context, shouldConvergeLoops))) return zv::Val(); + if (shouldConvergeLoops) { zend_long count = 0; zv::Val prevEntryScope; // each pass after the first re-walks only the statements whose @@ -260,9 +262,9 @@ class ForHandler zv::Val stmtsHold = zv::Val::copyOf(zv::Ref(stmts)); zv::Val noop = ptlh::newNoopNodeCallback(); if (UNEXPECTED(noop.isUndef())) return; - zv::Val deepContext = pt_statement_context_enter_deep(context); - if (UNEXPECTED(deepContext.isUndef())) return; - zv::Val passContext = pt_statement_context_without_template_argument_resolution(deepContext.raw()); + zv::Val loopPassContext = pt_statement_context_enter_loop_pass(context); + if (UNEXPECTED(loopPassContext.isUndef())) return; + zv::Val passContext = pt_statement_context_without_template_argument_resolution(loopPassContext.raw()); if (UNEXPECTED(passContext.isUndef())) return; zv::Val passRecord = zv::Val::null(); zv::Val walked = pt_node_scope_resolver_process_loop_pass_stmt_nodes(nodeScopeResolver, stmt, stmtsHold.raw(), bodyScope.raw(), storage.raw(), noop.raw(), passContext.raw(), previousPass.raw(), passRecord); diff --git a/turbo-ext/src/ForeachHandler.cpp b/turbo-ext/src/ForeachHandler.cpp index 93d6fd6990c..ca2585f124a 100644 --- a/turbo-ext/src/ForeachHandler.cpp +++ b/turbo-ext/src/ForeachHandler.cpp @@ -453,7 +453,9 @@ class ForeachHandler zend_long unrolledTotalKeys = 0; bool isTopLevel; if (UNEXPECTED(!pt_statement_context_is_top_level(context, isTopLevel))) return zv::Val(); - if (isTopLevel) { + bool shouldConvergeLoops; + if (UNEXPECTED(!pt_statement_context_should_converge_loops(context, shouldConvergeLoops))) return zv::Val(); + if (shouldConvergeLoops) { zv::Val storage = pt_expression_result_storage_duplicate(originalStorage); if (UNEXPECTED(storage.isUndef())) return zv::Val(); @@ -485,7 +487,7 @@ class ForeachHandler if (UNEXPECTED(foreachNativeIterateeType.isUndef())) return zv::Val(); } UnrolledResult unrolledResult; - if (UNEXPECTED(!tryProcessUnrolledConstantArrayForeach(nodeScopeResolver, stmt, parts, originalScope.raw(), originalStorage, context, foreachIterateeType.raw(), foreachNativeIterateeType.raw(), unrolledResult))) return zv::Val(); + if (isTopLevel && UNEXPECTED(!tryProcessUnrolledConstantArrayForeach(nodeScopeResolver, stmt, parts, originalScope.raw(), originalStorage, context, foreachIterateeType.raw(), foreachNativeIterateeType.raw(), unrolledResult))) return zv::Val(); if (unrolledResult.found) { bodyScope = std::move(unrolledResult.bodyScope); unrolledEndScope = std::move(unrolledResult.endScope); @@ -530,9 +532,9 @@ class ForeachHandler zv::Val entered = enterForeach(nodeScopeResolver, bodyScope.raw(), storage.raw(), originalScope.raw(), stmt, parts, foreachIterateeType.raw(), foreachNativeIterateeType.raw(), nodeCallback); if (UNEXPECTED(entered.isUndef())) return; bodyScope = std::move(entered); - zv::Val deepContext = pt_statement_context_enter_deep(context); - if (UNEXPECTED(deepContext.isUndef())) return; - zv::Val passContext = pt_statement_context_without_template_argument_resolution(deepContext.raw()); + zv::Val loopPassContext = pt_statement_context_enter_loop_pass(context); + if (UNEXPECTED(loopPassContext.isUndef())) return; + zv::Val passContext = pt_statement_context_without_template_argument_resolution(loopPassContext.raw()); if (UNEXPECTED(passContext.isUndef())) return; zv::Val passRecord = zv::Val::null(); zv::Val walked = pt_node_scope_resolver_process_loop_pass_stmt_nodes(nodeScopeResolver, stmt, parts.stmts.raw(), bodyScope.raw(), storage.raw(), bodyRecording.raw(), passContext.raw(), previousPass.raw(), passRecord); diff --git a/turbo-ext/src/StatementContext.cpp b/turbo-ext/src/StatementContext.cpp index 0bcc817e5c9..db3240a74f9 100644 --- a/turbo-ext/src/StatementContext.cpp +++ b/turbo-ext/src/StatementContext.cpp @@ -4,7 +4,7 @@ * * A final value class with a private constructor: instances come only from * the static factories and the enter*() / with*() derivations. The state - * lives in the twin's three promoted property slots (generated + * lives in the twin's promoted property slots (generated * declarations). Native callers use the pt_statement_context_* direct * entries (support.h). */ @@ -29,28 +29,43 @@ class StatementContext explicit StatementContext(zend_object *self) : self(self) {} /* the constructor body: the promoted properties; a NULL type is null */ - void construct(bool isTopLevel, zend_long foreachUnrollFactor, bool resolveTemplateArguments, zval *expectedReturnType, zval *nativeExpectedReturnType) + void construct(bool isTopLevel, zend_long foreachUnrollFactor, bool resolveTemplateArguments, zval *expectedReturnType, zval *nativeExpectedReturnType, bool convergesLoops) { writeSlot(slots::isTopLevel, isTopLevel ? IS_TRUE : IS_FALSE, 0); writeSlot(slots::foreachUnrollFactor, IS_LONG, foreachUnrollFactor); writeSlot(slots::resolveTemplateArguments, resolveTemplateArguments ? IS_TRUE : IS_FALSE, 0); writeObjectSlot(slots::expectedReturnType, expectedReturnType); writeObjectSlot(slots::nativeExpectedReturnType, nativeExpectedReturnType); + writeSlot(slots::convergesLoops, convergesLoops ? IS_TRUE : IS_FALSE, 0); } /* Mirrors createTopLevel(). */ static zv::Val createTopLevel(bool resolveTemplateArguments) { - return newSelf(true, 1, resolveTemplateArguments, nullptr, nullptr); + return newSelf(true, 1, resolveTemplateArguments, nullptr, nullptr, false); } /* Mirrors createDeep(). */ static zv::Val createDeep(bool resolveTemplateArguments) { - return newSelf(false, 1, resolveTemplateArguments, nullptr, nullptr); + return newSelf(false, 1, resolveTemplateArguments, nullptr, nullptr, false); } bool isTopLevel() const { return Z_TYPE_P(slot(slots::isTopLevel)) == IS_TRUE; } + bool convergesLoops() const { return Z_TYPE_P(slot(slots::convergesLoops)) == IS_TRUE; } + + /* Mirrors shouldConvergeLoops(). */ + bool shouldConvergeLoops() const { return isTopLevel() || convergesLoops(); } + + /* Mirrors enterLoopPass(). */ + zv::Val enterLoopPass() const + { + if (isTopLevel()) return newSelf(false, getForeachUnrollFactor(), shouldResolveTemplateArguments(), expectedReturnType(), nativeExpectedReturnType(), true); + if (convergesLoops()) return newSelf(false, getForeachUnrollFactor(), shouldResolveTemplateArguments(), expectedReturnType(), nativeExpectedReturnType(), false); + + return thisValue(); + } + zend_long getForeachUnrollFactor() const { return Z_LVAL_P(slot(slots::foreachUnrollFactor)); } bool shouldResolveTemplateArguments() const { return Z_TYPE_P(slot(slots::resolveTemplateArguments)) == IS_TRUE; } /* borrowed: the Type object, or NULL for null */ @@ -65,7 +80,7 @@ class StatementContext { if (expectedReturnType == nullptr && nativeExpectedReturnType == nullptr) return thisValue(); - return newSelf(isTopLevel(), getForeachUnrollFactor(), shouldResolveTemplateArguments(), expectedReturnType, nativeExpectedReturnType); + return newSelf(isTopLevel(), getForeachUnrollFactor(), shouldResolveTemplateArguments(), expectedReturnType, nativeExpectedReturnType, convergesLoops()); } /* Mirrors withoutTemplateArgumentResolution(). */ @@ -73,13 +88,13 @@ class StatementContext { if (!shouldResolveTemplateArguments()) return thisValue(); - return newSelf(isTopLevel(), getForeachUnrollFactor(), false, expectedReturnType(), nativeExpectedReturnType()); + return newSelf(isTopLevel(), getForeachUnrollFactor(), false, expectedReturnType(), nativeExpectedReturnType(), convergesLoops()); } /* Mirrors enterDeep(). */ zv::Val enterDeep() const { - if (isTopLevel()) return newSelf(false, getForeachUnrollFactor(), shouldResolveTemplateArguments(), expectedReturnType(), nativeExpectedReturnType()); + if (isTopLevel()) return newSelf(false, getForeachUnrollFactor(), shouldResolveTemplateArguments(), expectedReturnType(), nativeExpectedReturnType(), false); return thisValue(); } @@ -95,7 +110,7 @@ class StatementContext return zv::Val(); } - return newSelf(isTopLevel(), product, shouldResolveTemplateArguments(), expectedReturnType(), nativeExpectedReturnType()); + return newSelf(isTopLevel(), product, shouldResolveTemplateArguments(), expectedReturnType(), nativeExpectedReturnType(), convergesLoops()); } private: @@ -171,11 +186,11 @@ class StatementContext } /* new self(...) — the class is final */ - static zv::Val newSelf(bool isTopLevel, zend_long foreachUnrollFactor, bool resolveTemplateArguments, zval *expectedReturnType, zval *nativeExpectedReturnType) + static zv::Val newSelf(bool isTopLevel, zend_long foreachUnrollFactor, bool resolveTemplateArguments, zval *expectedReturnType, zval *nativeExpectedReturnType, bool convergesLoops) { zval object; if (UNEXPECTED(object_init_ex(&object, pt_ce_statement_context) != SUCCESS)) return zv::Val(); - StatementContext(Z_OBJ(object)).construct(isTopLevel, foreachUnrollFactor, resolveTemplateArguments, expectedReturnType, nativeExpectedReturnType); + StatementContext(Z_OBJ(object)).construct(isTopLevel, foreachUnrollFactor, resolveTemplateArguments, expectedReturnType, nativeExpectedReturnType, convergesLoops); return zv::Val::adopt(object); } }; @@ -224,6 +239,21 @@ bool pt_statement_context_is_top_level(zval *context, bool &out) return boolMethod(context, PT_LC("istoplevel"), out); } +bool pt_statement_context_should_converge_loops(zval *context, bool &out) +{ + if (isNative(context)) { + out = StatementContext(Z_OBJ_P(context)).shouldConvergeLoops(); + return true; + } + return boolMethod(context, PT_LC("shouldconvergeloops"), out); +} + +zv::Val pt_statement_context_enter_loop_pass(zval *context) +{ + if (isNative(context)) return StatementContext(Z_OBJ_P(context)).enterLoopPass(); + return pt_type_call(Z_OBJ_P(context), PT_LC("enterlooppass"), 0, NULL); +} + bool pt_statement_context_should_resolve_template_arguments(zval *context, bool &out) { if (isNative(context)) { @@ -305,15 +335,17 @@ PT_MINIT_REGISTRATION(pt_register_statement_context) bool resolveTemplateArguments = true; zval *expectedReturnType = nullptr; zval *nativeExpectedReturnType = nullptr; - ZEND_PARSE_PARAMETERS_START(1, 5) + bool convergesLoops = false; + ZEND_PARSE_PARAMETERS_START(1, 6) Z_PARAM_BOOL(isTopLevel) Z_PARAM_OPTIONAL Z_PARAM_LONG(foreachUnrollFactor) Z_PARAM_BOOL(resolveTemplateArguments) Z_PARAM_OBJECT_OR_NULL(expectedReturnType) Z_PARAM_OBJECT_OR_NULL(nativeExpectedReturnType) + Z_PARAM_BOOL(convergesLoops) ZEND_PARSE_PARAMETERS_END(); - StatementContext(Z_OBJ_P(ZEND_THIS)).construct(isTopLevel, foreachUnrollFactor, resolveTemplateArguments, expectedReturnType, nativeExpectedReturnType); + StatementContext(Z_OBJ_P(ZEND_THIS)).construct(isTopLevel, foreachUnrollFactor, resolveTemplateArguments, expectedReturnType, nativeExpectedReturnType, convergesLoops); }); cls.method(sigs::createTopLevel, [](INTERNAL_FUNCTION_PARAMETERS) { @@ -333,6 +365,13 @@ PT_MINIT_REGISTRATION(pt_register_statement_context) RETURN_BOOL(StatementContext(Z_OBJ_P(ZEND_THIS)).isTopLevel()); }); + cls.method(sigs::shouldConvergeLoops, [](INTERNAL_FUNCTION_PARAMETERS) { + ZEND_PARSE_PARAMETERS_NONE(); + RETURN_BOOL(StatementContext(Z_OBJ_P(ZEND_THIS)).shouldConvergeLoops()); + }); + + cls.method<&StatementContext::enterLoopPass>(sigs::enterLoopPass); + cls.method(sigs::getForeachUnrollFactor, [](INTERNAL_FUNCTION_PARAMETERS) { ZEND_PARSE_PARAMETERS_NONE(); RETURN_LONG(StatementContext(Z_OBJ_P(ZEND_THIS)).getForeachUnrollFactor()); diff --git a/turbo-ext/src/WhileHandler.cpp b/turbo-ext/src/WhileHandler.cpp index d87791b76ca..3efee6ca812 100644 --- a/turbo-ext/src/WhileHandler.cpp +++ b/turbo-ext/src/WhileHandler.cpp @@ -79,6 +79,8 @@ class WhileHandler zv::Val scope = zv::Val::copyOf(zv::Ref(scopeArg)); bool isTopLevel; if (UNEXPECTED(!pt_statement_context_is_top_level(context, isTopLevel))) return zv::Val(); + bool shouldConvergeLoops; + if (UNEXPECTED(!pt_statement_context_should_converge_loops(context, shouldConvergeLoops))) return zv::Val(); zv::Val storage = pt_expression_result_storage_duplicate(originalStorage); if (UNEXPECTED(storage.isUndef())) return zv::Val(); @@ -134,7 +136,7 @@ class WhileHandler zv::Val prevEntryScope; zv::Val previousPass = zv::Val::null(); zv::Val previousPassStorage = zv::Val::null(); - if (isTopLevel) { + if (shouldConvergeLoops) { zend_long count = 0; zval *stmts = ptsh::readNodeProperty(pt_wh_stmts_site, stmt, PT_LC("stmts")); if (UNEXPECTED(stmts == NULL)) return zv::Val(); @@ -181,9 +183,9 @@ class WhileHandler zval *passStmts = ptsh::readNodeProperty(pt_wh_stmts_site, stmt, PT_LC("stmts")); if (UNEXPECTED(passStmts == NULL)) return; zv::Val stmtsHold = zv::Val::copyOf(zv::Ref(passStmts)); - zv::Val deepContext = pt_statement_context_enter_deep(context); - if (UNEXPECTED(deepContext.isUndef())) return; - zv::Val passContext = pt_statement_context_without_template_argument_resolution(deepContext.raw()); + zv::Val loopPassContext = pt_statement_context_enter_loop_pass(context); + if (UNEXPECTED(loopPassContext.isUndef())) return; + zv::Val passContext = pt_statement_context_without_template_argument_resolution(loopPassContext.raw()); if (UNEXPECTED(passContext.isUndef())) return; zv::Val passRecord = zv::Val::null(); zv::Val walked = pt_node_scope_resolver_process_loop_pass_stmt_nodes(nodeScopeResolver, stmt, stmtsHold.raw(), bodyScope.raw(), storage.raw(), bodyRecording.raw(), passContext.raw(), previousPass.raw(), passRecord); diff --git a/turbo-ext/src/generated/StatementContext.h b/turbo-ext/src/generated/StatementContext.h index 5c3025b6b0c..db56097e988 100644 --- a/turbo-ext/src/generated/StatementContext.h +++ b/turbo-ext/src/generated/StatementContext.h @@ -15,6 +15,7 @@ inline constexpr uint32_t foreachUnrollFactor = 1; inline constexpr uint32_t resolveTemplateArguments = 2; inline constexpr uint32_t expectedReturnType = 3; inline constexpr uint32_t nativeExpectedReturnType = 4; +inline constexpr uint32_t convergesLoops = 5; } // namespace slot inline void declareClass(reg::Class &cls) @@ -30,6 +31,7 @@ inline void declareProperties(reg::Class &cls) cls.property("resolveTemplateArguments", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_BOOL); cls.property("expectedReturnType", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_NULL, "PHPStan\\Type\\Type"); cls.property("nativeExpectedReturnType", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_NULL, "PHPStan\\Type\\Type"); + cls.property("convergesLoops", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_BOOL); } /* the string and parameter tables the signatures below index into (see reg::Sig) */ @@ -44,60 +46,69 @@ inline constexpr char strings[] = "PHPStan\\Type\\Type\0" /* 82 */ "null\0" /* 100 */ "nativeExpectedReturnType\0" /* 105 */ - "__construct\0" /* 130 */ - "\0" /* 142 */ - "PHPStan\\Analyser\\StatementContext\0" /* 143 */ - "createTopLevel\0" /* 177 */ - "createDeep\0" /* 192 */ - "getForeachUnrollFactor\0" /* 203 */ - "shouldResolveTemplateArguments\0" /* 226 */ - "withoutTemplateArgumentResolution\0" /* 257 */ - "withExpectedReturnType\0" /* 291 */ - "getExpectedReturnType\0" /* 314 */ - "getNativeExpectedReturnType\0" /* 336 */ - "enterDeep\0" /* 364 */ - "totalKeys\0" /* 374 */ - "enterUnrolledForeach"; /* 384 */ + "convergesLoops\0" /* 130 */ + "false\0" /* 145 */ + "__construct\0" /* 151 */ + "\0" /* 163 */ + "PHPStan\\Analyser\\StatementContext\0" /* 164 */ + "createTopLevel\0" /* 198 */ + "createDeep\0" /* 213 */ + "shouldConvergeLoops\0" /* 224 */ + "enterLoopPass\0" /* 244 */ + "getForeachUnrollFactor\0" /* 258 */ + "shouldResolveTemplateArguments\0" /* 281 */ + "withoutTemplateArgumentResolution\0" /* 312 */ + "withExpectedReturnType\0" /* 346 */ + "getExpectedReturnType\0" /* 369 */ + "getNativeExpectedReturnType\0" /* 391 */ + "enterDeep\0" /* 419 */ + "totalKeys\0" /* 429 */ + "enterUnrolledForeach"; /* 439 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, MAY_BE_BOOL), /* __construct $isTopLevel */ reg::packed(11, MAY_BE_LONG, reg::NoString, false, false, 31), /* __construct $foreachUnrollFactor */ reg::packed(33, MAY_BE_BOOL, reg::NoString, false, false, 58), /* __construct $resolveTemplateArguments */ reg::packed(63, MAY_BE_NULL, 82, false, false, 100), /* __construct $expectedReturnType */ reg::packed(105, MAY_BE_NULL, 82, false, false, 100), /* __construct $nativeExpectedReturnType */ + reg::packed(130, MAY_BE_BOOL, reg::NoString, false, false, 145), /* __construct $convergesLoops */ reg::packed(33, MAY_BE_BOOL, reg::NoString, false, false, 58), /* createTopLevel $resolveTemplateArguments */ - reg::packed(142, 0, 143), /* createTopLevel return */ + reg::packed(163, 0, 164), /* createTopLevel return */ reg::packed(33, MAY_BE_BOOL, reg::NoString, false, false, 58), /* createDeep $resolveTemplateArguments */ - reg::packed(142, 0, 143), /* createDeep return */ - reg::packed(142, MAY_BE_BOOL), /* isTopLevel return */ - reg::packed(142, MAY_BE_LONG), /* getForeachUnrollFactor return */ - reg::packed(142, MAY_BE_BOOL), /* shouldResolveTemplateArguments return */ - reg::packed(142, 0, 143), /* withoutTemplateArgumentResolution return */ + reg::packed(163, 0, 164), /* createDeep return */ + reg::packed(163, MAY_BE_BOOL), /* isTopLevel return */ + reg::packed(163, MAY_BE_BOOL), /* shouldConvergeLoops return */ + reg::packed(163, 0, 164), /* enterLoopPass return */ + reg::packed(163, MAY_BE_LONG), /* getForeachUnrollFactor return */ + reg::packed(163, MAY_BE_BOOL), /* shouldResolveTemplateArguments return */ + reg::packed(163, 0, 164), /* withoutTemplateArgumentResolution return */ reg::packed(63, MAY_BE_NULL, 82), /* withExpectedReturnType $expectedReturnType */ reg::packed(105, MAY_BE_NULL, 82), /* withExpectedReturnType $nativeExpectedReturnType */ - reg::packed(142, 0, 143), /* withExpectedReturnType return */ - reg::packed(142, MAY_BE_NULL, 82), /* getExpectedReturnType return */ - reg::packed(142, MAY_BE_NULL, 82), /* getNativeExpectedReturnType return */ - reg::packed(142, 0, 143), /* enterDeep return */ - reg::packed(374, MAY_BE_LONG), /* enterUnrolledForeach $totalKeys */ - reg::packed(142, 0, 143), /* enterUnrolledForeach return */ + reg::packed(163, 0, 164), /* withExpectedReturnType return */ + reg::packed(163, MAY_BE_NULL, 82), /* getExpectedReturnType return */ + reg::packed(163, MAY_BE_NULL, 82), /* getNativeExpectedReturnType return */ + reg::packed(163, 0, 164), /* enterDeep return */ + reg::packed(429, MAY_BE_LONG), /* enterUnrolledForeach $totalKeys */ + reg::packed(163, 0, 164), /* enterUnrolledForeach return */ }; using Sig = reg::Sig; } // namespace sigtab /* the signatures of the methods the class declares itself (a used trait's are in the trait's header) */ namespace sig { -inline constexpr sigtab::Sig __construct = { { 130 /* __construct */, 1, 0, 5, reg::NoArg, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig createTopLevel = { { 177 /* createTopLevel */, 0, 5, 1, 6, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig createDeep = { { 192 /* createDeep */, 0, 7, 1, 8, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig isTopLevel = { { 0 /* isTopLevel */, 0, 9, 0, 9, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getForeachUnrollFactor = { { 203 /* getForeachUnrollFactor */, 0, 10, 0, 10, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig shouldResolveTemplateArguments = { { 226 /* shouldResolveTemplateArguments */, 0, 11, 0, 11, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig withoutTemplateArgumentResolution = { { 257 /* withoutTemplateArgumentResolution */, 0, 12, 0, 12, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig withExpectedReturnType = { { 291 /* withExpectedReturnType */, 2, 13, 2, 15, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getExpectedReturnType = { { 314 /* getExpectedReturnType */, 0, 16, 0, 16, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getNativeExpectedReturnType = { { 336 /* getNativeExpectedReturnType */, 0, 17, 0, 17, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig enterDeep = { { 364 /* enterDeep */, 0, 18, 0, 18, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig enterUnrolledForeach = { { 384 /* enterUnrolledForeach */, 1, 19, 1, 20, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig __construct = { { 151 /* __construct */, 1, 0, 6, reg::NoArg, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig createTopLevel = { { 198 /* createTopLevel */, 0, 6, 1, 7, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig createDeep = { { 213 /* createDeep */, 0, 8, 1, 9, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig isTopLevel = { { 0 /* isTopLevel */, 0, 10, 0, 10, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig shouldConvergeLoops = { { 224 /* shouldConvergeLoops */, 0, 11, 0, 11, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig enterLoopPass = { { 244 /* enterLoopPass */, 0, 12, 0, 12, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getForeachUnrollFactor = { { 258 /* getForeachUnrollFactor */, 0, 13, 0, 13, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig shouldResolveTemplateArguments = { { 281 /* shouldResolveTemplateArguments */, 0, 14, 0, 14, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig withoutTemplateArgumentResolution = { { 312 /* withoutTemplateArgumentResolution */, 0, 15, 0, 15, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig withExpectedReturnType = { { 346 /* withExpectedReturnType */, 2, 16, 2, 18, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getExpectedReturnType = { { 369 /* getExpectedReturnType */, 0, 19, 0, 19, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getNativeExpectedReturnType = { { 391 /* getNativeExpectedReturnType */, 0, 20, 0, 20, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig enterDeep = { { 419 /* enterDeep */, 0, 21, 0, 21, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig enterUnrolledForeach = { { 439 /* enterUnrolledForeach */, 1, 22, 1, 23, ZEND_ACC_PUBLIC } }; } // namespace sig } // namespace ptdecl::StatementContext diff --git a/turbo-ext/src/support.h b/turbo-ext/src/support.h index 043560faaa0..80c99ba05d3 100644 --- a/turbo-ext/src/support.h +++ b/turbo-ext/src/support.h @@ -1927,6 +1927,8 @@ zv::Val pt_expression_context_get_value_flow_target(zval *context); zv::Val pt_statement_context_create_top_level(bool resolveTemplateArguments = true); zv::Val pt_statement_context_create_deep(bool resolveTemplateArguments = true); [[nodiscard]] bool pt_statement_context_is_top_level(zval *context, bool &out); +[[nodiscard]] bool pt_statement_context_should_converge_loops(zval *context, bool &out); +zv::Val pt_statement_context_enter_loop_pass(zval *context); [[nodiscard]] bool pt_statement_context_should_resolve_template_arguments(zval *context, bool &out); [[nodiscard]] bool pt_statement_context_get_foreach_unroll_factor(zval *context, zend_long &out); zv::Val pt_statement_context_without_template_argument_resolution(zval *context); From 5ef8131ccd7bdbe089c9ff60104b4ce1f6ada14d Mon Sep 17 00:00:00 2001 From: "ondrej@mirtes.cz" Date: Tue, 27 Oct 2026 21:38:55 +0000 Subject: [PATCH 2/2] Add regression tests for #15420, #10666 and #14935 Closes https://github.com/phpstan/phpstan/issues/15420 Closes https://github.com/phpstan/phpstan/issues/10666 Closes https://github.com/phpstan/phpstan/issues/14935 Amp-Thread-ID: https://ampcode.com/threads/T-01a17eac-1199-74a6-b3f7-dfbd8a084465 --- tests/PHPStan/Analyser/nsrt/bug-10666.php | 26 +++++++++++++++++ tests/PHPStan/Analyser/nsrt/bug-14935.php | 29 +++++++++++++++++++ tests/PHPStan/Analyser/nsrt/bug-15420.php | 29 +++++++++++++++++++ ...isonOperatorsConstantConditionRuleTest.php | 5 ++++ .../Rules/Playground/MethodNeverRuleTest.php | 6 ++++ .../PHPStan/Rules/Variables/EmptyRuleTest.php | 7 +++++ 6 files changed, 102 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-10666.php create mode 100644 tests/PHPStan/Analyser/nsrt/bug-14935.php create mode 100644 tests/PHPStan/Analyser/nsrt/bug-15420.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-10666.php b/tests/PHPStan/Analyser/nsrt/bug-10666.php new file mode 100644 index 00000000000..625ef668448 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10666.php @@ -0,0 +1,26 @@ +", $text); + array_pop($text); + assertType("list<'x'>", $text); + } + + return implode('', $text); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-14935.php b/tests/PHPStan/Analyser/nsrt/bug-14935.php new file mode 100644 index 00000000000..ba4cbe192a0 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14935.php @@ -0,0 +1,29 @@ +> $groups + */ +function countRejected(array $groups): int +{ + $rejected = 0; + + foreach ($groups as $readings) { + $copy = []; + foreach ($readings as $reading) { + $copy[] = $reading; + } + assertType('list', $copy); + + if (count($copy) > 1) { + $rejected++; + } + } + + assertType('int<0, max>', $rejected); + + return $rejected; +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-15420.php b/tests/PHPStan/Analyser/nsrt/bug-15420.php new file mode 100644 index 00000000000..02852b98b45 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-15420.php @@ -0,0 +1,29 @@ +', $i); + if ($later !== []) { + $cards = $later; + } else { + break; + } + } + } +} diff --git a/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php index 4e1074b480a..1e46c065b95 100644 --- a/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php @@ -326,6 +326,11 @@ public function testBug15243(): void $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-15243.php'], []); } + public function testBug15420(): void + { + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-15420.php'], []); + } + public function testShiftLeftUnboundedRange(): void { $this->analyse([__DIR__ . '/../../Analyser/nsrt/shift-left-unbounded-range.php'], []); diff --git a/tests/PHPStan/Rules/Playground/MethodNeverRuleTest.php b/tests/PHPStan/Rules/Playground/MethodNeverRuleTest.php index 5715a8fc38e..dcb88b7e561 100644 --- a/tests/PHPStan/Rules/Playground/MethodNeverRuleTest.php +++ b/tests/PHPStan/Rules/Playground/MethodNeverRuleTest.php @@ -36,4 +36,10 @@ public function testRule(): void ]); } + #[RequiresPhp('>= 8.1.0')] + public function testBug10666(): void + { + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-10666.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Variables/EmptyRuleTest.php b/tests/PHPStan/Rules/Variables/EmptyRuleTest.php index 81f3d1307aa..0416d95f978 100644 --- a/tests/PHPStan/Rules/Variables/EmptyRuleTest.php +++ b/tests/PHPStan/Rules/Variables/EmptyRuleTest.php @@ -311,4 +311,11 @@ public function testBug10783(): void $this->analyse([__DIR__ . '/data/bug-10783.php'], []); } + public function testBug10666(): void + { + $this->treatPhpDocTypesAsCertain = true; + + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-10666.php'], []); + } + }