From 3fbfffc5277bc26ec1fdb222e52d1a95a83c8299 Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Fri, 14 Aug 2026 10:36:28 +0200 Subject: [PATCH 01/11] feat(phpstan): add MissingClosureReturnTypehintRule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closures and arrow functions without a return type force readers and static analysis to infer behavior from the implementation. Rector's TYPE_DECLARATION set cannot infer types from magic properties, so a dedicated rule closes the gap. Disabled by default in rules.neon, like its parameter counterpart. 🤖 Generated with Claude Code --- .php-cs-fixer.php | 2 + rector.php | 1 + rules.neon | 1 + .../MissingClosureReturnTypehintRule.php | 47 +++++++++++++++++++ .../MissingClosureReturnTypehintRuleTest.php | 26 ++++++++++ tests/PHPStan/data/closure-return-types.php | 13 +++++ 6 files changed, 90 insertions(+) create mode 100644 src/PHPStan/Rules/MissingClosureReturnTypehintRule.php create mode 100644 tests/PHPStan/MissingClosureReturnTypehintRuleTest.php create mode 100644 tests/PHPStan/data/closure-return-types.php diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index ca30df19..9cd2df31 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -4,6 +4,8 @@ $finder = PhpCsFixer\Finder::create() ->notPath('vendor') + // Fixtures intentionally violate the rules under test + ->exclude('tests/PHPStan/data') ->in(__DIR__) ->name('*.php') ->ignoreDotFiles(true) diff --git a/rector.php b/rector.php index 88c0015d..56d17aef 100644 --- a/rector.php +++ b/rector.php @@ -24,6 +24,7 @@ Rector\PHPUnit\CodeQuality\Rector\Class_\PreferPHPUnitSelfCallRector::class, ]) ->withSkip([ + __DIR__ . '/tests/PHPStan/data', // fixtures intentionally violate the rules under test Rector\PHPUnit\CodeQuality\Rector\Class_\PreferPHPUnitThisCallRector::class, // breaks tests Rector\CodeQuality\Rector\Concat\JoinStringConcatRector::class => [ __DIR__ . '/tests/CSVArrayTest.php', // keep `\r\n` for readability diff --git a/rules.neon b/rules.neon index f354ac38..fe965166 100644 --- a/rules.neon +++ b/rules.neon @@ -4,6 +4,7 @@ rules: #- MLL\Utils\PHPStan\Rules\VariableNameIdToIDRule #- MLL\Utils\PHPStan\Rules\PropertyNameIdToIDRule #- MLL\Utils\PHPStan\Rules\MissingClosureParameterTypehintRule +#- MLL\Utils\PHPStan\Rules\MissingClosureReturnTypehintRule parameters: # https://github.com/spaze/phpstan-disallowed-calls/blob/main/docs/custom-rules.md disallowedFunctionCalls: diff --git a/src/PHPStan/Rules/MissingClosureReturnTypehintRule.php b/src/PHPStan/Rules/MissingClosureReturnTypehintRule.php new file mode 100644 index 00000000..53e03cba --- /dev/null +++ b/src/PHPStan/Rules/MissingClosureReturnTypehintRule.php @@ -0,0 +1,47 @@ + + */ +final class MissingClosureReturnTypehintRule implements Rule +{ + /** @return class-string */ + public function getNodeType(): string + { + return Node\Expr::class; + } + + /** + * @param Node\Expr $node + * + * @return list + */ + public function processNode(Node $node, Scope $scope): array + { + if (! $node instanceof Closure && ! $node instanceof ArrowFunction) { + return []; + } + + if ($node->returnType instanceof Node) { + return []; + } + + $kind = $node instanceof ArrowFunction ? 'Arrow function' : 'Closure'; + + return [ + RuleErrorBuilder::message("{$kind} is missing a native return type hint.") + ->identifier('missingType.closureReturn') + ->build(), + ]; + } +} diff --git a/tests/PHPStan/MissingClosureReturnTypehintRuleTest.php b/tests/PHPStan/MissingClosureReturnTypehintRuleTest.php new file mode 100644 index 00000000..db50b345 --- /dev/null +++ b/tests/PHPStan/MissingClosureReturnTypehintRuleTest.php @@ -0,0 +1,26 @@ + + */ +final class MissingClosureReturnTypehintRuleTest extends RuleTestCase +{ + protected function getRule(): Rule + { + return new MissingClosureReturnTypehintRule(); + } + + public function testMissingReturnTypes(): void + { + $this->analyse([__DIR__ . '/data/closure-return-types.php'], [ + ['Closure is missing a native return type hint.', 3], + ['Arrow function is missing a native return type hint.', 7], + ]); + } +} diff --git a/tests/PHPStan/data/closure-return-types.php b/tests/PHPStan/data/closure-return-types.php new file mode 100644 index 00000000..fe115c1a --- /dev/null +++ b/tests/PHPStan/data/closure-return-types.php @@ -0,0 +1,13 @@ + $value * 2; + +$typedClosure = static function (int $value): int { + return $value * 2; +}; + +$typedArrow = static fn (int $value): int => $value * 2; From 4f77b3e57891c070c93cfa8c0dac128801af2e5c Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Fri, 14 Aug 2026 11:32:54 +0200 Subject: [PATCH 02/11] refactor(phpstan): extract ClosureTypehintRule base for closure rules Both closure typehint rules shared getNodeType() and the same Closure/ArrowFunction filter. --- src/PHPStan/Rules/ClosureTypehintRule.php | 43 +++++++++++++++++++ .../MissingClosureParameterTypehintRule.php | 29 ++----------- .../MissingClosureReturnTypehintRule.php | 30 ++----------- 3 files changed, 50 insertions(+), 52 deletions(-) create mode 100644 src/PHPStan/Rules/ClosureTypehintRule.php diff --git a/src/PHPStan/Rules/ClosureTypehintRule.php b/src/PHPStan/Rules/ClosureTypehintRule.php new file mode 100644 index 00000000..58b775c5 --- /dev/null +++ b/src/PHPStan/Rules/ClosureTypehintRule.php @@ -0,0 +1,43 @@ + + */ +abstract class ClosureTypehintRule implements Rule +{ + /** + * @param Closure|ArrowFunction $closure + * + * @return list + */ + abstract protected function processClosure(Node\FunctionLike $closure): array; + + /** @return class-string */ + public function getNodeType(): string + { + return Node\Expr::class; + } + + /** + * @param Node\Expr $node + * + * @return list + */ + public function processNode(Node $node, Scope $scope): array + { + if (! $node instanceof Closure && ! $node instanceof ArrowFunction) { + return []; + } + + return $this->processClosure($node); + } +} diff --git a/src/PHPStan/Rules/MissingClosureParameterTypehintRule.php b/src/PHPStan/Rules/MissingClosureParameterTypehintRule.php index d5c41062..60f7d99e 100644 --- a/src/PHPStan/Rules/MissingClosureParameterTypehintRule.php +++ b/src/PHPStan/Rules/MissingClosureParameterTypehintRule.php @@ -3,38 +3,15 @@ namespace MLL\Utils\PHPStan\Rules; use PhpParser\Node; -use PhpParser\Node\Expr\ArrowFunction; -use PhpParser\Node\Expr\Closure; use PhpParser\Node\Expr\Variable; -use PHPStan\Analyser\Scope; -use PHPStan\Rules\IdentifierRuleError; -use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -/** - * @implements Rule - */ -final class MissingClosureParameterTypehintRule implements Rule +final class MissingClosureParameterTypehintRule extends ClosureTypehintRule { - /** @return class-string */ - public function getNodeType(): string + protected function processClosure(Node\FunctionLike $closure): array { - return Node\Expr::class; - } - - /** - * @param Node\Expr $node - * - * @return list - */ - public function processNode(Node $node, Scope $scope): array - { - if (! $node instanceof Closure && ! $node instanceof ArrowFunction) { - return []; - } - $errors = []; - foreach ($node->params as $param) { + foreach ($closure->getParams() as $param) { if ($param->type !== null) { continue; } diff --git a/src/PHPStan/Rules/MissingClosureReturnTypehintRule.php b/src/PHPStan/Rules/MissingClosureReturnTypehintRule.php index 53e03cba..e049352b 100644 --- a/src/PHPStan/Rules/MissingClosureReturnTypehintRule.php +++ b/src/PHPStan/Rules/MissingClosureReturnTypehintRule.php @@ -4,39 +4,17 @@ use PhpParser\Node; use PhpParser\Node\Expr\ArrowFunction; -use PhpParser\Node\Expr\Closure; -use PHPStan\Analyser\Scope; -use PHPStan\Rules\IdentifierRuleError; -use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -/** - * @implements Rule - */ -final class MissingClosureReturnTypehintRule implements Rule +final class MissingClosureReturnTypehintRule extends ClosureTypehintRule { - /** @return class-string */ - public function getNodeType(): string + protected function processClosure(Node\FunctionLike $closure): array { - return Node\Expr::class; - } - - /** - * @param Node\Expr $node - * - * @return list - */ - public function processNode(Node $node, Scope $scope): array - { - if (! $node instanceof Closure && ! $node instanceof ArrowFunction) { - return []; - } - - if ($node->returnType instanceof Node) { + if ($closure->getReturnType() instanceof Node) { return []; } - $kind = $node instanceof ArrowFunction ? 'Arrow function' : 'Closure'; + $kind = $closure instanceof ArrowFunction ? 'Arrow function' : 'Closure'; return [ RuleErrorBuilder::message("{$kind} is missing a native return type hint.") From efbb895afff71cc0c657af28ba0707763be62e8c Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Fri, 14 Aug 2026 11:35:54 +0200 Subject: [PATCH 03/11] chore(phpstan): self-apply MissingClosureReturnTypehintRule --- phpstan.neon | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/phpstan.neon b/phpstan.neon index e3ad45ee..5692d938 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -6,6 +6,7 @@ rules: #- MLL\Utils\PHPStan\Rules\ThrowableClassNameRule - MLL\Utils\PHPStan\Rules\VariableNameIdToIDRule - MLL\Utils\PHPStan\Rules\MissingClosureParameterTypehintRule +- MLL\Utils\PHPStan\Rules\MissingClosureReturnTypehintRule parameters: level: max paths: @@ -36,6 +37,11 @@ parameters: paths: - tests/PHPStan/data/ + # Test fixtures intentionally omit closure return type hints + - message: '#is missing a native return type hint\.#' + paths: + - tests/PHPStan/data/ + # PHPStan internal API usage is acceptable in tests - message: '#is not covered by backward compatibility promise#' paths: From 6cdbf74335641287f1a8ff8bbb277f225f99bf9c Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Fri, 14 Aug 2026 11:37:55 +0200 Subject: [PATCH 04/11] perf(phpstan): narrow closure rule node type to FunctionLike PHPStan dispatched the rule for every expression node to reach two instanceof checks. No measurable difference on this repo (276 files), but consumers analyse far more code. --- src/PHPStan/Rules/ClosureTypehintRule.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/PHPStan/Rules/ClosureTypehintRule.php b/src/PHPStan/Rules/ClosureTypehintRule.php index 58b775c5..79dfc2fa 100644 --- a/src/PHPStan/Rules/ClosureTypehintRule.php +++ b/src/PHPStan/Rules/ClosureTypehintRule.php @@ -10,7 +10,7 @@ use PHPStan\Rules\Rule; /** - * @implements Rule + * @implements Rule */ abstract class ClosureTypehintRule implements Rule { @@ -21,14 +21,14 @@ abstract class ClosureTypehintRule implements Rule */ abstract protected function processClosure(Node\FunctionLike $closure): array; - /** @return class-string */ + /** @return class-string */ public function getNodeType(): string { - return Node\Expr::class; + return Node\FunctionLike::class; } /** - * @param Node\Expr $node + * @param Node\FunctionLike $node * * @return list */ From 0a7371ad296b0951deb07b0fce7958f5f9bee320 Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Fri, 14 Aug 2026 11:44:33 +0200 Subject: [PATCH 05/11] test(phpstan): cover MissingClosureParameterTypehintRule The rule shipped untested and now shares a base class with the new return typehint rule. Documents RuleTestCase as the convention. --- .claude/CLAUDE.md | 4 +++ phpstan.neon | 5 +++- ...issingClosureParameterTypehintRuleTest.php | 26 +++++++++++++++++++ .../PHPStan/data/closure-parameter-types.php | 13 ++++++++++ 4 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/MissingClosureParameterTypehintRuleTest.php create mode 100644 tests/PHPStan/data/closure-parameter-types.php diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 96eb377f..1eaabba7 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -59,6 +59,10 @@ This library ships custom PHPStan rules (`src/PHPStan/Rules/`) and disallowed ca Consumer projects get these automatically via `phpstan/extension-installer`. The `phpstan.neon` in this repo includes additional rules enabled only for this project itself. +A rule enabled there needs an `ignoreErrors` entry scoped to `tests/PHPStan/data/`, since fixtures violate rules on purpose. + +Test new rules with PHPStan's `RuleTestCase` against a fixture in `tests/PHPStan/data/`. +That directory is excluded from rector and php-cs-fixer — both would otherwise normalize away the violations under test. ## Conventions diff --git a/phpstan.neon b/phpstan.neon index 5692d938..f3680361 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -37,10 +37,13 @@ parameters: paths: - tests/PHPStan/data/ - # Test fixtures intentionally omit closure return type hints + # Test fixtures intentionally omit closure type hints - message: '#is missing a native return type hint\.#' paths: - tests/PHPStan/data/ + - message: '#is missing a native type hint\.#' + paths: + - tests/PHPStan/data/ # PHPStan internal API usage is acceptable in tests - message: '#is not covered by backward compatibility promise#' diff --git a/tests/PHPStan/MissingClosureParameterTypehintRuleTest.php b/tests/PHPStan/MissingClosureParameterTypehintRuleTest.php new file mode 100644 index 00000000..bea24e25 --- /dev/null +++ b/tests/PHPStan/MissingClosureParameterTypehintRuleTest.php @@ -0,0 +1,26 @@ + + */ +final class MissingClosureParameterTypehintRuleTest extends RuleTestCase +{ + protected function getRule(): Rule + { + return new MissingClosureParameterTypehintRule(); + } + + public function testMissingParameterTypes(): void + { + $this->analyse([__DIR__ . '/data/closure-parameter-types.php'], [ + ['Closure parameter factor is missing a native type hint.', 3], + ['Closure parameter factor is missing a native type hint.', 7], + ]); + } +} diff --git a/tests/PHPStan/data/closure-parameter-types.php b/tests/PHPStan/data/closure-parameter-types.php new file mode 100644 index 00000000..6483f109 --- /dev/null +++ b/tests/PHPStan/data/closure-parameter-types.php @@ -0,0 +1,13 @@ + 2; + +$typedClosureParameter = static function (int $factor): int { + return 2 * $factor; +}; + +$typedArrowParameter = static fn (int $factor): int => 2 * $factor; From 38b4f26598215881923fac4c4ed7d4492c536049 Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Fri, 14 Aug 2026 12:04:35 +0200 Subject: [PATCH 06/11] fix(phpstan): label arrow functions correctly and pin closure filter Arrow function parameters were reported as "Closure parameter". Extracts closureKind() so both rules phrase the node kind the same way. The FunctionLike filter was load-bearing but untested - deleting it kept both suites green. Fixtures now include an untyped plain function and class method that the rules must ignore. --- phpstan.neon | 8 ++++++++ src/PHPStan/Rules/ClosureTypehintRule.php | 8 ++++++++ .../Rules/MissingClosureParameterTypehintRule.php | 4 +++- .../Rules/MissingClosureReturnTypehintRule.php | 8 ++++++-- .../MissingClosureParameterTypehintRuleTest.php | 2 +- tests/PHPStan/data/closure-parameter-types.php | 14 ++++++++++++++ tests/PHPStan/data/closure-return-types.php | 14 ++++++++++++++ 7 files changed, 54 insertions(+), 4 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index f3680361..266d79a8 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -45,6 +45,14 @@ parameters: paths: - tests/PHPStan/data/ + # Test fixtures include untyped functions and methods to prove the closure rules ignore them + - message: '#has no return type specified\.#' + paths: + - tests/PHPStan/data/ + - message: '#with no type specified\.#' + paths: + - tests/PHPStan/data/ + # PHPStan internal API usage is acceptable in tests - message: '#is not covered by backward compatibility promise#' paths: diff --git a/src/PHPStan/Rules/ClosureTypehintRule.php b/src/PHPStan/Rules/ClosureTypehintRule.php index 79dfc2fa..3ac9797d 100644 --- a/src/PHPStan/Rules/ClosureTypehintRule.php +++ b/src/PHPStan/Rules/ClosureTypehintRule.php @@ -21,6 +21,14 @@ abstract class ClosureTypehintRule implements Rule */ abstract protected function processClosure(Node\FunctionLike $closure): array; + /** @param Closure|ArrowFunction $closure */ + protected function closureKind(Node\FunctionLike $closure): string + { + return $closure instanceof ArrowFunction + ? 'Arrow function' + : 'Closure'; + } + /** @return class-string */ public function getNodeType(): string { diff --git a/src/PHPStan/Rules/MissingClosureParameterTypehintRule.php b/src/PHPStan/Rules/MissingClosureParameterTypehintRule.php index 60f7d99e..818e0de3 100644 --- a/src/PHPStan/Rules/MissingClosureParameterTypehintRule.php +++ b/src/PHPStan/Rules/MissingClosureParameterTypehintRule.php @@ -10,6 +10,8 @@ final class MissingClosureParameterTypehintRule extends ClosureTypehintRule { protected function processClosure(Node\FunctionLike $closure): array { + $kind = $this->closureKind($closure); + $errors = []; foreach ($closure->getParams() as $param) { if ($param->type !== null) { @@ -28,7 +30,7 @@ protected function processClosure(Node\FunctionLike $closure): array $varName = $paramVar->name; - $errors[] = RuleErrorBuilder::message("Closure parameter {$varName} is missing a native type hint.") + $errors[] = RuleErrorBuilder::message("{$kind} parameter {$varName} is missing a native type hint.") ->identifier('missingType.parameter') ->build(); } diff --git a/src/PHPStan/Rules/MissingClosureReturnTypehintRule.php b/src/PHPStan/Rules/MissingClosureReturnTypehintRule.php index e049352b..03c0b123 100644 --- a/src/PHPStan/Rules/MissingClosureReturnTypehintRule.php +++ b/src/PHPStan/Rules/MissingClosureReturnTypehintRule.php @@ -3,9 +3,13 @@ namespace MLL\Utils\PHPStan\Rules; use PhpParser\Node; -use PhpParser\Node\Expr\ArrowFunction; use PHPStan\Rules\RuleErrorBuilder; +/** + * Assumes PHP 8.0+, where every return type is natively expressible. + * On PHP 7.4 a closure returning `mixed` has no native type to declare, + * which is why `phpstan/include-by-php-version.php` gates `rules.neon`. + */ final class MissingClosureReturnTypehintRule extends ClosureTypehintRule { protected function processClosure(Node\FunctionLike $closure): array @@ -14,7 +18,7 @@ protected function processClosure(Node\FunctionLike $closure): array return []; } - $kind = $closure instanceof ArrowFunction ? 'Arrow function' : 'Closure'; + $kind = $this->closureKind($closure); return [ RuleErrorBuilder::message("{$kind} is missing a native return type hint.") diff --git a/tests/PHPStan/MissingClosureParameterTypehintRuleTest.php b/tests/PHPStan/MissingClosureParameterTypehintRuleTest.php index bea24e25..016850b0 100644 --- a/tests/PHPStan/MissingClosureParameterTypehintRuleTest.php +++ b/tests/PHPStan/MissingClosureParameterTypehintRuleTest.php @@ -20,7 +20,7 @@ public function testMissingParameterTypes(): void { $this->analyse([__DIR__ . '/data/closure-parameter-types.php'], [ ['Closure parameter factor is missing a native type hint.', 3], - ['Closure parameter factor is missing a native type hint.', 7], + ['Arrow function parameter factor is missing a native type hint.', 7], ]); } } diff --git a/tests/PHPStan/data/closure-parameter-types.php b/tests/PHPStan/data/closure-parameter-types.php index 6483f109..b930da72 100644 --- a/tests/PHPStan/data/closure-parameter-types.php +++ b/tests/PHPStan/data/closure-parameter-types.php @@ -11,3 +11,17 @@ }; $typedArrowParameter = static fn (int $factor): int => 2 * $factor; + +// Only closures are in scope - these must not be reported. +function plainFunctionWithoutParameterType($factor): int +{ + return 2; +} + +class MethodWithoutParameterType +{ + public function untyped($factor): int + { + return 2; + } +} diff --git a/tests/PHPStan/data/closure-return-types.php b/tests/PHPStan/data/closure-return-types.php index fe115c1a..a4feae51 100644 --- a/tests/PHPStan/data/closure-return-types.php +++ b/tests/PHPStan/data/closure-return-types.php @@ -11,3 +11,17 @@ }; $typedArrow = static fn (int $value): int => $value * 2; + +// Only closures are in scope - these must not be reported. +function plainFunctionWithoutReturnType() +{ + return 1; +} + +class MethodWithoutReturnType +{ + public function untyped() + { + return 1; + } +} From 297664917f278efe319214efabdb3537c075e9af Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Fri, 14 Aug 2026 12:06:37 +0200 Subject: [PATCH 07/11] fix(phpstan): report closure parameter errors on the parameter line RuleErrorBuilder defaults to the closure start line, so every parameter of a multi-line signature was misreported. --- src/PHPStan/Rules/MissingClosureParameterTypehintRule.php | 1 + tests/PHPStan/MissingClosureParameterTypehintRuleTest.php | 2 ++ tests/PHPStan/data/closure-parameter-types.php | 7 +++++++ 3 files changed, 10 insertions(+) diff --git a/src/PHPStan/Rules/MissingClosureParameterTypehintRule.php b/src/PHPStan/Rules/MissingClosureParameterTypehintRule.php index 818e0de3..28b08010 100644 --- a/src/PHPStan/Rules/MissingClosureParameterTypehintRule.php +++ b/src/PHPStan/Rules/MissingClosureParameterTypehintRule.php @@ -32,6 +32,7 @@ protected function processClosure(Node\FunctionLike $closure): array $errors[] = RuleErrorBuilder::message("{$kind} parameter {$varName} is missing a native type hint.") ->identifier('missingType.parameter') + ->line($param->getStartLine()) ->build(); } diff --git a/tests/PHPStan/MissingClosureParameterTypehintRuleTest.php b/tests/PHPStan/MissingClosureParameterTypehintRuleTest.php index 016850b0..df10fe23 100644 --- a/tests/PHPStan/MissingClosureParameterTypehintRuleTest.php +++ b/tests/PHPStan/MissingClosureParameterTypehintRuleTest.php @@ -21,6 +21,8 @@ public function testMissingParameterTypes(): void $this->analyse([__DIR__ . '/data/closure-parameter-types.php'], [ ['Closure parameter factor is missing a native type hint.', 3], ['Arrow function parameter factor is missing a native type hint.', 7], + ['Closure parameter first is missing a native type hint.', 16], + ['Closure parameter second is missing a native type hint.', 17], ]); } } diff --git a/tests/PHPStan/data/closure-parameter-types.php b/tests/PHPStan/data/closure-parameter-types.php index b930da72..4ce4d439 100644 --- a/tests/PHPStan/data/closure-parameter-types.php +++ b/tests/PHPStan/data/closure-parameter-types.php @@ -12,6 +12,13 @@ $typedArrowParameter = static fn (int $factor): int => 2 * $factor; +$missingMultiLineParameters = static function ( + $first, + $second +): int { + return 2; +}; + // Only closures are in scope - these must not be reported. function plainFunctionWithoutParameterType($factor): int { From dca5fdbb1497cf4e2655b70c1968e6e7d37eac6e Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Fri, 14 Aug 2026 12:24:23 +0200 Subject: [PATCH 08/11] refactor(phpstan): hoist parameter name extraction above its guard $paramVar->name was written twice: once in the is_string() guard and once when assigned to $varName. --- src/PHPStan/Rules/MissingClosureParameterTypehintRule.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PHPStan/Rules/MissingClosureParameterTypehintRule.php b/src/PHPStan/Rules/MissingClosureParameterTypehintRule.php index 28b08010..d8294ac1 100644 --- a/src/PHPStan/Rules/MissingClosureParameterTypehintRule.php +++ b/src/PHPStan/Rules/MissingClosureParameterTypehintRule.php @@ -24,12 +24,12 @@ protected function processClosure(Node\FunctionLike $closure): array continue; } - if (! is_string($paramVar->name)) { + $varName = $paramVar->name; + + if (! is_string($varName)) { continue; } - $varName = $paramVar->name; - $errors[] = RuleErrorBuilder::message("{$kind} parameter {$varName} is missing a native type hint.") ->identifier('missingType.parameter') ->line($param->getStartLine()) From 5ead3a0767eaebd6dc8741f7cdcc814353b9fc5f Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Fri, 14 Aug 2026 12:25:49 +0200 Subject: [PATCH 09/11] test(phpstan): drop redundant comment from closure fixtures --- tests/PHPStan/data/closure-parameter-types.php | 1 - tests/PHPStan/data/closure-return-types.php | 1 - 2 files changed, 2 deletions(-) diff --git a/tests/PHPStan/data/closure-parameter-types.php b/tests/PHPStan/data/closure-parameter-types.php index 4ce4d439..957cf7c3 100644 --- a/tests/PHPStan/data/closure-parameter-types.php +++ b/tests/PHPStan/data/closure-parameter-types.php @@ -19,7 +19,6 @@ return 2; }; -// Only closures are in scope - these must not be reported. function plainFunctionWithoutParameterType($factor): int { return 2; diff --git a/tests/PHPStan/data/closure-return-types.php b/tests/PHPStan/data/closure-return-types.php index a4feae51..6515ba30 100644 --- a/tests/PHPStan/data/closure-return-types.php +++ b/tests/PHPStan/data/closure-return-types.php @@ -12,7 +12,6 @@ $typedArrow = static fn (int $value): int => $value * 2; -// Only closures are in scope - these must not be reported. function plainFunctionWithoutReturnType() { return 1; From 9cbba88cf5381e4806bbcb427a989806c4eeeb97 Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Fri, 14 Aug 2026 12:32:22 +0200 Subject: [PATCH 10/11] fix(phpstan): update PHP 7.4/8.0 ignores for renamed closure rule members The arrow function label and the processClosure() extraction both escaped the message patterns in phpstan/php-below-8.1.neon. --- phpstan/php-below-8.1.neon | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/phpstan/php-below-8.1.neon b/phpstan/php-below-8.1.neon index 9a45de45..815380c6 100644 --- a/phpstan/php-below-8.1.neon +++ b/phpstan/php-below-8.1.neon @@ -4,7 +4,7 @@ parameters: - '#Unknown PHPDoc tag: @phpstan-ignore#' # Older PHPStan has stricter/different closure parameter typehint checking - - '#Closure parameter .* is missing a native type hint\.#' + - '#(Closure|Arrow function) parameter .* is missing a native type hint\.#' # Differences in type inference between PHPStan versions - '#Cannot access property .* on mixed\.#' @@ -29,7 +29,7 @@ parameters: - '#PHPDoc tag @param has invalid value.*covariant.*#' # Return type differences in older PHPStan rule interfaces - - '#Method MLL\\Utils\\PHPStan\\Rules\\MissingClosureParameterTypehintRule::processNode\(\) should return array but returns array\.#' + - '#Method MLL\\Utils\\PHPStan\\Rules\\MissingClosure(Parameter|Return)TypehintRule::processClosure\(\) should return array but returns array\.#' # Existing code with @phpstan-ignore that older versions don't understand - message: '#Cannot access property \$name on SimpleXMLElement\|null\.#' From cb096dcb5d4b5a126b15abc5af4d60f9e6efc92a Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Fri, 14 Aug 2026 12:50:37 +0200 Subject: [PATCH 11/11] Update .php-cs-fixer.php --- .php-cs-fixer.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index 9cd2df31..c51fe754 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -4,8 +4,7 @@ $finder = PhpCsFixer\Finder::create() ->notPath('vendor') - // Fixtures intentionally violate the rules under test - ->exclude('tests/PHPStan/data') + ->exclude('tests/PHPStan/data') // Fixtures intentionally violate the rules under test ->in(__DIR__) ->name('*.php') ->ignoreDotFiles(true)