diff --git a/src/Psalm/Internal/Analyzer/ScopeAnalyzer.php b/src/Psalm/Internal/Analyzer/ScopeAnalyzer.php index ccc82414485..dfc9b630fb0 100644 --- a/src/Psalm/Internal/Analyzer/ScopeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ScopeAnalyzer.php @@ -345,6 +345,22 @@ function ($action) { return $action !== self::ACTION_NONE; } ); + + if ($stmt instanceof PhpParser\Node\Stmt\While_ + && $nodes + && ($stmt_expr_type = $nodes->getType($stmt->cond)) + && $stmt_expr_type->isAlwaysTruthy() + ) { + //infinite while loop that only return don't have an exit path + $have_exit_path = (bool)array_diff( + $control_actions, + [self::ACTION_END, self::ACTION_RETURN] + ); + + if (!$have_exit_path) { + return array_values(array_unique($control_actions)); + } + } } if ($stmt instanceof PhpParser\Node\Stmt\TryCatch) { diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/IfConditionalAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/IfConditionalAnalyzer.php index edb08413c00..571179c6f33 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/IfConditionalAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/IfConditionalAnalyzer.php @@ -218,7 +218,7 @@ function (Type\Union $_): bool { $cond_type = $statements_analyzer->node_data->getType($cond); if ($cond_type !== null) { - if ($cond_type->isFalse()) { + if ($cond_type->isAlwaysFalsy()) { if ($cond_type->from_docblock) { if (IssueBuffer::accepts( new DocblockTypeContradiction( diff --git a/src/Psalm/Type/Union.php b/src/Psalm/Type/Union.php index 118416c7718..653a5d06a98 100644 --- a/src/Psalm/Type/Union.php +++ b/src/Psalm/Type/Union.php @@ -958,11 +958,130 @@ public function isFalse(): bool return count($this->types) === 1 && isset($this->types['false']); } + public function isAlwaysFalsy(): bool + { + foreach ($this->getAtomicTypes() as $atomic_type) { + if ($atomic_type instanceof Type\Atomic\TFalse) { + continue; + } + + if ($atomic_type instanceof Type\Atomic\TLiteralInt && $atomic_type->value === 0) { + continue; + } + + if ($atomic_type instanceof Type\Atomic\TLiteralFloat && $atomic_type->value === 0.0) { + continue; + } + + if ($atomic_type instanceof Type\Atomic\TLiteralString && + ($atomic_type->value === '' || $atomic_type->value === '0') + ) { + continue; + } + + if ($atomic_type instanceof Type\Atomic\TNull) { + continue; + } + + if ($atomic_type instanceof TTemplateParam && $atomic_type->as->isAlwaysFalsy()) { + continue; + } + + if ($atomic_type instanceof Type\Atomic\TIntRange && + $atomic_type->min_bound === 0 && + $atomic_type->max_bound === 0 + ) { + continue; + } + + //we can't be sure the type is always falsy + return false; + } + + return true; + } + public function isTrue(): bool { return count($this->types) === 1 && isset($this->types['true']); } + public function isAlwaysTruthy(): bool + { + foreach ($this->getAtomicTypes() as $atomic_type) { + if ($atomic_type instanceof Type\Atomic\TTrue) { + continue; + } + + if ($atomic_type instanceof Type\Atomic\TLiteralInt && $atomic_type->value !== 0) { + continue; + } + + if ($atomic_type instanceof Type\Atomic\TLiteralFloat && $atomic_type->value !== 0.0) { + continue; + } + + if ($atomic_type instanceof Type\Atomic\TLiteralString && + ($atomic_type->value !== '' && $atomic_type->value !== '0') + ) { + continue; + } + + if ($atomic_type instanceof Type\Atomic\TNonFalsyString) { + continue; + } + + if ($atomic_type instanceof Type\Atomic\TNonEmptyArray) { + continue; + } + + if ($atomic_type instanceof Type\Atomic\TNonEmptyList) { + continue; + } + + if ($atomic_type instanceof Type\Atomic\TObject) { + continue; + } + + if ($atomic_type instanceof Type\Atomic\TNamedObject) { + continue; + } + + /*if ($atomic_type instanceof Type\Atomic\TIntRange && !$atomic_type->contains(0)) { + continue; + }*/ + + if ($atomic_type instanceof Type\Atomic\TPositiveInt) { + continue; + } + + if ($atomic_type instanceof Type\Atomic\TLiteralClassString) { + continue; + } + + if ($atomic_type instanceof Type\Atomic\TClassString) { + continue; + } + + if ($atomic_type instanceof Type\Atomic\TKeyedArray) { + foreach ($atomic_type->properties as $property) { + if ($property->possibly_undefined === false) { + continue; + } + } + } + + if ($atomic_type instanceof TTemplateParam && $atomic_type->as->isAlwaysTruthy()) { + continue; + } + + //we can't be sure the type is always truthy + return false; + } + + return true; + } + public function isVoid(): bool { return isset($this->types['void']); diff --git a/tests/Loop/WhileTest.php b/tests/Loop/WhileTest.php index a60999486ea..8550ed5a219 100644 --- a/tests/Loop/WhileTest.php +++ b/tests/Loop/WhileTest.php @@ -708,6 +708,46 @@ function bar(A $a): void { if ($a->foo !== null) {} }' ], + 'whileTrueDontHaveExitPathForReturn' => [ + ' [ + 'getResult(); + } catch (Throwable $exception) { + if ($attempt >= $this->retryAttempts) { + throw $exception; + } + + $attempt++; + + continue; + } + } + } + }' + ], ]; } diff --git a/tests/TypeReconciliation/AssignmentInConditionalTest.php b/tests/TypeReconciliation/AssignmentInConditionalTest.php index dfe0aecf882..140bcd18360 100644 --- a/tests/TypeReconciliation/AssignmentInConditionalTest.php +++ b/tests/TypeReconciliation/AssignmentInConditionalTest.php @@ -383,7 +383,7 @@ function foo(string $str): ?int { 'repeatedSet' => [ ' [ ' 'TypeDoesNotContainType', ], + 'falsyValuesInIf' => [ + ' 'TypeDoesNotContainType', + ], ]; } }