From f598a823fe90c6549b7aaa299afa0e1b8729a8ac Mon Sep 17 00:00:00 2001 From: Dawid Parafinski Date: Wed, 22 Oct 2025 09:42:50 +0200 Subject: [PATCH] Added rule to enforce typehints to closure and arrow functions --- extension.neon | 1 + rules/RequireClosureReturnTypeRule.php | 44 +++++++++++ .../RequireClosureReturnTypeFixture.php | 73 +++++++++++++++++++ .../RequireClosureReturnTypeRuleTest.php | 55 ++++++++++++++ 4 files changed, 173 insertions(+) create mode 100644 rules/RequireClosureReturnTypeRule.php create mode 100644 tests/rules/Fixtures/RequireClosureReturnTypeFixture.php create mode 100644 tests/rules/RequireClosureReturnTypeRuleTest.php diff --git a/extension.neon b/extension.neon index 80cf7ed..5f1b453 100644 --- a/extension.neon +++ b/extension.neon @@ -7,3 +7,4 @@ parameters: - stubs/Money/MoneyParser.stub rules: - Ibexa\PHPStan\Rules\NoConfigResolverParametersInConstructorRule + - Ibexa\PHPStan\Rules\RequireClosureReturnTypeRule diff --git a/rules/RequireClosureReturnTypeRule.php b/rules/RequireClosureReturnTypeRule.php new file mode 100644 index 0000000..5a9975e --- /dev/null +++ b/rules/RequireClosureReturnTypeRule.php @@ -0,0 +1,44 @@ + + */ +final class RequireClosureReturnTypeRule implements Rule +{ + public function getNodeType(): string + { + return Node\Expr::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node instanceof Node\Expr\Closure && !$node instanceof Node\Expr\ArrowFunction) { + return []; + } + + if ($node->returnType === null) { + $nodeType = $node instanceof Node\Expr\Closure ? 'Closure' : 'Arrow function'; + + return [ + RuleErrorBuilder::message( + sprintf('%s is missing a return type declaration', $nodeType) + )->build(), + ]; + } + + return []; + } +} diff --git a/tests/rules/Fixtures/RequireClosureReturnTypeFixture.php b/tests/rules/Fixtures/RequireClosureReturnTypeFixture.php new file mode 100644 index 0000000..f5023d2 --- /dev/null +++ b/tests/rules/Fixtures/RequireClosureReturnTypeFixture.php @@ -0,0 +1,73 @@ + $x * 2; + } + + public function arrowFunctionWithReturnType(): void + { + // OK: Arrow function has return type + $arrow = static fn (int $x): int => $x * 2; + } + + public function closureWithVoidReturnType(): void + { + // OK: Closure has void return type + $closure = static function (): void { + echo 'Hello'; + }; + } + + public function arrowFunctionWithMixedReturnType(): void + { + // OK: Arrow function has mixed return type + $arrow = static fn ($x): mixed => $x; + } + + public function nestedClosuresWithoutReturnType(): void + { + // Error: Outer closure without return type + $outer = static function () { + // Error: Inner closure without return type + return static function ($x) { + return $x * 2; + }; + }; + } + + public function arrayMapWithoutReturnType(): void + { + // Error: Closure without return type + $result = array_map(static function ($x) { + return $x * 2; + }, [1, 2, 3]); + } +} diff --git a/tests/rules/RequireClosureReturnTypeRuleTest.php b/tests/rules/RequireClosureReturnTypeRuleTest.php new file mode 100644 index 0000000..265481b --- /dev/null +++ b/tests/rules/RequireClosureReturnTypeRuleTest.php @@ -0,0 +1,55 @@ + + */ +final class RequireClosureReturnTypeRuleTest extends RuleTestCase +{ + protected function getRule(): Rule + { + return new RequireClosureReturnTypeRule(); + } + + public function testRule(): void + { + $this->analyse( + [ + __DIR__ . '/Fixtures/RequireClosureReturnTypeFixture.php', + ], + [ + [ + 'Closure is missing a return type declaration', + 16, + ], + [ + 'Arrow function is missing a return type declaration', + 32, + ], + [ + 'Closure is missing a return type declaration', + 58, + ], + [ + 'Closure is missing a return type declaration', + 60, + ], + [ + 'Closure is missing a return type declaration', + 69, + ], + ] + ); + } +}