Skip to content

Commit 000b2ce

Browse files
committed
added ArrowFunctionVoidIgnoreExtension
1 parent 2a42c76 commit 000b2ce

8 files changed

Lines changed: 150 additions & 0 deletions

CLAUDE.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ Extensions for specific Nette packages use dedicated namespaces: `Nette\PHPStan\
4444

4545
`ExpectArrayReturnTypeExtension` (`DynamicStaticMethodReturnTypeExtension`) narrows the return type of `Expect::array()` from `Structure|Type` to `Structure` or `Type`. It inspects the argument: no argument, null, empty array, or non-Schema values → `Type`; all values implementing `Schema``Structure`; mixed/unknown → fallback to declared union. Config: `extension-schema.neon`.
4646

47+
### ArrowFunctionVoidIgnoreExtension
48+
49+
`ArrowFunctionVoidIgnoreExtension` (`IgnoreErrorExtension`) suppresses `argument.type` when an arrow function (which always returns a value) is passed to a parameter typed as `Closure(): void`. The list of affected functions/methods is configurable via a flat NEON list — plain names for functions (`testException`), `Class::method` notation for methods (`Tester\Assert::exception`). Config: `extension-tester.neon`.
50+
4751
### ClosureTypeCheckIgnoreExtension
4852

4953
`ClosureTypeCheckIgnoreExtension` (`IgnoreErrorExtension`) suppresses `expr.resultUnused` for the runtime type validation pattern `(function(Type ...$p) {})(...$args)`. Config: `extension-php.neon`.

extension-tester.neon

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
services:
2+
-
3+
class: Nette\PHPStan\Php\ArrowFunctionVoidIgnoreExtension([
4+
test
5+
testException
6+
testNoError
7+
Tester\Assert::exception
8+
Tester\Assert::throws
9+
Tester\Assert::error
10+
Tester\Assert::noError
11+
])
12+
tags: [phpstan.ignoreErrorExtension]

extension.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@ parameters:
1616
includes:
1717
- extension-php.neon
1818
- extension-schema.neon
19+
- extension-tester.neon

readme.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,18 @@ $cwd = getcwd();
5555

5656
 <!---->
5757

58+
### Arrow Function Void Ignore
59+
60+
Suppresses `argument.type` when an arrow function is passed to a parameter typed as `Closure(): void`. Arrow functions always return a value, so PHPStan reports a type mismatch even though the return value is simply discarded. This extension ignores the error for [configured functions](extension-tester.neon) like `test`, `testException`, `testNoError`, `Assert::exception`, `Assert::throws`, `Assert::error`, and `Assert::noError`.
61+
62+
```php
63+
// Without extension: argument.type error
64+
// With extension: no error
65+
testException('listOf() & error', fn() => Expect::listOf(['a' => Expect::string()]), TypeError::class);
66+
```
67+
68+
<!---->
69+
5870
### Other
5971

6072
- Narrows the return type of `Expect::array()` from `Structure|Type` to `Structure` or `Type` based on the argument
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Nette\PHPStan\Php;
6+
7+
use PhpParser\Node;
8+
use PHPStan\Analyser\Error;
9+
use PHPStan\Analyser\IgnoreErrorExtension;
10+
use PHPStan\Analyser\Scope;
11+
use function str_contains, substr_count;
12+
13+
14+
/**
15+
* Suppresses 'argument.type' when an arrow function (which always returns a value) is passed
16+
* to a parameter typed as Closure(): void. The list of affected functions/methods is configurable.
17+
*/
18+
class ArrowFunctionVoidIgnoreExtension implements IgnoreErrorExtension
19+
{
20+
/** @var list<string> */
21+
private array $functions = [];
22+
23+
/** @var list<string> */
24+
private array $methods = [];
25+
26+
27+
/** @param list<string> $items plain names for functions, Class::method for methods */
28+
public function __construct(array $items)
29+
{
30+
foreach ($items as $item) {
31+
if (str_contains($item, '::')) {
32+
$this->methods[] = $item;
33+
} else {
34+
$this->functions[] = $item;
35+
}
36+
}
37+
}
38+
39+
40+
public function shouldIgnore(Error $error, Node $node, Scope $scope): bool
41+
{
42+
if ($error->getIdentifier() !== 'argument.type') {
43+
return false;
44+
}
45+
46+
$message = $error->getMessage();
47+
48+
if (!str_contains($message, 'Closure(): void')) {
49+
return false;
50+
}
51+
52+
if (substr_count($message, 'Closure(') < 2) {
53+
return false;
54+
}
55+
56+
foreach ($this->functions as $function) {
57+
if (str_contains($message, "function $function(")) {
58+
return true;
59+
}
60+
}
61+
62+
foreach ($this->methods as $method) {
63+
if (str_contains($message, "$method(")) {
64+
return true;
65+
}
66+
}
67+
68+
return false;
69+
}
70+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php declare(strict_types=1);
2+
3+
require __DIR__ . '/../bootstrap.php';
4+
5+
use Nette\PHPStan\Tester\TypeAssert;
6+
7+
TypeAssert::assertNoErrors(__DIR__ . '/../data/arrow-function-void.php', [__DIR__ . '/../data/arrow-function-void.neon']);
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
includes:
2+
- ../../extension.neon
3+
4+
parameters:
5+
checkMissingCallableSignature: false

tests/data/arrow-function-void.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tester {
6+
class Assert
7+
{
8+
/** @param \Closure(): void $function */
9+
public static function exception(\Closure $function, string $class): void
10+
{
11+
}
12+
13+
14+
/** @param \Closure(): void $function */
15+
public static function noError(\Closure $function): void
16+
{
17+
}
18+
}
19+
}
20+
21+
namespace {
22+
/** @param Closure(): void $function */
23+
function testException(string $description, Closure $function, string $class): void
24+
{
25+
}
26+
27+
28+
/** @param Closure(): void $function */
29+
function testNoError(string $description, Closure $function): void
30+
{
31+
}
32+
33+
34+
testException('test', fn() => strlen('hello'), TypeError::class);
35+
testNoError('test', fn() => strlen('hello'));
36+
37+
Tester\Assert::exception(fn() => strlen('hello'), TypeError::class);
38+
Tester\Assert::noError(fn() => strlen('hello'));
39+
}

0 commit comments

Comments
 (0)