Skip to content

Commit b11162b

Browse files
authored
EnforceClosureParamNativeTypehintRule (#192)
1 parent 0630e81 commit b11162b

File tree

8 files changed

+248
-1
lines changed

8 files changed

+248
-1
lines changed

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ parameters:
3030
classSuffixNaming:
3131
enabled: true
3232
superclassToSuffixMapping: []
33+
enforceClosureParamNativeTypehint:
34+
enabled: true
35+
allowMissingTypeWhenInferred: false
3336
enforceEnumMatch:
3437
enabled: true
3538
enforceIteratorToArrayPreserveKeys:
@@ -210,6 +213,27 @@ enum MyEnum: string { // missing @implements tag
210213
```
211214

212215

216+
### enforceClosureParamNativeTypehint
217+
- Enforces usage of native typehints for closure & arrow function parameters
218+
- Does nothing on PHP 7.4 and below as native `mixed` is not available there
219+
- Can be configured by `allowMissingTypeWhenInferred: true` to allow missing typehint when it can be inferred from the context
220+
221+
```php
222+
/**
223+
* @param list<Entity> $entities
224+
* @return list<Uuid>
225+
*/
226+
public function getIds(array $entities): array {
227+
return array_map(
228+
function ($entity) { // missing native typehint; not reported with allowMissingTypeWhenInferred: true
229+
return $entity->id;
230+
},
231+
$entities
232+
);
233+
}
234+
```
235+
236+
213237
### enforceEnumMatchRule
214238
- Enforces usage of `match ($enum)` instead of exhaustive conditions like `if ($enum === Enum::One) elseif ($enum === Enum::Two)`
215239
- This rule aims to "fix" a bit problematic behaviour of PHPStan (introduced at 1.10.0 and fixed in [1.10.34](https://github.com/phpstan/phpstan-src/commit/fc7c0283176e5dc3867ade26ac835ee7f52599a9)). It understands enum cases very well and forces you to adjust following code:

phpstan.neon.dist

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ parameters:
3838
ShipMonk\PHPStan\RuleTestCase: RuleTest
3939
forbidAssignmentNotMatchingVarDoc:
4040
enabled: false # native check is better now; this rule will be dropped / reworked in 3.0
41+
enforceClosureParamNativeTypehint:
42+
enabled: false # we support even PHP 7.4, some typehints cannot be used
4143

4244
ignoreErrors:
4345
-

rules.neon

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ parameters:
99
classSuffixNaming:
1010
enabled: true
1111
superclassToSuffixMapping: []
12+
enforceClosureParamNativeTypehint:
13+
enabled: true
14+
allowMissingTypeWhenInferred: false
1215
enforceEnumMatch:
1316
enabled: true
1417
enforceIteratorToArrayPreserveKeys:
@@ -119,6 +122,10 @@ parametersSchema:
119122
enabled: bool()
120123
superclassToSuffixMapping: arrayOf(string(), string())
121124
])
125+
enforceClosureParamNativeTypehint: structure([
126+
enabled: bool()
127+
allowMissingTypeWhenInferred: bool()
128+
])
122129
enforceEnumMatch: structure([
123130
enabled: bool()
124131
])
@@ -238,6 +245,8 @@ conditionalTags:
238245
phpstan.rules.rule: %shipmonkRules.backedEnumGenerics.enabled%
239246
ShipMonk\PHPStan\Rule\ClassSuffixNamingRule:
240247
phpstan.rules.rule: %shipmonkRules.classSuffixNaming.enabled%
248+
ShipMonk\PHPStan\Rule\EnforceClosureParamNativeTypehintRule:
249+
phpstan.rules.rule: %shipmonkRules.enforceClosureParamNativeTypehint.enabled%
241250
ShipMonk\PHPStan\Rule\EnforceEnumMatchRule:
242251
phpstan.rules.rule: %shipmonkRules.enforceEnumMatch.enabled%
243252
ShipMonk\PHPStan\Rule\EnforceIteratorToArrayPreserveKeysRule:
@@ -334,6 +343,10 @@ services:
334343
class: ShipMonk\PHPStan\Rule\ClassSuffixNamingRule
335344
arguments:
336345
superclassToSuffixMapping: %shipmonkRules.classSuffixNaming.superclassToSuffixMapping%
346+
-
347+
class: ShipMonk\PHPStan\Rule\EnforceClosureParamNativeTypehintRule
348+
arguments:
349+
allowMissingTypeWhenInferred: %shipmonkRules.enforceClosureParamNativeTypehint.allowMissingTypeWhenInferred%
337350
-
338351
class: ShipMonk\PHPStan\Rule\EnforceEnumMatchRule
339352
-
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ShipMonk\PHPStan\Rule;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\Expr\Variable;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\Node\InArrowFunctionNode;
9+
use PHPStan\Node\InClosureNode;
10+
use PHPStan\Php\PhpVersion;
11+
use PHPStan\Rules\Rule;
12+
use PHPStan\Rules\RuleError;
13+
use PHPStan\Rules\RuleErrorBuilder;
14+
use PHPStan\Type\MixedType;
15+
use function is_string;
16+
17+
/**
18+
* @implements Rule<Node>
19+
*/
20+
class EnforceClosureParamNativeTypehintRule implements Rule
21+
{
22+
23+
private PhpVersion $phpVersion;
24+
25+
private bool $allowMissingTypeWhenInferred;
26+
27+
public function __construct(
28+
PhpVersion $phpVersion,
29+
bool $allowMissingTypeWhenInferred
30+
)
31+
{
32+
$this->phpVersion = $phpVersion;
33+
$this->allowMissingTypeWhenInferred = $allowMissingTypeWhenInferred;
34+
}
35+
36+
public function getNodeType(): string
37+
{
38+
return Node::class;
39+
}
40+
41+
/**
42+
* @return list<RuleError>
43+
*/
44+
public function processNode(
45+
Node $node,
46+
Scope $scope
47+
): array
48+
{
49+
if (!$node instanceof InClosureNode && !$node instanceof InArrowFunctionNode) { // @phpstan-ignore-line bc promise
50+
return [];
51+
}
52+
53+
if ($this->phpVersion->getVersionId() < 80_000) {
54+
return []; // unable to add mixed native typehint there
55+
}
56+
57+
$errors = [];
58+
$type = $node instanceof InClosureNode ? 'closure' : 'arrow function';
59+
60+
foreach ($node->getOriginalNode()->getParams() as $param) {
61+
if (!$param->var instanceof Variable || !is_string($param->var->name)) {
62+
continue;
63+
}
64+
65+
if ($param->type !== null) {
66+
continue;
67+
}
68+
69+
$paramType = $scope->getType($param->var);
70+
71+
if ($this->allowMissingTypeWhenInferred && (!$paramType instanceof MixedType || $paramType->isExplicitMixed())) {
72+
continue;
73+
}
74+
75+
$errors[] = RuleErrorBuilder::message("Missing parameter typehint for {$type} parameter \${$param->var->name}.")
76+
->identifier('shipmonk.unknownClosureParamType')
77+
->line($param->getLine())
78+
->build();
79+
}
80+
81+
return $errors;
82+
}
83+
84+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ShipMonk\PHPStan\Rule;
4+
5+
use LogicException;
6+
use PHPStan\Php\PhpVersion;
7+
use PHPStan\Rules\Rule;
8+
use ShipMonk\PHPStan\RuleTestCase;
9+
10+
/**
11+
* @extends RuleTestCase<EnforceClosureParamNativeTypehintRule>
12+
*/
13+
class EnforceClosureParamNativeTypehintRuleTest extends RuleTestCase
14+
{
15+
16+
private ?bool $allowMissingTypeWhenInferred = null;
17+
18+
private ?PhpVersion $phpVersion = null;
19+
20+
protected function getRule(): Rule
21+
{
22+
if ($this->allowMissingTypeWhenInferred === null || $this->phpVersion === null) {
23+
throw new LogicException('Missing phpVersion or allowMissingTypeWhenInferred');
24+
}
25+
26+
return new EnforceClosureParamNativeTypehintRule(
27+
$this->phpVersion,
28+
$this->allowMissingTypeWhenInferred,
29+
);
30+
}
31+
32+
public function testAllowInferring(): void
33+
{
34+
$this->allowMissingTypeWhenInferred = true;
35+
$this->phpVersion = $this->createPhpVersion(80_000);
36+
37+
$this->analyseFile(__DIR__ . '/data/EnforceClosureParamNativeTypehintRule/allow-inferring.php');
38+
}
39+
40+
public function testEnforceEverywhere(): void
41+
{
42+
$this->allowMissingTypeWhenInferred = false;
43+
$this->phpVersion = $this->createPhpVersion(80_000);
44+
45+
$this->analyseFile(__DIR__ . '/data/EnforceClosureParamNativeTypehintRule/enforce-everywhere.php');
46+
}
47+
48+
public function testNoErrorOnPhp74(): void
49+
{
50+
$this->allowMissingTypeWhenInferred = false;
51+
$this->phpVersion = $this->createPhpVersion(70_400);
52+
53+
self::assertEmpty($this->processActualErrors($this->gatherAnalyserErrors([
54+
__DIR__ . '/data/EnforceClosureParamNativeTypehintRule/allow-inferring.php',
55+
__DIR__ . '/data/EnforceClosureParamNativeTypehintRule/enforce-everywhere.php',
56+
])));
57+
}
58+
59+
private function createPhpVersion(int $version): PhpVersion
60+
{
61+
return new PhpVersion($version); // @phpstan-ignore-line ignore bc promise
62+
}
63+
64+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ForbidImplicitMixedInClosureParamsRule;
4+
5+
6+
/**
7+
* @param list<string> $a
8+
* @param list<mixed> $b
9+
*/
10+
function test($a, $b, $c, array $d): void
11+
{
12+
array_map(function ($item) {}, [1]);
13+
array_map(function ($item) {}, $a);
14+
array_map(function ($item) {}, $b);
15+
array_map(function ($item) {}, $c); // error: Missing parameter typehint for closure parameter $item.
16+
array_map(function ($item) {}, $d); // error: Missing parameter typehint for closure parameter $item.
17+
array_map(function (int $item) {}, $c);
18+
array_map(function (int $item) {}, $d);
19+
20+
array_map(static fn($item) => 1, [1]);
21+
array_map(static fn($item) => 1, $a);
22+
array_map(static fn($item) => 1, $b);
23+
array_map(static fn($item) => 1, $c); // error: Missing parameter typehint for arrow function parameter $item.
24+
array_map(static fn($item) => 1, $d); // error: Missing parameter typehint for arrow function parameter $item.
25+
array_map(static fn(int $item) => 1, $c);
26+
array_map(static fn(int $item) => 1, $d);
27+
28+
function ($item2) {}; // error: Missing parameter typehint for closure parameter $item2.
29+
function (mixed $item2) {};
30+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ForbidImplicitMixedInClosureParamsRule\Everywhere;
4+
5+
6+
/**
7+
* @param list<string> $a
8+
* @param list<mixed> $b
9+
*/
10+
function test($a, $b, $c, array $d): void
11+
{
12+
array_map(function ($item) {}, [1]); // error: Missing parameter typehint for closure parameter $item.
13+
array_map(function ($item) {}, $a); // error: Missing parameter typehint for closure parameter $item.
14+
array_map(function ($item) {}, $b); // error: Missing parameter typehint for closure parameter $item.
15+
array_map(function ($item) {}, $c); // error: Missing parameter typehint for closure parameter $item.
16+
array_map(function ($item) {}, $d); // error: Missing parameter typehint for closure parameter $item.
17+
array_map(function (int $item) {}, $c);
18+
array_map(function (int $item) {}, $d);
19+
20+
array_map(static fn($item) => 1, [1]); // error: Missing parameter typehint for arrow function parameter $item.
21+
array_map(static fn($item) => 1, $a); // error: Missing parameter typehint for arrow function parameter $item.
22+
array_map(static fn($item) => 1, $b); // error: Missing parameter typehint for arrow function parameter $item.
23+
array_map(static fn($item) => 1, $c); // error: Missing parameter typehint for arrow function parameter $item.
24+
array_map(static fn($item) => 1, $d); // error: Missing parameter typehint for arrow function parameter $item.
25+
array_map(static fn(int $item) => 1, $c);
26+
array_map(static fn(int $item) => 1, $d);
27+
28+
function ($item2) {}; // error: Missing parameter typehint for closure parameter $item2.
29+
function (mixed $item2) {};
30+
}

tests/RuleTestCase.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ protected function analyseFile(string $file): void
3535
* @param list<Error> $actualErrors
3636
* @return list<string>
3737
*/
38-
private function processActualErrors(array $actualErrors): array
38+
protected function processActualErrors(array $actualErrors): array
3939
{
4040
$resultToAssert = [];
4141

0 commit comments

Comments
 (0)