Skip to content

Commit

Permalink
It's okay to have always-throwing expression in arrow function
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Nov 28, 2023
1 parent f449d98 commit 758e5f1
Show file tree
Hide file tree
Showing 7 changed files with 72 additions and 4 deletions.
5 changes: 5 additions & 0 deletions src/Php/PhpVersion.php
Original file line number Diff line number Diff line change
Expand Up @@ -272,4 +272,9 @@ public function supportsReadOnlyAnonymousClasses(): bool
return $this->versionId >= 80300;
}

public function supportsNeverReturnTypeInArrowFunction(): bool
{
return $this->versionId >= 80200;
}

}
11 changes: 11 additions & 0 deletions src/Rules/Functions/ArrowFunctionReturnTypeRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use PHPStan\Rules\FunctionReturnTypeCheck;
use PHPStan\Rules\Rule;
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\NeverType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;

Expand Down Expand Up @@ -43,6 +44,16 @@ public function processNode(Node $node, Scope $scope): array
return [];
}

$exprType = $scope->getType($originalNode->expr);
if (
$returnType instanceof NeverType
&& $returnType->isExplicit()
&& $exprType instanceof NeverType
&& $exprType->isExplicit()
) {
return [];
}

return $this->returnTypeCheck->checkReturnType(
$scope,
$returnType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,21 @@

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Php\PhpVersion;
use PHPStan\Rules\FunctionDefinitionCheck;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\NonAcceptingNeverType;
use PHPStan\Type\ParserNodeTypeToPHPStanType;
use function array_merge;

/**
* @implements Rule<Node\Expr\ArrowFunction>
*/
class ExistingClassesInArrowFunctionTypehintsRule implements Rule
{

public function __construct(private FunctionDefinitionCheck $check)
public function __construct(private FunctionDefinitionCheck $check, private PhpVersion $phpVersion)
{
}

Expand All @@ -24,7 +29,17 @@ public function getNodeType(): string

public function processNode(Node $node, Scope $scope): array
{
return $this->check->checkAnonymousFunction(
$messages = [];
if ($node->returnType !== null && !$this->phpVersion->supportsNeverReturnTypeInArrowFunction()) {
$returnType = ParserNodeTypeToPHPStanType::resolve($node->returnType, $scope->isInClass() ? $scope->getClassReflection() : null);
if ($returnType instanceof NonAcceptingNeverType) {
$messages[] = RuleErrorBuilder::message('Never return type in arrow function is supported only on PHP 8.2 and later.')
->nonIgnorable()
->build();
}
}

return array_merge($messages, $this->check->checkAnonymousFunction(
$scope,
$node->getParams(),
$node->getReturnType(),
Expand All @@ -33,7 +48,7 @@ public function processNode(Node $node, Scope $scope): array
'Anonymous function uses native union types but they\'re supported only on PHP 8.0 and later.',
'Parameter $%s of anonymous function has unresolvable native type.',
'Anonymous function has unresolvable native return type.',
);
));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ public function testRule(): void
'Anonymous function should return int but returns string.',
14,
],
[
'Anonymous function should never return but return statement found.',
44,
],
]);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class ExistingClassesInArrowFunctionTypehintsRuleTest extends RuleTestCase
protected function getRule(): Rule
{
$broker = $this->createReflectionProvider();
return new ExistingClassesInArrowFunctionTypehintsRule(new FunctionDefinitionCheck($broker, new ClassCaseSensitivityCheck($broker, true), new UnresolvableTypeHelper(), new PhpVersion($this->phpVersionId), true, false));
return new ExistingClassesInArrowFunctionTypehintsRule(new FunctionDefinitionCheck($broker, new ClassCaseSensitivityCheck($broker, true), new UnresolvableTypeHelper(), new PhpVersion($this->phpVersionId), true, false), new PhpVersion(PHP_VERSION_ID));
}

public function testRule(): void
Expand Down Expand Up @@ -147,4 +147,18 @@ public function testIntersectionTypes(int $phpVersion, array $errors): void
$this->analyse([__DIR__ . '/data/arrow-function-intersection-types.php'], $errors);
}

public function testNever(): void
{
$errors = [];
if (PHP_VERSION_ID < 80200) {
$errors = [
[
'Never return type in arrow function is supported only on PHP 8.2 and later.',
6,
],
];
}
$this->analyse([__DIR__ . '/data/arrow-function-never.php'], $errors);
}

}
7 changes: 7 additions & 0 deletions tests/PHPStan/Rules/Functions/data/arrow-function-never.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php // lint >= 7.4

namespace ArrowFunctionNever;

function (): void {
$g = fn (): never => throw new \Exception();
};
12 changes: 12 additions & 0 deletions tests/PHPStan/Rules/Functions/data/arrow-functions-return-type.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,15 @@ public function doBar(): void
}

static fn (int $value): iterable => yield $value;

class Baz
{

public function doFoo(): void
{
$f = fn () => throw new \Exception();
$g = fn (): never => throw new \Exception();
$g = fn (): never => 1;
}

}

0 comments on commit 758e5f1

Please sign in to comment.