From 022657989ded8cc2dc288db05e968432c437e6d8 Mon Sep 17 00:00:00 2001 From: Brad Miller <28307684+mad-briller@users.noreply.github.com> Date: Mon, 26 Feb 2024 18:07:24 +0000 Subject: [PATCH] Add rule to check for existing classes, type aliases and shadowing of generic callables in properties. --- src/PhpDoc/StubValidator.php | 10 ++--- .../PhpDoc/GenericCallableRuleHelper.php | 33 ++++++++------ .../IncompatiblePropertyPhpDocTypeRule.php | 13 ++++++ .../PhpDoc/IncompatiblePhpDocTypeRuleTest.php | 4 ++ ...IncompatiblePropertyPhpDocTypeRuleTest.php | 45 +++++++++++++++++++ .../data/generic-callable-properties.php | 42 +++++++++++++++++ .../data/generic-callables-incompatible.php | 13 +++++- 7 files changed, 138 insertions(+), 22 deletions(-) create mode 100644 tests/PHPStan/Rules/PhpDoc/data/generic-callable-properties.php diff --git a/src/PhpDoc/StubValidator.php b/src/PhpDoc/StubValidator.php index 0f842fe69e..f707c85895 100644 --- a/src/PhpDoc/StubValidator.php +++ b/src/PhpDoc/StubValidator.php @@ -157,6 +157,7 @@ private function getRuleRegistry(Container $container): RuleRegistry $phpVersion = $container->getByType(PhpVersion::class); $localTypeAliasesCheck = $container->getByType(LocalTypeAliasesCheck::class); $phpClassReflectionExtension = $container->getByType(PhpClassReflectionExtension::class); + $genericCallableRuleHelper = $container->getByType(GenericCallableRuleHelper::class); $rules = [ // level 0 @@ -182,13 +183,8 @@ private function getRuleRegistry(Container $container): RuleRegistry new MethodTemplateTypeRule($fileTypeMapper, $templateTypeCheck), new MethodSignatureVarianceRule($varianceCheck), new TraitTemplateTypeRule($fileTypeMapper, $templateTypeCheck), - new IncompatiblePhpDocTypeRule( - $fileTypeMapper, - $genericObjectTypeCheck, - $unresolvableTypeHelper, - $container->getByType(GenericCallableRuleHelper::class), - ), - new IncompatiblePropertyPhpDocTypeRule($genericObjectTypeCheck, $unresolvableTypeHelper), + new IncompatiblePhpDocTypeRule($fileTypeMapper, $genericObjectTypeCheck, $unresolvableTypeHelper, $genericCallableRuleHelper), + new IncompatiblePropertyPhpDocTypeRule($genericObjectTypeCheck, $unresolvableTypeHelper, $genericCallableRuleHelper), new InvalidPhpDocTagValueRule( $container->getByType(Lexer::class), $container->getByType(PhpDocParser::class), diff --git a/src/Rules/PhpDoc/GenericCallableRuleHelper.php b/src/Rules/PhpDoc/GenericCallableRuleHelper.php index bfba65f953..5b6dfb115d 100644 --- a/src/Rules/PhpDoc/GenericCallableRuleHelper.php +++ b/src/Rules/PhpDoc/GenericCallableRuleHelper.php @@ -37,7 +37,7 @@ public function check( Scope $scope, string $location, Type $callableType, - string $functionName, + ?string $functionName, array $functionTemplateTags, ?ClassReflection $classReflection, ): array @@ -64,26 +64,31 @@ public function check( $templateTags = $type->getTemplateTags(); - $functionDescription = sprintf('function %s', $functionName); $classDescription = null; if ($classReflection !== null) { $classDescription = $classReflection->getDisplayName(); - $functionDescription = sprintf('method %s::%s', $classDescription, $functionName); } - foreach (array_keys($functionTemplateTags) as $name) { - if (!isset($templateTags[$name])) { - continue; + if ($functionName !== null) { + $functionDescription = sprintf('function %s', $functionName); + if ($classReflection !== null) { + $functionDescription = sprintf('method %s::%s', $classDescription, $functionName); } - $errors[] = RuleErrorBuilder::message(sprintf( - 'PHPDoc tag %s template %s of %s shadows @template %s for %s.', - $location, - $name, - $typeDescription, - $name, - $functionDescription, - ))->build(); + foreach (array_keys($functionTemplateTags) as $name) { + if (!isset($templateTags[$name])) { + continue; + } + + $errors[] = RuleErrorBuilder::message(sprintf( + 'PHPDoc tag %s template %s of %s shadows @template %s for %s.', + $location, + $name, + $typeDescription, + $name, + $functionDescription, + ))->build(); + } } if ($classReflection !== null) { diff --git a/src/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRule.php b/src/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRule.php index 10fe3f704d..9136769c42 100644 --- a/src/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRule.php +++ b/src/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRule.php @@ -24,6 +24,7 @@ class IncompatiblePropertyPhpDocTypeRule implements Rule public function __construct( private GenericObjectTypeCheck $genericObjectTypeCheck, private UnresolvableTypeHelper $unresolvableTypeHelper, + private GenericCallableRuleHelper $genericCallableRuleHelper, ) { } @@ -93,6 +94,18 @@ public function processNode(Node $node, Scope $scope): array $className = SprintfHelper::escapeFormatString($classReflection->getDisplayName()); $escapedPropertyName = SprintfHelper::escapeFormatString($propertyName); + if ($node->isPromoted() === false) { + $messages = array_merge($messages, $this->genericCallableRuleHelper->check( + $node, + $scope, + '@var', + $phpDocType, + null, + [], + $classReflection, + )); + } + $messages = array_merge($messages, $this->genericObjectTypeCheck->check( $phpDocType, sprintf( diff --git a/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php index 18ee87fc12..b15d53dac5 100644 --- a/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php @@ -417,6 +417,10 @@ public function testGenericCallables(): void 'PHPDoc tag @return template T of Closure(T): T shadows @template T for function GenericCallablesIncompatible\shadowsReturnArray.', 191, ], + [ + 'PHPDoc tag @param for parameter $shadows template T of Closure(T): T shadows @template T for class GenericCallablesIncompatible\Test3.', + 203, + ], ]); } diff --git a/tests/PHPStan/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRuleTest.php index 5f68803d9d..2389784fe0 100644 --- a/tests/PHPStan/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRuleTest.php @@ -2,7 +2,11 @@ namespace PHPStan\Rules\PhpDoc; +use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Generics\GenericObjectTypeCheck; +use PHPStan\Rules\Generics\TemplateTypeCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; @@ -14,9 +18,24 @@ class IncompatiblePropertyPhpDocTypeRuleTest extends RuleTestCase protected function getRule(): Rule { + $reflectionProvider = $this->createReflectionProvider(); + $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $reflectionProvider); + return new IncompatiblePropertyPhpDocTypeRule( new GenericObjectTypeCheck(), new UnresolvableTypeHelper(), + new GenericCallableRuleHelper( + new TemplateTypeCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(), + ), + new GenericObjectTypeCheck(), + $typeAliasResolver, + true, + ), + ), ); } @@ -150,4 +169,30 @@ public function testBug7240(): void $this->analyse([__DIR__ . '/data/bug-7240.php'], []); } + public function testGenericCallables(): void + { + $this->analyse([__DIR__ . '/data/generic-callable-properties.php'], [ + [ + 'PHPDoc tag @var template T of Closure(T): T shadows @template T for class GenericCallableProperties\Test.', + 16, + ], + [ + 'PHPDoc tag @var template of Closure(stdClass): stdClass cannot have existing class stdClass as its name.', + 21, + ], + [ + 'PHPDoc tag @var template of callable(TypeAlias): TypeAlias cannot have existing type alias TypeAlias as its name.', + 26, + ], + [ + 'PHPDoc tag @var template TNull of callable(TNull): TNull with bound type null is not supported.', + 31, + ], + [ + 'PHPDoc tag @var template TInvalid of callable(TInvalid): TInvalid has invalid bound type GenericCallableProperties\Invalid.', + 36, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/PhpDoc/data/generic-callable-properties.php b/tests/PHPStan/Rules/PhpDoc/data/generic-callable-properties.php new file mode 100644 index 0000000000..0e81027bb4 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/generic-callable-properties.php @@ -0,0 +1,42 @@ += 8.0 + +namespace GenericCallableProperties; + +use Closure; +use stdClass; + +/** + * @template T + */ +class Test +{ + /** + * @var Closure(T): T + */ + private Closure $shadows; + + /** + * @var Closure(stdClass): stdClass + */ + private Closure $existingClass; + + /** + * @var callable(TypeAlias): TypeAlias + */ + private $typeAlias; + + /** + * @var callable(TNull): TNull + */ + private $unsupported; + + /** + * @var callable(TInvalid): TInvalid + */ + private $invalid; + + /** + * @param Closure(T): T $notReported + */ + public function __construct(private Closure $notReported) {} +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/generic-callables-incompatible.php b/tests/PHPStan/Rules/PhpDoc/data/generic-callables-incompatible.php index 9f8356c55f..238d822894 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/generic-callables-incompatible.php +++ b/tests/PHPStan/Rules/PhpDoc/data/generic-callables-incompatible.php @@ -1,4 +1,4 @@ - 8.0 namespace GenericCallablesIncompatible; @@ -191,3 +191,14 @@ function shadowsParamOutArray(array &$existingClasses): void function shadowsReturnArray(): array { } + +/** + * @template T + */ +class Test3 +{ + /** + * @param Closure(T): T $shadows + */ + public function __construct(private Closure $shadows) {} +}