Skip to content

Commit

Permalink
Infer return type never from closure
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Jan 23, 2021
1 parent 253178d commit 98c8dd6
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 2 deletions.
32 changes: 30 additions & 2 deletions src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
use PhpParser\Node\Scalar\EncapsedStringPart;
use PhpParser\Node\Scalar\LNumber;
use PhpParser\Node\Scalar\String_;
use PHPStan\Node\ExecutionEndNode;
use PHPStan\Parser\Parser;
use PHPStan\Reflection\ClassMemberReflection;
use PHPStan\Reflection\ClassReflection;
Expand Down Expand Up @@ -1327,11 +1328,31 @@ private function resolveType(Expr $node): Type
$closureScope = $this->enterAnonymousFunctionWithoutReflection($node);
$closureReturnStatements = [];
$closureYieldStatements = [];
$this->nodeScopeResolver->processStmtNodes($node, $node->stmts, $closureScope, static function (Node $node, Scope $scope) use ($closureScope, &$closureReturnStatements, &$closureYieldStatements): void {
$closureExecutionEnds = [];
$this->nodeScopeResolver->processStmtNodes($node, $node->stmts, $closureScope, static function (Node $node, Scope $scope) use ($closureScope, &$closureReturnStatements, &$closureYieldStatements, &$closureExecutionEnds): void {
if ($scope->getAnonymousFunctionReflection() !== $closureScope->getAnonymousFunctionReflection()) {
return;
}

if ($node instanceof ExecutionEndNode) {
if ($node->getStatementResult()->isAlwaysTerminating()) {
foreach ($node->getStatementResult()->getExitPoints() as $exitPoint) {
if ($exitPoint->getStatement() instanceof Node\Stmt\Return_) {
continue;
}

$closureExecutionEnds[] = $node;
break;
}

if (count($node->getStatementResult()->getExitPoints()) === 0) {
$closureExecutionEnds[] = $node;
}
}

return;
}

if ($node instanceof Node\Stmt\Return_) {
$closureReturnStatements[] = [$node, $scope];
}
Expand All @@ -1355,8 +1376,15 @@ private function resolveType(Expr $node): Type
}

if (count($returnTypes) === 0) {
$returnType = new VoidType();
if (count($closureExecutionEnds) > 0 && !$hasNull) {
$returnType = new NeverType(true);
} else {
$returnType = new VoidType();
}
} else {
if (count($closureExecutionEnds) > 0) {
$returnTypes[] = new NeverType(true);
}
if ($hasNull) {
$returnTypes[] = new NullType();
}
Expand Down
53 changes: 53 additions & 0 deletions tests/PHPStan/Analyser/data/closure-return-type.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,57 @@ public function doFoo() {
assertType('1|2', $f());
}

/**
* @return never
*/
public function returnNever(): void
{

}

public function doBaz(): void
{
$f = function() {
$this->returnNever();
};
assertType('*NEVER*', $f());

$f = function(): void {
$this->returnNever();
};
assertType('*NEVER*', $f());

$f = function() {
if (rand(0, 1)) {
return;
}

$this->returnNever();
};
assertType('void', $f());

$f = function(array $a) {
foreach ($a as $v) {
continue;
}

$this->returnNever();
};
assertType('*NEVER*', $f([]));

$f = function(array $a) {
foreach ($a as $v) {
$this->returnNever();
}
};
assertType('void', $f([]));

$f = function() {
foreach ([1, 2, 3] as $v) {
$this->returnNever();
}
};
assertType('*NEVER*', $f());
}

}

0 comments on commit 98c8dd6

Please sign in to comment.