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
26 changes: 26 additions & 0 deletions src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -1726,6 +1726,18 @@
$condBooleanType = ($this->treatPhpDocTypesAsCertain ? $bodyScopeMaybeRan->getType($stmt->cond) : $bodyScopeMaybeRan->getNativeType($stmt->cond))->toBoolean();
$alwaysIterates = $condBooleanType->isTrue()->yes();
$neverIterates = $condBooleanType->isFalse()->yes();

// When every non-terminating path through the body keeps the loop condition
// satisfied (e.g. the boundary iteration always returns or throws), the loop can
// never exit normally, so the code after it is unreachable.
if (
!$alwaysIterates
&& $beforeCondBooleanType->isTrue()->yes()

Check warning on line 1735 in src/Analyser/NodeScopeResolver.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ // never exit normally, so the code after it is unreachable. if ( !$alwaysIterates - && $beforeCondBooleanType->isTrue()->yes() + && !$beforeCondBooleanType->toBoolean()->isTrue()->no() && !$finalScopeResult->isAlwaysTerminating() && $finalScopeResult->getScope()->getType($stmt->cond)->toBoolean()->isTrue()->yes() ) {
&& !$finalScopeResult->isAlwaysTerminating()
&& $finalScopeResult->getScope()->getType($stmt->cond)->toBoolean()->isTrue()->yes()

Check warning on line 1737 in src/Analyser/NodeScopeResolver.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ !$alwaysIterates && $beforeCondBooleanType->isTrue()->yes() && !$finalScopeResult->isAlwaysTerminating() - && $finalScopeResult->getScope()->getType($stmt->cond)->toBoolean()->isTrue()->yes() + && !$finalScopeResult->getScope()->getType($stmt->cond)->toBoolean()->isTrue()->no() ) { $alwaysIterates = true; }

Check warning on line 1737 in src/Analyser/NodeScopeResolver.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ !$alwaysIterates && $beforeCondBooleanType->isTrue()->yes() && !$finalScopeResult->isAlwaysTerminating() - && $finalScopeResult->getScope()->getType($stmt->cond)->toBoolean()->isTrue()->yes() + && !$finalScopeResult->getScope()->getType($stmt->cond)->toBoolean()->isTrue()->no() ) { $alwaysIterates = true; }
) {
$alwaysIterates = true;
}
}
if (!$alwaysIterates) {
foreach ($finalScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
Expand Down Expand Up @@ -1957,6 +1969,20 @@
foreach ($stmt->loop as $loopExpr) {
$loopScope = $this->processExprNode($stmt, $loopExpr, $loopScope, $storage, $nodeCallback, ExpressionContext::createTopLevel())->getScope();
}

// When every non-terminating path through the body keeps the loop condition
// satisfied (e.g. the boundary iteration always returns or throws), the loop can
// never exit normally, so the code after it is unreachable.
if (
$context->isTopLevel()
&& $lastCondExpr !== null
&& $isIterableAtLeastOnce->yes()

Check warning on line 1979 in src/Analyser/NodeScopeResolver.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ if ( $context->isTopLevel() && $lastCondExpr !== null - && $isIterableAtLeastOnce->yes() + && !$isIterableAtLeastOnce->no() && !$finalScopeResult->isAlwaysTerminating() && $loopScope->getType($lastCondExpr)->toBoolean()->isTrue()->yes() ) {
&& !$finalScopeResult->isAlwaysTerminating()
&& $loopScope->getType($lastCondExpr)->toBoolean()->isTrue()->yes()
) {
$alwaysIterates = TrinaryLogic::createYes();
}

$finalScope = $finalScope->generalizeWith($loopScope);

if ($lastCondExpr !== null) {
Expand Down
7 changes: 7 additions & 0 deletions tests/PHPStan/Rules/Missing/MissingReturnRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -380,4 +380,11 @@ public function testBug14638(): void
$this->analyse([__DIR__ . '/data/bug-14638.php'], []);
}

#[RequiresPhp('>= 8.0')]
public function testBug9444(): void
{
$this->checkExplicitMixedMissingReturn = true;
$this->analyse([__DIR__ . '/data/bug-9444.php'], []);
}

}
72 changes: 72 additions & 0 deletions tests/PHPStan/Rules/Missing/data/bug-9444.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php // lint >= 8.0

declare(strict_types = 1);

namespace Bug9444;

class One
{
private static int $i = 0;

public static function run(): string
{
self::$i++;
if (self::$i <= 3) {
throw new \Exception('One:run');
}

return 'Ok';
}
}

class Main
{
public function process(): string
{
for ($i = 0; $i <= 5; $i++) {
try {
return One::run();
} catch (\Throwable $e) {
$sleep = match ($i) {
0 => 0.5,
1 => 1,
2 => 3,
3 => 6,
4 => 9,
default => throw $e,
};

echo $sleep . PHP_EOL;
}
}
}

public function processWithIf(): string
{
for ($i = 0; $i <= 5; $i++) {
try {
return One::run();
} catch (\Throwable $e) {
if ($i >= 5) {
throw $e;
}
echo $i . PHP_EOL;
}
}
}

public function processWhile(): string
{
$i = 0;
while ($i <= 5) {
try {
return One::run();
} catch (\Throwable $e) {
if ($i >= 5) {
throw $e;
}
$i++;
}
}
}
}
Loading