From 758e5f118ac5781d597707666104511258fcaf67 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 28 Nov 2023 15:24:46 +0100 Subject: [PATCH] It's okay to have always-throwing expression in arrow function --- src/Php/PhpVersion.php | 5 +++++ .../Functions/ArrowFunctionReturnTypeRule.php | 11 ++++++++++ ...ingClassesInArrowFunctionTypehintsRule.php | 21 ++++++++++++++++--- .../ArrowFunctionReturnTypeRuleTest.php | 4 ++++ ...lassesInArrowFunctionTypehintsRuleTest.php | 16 +++++++++++++- .../Functions/data/arrow-function-never.php | 7 +++++++ .../data/arrow-functions-return-type.php | 12 +++++++++++ 7 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 tests/PHPStan/Rules/Functions/data/arrow-function-never.php diff --git a/src/Php/PhpVersion.php b/src/Php/PhpVersion.php index 062b76d76e..2ca9954a25 100644 --- a/src/Php/PhpVersion.php +++ b/src/Php/PhpVersion.php @@ -272,4 +272,9 @@ public function supportsReadOnlyAnonymousClasses(): bool return $this->versionId >= 80300; } + public function supportsNeverReturnTypeInArrowFunction(): bool + { + return $this->versionId >= 80200; + } + } diff --git a/src/Rules/Functions/ArrowFunctionReturnTypeRule.php b/src/Rules/Functions/ArrowFunctionReturnTypeRule.php index dd62d3f5da..aa0a9436de 100644 --- a/src/Rules/Functions/ArrowFunctionReturnTypeRule.php +++ b/src/Rules/Functions/ArrowFunctionReturnTypeRule.php @@ -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; @@ -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, diff --git a/src/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRule.php b/src/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRule.php index ecaf404573..04c9bc7c3a 100644 --- a/src/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRule.php +++ b/src/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRule.php @@ -4,8 +4,13 @@ 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 @@ -13,7 +18,7 @@ class ExistingClassesInArrowFunctionTypehintsRule implements Rule { - public function __construct(private FunctionDefinitionCheck $check) + public function __construct(private FunctionDefinitionCheck $check, private PhpVersion $phpVersion) { } @@ -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(), @@ -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.', - ); + )); } } diff --git a/tests/PHPStan/Rules/Functions/ArrowFunctionReturnTypeRuleTest.php b/tests/PHPStan/Rules/Functions/ArrowFunctionReturnTypeRuleTest.php index 5e3223a334..e34d62059b 100644 --- a/tests/PHPStan/Rules/Functions/ArrowFunctionReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ArrowFunctionReturnTypeRuleTest.php @@ -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, + ], ]); } diff --git a/tests/PHPStan/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRuleTest.php b/tests/PHPStan/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRuleTest.php index d699e8efaf..e2cdf27f1b 100644 --- a/tests/PHPStan/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRuleTest.php @@ -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 @@ -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); + } + } diff --git a/tests/PHPStan/Rules/Functions/data/arrow-function-never.php b/tests/PHPStan/Rules/Functions/data/arrow-function-never.php new file mode 100644 index 0000000000..227ad163b2 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/arrow-function-never.php @@ -0,0 +1,7 @@ += 7.4 + +namespace ArrowFunctionNever; + +function (): void { + $g = fn (): never => throw new \Exception(); +}; diff --git a/tests/PHPStan/Rules/Functions/data/arrow-functions-return-type.php b/tests/PHPStan/Rules/Functions/data/arrow-functions-return-type.php index 4a18708fba..552bf901c6 100644 --- a/tests/PHPStan/Rules/Functions/data/arrow-functions-return-type.php +++ b/tests/PHPStan/Rules/Functions/data/arrow-functions-return-type.php @@ -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; + } + +}