Skip to content

Commit 720a146

Browse files
authored
[rule] Param name to type convention (#187)
1 parent c0ed132 commit 720a146

File tree

9 files changed

+208
-4
lines changed

9 files changed

+208
-4
lines changed

README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,40 @@ parameters:
5353

5454
<br>
5555

56+
### ParamNameToTypeConventionRule
57+
58+
By convention, we can define parameter type by its name. If we know the "userId" is always an `int`, PHPStan can warn us about it and let us know to fill the type.
59+
60+
```yaml
61+
services:
62+
-
63+
class: Symplify\PHPStanRules\Rules\Convention\ParamNameToTypeConventionRule
64+
tags: [phpstan.rules.rule]
65+
arguments:
66+
paramNamesToTypes:
67+
userId: int
68+
```
69+
70+
```php
71+
function run($userId)
72+
{
73+
}
74+
```
75+
76+
:x:
77+
78+
<br>
79+
80+
```php
81+
function run(int $userId)
82+
{
83+
}
84+
```
85+
86+
:+1:
87+
88+
<br>
89+
5690
### CheckRequiredInterfaceInContractNamespaceRule
5791

5892
Interface must be located in "Contract" or "Contracts" namespace

composer.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@
88
"webmozart/assert": "^1.11",
99
"phpstan/phpstan": "^2.1.8",
1010
"nette/utils": "^3.2|^4.0",
11-
"phpstan/phpdoc-parser": "^2.0"
11+
"phpstan/phpdoc-parser": "^2.1"
1212
},
1313
"require-dev": {
14-
"nikic/php-parser": "^5.3",
14+
"nikic/php-parser": "^5.4",
1515
"phpunit/phpunit": "^11.5",
1616
"symfony/framework-bundle": "6.1.*",
17-
"symplify/easy-coding-standard": "^12.5",
17+
"phpecs/phpecs": "^2.0",
1818
"tomasvotruba/class-leak": "^2.0",
19-
"rector/rector": "^2.0.6",
19+
"rector/rector": "^2.0.10",
2020
"phpstan/extension-installer": "^1.4",
2121
"symplify/phpstan-extensions": "^12.0",
2222
"tomasvotruba/unused-public": "^2.0",

src/Enum/RuleIdentifier.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,6 @@ final class RuleIdentifier
7171
public const NO_JUST_PROPERTY_ASSIGN = 'symplify.noJustPropertyAssign';
7272

7373
public const NO_PROTECTED_CLASS_STMT = 'symplify.noProtectedClassStmt';
74+
75+
public const CONVENTION_PARAM_NAME_TO_TYPE = 'symplify.conventionParamNameToType';
7476
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symplify\PHPStanRules\Rules\Convention;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Expr\Variable;
9+
use PhpParser\Node\Param;
10+
use PHPStan\Analyser\Scope;
11+
use PHPStan\Rules\Rule;
12+
use PHPStan\Rules\RuleError;
13+
use PHPStan\Rules\RuleErrorBuilder;
14+
use Symplify\PHPStanRules\Enum\RuleIdentifier;
15+
use Webmozart\Assert\Assert;
16+
17+
/**
18+
* @implements Rule<Param>
19+
* @see \Symplify\PHPStanRules\Tests\Rules\Convention\ParamNameToTypeConventionRule\ParamNameToTypeConventionRuleTest
20+
*/
21+
final class ParamNameToTypeConventionRule implements Rule
22+
{
23+
/**
24+
* @var string
25+
*/
26+
public const ERROR_MESSAGE = 'Parameter name "$%s" should probably have "%s" type';
27+
28+
/**
29+
* @param array<string, string> $paramNamesToTypes
30+
*/
31+
public function __construct(
32+
private array $paramNamesToTypes
33+
) {
34+
Assert::notEmpty($paramNamesToTypes);
35+
36+
Assert::allString(array_keys($paramNamesToTypes));
37+
Assert::allString($paramNamesToTypes);
38+
}
39+
40+
public function getNodeType(): string
41+
{
42+
return Param::class;
43+
}
44+
45+
/**
46+
* @param Param $node
47+
* @return RuleError[]
48+
*/
49+
public function processNode(Node $node, Scope $scope): array
50+
{
51+
// param type is known, let's skip it
52+
if ($node->type instanceof Node) {
53+
return [];
54+
}
55+
56+
// unable to fill the type
57+
if ($node->variadic) {
58+
return [];
59+
}
60+
61+
if (! $node->var instanceof Variable) {
62+
return [];
63+
}
64+
65+
if (! is_string($node->var->name)) {
66+
return [];
67+
}
68+
69+
$variableName = $node->var->name;
70+
71+
$expectedType = $this->paramNamesToTypes[$variableName] ?? null;
72+
if ($expectedType === null) {
73+
return [];
74+
}
75+
76+
$errorMessage = sprintf(self::ERROR_MESSAGE, $variableName, $expectedType);
77+
78+
$identifierRuleError = RuleErrorBuilder::message($errorMessage)
79+
->identifier(RuleIdentifier::CONVENTION_PARAM_NAME_TO_TYPE)
80+
->build();
81+
82+
return [$identifierRuleError];
83+
}
84+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace Symplify\PHPStanRules\Tests\Rules\Convention\ParamNameToTypeConventionRule\Fixture;
4+
5+
final class SkipAlreadyTyped
6+
{
7+
public function run(int $userId)
8+
{
9+
}
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace Symplify\PHPStanRules\Tests\Rules\Convention\ParamNameToTypeConventionRule\Fixture;
4+
5+
final class SkipVariadic
6+
{
7+
public function run(...$userId)
8+
{
9+
}
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace Symplify\PHPStanRules\Tests\Rules\Convention\ParamNameToTypeConventionRule\Fixture;
4+
5+
final class SomeUntypedParam
6+
{
7+
public function run($userId)
8+
{
9+
}
10+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symplify\PHPStanRules\Tests\Rules\Convention\ParamNameToTypeConventionRule;
6+
7+
use Iterator;
8+
use PHPStan\Rules\Rule;
9+
use PHPStan\Testing\RuleTestCase;
10+
use PHPUnit\Framework\Attributes\DataProvider;
11+
use Symplify\PHPStanRules\Rules\Convention\ParamNameToTypeConventionRule;
12+
13+
final class ParamNameToTypeConventionRuleTest extends RuleTestCase
14+
{
15+
/**
16+
* @param mixed[] $expectedErrorsWithLines
17+
*/
18+
#[DataProvider('provideData')]
19+
public function testRule(string $filePath, array $expectedErrorsWithLines): void
20+
{
21+
$this->analyse([$filePath], $expectedErrorsWithLines);
22+
}
23+
24+
public static function provideData(): Iterator
25+
{
26+
$errorMessage = sprintf(ParamNameToTypeConventionRule::ERROR_MESSAGE, 'userId', 'int');
27+
28+
yield [__DIR__ . '/Fixture/SomeUntypedParam.php', [
29+
[$errorMessage, 7],
30+
]];
31+
32+
yield [__DIR__ . '/Fixture/SkipVariadic.php', []];
33+
yield [__DIR__ . '/Fixture/SkipAlreadyTyped.php', []];
34+
}
35+
36+
/**
37+
* @return string[]
38+
*/
39+
public static function getAdditionalConfigFiles(): array
40+
{
41+
return [__DIR__ . '/config/configured_rule.neon'];
42+
}
43+
44+
protected function getRule(): Rule
45+
{
46+
return self::getContainer()->getByType(ParamNameToTypeConventionRule::class);
47+
}
48+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
services:
2+
-
3+
class: Symplify\PHPStanRules\Rules\Convention\ParamNameToTypeConventionRule
4+
arguments:
5+
paramNamesToTypes:
6+
userId: int

0 commit comments

Comments
 (0)