Skip to content

Commit fbc6bca

Browse files
mad-brillerondrejmirtes
authored andcommitted
Add rule to check @method template tags don't clash with class templates, type aliases or existing classes.
1 parent 58ebacf commit fbc6bca

File tree

4 files changed

+159
-0
lines changed

4 files changed

+159
-0
lines changed

conf/config.level2.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ rules:
2424
- PHPStan\Rules\Generics\InterfaceAncestorsRule
2525
- PHPStan\Rules\Generics\InterfaceTemplateTypeRule
2626
- PHPStan\Rules\Generics\MethodTemplateTypeRule
27+
- PHPStan\Rules\Generics\MethodTagTemplateTypeRule
2728
- PHPStan\Rules\Generics\MethodSignatureVarianceRule
2829
- PHPStan\Rules\Generics\TraitTemplateTypeRule
2930
- PHPStan\Rules\Generics\UsedTraitsRule
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Generics;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Internal\SprintfHelper;
8+
use PHPStan\Node\InClassNode;
9+
use PHPStan\Rules\Rule;
10+
use PHPStan\Rules\RuleErrorBuilder;
11+
use PHPStan\Type\FileTypeMapper;
12+
use PHPStan\Type\Generic\TemplateTypeScope;
13+
use PHPStan\Type\VerbosityLevel;
14+
use function array_keys;
15+
use function sprintf;
16+
17+
/**
18+
* @implements Rule<InClassNode>
19+
*/
20+
class MethodTagTemplateTypeRule implements Rule
21+
{
22+
23+
public function __construct(
24+
private FileTypeMapper $fileTypeMapper,
25+
private TemplateTypeCheck $templateTypeCheck,
26+
)
27+
{
28+
}
29+
30+
public function getNodeType(): string
31+
{
32+
return InClassNode::class;
33+
}
34+
35+
public function processNode(Node $node, Scope $scope): array
36+
{
37+
$docComment = $node->getDocComment();
38+
if ($docComment === null) {
39+
return [];
40+
}
41+
42+
$classReflection = $node->getClassReflection();
43+
$className = $classReflection->getDisplayName();
44+
$resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
45+
$scope->getFile(),
46+
$classReflection->getName(),
47+
$scope->isInTrait() ? $scope->getTraitReflection()->getName() : null,
48+
null,
49+
$docComment->getText(),
50+
);
51+
52+
$messages = [];
53+
$escapedClassName = SprintfHelper::escapeFormatString($className);
54+
$classTemplateTypes = $classReflection->getTemplateTypeMap()->getTypes();
55+
56+
foreach ($resolvedPhpDoc->getMethodTags() as $methodName => $methodTag) {
57+
$methodTemplateTags = $methodTag->getTemplateTags();
58+
$escapedMethodName = SprintfHelper::escapeFormatString($methodName);
59+
60+
$messages = array_merge($messages, $this->templateTypeCheck->check(
61+
$scope,
62+
$node,
63+
TemplateTypeScope::createWithMethod($className, $methodName),
64+
$methodTemplateTags,
65+
sprintf('PHPDoc tag @method template for method %s::%s() cannot have existing class %%s as its name.', $escapedClassName, $escapedMethodName),
66+
sprintf('PHPDoc tag @method template for method %s::%s() cannot have existing type alias %%s as its name.', $escapedClassName, $escapedMethodName),
67+
sprintf('PHPDoc tag @method template %%s for method %s::%s() has invalid bound type %%s.', $escapedClassName, $escapedMethodName),
68+
sprintf('PHPDoc tag @method template %%s for method %s::%s() with bound type %%s is not supported.', $escapedClassName, $escapedMethodName),
69+
));
70+
71+
foreach (array_keys($methodTemplateTags) as $name) {
72+
if (!isset($classTemplateTypes[$name])) {
73+
continue;
74+
}
75+
76+
$messages[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @method template %s for method %s::%s() shadows @template %s for class %s.', $name, $className, $methodName, $classTemplateTypes[$name]->describe(VerbosityLevel::typeOnly()), $classReflection->getDisplayName(false)))->build();
77+
}
78+
}
79+
80+
return $messages;
81+
}
82+
83+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Generics;
4+
5+
use PHPStan\Rules\ClassCaseSensitivityCheck;
6+
use PHPStan\Rules\ClassForbiddenNameCheck;
7+
use PHPStan\Rules\ClassNameCheck;
8+
use PHPStan\Rules\Rule;
9+
use PHPStan\Testing\RuleTestCase;
10+
use PHPStan\Type\FileTypeMapper;
11+
12+
/**
13+
* @extends RuleTestCase<MethodTagTemplateTypeRule>
14+
*/
15+
class MethodTagTemplateTypeRuleTest extends RuleTestCase
16+
{
17+
18+
protected function getRule(): Rule
19+
{
20+
$reflectionProvider = $this->createReflectionProvider();
21+
$typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $reflectionProvider);
22+
23+
return new MethodTagTemplateTypeRule(
24+
self::getContainer()->getByType(FileTypeMapper::class),
25+
new TemplateTypeCheck(
26+
$reflectionProvider,
27+
new ClassNameCheck(
28+
new ClassCaseSensitivityCheck($reflectionProvider, true),
29+
new ClassForbiddenNameCheck(),
30+
),
31+
new GenericObjectTypeCheck(),
32+
$typeAliasResolver,
33+
true,
34+
),
35+
);
36+
}
37+
38+
public function testRule(): void
39+
{
40+
$this->analyse([__DIR__ . '/data/method-tag-template.php'], [
41+
[
42+
'PHPDoc tag @method template U for method MethodTagTemplate\HelloWorld::sayHello() has invalid bound type MethodTagTemplate\Nonexisting.',
43+
13,
44+
],
45+
[
46+
'PHPDoc tag @method template for method MethodTagTemplate\HelloWorld::sayHello() cannot have existing class stdClass as its name.',
47+
13,
48+
],
49+
[
50+
'PHPDoc tag @method template T for method MethodTagTemplate\HelloWorld::sayHello() shadows @template T for class MethodTagTemplate\HelloWorld.',
51+
13,
52+
],
53+
[
54+
'PHPDoc tag @method template for method MethodTagTemplate\HelloWorld::typeAlias() cannot have existing type alias TypeAlias as its name.',
55+
13,
56+
],
57+
]);
58+
}
59+
60+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace MethodTagTemplate;
4+
5+
use stdClass;
6+
7+
/**
8+
* @template T
9+
*
10+
* @method void sayHello<T, U of Nonexisting, stdClass>(T $a, U $b, stdClass $c)
11+
* @method void typeAlias<TypeAlias of mixed>(TypeAlias $a)
12+
*/
13+
class HelloWorld
14+
{
15+
}

0 commit comments

Comments
 (0)