diff --git a/conf/config.level2.neon b/conf/config.level2.neon index 40f4384efc..bb27e9271e 100644 --- a/conf/config.level2.neon +++ b/conf/config.level2.neon @@ -21,6 +21,7 @@ rules: - PHPStan\Rules\Generics\MethodTemplateTypeRule - PHPStan\Rules\Generics\MethodSignatureVarianceRule - PHPStan\Rules\Generics\TraitTemplateTypeRule + - PHPStan\Rules\Generics\UsedTraitsRule - PHPStan\Rules\Methods\IncompatibleDefaultParameterTypeRule - PHPStan\Rules\Operators\InvalidBinaryOperationRule - PHPStan\Rules\Operators\InvalidUnaryOperationRule diff --git a/src/Rules/Generics/GenericAncestorsCheck.php b/src/Rules/Generics/GenericAncestorsCheck.php index 928156ede0..9193000148 100644 --- a/src/Rules/Generics/GenericAncestorsCheck.php +++ b/src/Rules/Generics/GenericAncestorsCheck.php @@ -90,10 +90,7 @@ public function check( $messages = array_merge($messages, $genericObjectTypeCheckMessages); foreach ($ancestorType->getReferencedClasses() as $referencedClass) { - if ( - $this->reflectionProvider->hasClass($referencedClass) - && !$this->reflectionProvider->getClass($referencedClass)->isTrait() - ) { + if ($this->reflectionProvider->hasClass($referencedClass)) { continue; } diff --git a/src/Rules/Generics/UsedTraitsRule.php b/src/Rules/Generics/UsedTraitsRule.php new file mode 100644 index 0000000000..9e102bf341 --- /dev/null +++ b/src/Rules/Generics/UsedTraitsRule.php @@ -0,0 +1,85 @@ + + */ +class UsedTraitsRule implements Rule +{ + + private \PHPStan\Type\FileTypeMapper $fileTypeMapper; + + private \PHPStan\Rules\Generics\GenericAncestorsCheck $genericAncestorsCheck; + + public function __construct( + FileTypeMapper $fileTypeMapper, + GenericAncestorsCheck $genericAncestorsCheck + ) + { + $this->fileTypeMapper = $fileTypeMapper; + $this->genericAncestorsCheck = $genericAncestorsCheck; + } + + public function getNodeType(): string + { + return Node\Stmt\TraitUse::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$scope->isInClass()) { + throw new \PHPStan\ShouldNotHappenException(); + } + + $className = $scope->getClassReflection()->getName(); + $traitName = null; + if ($scope->isInTrait()) { + $traitName = $scope->getTraitReflection()->getName(); + } + $useTags = []; + $docComment = $node->getDocComment(); + if ($docComment !== null) { + $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( + $scope->getFile(), + $className, + $traitName, + null, + $docComment->getText() + ); + $useTags = $resolvedPhpDoc->getUsesTags(); + } + + $description = sprintf('class %s', $className); + $typeDescription = 'class'; + if ($traitName !== null) { + $description = sprintf('trait %s', $traitName); + $typeDescription = 'trait'; + } + + return $this->genericAncestorsCheck->check( + $node->traits, + array_map(static function (UsesTag $tag): Type { + return $tag->getType(); + }, $useTags), + sprintf('%s @use tag contains incompatible type %%s.', ucfirst($description)), + sprintf('%s has @use tag, but does not use any trait.', ucfirst($description)), + sprintf('The @use tag of %s describes %%s but the %s uses %%s.', $description, $typeDescription), + 'PHPDoc tag @use contains generic type %s but trait %s is not generic.', + 'Generic type %s in PHPDoc tag @use does not specify all template types of trait %s: %s', + 'Generic type %s in PHPDoc tag @use specifies %d template types, but trait %s supports only %d: %s', + 'Type %s in generic type %s in PHPDoc tag @use is not subtype of template type %s of trait %s.', + 'PHPDoc tag @use has invalid type %s.', + sprintf('%s uses generic trait %%s but does not specify its types: %%s', ucfirst($description)), + sprintf('in used type %%s of %s', $description) + ); + } + +} diff --git a/src/Type/FileTypeMapper.php b/src/Type/FileTypeMapper.php index 6446298d34..5a345037a1 100644 --- a/src/Type/FileTypeMapper.php +++ b/src/Type/FileTypeMapper.php @@ -16,8 +16,10 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\InvalidTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode; use PHPStan\Reflection\ReflectionProvider\ReflectionProviderProvider; +use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeFactory; +use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateTypeMap; use function array_key_exists; use function file_exists; @@ -214,7 +216,7 @@ private function shouldPhpDocNodeBeCachedToDisk(PhpDocNode $phpDocNode): bool private function getResolvedPhpDocMap(string $fileName): array { if (!isset($this->memoryCache[$fileName])) { - $cacheKey = sprintf('%s-phpdocstring-v6-generic-bound', $fileName); + $cacheKey = sprintf('%s-phpdocstring-v7-generic-traits', $fileName); $variableCacheKey = implode(',', array_map(static function (array $file): string { return sprintf('%s-%d', $file['filename'], $file['modifiedTime']); }, $this->getCachedDependentFilesWithTimestamps($fileName))); @@ -313,56 +315,7 @@ function (\PhpParser\Node $node) use ($fileName, $lookForTrait, $traitMethodAlia $resolvableTemplateTypes = true; } } elseif ($node instanceof Node\Stmt\TraitUse) { - $traitMethodAliases = []; - foreach ($node->adaptations as $traitUseAdaptation) { - if (!$traitUseAdaptation instanceof Node\Stmt\TraitUseAdaptation\Alias) { - continue; - } - - if ($traitUseAdaptation->trait === null) { - continue; - } - - if ($traitUseAdaptation->newName === null) { - continue; - } - - $traitMethodAliases[$traitUseAdaptation->trait->toString()][$traitUseAdaptation->method->toString()] = $traitUseAdaptation->newName->toString(); - } - - foreach ($node->traits as $traitName) { - /** @var class-string $traitName */ - $traitName = (string) $traitName; - $reflectionProvider = $this->reflectionProviderProvider->getReflectionProvider(); - if (!$reflectionProvider->hasClass($traitName)) { - continue; - } - - $traitReflection = $reflectionProvider->getClass($traitName); - if (!$traitReflection->isTrait()) { - continue; - } - if ($traitReflection->getFileName() === false) { - continue; - } - if (!file_exists($traitReflection->getFileName())) { - continue; - } - - $className = $classStack[count($classStack) - 1] ?? null; - if ($className === null) { - throw new \PHPStan\ShouldNotHappenException(); - } - - $traitPhpDocMap = $this->createFilePhpDocMap( - $traitReflection->getFileName(), - $traitName, - $className, - $traitMethodAliases[$traitName] ?? [] - ); - $phpDocMap = array_merge($phpDocMap, $traitPhpDocMap); - } - return null; + $resolvableTemplateTypes = true; } elseif ($node instanceof Node\Stmt\ClassMethod) { $functionName = $node->name->name; if (array_key_exists($functionName, $traitMethodAliases)) { @@ -431,10 +384,6 @@ function (\PhpParser\Node $node) use ($fileName, $lookForTrait, $traitMethodAlia } $typeMapStack[] = function () use ($fileName, $className, $lookForTrait, $functionName, $phpDocString, $typeMapCb): TemplateTypeMap { - static $typeMap = null; - if ($typeMap !== null) { - return $typeMap; - } $resolvedPhpDoc = $this->getResolvedPhpDoc( $fileName, $className, @@ -466,6 +415,113 @@ function (\PhpParser\Node $node) use ($fileName, $lookForTrait, $traitMethodAlia $uses[strtolower($use->getAlias()->name)] = sprintf('%s\\%s', $prefix, (string) $use->name); } + } elseif ($node instanceof Node\Stmt\TraitUse) { + $traitMethodAliases = []; + foreach ($node->adaptations as $traitUseAdaptation) { + if (!$traitUseAdaptation instanceof Node\Stmt\TraitUseAdaptation\Alias) { + continue; + } + + if ($traitUseAdaptation->trait === null) { + continue; + } + + if ($traitUseAdaptation->newName === null) { + continue; + } + + $traitMethodAliases[$traitUseAdaptation->trait->toString()][$traitUseAdaptation->method->toString()] = $traitUseAdaptation->newName->toString(); + } + + $useDocComment = null; + if ($node->getDocComment() !== null) { + $useDocComment = $node->getDocComment()->getText(); + } + + foreach ($node->traits as $traitName) { + /** @var class-string $traitName */ + $traitName = (string) $traitName; + $reflectionProvider = $this->reflectionProviderProvider->getReflectionProvider(); + if (!$reflectionProvider->hasClass($traitName)) { + continue; + } + + $traitReflection = $reflectionProvider->getClass($traitName); + if (!$traitReflection->isTrait()) { + continue; + } + if ($traitReflection->getFileName() === false) { + continue; + } + if (!file_exists($traitReflection->getFileName())) { + continue; + } + + $className = $classStack[count($classStack) - 1] ?? null; + if ($className === null) { + throw new \PHPStan\ShouldNotHappenException(); + } + + $traitPhpDocMap = $this->createFilePhpDocMap( + $traitReflection->getFileName(), + $traitName, + $className, + $traitMethodAliases[$traitName] ?? [] + ); + $finalTraitPhpDocMap = []; + foreach ($traitPhpDocMap as $phpDocKey => $callback) { + $finalTraitPhpDocMap[$phpDocKey] = function () use ($callback, $traitReflection, $fileName, $className, $lookForTrait, $useDocComment): NameScopedPhpDocString { + /** @var NameScopedPhpDocString $original */ + $original = $callback(); + if (!$traitReflection->isGeneric()) { + return $original; + } + + $traitTemplateTypeMap = $traitReflection->getTemplateTypeMap(); + + $useType = null; + if ($useDocComment !== null) { + $useTags = $this->getResolvedPhpDoc( + $fileName, + $className, + $lookForTrait, + null, + $useDocComment + )->getUsesTags(); + foreach ($useTags as $useTag) { + $useTagType = $useTag->getType(); + if (!$useTagType instanceof GenericObjectType) { + continue; + } + + if ($useTagType->getClassName() !== $traitReflection->getName()) { + continue; + } + + $useType = $useTagType; + break; + } + } + + if ($useType === null) { + return new NameScopedPhpDocString( + $original->getPhpDocString(), + $original->getNameScope()->withTemplateTypeMap($traitTemplateTypeMap->resolveToBounds()) + ); + } + + $transformedTraitTypeMap = $traitReflection->typeMapFromList($useType->getTypes()); + + return new NameScopedPhpDocString( + $original->getPhpDocString(), + $original->getNameScope()->withTemplateTypeMap($traitTemplateTypeMap->map(static function (string $name, Type $type) use ($transformedTraitTypeMap): Type { + return TemplateTypeHelper::resolveTemplateTypes($type, $transformedTraitTypeMap); + })) + ); + }; + } + $phpDocMap = array_merge($phpDocMap, $finalTraitPhpDocMap); + } } return null; diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 0268691048..34afc5e446 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -5636,6 +5636,16 @@ public function dataPseudoTypeGlobal(): array return $this->gatherAssertTypes(__DIR__ . '/data/phpdoc-pseudotype-global.php'); } + public function dataGenericTraits(): array + { + return $this->gatherAssertTypes(__DIR__ . '/data/generic-traits.php'); + } + + public function dataBug4423(): array + { + return $this->gatherAssertTypes(__DIR__ . '/data/bug-4423.php'); + } + /** * @dataProvider dataArrayFunctions * @param string $description @@ -11246,6 +11256,8 @@ private function gatherAssertTypes(string $file): array * @dataProvider dataPseudoTypeGlobal * @dataProvider dataPseudoTypeNamespace * @dataProvider dataPseudoTypeOverrides + * @dataProvider dataGenericTraits + * @dataProvider dataBug4423 * @param string $assertType * @param string $file * @param mixed ...$args diff --git a/tests/PHPStan/Analyser/data/bug-4423.php b/tests/PHPStan/Analyser/data/bug-4423.php new file mode 100644 index 0000000000..a1ecd4785d --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-4423.php @@ -0,0 +1,64 @@ + $bar + * @method Bar doBar() + */ +trait Foo { + + /** @var Bar */ + public $baz; + + /** @param K $k */ + public function doFoo($k) + { + assertType('T (class Bug4423\Child, argument)', $k); + assertType('Bug4423\Bar', $this->bar); + assertType('Bug4423\Bar', $this->baz); + assertType('Bug4423\Bar', $this->doBar()); + assertType('Bug4423\Bar', $this->doBaz()); + } + + /** @return Bar */ + public function doBaz() + { + + } + +} + +/** + * @template T + * @template K + */ +class Base { + +} + +/** + * @template T + * @extends Base + */ +class Child extends Base { + /** @phpstan-use Foo */ + use Foo; +} + +function (Child $child): void { + /** @var Child $child */ + assertType('Bug4423\Child', $child); + assertType('Bug4423\Bar', $child->bar); + assertType('Bug4423\Bar', $child->baz); + assertType('Bug4423\Bar', $child->doBar()); + assertType('Bug4423\Bar', $child->doBaz()); +}; diff --git a/tests/PHPStan/Analyser/data/generic-traits.php b/tests/PHPStan/Analyser/data/generic-traits.php new file mode 100644 index 0000000000..a0a7dcfc57 --- /dev/null +++ b/tests/PHPStan/Analyser/data/generic-traits.php @@ -0,0 +1,226 @@ +doFoo(1)); + } + +} + +/** @template T of object */ +trait BarTrait +{ + + /** + * @param T $t + * @return T + */ + public function doFoo($t) + { + assertType('object', $t); + } + +} + +/** @template T */ +class Bar +{ + + use BarTrait; + + public function doBar(): void + { + assertType('object', $this->doFoo()); + } + +} + +/** @template T of object */ +trait Bar2Trait +{ + + /** + * @param T $t + * @return T + */ + public function doFoo($t) + { + assertType('object', $t); + } + +} + +/** @template U */ +class Bar2 +{ + + use Bar2Trait; + + public function doBar(): void + { + assertType('object', $this->doFoo()); + } + +} + +/** @template T of object */ +trait Bar3Trait +{ + + /** + * @param T $t + * @return T + */ + public function doFoo($t) + { + assertType('stdClass', $t); + } + +} + +class Bar3 +{ + + /** @use Bar3Trait<\stdClass> */ + use Bar3Trait; + + public function doBar(): void + { + assertType('stdClass', $this->doFoo()); + } + +} + +/** @template T of object */ +trait Bar4Trait +{ + + /** + * @param T $t + * @return T + */ + public function doFoo($t) + { + assertType('U (class GenericTraits\Bar4, argument)', $t); + } + +} + +/** @template U */ +class Bar4 +{ + + /** @use Bar4Trait */ + use Bar4Trait; + + public function doBar(): void + { + assertType('U (class GenericTraits\Bar4, argument)', $this->doFoo()); + } + +} + +/** @template T of object */ +trait Bar5Trait +{ + + /** + * @param T $t + * @return T + */ + public function doFoo($t) + { + assertType('T (class GenericTraits\Bar5, argument)', $t); + } + +} + +/** @template T */ +class Bar5 +{ + + /** @use Bar5Trait */ + use Bar5Trait; + + public function doBar(): void + { + assertType('T (class GenericTraits\Bar5, argument)', $this->doFoo()); + } + + // sanity checks below (is T supposed to be an argument? yes) + + /** + * @param T $t + */ + public function doBaz($t) + { + assertType('T (class GenericTraits\Bar5, argument)', $t); + } + + /** + * @return T + */ + public function returnT() + { + + } + + public function doLorem() + { + assertType('T (class GenericTraits\Bar5, argument)', $this->returnT()); + } + +} + +/** @template T */ +trait Bar6Trait +{ + + /** @param T $t */ + public function doFoo($t) + { + assertType('int', $t); + } + +} + +/** @template U */ +trait Bar7Trait +{ + + /** @use Bar6Trait */ + use Bar6Trait; + +} + +class Bar7 +{ + + /** @use Bar7Trait */ + use Bar7Trait; + +} diff --git a/tests/PHPStan/Rules/Generics/UsedTraitsRuleTest.php b/tests/PHPStan/Rules/Generics/UsedTraitsRuleTest.php new file mode 100644 index 0000000000..340d663e34 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/UsedTraitsRuleTest.php @@ -0,0 +1,60 @@ + + */ +class UsedTraitsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new UsedTraitsRule( + self::getContainer()->getByType(FileTypeMapper::class), + new GenericAncestorsCheck( + $this->createReflectionProvider(), + new GenericObjectTypeCheck(), + new VarianceCheck(), + true + ) + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/used-traits.php'], [ + [ + 'PHPDoc tag @use contains generic type UsedTraits\NongenericTrait but trait UsedTraits\NongenericTrait is not generic.', + 20, + ], + [ + 'Type int in generic type UsedTraits\GenericTrait in PHPDoc tag @use is not subtype of template type T of object of trait UsedTraits\GenericTrait.', + 31, + ], + [ + 'Class UsedTraits\Baz uses generic trait UsedTraits\GenericTrait but does not specify its types: T', + 38, + 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', + ], + [ + 'Generic type UsedTraits\GenericTrait in PHPDoc tag @use specifies 2 template types, but trait UsedTraits\GenericTrait supports only 1: T', + 46, + ], + [ + 'The @use tag of trait UsedTraits\NestedTrait describes UsedTraits\NongenericTrait but the trait uses UsedTraits\GenericTrait.', + 54, + ], + [ + 'Trait UsedTraits\NestedTrait uses generic trait UsedTraits\GenericTrait but does not specify its types: T', + 54, + 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Generics/data/used-traits.php b/tests/PHPStan/Rules/Generics/data/used-traits.php new file mode 100644 index 0000000000..855d38aa02 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/used-traits.php @@ -0,0 +1,63 @@ + */ + use NongenericTrait; + + /** @use GenericTrait<\stdClass> */ + use GenericTrait; + +} + +class Bar +{ + + /** @use GenericTrait */ + use GenericTrait; + +} + +class Baz +{ + + use GenericTrait; + +} + +class Lorem +{ + + /** @use GenericTrait<\stdClass, \Exception> */ + use GenericTrait; + +} + +trait NestedTrait +{ + + /** @use NongenericTrait */ + use GenericTrait; + +} + +class Ipsum +{ + + use NestedTrait; + +}