Skip to content

Commit

Permalink
Support for break X and continue X
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Feb 13, 2021
1 parent 990ba51 commit 31fcad6
Show file tree
Hide file tree
Showing 12 changed files with 410 additions and 11 deletions.
12 changes: 7 additions & 5 deletions src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -809,7 +809,7 @@ private function processStmtNode(
$finalScope,
$finalScopeResult->hasYield() || $condResult->hasYield(),
$isIterableAtLeastOnce->yes() && $finalScopeResult->isAlwaysTerminating(),
[]
$finalScopeResult->getExitPointsForOuterLoop()
);
} elseif ($stmt instanceof While_) {
$condResult = $this->processExprNode($stmt->cond, $scope, static function (): void {
Expand Down Expand Up @@ -875,7 +875,7 @@ private function processStmtNode(
$finalScope,
$finalScopeResult->hasYield() || $condResult->hasYield(),
$isAlwaysTerminating,
[]
$finalScopeResult->getExitPointsForOuterLoop()
);
} elseif ($stmt instanceof Do_) {
$finalScope = null;
Expand Down Expand Up @@ -940,7 +940,7 @@ private function processStmtNode(
$finalScope,
$bodyScopeResult->hasYield() || $hasYield,
$alwaysTerminating,
[]
$bodyScopeResult->getExitPointsForOuterLoop()
);
} elseif ($stmt instanceof For_) {
$initScope = $scope;
Expand Down Expand Up @@ -1014,7 +1014,7 @@ private function processStmtNode(
$finalScope,
$finalScopeResult->hasYield() || $hasYield,
false/* $finalScopeResult->isAlwaysTerminating() && $isAlwaysIterable*/,
[]
$finalScopeResult->getExitPointsForOuterLoop()
);
} elseif ($stmt instanceof Switch_) {
$condResult = $this->processExprNode($stmt->cond, $scope, $nodeCallback, ExpressionContext::createDeep());
Expand All @@ -1025,6 +1025,7 @@ private function processStmtNode(
$hasDefaultCase = false;
$alwaysTerminating = true;
$hasYield = $condResult->hasYield();
$exitPointsForOuterLoop = [];
foreach ($stmt->cases as $caseNode) {
if ($caseNode->cond !== null) {
$condExpr = new BinaryOp\Equal($stmt->cond, $caseNode->cond);
Expand All @@ -1047,6 +1048,7 @@ private function processStmtNode(
foreach ($branchScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
$finalScope = $continueExitPoint->getScope()->mergeWith($finalScope);
}
$exitPointsForOuterLoop = array_merge($exitPointsForOuterLoop, $branchFinalScopeResult->getExitPointsForOuterLoop());
if ($branchScopeResult->isAlwaysTerminating()) {
$alwaysTerminating = $alwaysTerminating && $branchFinalScopeResult->isAlwaysTerminating();
$prevScope = null;
Expand Down Expand Up @@ -1074,7 +1076,7 @@ private function processStmtNode(
$finalScope = $scope->mergeWith($finalScope);
}

return new StatementResult($finalScope, $hasYield, $alwaysTerminating, []);
return new StatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPointsForOuterLoop);
} elseif ($stmt instanceof TryCatch) {
$branchScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $scope, $nodeCallback);
$branchScope = $branchScopeResult->getScope();
Expand Down
76 changes: 70 additions & 6 deletions src/Analyser/StatementResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace PHPStan\Analyser;

use PhpParser\Node\Scalar\LNumber;
use PhpParser\Node\Stmt;

class StatementResult
Expand Down Expand Up @@ -58,12 +59,20 @@ public function filterOutLoopExitPoints(): self

foreach ($this->exitPoints as $exitPoint) {
$statement = $exitPoint->getStatement();
if (
$statement instanceof Stmt\Break_
|| $statement instanceof Stmt\Continue_
) {
if (!$statement instanceof Stmt\Break_ && !$statement instanceof Stmt\Continue_) {
continue;
}

$num = $statement->num;
if (!$num instanceof LNumber) {
return new self($this->scope, $this->hasYield, false, $this->exitPoints);
}

if ($num->value !== 1) {
continue;
}

return new self($this->scope, $this->hasYield, false, $this->exitPoints);
}

return $this;
Expand All @@ -78,14 +87,31 @@ public function getExitPoints(): array
}

/**
* @param string $stmtClass
* @param class-string<Stmt\Continue_>|class-string<Stmt\Break_> $stmtClass
* @return StatementExitPoint[]
*/
public function getExitPointsByType(string $stmtClass): array
{
$exitPoints = [];
foreach ($this->exitPoints as $exitPoint) {
if (!$exitPoint->getStatement() instanceof $stmtClass) {
$statement = $exitPoint->getStatement();
if (!$statement instanceof $stmtClass) {
continue;
}

$value = $statement->num;
if ($value === null) {
$exitPoints[] = $exitPoint;
continue;
}

if (!$value instanceof LNumber) {
$exitPoints[] = $exitPoint;
continue;
}

$value = $value->value;
if ($value !== 1) {
continue;
}

Expand All @@ -95,4 +121,42 @@ public function getExitPointsByType(string $stmtClass): array
return $exitPoints;
}

/**
* @return StatementExitPoint[]
*/
public function getExitPointsForOuterLoop(): array
{
$exitPoints = [];
foreach ($this->exitPoints as $exitPoint) {
$statement = $exitPoint->getStatement();
if (!$statement instanceof Stmt\Continue_ && !$statement instanceof Stmt\Break_) {
continue;
}
if ($statement->num === null) {
continue;
}
if (!$statement->num instanceof LNumber) {
continue;
}
$value = $statement->num->value;
if ($value === 1) {
continue;
}

$newNode = null;
if ($value > 2) {
$newNode = new LNumber($value - 1);
}
if ($statement instanceof Stmt\Continue_) {
$newStatement = new Stmt\Continue_($newNode);
} else {
$newStatement = new Stmt\Break_($newNode);
}

$exitPoints[] = new StatementExitPoint($newStatement, $exitPoint->getScope());
}

return $exitPoints;
}

}
30 changes: 30 additions & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10777,6 +10777,31 @@ public function dataBug3777(): array
return $this->gatherAssertTypes(__DIR__ . '/../Rules/Properties/data/bug-3777.php');
}

public function dataBug2549(): array
{
return $this->gatherAssertTypes(__DIR__ . '/data/bug-2549.php');
}

public function dataBug1945(): array
{
return $this->gatherAssertTypes(__DIR__ . '/data/bug-1945.php');
}

public function dataBug2003(): array
{
return $this->gatherAssertTypes(__DIR__ . '/data/bug-2003.php');
}

public function dataBug651(): array
{
return $this->gatherAssertTypes(__DIR__ . '/data/bug-651.php');
}

public function dataBug1283(): array
{
return $this->gatherAssertTypes(__DIR__ . '/data/bug-1283.php');
}

/**
* @param string $file
* @return array<string, mixed[]>
Expand Down Expand Up @@ -10994,6 +11019,11 @@ private function gatherAssertTypes(string $file): array
* @dataProvider dataBug4504
* @dataProvider dataBug4436
* @dataProvider dataBug3777
* @dataProvider dataBug2549
* @dataProvider dataBug1945
* @dataProvider dataBug2003
* @dataProvider dataBug651
* @dataProvider dataBug1283
* @param string $assertType
* @param string $file
* @param mixed ...$args
Expand Down
27 changes: 27 additions & 0 deletions tests/PHPStan/Analyser/data/bug-1283.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

namespace Bug1283;

use PHPStan\TrinaryLogic;
use function PHPStan\Analyser\assertType;
use function PHPStan\Analyser\assertVariableCertainty;

function (array $levels): void {
foreach ($levels as $level) {
switch ($level) {
case 'all':
continue 2;
case 'some':
$allowedElements = array(1, 3);
break;
case 'one':
$allowedElements = array(1);
break;
default:
throw new \UnexpectedValueException(sprintf('Unsupported level `%s`', $level));
}

assertType('array(0 => 1, ?1 => 3)', $allowedElements);
assertVariableCertainty(TrinaryLogic::createYes(), $allowedElements);
}
};
106 changes: 106 additions & 0 deletions tests/PHPStan/Analyser/data/bug-1945.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?php

namespace Bug1945;

use PHPStan\TrinaryLogic;
use function PHPStan\Analyser\assertType;
use function PHPStan\Analyser\assertVariableCertainty;

function (): void {
foreach (["a", "b", "c"] as $letter) {
switch ($letter) {
case "b":
$foo = 1;
break;
case "c":
$foo = 2;
break;
default:
continue 2;
}

assertType('1|2', $foo);
assertVariableCertainty(TrinaryLogic::createYes(), $foo);
}
};

function (): void {
foreach (["a", "b", "c"] as $letter) {
switch ($letter) {
case "a":
if (rand(0, 10) === 1) {
continue 2;
}
$foo = 1;
break;
case "b":
if (rand(0, 10) === 1) {
continue 2;
}
$foo = 2;
break;
default:
continue 2;
}

assertType('1|2', $foo);
assertVariableCertainty(TrinaryLogic::createYes(), $foo);
}
};

function (array $docs): void {
foreach ($docs as $doc) {
switch (true) {
case 'bar':
continue 2;
break;
default:
$foo = $doc;
break;
}

assertVariableCertainty(TrinaryLogic::createYes(), $foo);
if (!$foo) {
return;
}
}
};

function (array $docs): void {
foreach ($docs as $doc) {
switch (true) {
case 'bar':
continue 2;
default:
$foo = $doc;
break;
}

assertVariableCertainty(TrinaryLogic::createYes(), $foo);
if (!$foo) {
return;
}
}
};

function (array $items): string {
foreach ($items as $item) {
switch ($item) {
case 1:
$string = 'a';
break;
case 2:
$string = 'b';
break;
default:
continue 2;
}

assertType('\'a\'|\'b\'', $string);
assertVariableCertainty(TrinaryLogic::createYes(), $string);

return 'result: ' . $string;
}

return 'ok';
};
25 changes: 25 additions & 0 deletions tests/PHPStan/Analyser/data/bug-2003.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php declare(strict_types=1);

namespace Bug2003;

use PHPStan\TrinaryLogic;
use function PHPStan\Analyser\assertType;
use function PHPStan\Analyser\assertVariableCertainty;

function (array $list): void {
foreach ($list as $part) {
switch (true) {
case isset($list['magic']):
$key = 'to-success';
break;

default:
continue 2;
}

assertType('\'to-success\'', $key);
assertVariableCertainty(TrinaryLogic::createYes(), $key);

echo $key;
}
};
Loading

0 comments on commit 31fcad6

Please sign in to comment.