Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add rule to check @method template tags don't clash with class templates, type aliases or existing classes. #2935

Merged
merged 1 commit into from
Feb 23, 2024
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.
Jump to
Jump to file
Failed to load files.
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) {

Check failure on line 71 in src/Rules/Generics/MethodTagTemplateTypeRule.php

View workflow job for this annotation

GitHub Actions / PHPStan (7.2, ubuntu-latest)

Method PHPStan\Rules\Generics\MethodTagTemplateTypeRule::processNode() should return list<PHPStan\Rules\IdentifierRuleError> but returns list<PHPStan\Rules\RuleError>.

Check failure on line 71 in src/Rules/Generics/MethodTagTemplateTypeRule.php

View workflow job for this annotation

GitHub Actions / PHPStan (7.2, windows-latest)

Method PHPStan\Rules\Generics\MethodTagTemplateTypeRule::processNode() should return list<PHPStan\Rules\IdentifierRuleError> but returns list<PHPStan\Rules\RuleError>.
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;

Check failure on line 80 in src/Rules/Generics/MethodTagTemplateTypeRule.php

View workflow job for this annotation

GitHub Actions / PHPStan with result cache (8.1)

Method PHPStan\Rules\Generics\MethodTagTemplateTypeRule::processNode() should return list<PHPStan\Rules\IdentifierRuleError> but returns list<PHPStan\Rules\RuleError>.

Check failure on line 80 in src/Rules/Generics/MethodTagTemplateTypeRule.php

View workflow job for this annotation

GitHub Actions / PHPStan with result cache (8.2)

Method PHPStan\Rules\Generics\MethodTagTemplateTypeRule::processNode() should return list<PHPStan\Rules\IdentifierRuleError> but returns list<PHPStan\Rules\RuleError>.

Check failure on line 80 in src/Rules/Generics/MethodTagTemplateTypeRule.php

View workflow job for this annotation

GitHub Actions / PHPStan with result cache (8.3)

Method PHPStan\Rules\Generics\MethodTagTemplateTypeRule::processNode() should return list<PHPStan\Rules\IdentifierRuleError> but returns list<PHPStan\Rules\RuleError>.

Check failure on line 80 in src/Rules/Generics/MethodTagTemplateTypeRule.php

View workflow job for this annotation

GitHub Actions / PHPStan (7.4, ubuntu-latest)

Method PHPStan\Rules\Generics\MethodTagTemplateTypeRule::processNode() should return list<PHPStan\Rules\IdentifierRuleError> but returns list<PHPStan\Rules\RuleError>.

Check failure on line 80 in src/Rules/Generics/MethodTagTemplateTypeRule.php

View workflow job for this annotation

GitHub Actions / PHPStan (7.4, windows-latest)

Method PHPStan\Rules\Generics\MethodTagTemplateTypeRule::processNode() should return list<PHPStan\Rules\IdentifierRuleError> but returns list<PHPStan\Rules\RuleError>.

Check failure on line 80 in src/Rules/Generics/MethodTagTemplateTypeRule.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.0, ubuntu-latest)

Method PHPStan\Rules\Generics\MethodTagTemplateTypeRule::processNode() should return list<PHPStan\Rules\IdentifierRuleError> but returns list<PHPStan\Rules\RuleError>.

Check failure on line 80 in src/Rules/Generics/MethodTagTemplateTypeRule.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.0, windows-latest)

Method PHPStan\Rules\Generics\MethodTagTemplateTypeRule::processNode() should return list<PHPStan\Rules\IdentifierRuleError> but returns list<PHPStan\Rules\RuleError>.

Check failure on line 80 in src/Rules/Generics/MethodTagTemplateTypeRule.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.1, ubuntu-latest)

Method PHPStan\Rules\Generics\MethodTagTemplateTypeRule::processNode() should return list<PHPStan\Rules\IdentifierRuleError> but returns list<PHPStan\Rules\RuleError>.

Check failure on line 80 in src/Rules/Generics/MethodTagTemplateTypeRule.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.1, windows-latest)

Method PHPStan\Rules\Generics\MethodTagTemplateTypeRule::processNode() should return list<PHPStan\Rules\IdentifierRuleError> but returns list<PHPStan\Rules\RuleError>.

Check failure on line 80 in src/Rules/Generics/MethodTagTemplateTypeRule.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.2, ubuntu-latest)

Method PHPStan\Rules\Generics\MethodTagTemplateTypeRule::processNode() should return list<PHPStan\Rules\IdentifierRuleError> but returns list<PHPStan\Rules\RuleError>.

Check failure on line 80 in src/Rules/Generics/MethodTagTemplateTypeRule.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.2, windows-latest)

Method PHPStan\Rules\Generics\MethodTagTemplateTypeRule::processNode() should return list<PHPStan\Rules\IdentifierRuleError> but returns list<PHPStan\Rules\RuleError>.

Check failure on line 80 in src/Rules/Generics/MethodTagTemplateTypeRule.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.3, ubuntu-latest)

Method PHPStan\Rules\Generics\MethodTagTemplateTypeRule::processNode() should return list<PHPStan\Rules\IdentifierRuleError> but returns list<PHPStan\Rules\RuleError>.

Check failure on line 80 in src/Rules/Generics/MethodTagTemplateTypeRule.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.3, windows-latest)

Method PHPStan\Rules\Generics\MethodTagTemplateTypeRule::processNode() should return list<PHPStan\Rules\IdentifierRuleError> but returns list<PHPStan\Rules\RuleError>.
}

}
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
{
}