Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
{
}