Skip to content

Commit

Permalink
Add rule to check @method template tags don't clash with class templa…
Browse files Browse the repository at this point in the history
…tes, type aliases or existing classes.
  • Loading branch information
mad-briller authored and ondrejmirtes committed Feb 23, 2024
1 parent 58ebacf commit fbc6bca
Show file tree
Hide file tree
Showing 4 changed files with 159 additions and 0 deletions.
1 change: 1 addition & 0 deletions conf/config.level2.neon
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ rules:
- PHPStan\Rules\Generics\InterfaceAncestorsRule
- PHPStan\Rules\Generics\InterfaceTemplateTypeRule
- PHPStan\Rules\Generics\MethodTemplateTypeRule
- PHPStan\Rules\Generics\MethodTagTemplateTypeRule
- PHPStan\Rules\Generics\MethodSignatureVarianceRule
- PHPStan\Rules\Generics\TraitTemplateTypeRule
- PHPStan\Rules\Generics\UsedTraitsRule
Expand Down
83 changes: 83 additions & 0 deletions src/Rules/Generics/MethodTagTemplateTypeRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Generics;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Internal\SprintfHelper;
use PHPStan\Node\InClassNode;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\FileTypeMapper;
use PHPStan\Type\Generic\TemplateTypeScope;
use PHPStan\Type\VerbosityLevel;
use function array_keys;
use function sprintf;

/**
* @implements Rule<InClassNode>
*/
class MethodTagTemplateTypeRule implements Rule
{

public function __construct(
private FileTypeMapper $fileTypeMapper,
private TemplateTypeCheck $templateTypeCheck,
)
{
}

public function getNodeType(): string
{
return InClassNode::class;
}

public function processNode(Node $node, Scope $scope): array
{
$docComment = $node->getDocComment();
if ($docComment === null) {
return [];
}

$classReflection = $node->getClassReflection();
$className = $classReflection->getDisplayName();
$resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
$scope->getFile(),
$classReflection->getName(),
$scope->isInTrait() ? $scope->getTraitReflection()->getName() : null,
null,
$docComment->getText(),
);

$messages = [];
$escapedClassName = SprintfHelper::escapeFormatString($className);
$classTemplateTypes = $classReflection->getTemplateTypeMap()->getTypes();

foreach ($resolvedPhpDoc->getMethodTags() as $methodName => $methodTag) {
$methodTemplateTags = $methodTag->getTemplateTags();
$escapedMethodName = SprintfHelper::escapeFormatString($methodName);

$messages = array_merge($messages, $this->templateTypeCheck->check(
$scope,
$node,
TemplateTypeScope::createWithMethod($className, $methodName),
$methodTemplateTags,
sprintf('PHPDoc tag @method template for method %s::%s() cannot have existing class %%s as its name.', $escapedClassName, $escapedMethodName),
sprintf('PHPDoc tag @method template for method %s::%s() cannot have existing type alias %%s as its name.', $escapedClassName, $escapedMethodName),
sprintf('PHPDoc tag @method template %%s for method %s::%s() has invalid bound type %%s.', $escapedClassName, $escapedMethodName),
sprintf('PHPDoc tag @method template %%s for method %s::%s() with bound type %%s is not supported.', $escapedClassName, $escapedMethodName),
));

foreach (array_keys($methodTemplateTags) as $name) {
if (!isset($classTemplateTypes[$name])) {
continue;
}

$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();
}
}

return $messages;
}

}
60 changes: 60 additions & 0 deletions tests/PHPStan/Rules/Generics/MethodTagTemplateTypeRuleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Generics;

use PHPStan\Rules\ClassCaseSensitivityCheck;
use PHPStan\Rules\ClassForbiddenNameCheck;
use PHPStan\Rules\ClassNameCheck;
use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;
use PHPStan\Type\FileTypeMapper;

/**
* @extends RuleTestCase<MethodTagTemplateTypeRule>
*/
class MethodTagTemplateTypeRuleTest extends RuleTestCase
{

protected function getRule(): Rule
{
$reflectionProvider = $this->createReflectionProvider();
$typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $reflectionProvider);

return new MethodTagTemplateTypeRule(
self::getContainer()->getByType(FileTypeMapper::class),
new TemplateTypeCheck(
$reflectionProvider,
new ClassNameCheck(
new ClassCaseSensitivityCheck($reflectionProvider, true),
new ClassForbiddenNameCheck(),
),
new GenericObjectTypeCheck(),
$typeAliasResolver,
true,
),
);
}

public function testRule(): void
{
$this->analyse([__DIR__ . '/data/method-tag-template.php'], [
[
'PHPDoc tag @method template U for method MethodTagTemplate\HelloWorld::sayHello() has invalid bound type MethodTagTemplate\Nonexisting.',
13,
],
[
'PHPDoc tag @method template for method MethodTagTemplate\HelloWorld::sayHello() cannot have existing class stdClass as its name.',
13,
],
[
'PHPDoc tag @method template T for method MethodTagTemplate\HelloWorld::sayHello() shadows @template T for class MethodTagTemplate\HelloWorld.',
13,
],
[
'PHPDoc tag @method template for method MethodTagTemplate\HelloWorld::typeAlias() cannot have existing type alias TypeAlias as its name.',
13,
],
]);
}

}
15 changes: 15 additions & 0 deletions tests/PHPStan/Rules/Generics/data/method-tag-template.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php declare(strict_types = 1);

namespace MethodTagTemplate;

use stdClass;

/**
* @template T
*
* @method void sayHello<T, U of Nonexisting, stdClass>(T $a, U $b, stdClass $c)
* @method void typeAlias<TypeAlias of mixed>(TypeAlias $a)
*/
class HelloWorld
{
}

0 comments on commit fbc6bca

Please sign in to comment.