diff --git a/build.xml b/build.xml index cd530ab15d..91632acb53 100644 --- a/build.xml +++ b/build.xml @@ -221,7 +221,7 @@ checkreturn="true" > - + diff --git a/build/baseline-7.4.neon b/build/baseline-7.4.neon index 9217c32c19..2db001615e 100644 --- a/build/baseline-7.4.neon +++ b/build/baseline-7.4.neon @@ -93,6 +93,6 @@ parameters: count: 1 path: ../src/Reflection/ReflectionProvider/SetterReflectionProviderProvider.php - - message: "#^Class class@anonymous/src/Testing/TestCase\\.php\\:267 has an uninitialized property \\$reflectionProvider\\. Give it default value or assign it in the constructor\\.$#" + message: "#^Class class@anonymous/src/Testing/TestCase\\.php\\:270 has an uninitialized property \\$reflectionProvider\\. Give it default value or assign it in the constructor\\.$#" count: 1 path: ../src/Testing/TestCase.php diff --git a/conf/config.level0.neon b/conf/config.level0.neon index a615d9401f..728ddc8ec3 100644 --- a/conf/config.level0.neon +++ b/conf/config.level0.neon @@ -200,3 +200,10 @@ services: - class: PHPStan\Rules\Whitespace\FileWhitespaceRule + + - + class: PHPStan\Rules\Classes\LocalTypeAliasesRule + arguments: + globalTypeAliases: %typeAliases% + tags: + - phpstan.rules.rule diff --git a/conf/config.neon b/conf/config.neon index fa8ff89021..0464df88d3 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -366,13 +366,6 @@ services: - class: PHPStan\PhpDoc\ConstExprNodeResolver - - - class: PHPStan\PhpDoc\TypeAlias\TypeAliasesTypeNodeResolverExtension - arguments: - aliases: %typeAliases% - tags: - - phpstan.phpDoc.typeNodeResolverExtension - - class: PHPStan\PhpDoc\TypeNodeResolver @@ -747,7 +740,6 @@ services: class: PHPStan\Rules\Generics\TemplateTypeCheck arguments: checkClassCaseSensitivity: %checkClassCaseSensitivity% - typeAliases: %typeAliases% - class: PHPStan\Rules\Generics\VarianceCheck @@ -802,6 +794,11 @@ services: - class: PHPStan\Type\FileTypeMapper + - + class: PHPStan\Type\TypeAliasResolver + arguments: + globalTypeAliases: %typeAliases% + - class: PHPStan\Type\Php\ArgumentBasedFunctionReturnTypeExtension tags: diff --git a/src/Analyser/NameScope.php b/src/Analyser/NameScope.php index fe18eea6fc..44f2430847 100644 --- a/src/Analyser/NameScope.php +++ b/src/Analyser/NameScope.php @@ -20,18 +20,21 @@ class NameScope private TemplateTypeMap $templateTypeMap; + private bool $bypassTypeAliases; + /** * @param string|null $namespace * @param array $uses alias(string) => fullName(string) * @param string|null $className */ - public function __construct(?string $namespace, array $uses, ?string $className = null, ?string $functionName = null, ?TemplateTypeMap $templateTypeMap = null) + public function __construct(?string $namespace, array $uses, ?string $className = null, ?string $functionName = null, ?TemplateTypeMap $templateTypeMap = null, bool $bypassTypeAliases = false) { $this->namespace = $namespace; $this->uses = $uses; $this->className = $className; $this->functionName = $functionName; $this->templateTypeMap = $templateTypeMap ?? TemplateTypeMap::createEmpty(); + $this->bypassTypeAliases = $bypassTypeAliases; } public function getNamespace(): ?string @@ -137,6 +140,16 @@ public function unsetTemplateType(string $name): self ); } + public function bypassTypeAliases(): self + { + return new self($this->namespace, $this->uses, $this->className, $this->functionName, $this->templateTypeMap, true); + } + + public function shouldBypassTypeAliases(): bool + { + return $this->bypassTypeAliases; + } + /** * @param mixed[] $properties * @return self diff --git a/src/PhpDoc/PhpDocNodeResolver.php b/src/PhpDoc/PhpDocNodeResolver.php index ecc757874e..aa188fe918 100644 --- a/src/PhpDoc/PhpDocNodeResolver.php +++ b/src/PhpDoc/PhpDocNodeResolver.php @@ -14,6 +14,8 @@ use PHPStan\PhpDoc\Tag\ReturnTag; use PHPStan\PhpDoc\Tag\TemplateTag; use PHPStan\PhpDoc\Tag\ThrowsTag; +use PHPStan\PhpDoc\Tag\TypeAliasImportTag; +use PHPStan\PhpDoc\Tag\TypeAliasTag; use PHPStan\PhpDoc\Tag\UsesTag; use PHPStan\PhpDoc\Tag\VarTag; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprNullNode; @@ -365,6 +367,43 @@ public function resolveMixinTags(PhpDocNode $phpDocNode, NameScope $nameScope): }, $phpDocNode->getMixinTagValues()); } + /** + * @return array + */ + public function resolveTypeAliasTags(PhpDocNode $phpDocNode, NameScope $nameScope): array + { + $resolved = []; + + foreach (['@psalm-type', '@phpstan-type'] as $tagName) { + foreach ($phpDocNode->getTypeAliasTagValues($tagName) as $typeAliasTagValue) { + $alias = $typeAliasTagValue->alias; + $typeNode = $typeAliasTagValue->type; + $resolved[$alias] = new TypeAliasTag($alias, $typeNode, $nameScope); + } + } + + return $resolved; + } + + /** + * @return array + */ + public function resolveTypeAliasImportTags(PhpDocNode $phpDocNode, NameScope $nameScope): array + { + $resolved = []; + + foreach (['@psalm-import-type', '@phpstan-import-type'] as $tagName) { + foreach ($phpDocNode->getTypeAliasImportTagValues($tagName) as $typeAliasImportTagValue) { + $importedAlias = $typeAliasImportTagValue->importedAlias; + $importedFrom = $nameScope->resolveStringName($typeAliasImportTagValue->importedFrom->name); + $importedAs = $typeAliasImportTagValue->importedAs; + $resolved[$importedAs ?? $importedAlias] = new TypeAliasImportTag($importedAlias, $importedFrom, $importedAs); + } + } + + return $resolved; + } + public function resolveDeprecatedTag(PhpDocNode $phpDocNode, NameScope $nameScope): ?\PHPStan\PhpDoc\Tag\DeprecatedTag { foreach ($phpDocNode->getDeprecatedTagValues() as $deprecatedTagValue) { diff --git a/src/PhpDoc/ResolvedPhpDocBlock.php b/src/PhpDoc/ResolvedPhpDocBlock.php index ab9e29173a..d40ce8fa7d 100644 --- a/src/PhpDoc/ResolvedPhpDocBlock.php +++ b/src/PhpDoc/ResolvedPhpDocBlock.php @@ -7,6 +7,8 @@ use PHPStan\PhpDoc\Tag\ParamTag; use PHPStan\PhpDoc\Tag\ReturnTag; use PHPStan\PhpDoc\Tag\ThrowsTag; +use PHPStan\PhpDoc\Tag\TypeAliasImportTag; +use PHPStan\PhpDoc\Tag\TypeAliasTag; use PHPStan\PhpDoc\Tag\TypedTag; use PHPStan\PhpDoc\Tag\VarTag; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode; @@ -64,6 +66,12 @@ class ResolvedPhpDocBlock /** @var array|false */ private $mixinTags = false; + /** @var array|false */ + private $typeAliasTags = false; + + /** @var array|false */ + private $typeAliasImportTags = false; + /** @var \PHPStan\PhpDoc\Tag\DeprecatedTag|false|null */ private $deprecatedTag = false; @@ -133,6 +141,8 @@ public static function createEmpty(): self $self->returnTag = null; $self->throwsTag = null; $self->mixinTags = []; + $self->typeAliasTags = []; + $self->typeAliasImportTags = []; $self->deprecatedTag = null; $self->isDeprecated = false; $self->isInternal = false; @@ -176,6 +186,8 @@ public function merge(array $parents, array $parentPhpDocBlocks): self $result->returnTag = self::mergeReturnTags($this->getReturnTag(), $parents, $parentPhpDocBlocks); $result->throwsTag = self::mergeThrowsTags($this->getThrowsTag(), $parents); $result->mixinTags = $this->getMixinTags(); + $result->typeAliasTags = $this->getTypeAliasTags(); + $result->typeAliasImportTags = $this->getTypeAliasImportTags(); $result->deprecatedTag = $this->getDeprecatedTag(); $result->isDeprecated = $result->deprecatedTag !== null; $result->isInternal = $this->isInternal(); @@ -219,6 +231,9 @@ public function changeParameterNamesByMapping(array $parameterNameMapping): self $self->paramTags = $newParamTags; $self->returnTag = $this->returnTag; $self->throwsTag = $this->throwsTag; + $self->mixinTags = $this->mixinTags; + $self->typeAliasTags = $this->typeAliasTags; + $self->typeAliasImportTags = $this->typeAliasImportTags; $self->deprecatedTag = $this->deprecatedTag; $self->isDeprecated = $this->isDeprecated; $self->isInternal = $this->isInternal; @@ -399,6 +414,36 @@ public function getMixinTags(): array return $this->mixinTags; } + /** + * @return array + */ + public function getTypeAliasTags(): array + { + if ($this->typeAliasTags === false) { + $this->typeAliasTags = $this->phpDocNodeResolver->resolveTypeAliasTags( + $this->phpDocNode, + $this->getNameScope() + ); + } + + return $this->typeAliasTags; + } + + /** + * @return array + */ + public function getTypeAliasImportTags(): array + { + if ($this->typeAliasImportTags === false) { + $this->typeAliasImportTags = $this->phpDocNodeResolver->resolveTypeAliasImportTags( + $this->phpDocNode, + $this->getNameScope() + ); + } + + return $this->typeAliasImportTags; + } + public function getDeprecatedTag(): ?\PHPStan\PhpDoc\Tag\DeprecatedTag { if ($this->deprecatedTag === false) { diff --git a/src/PhpDoc/Tag/TypeAliasImportTag.php b/src/PhpDoc/Tag/TypeAliasImportTag.php new file mode 100644 index 0000000000..62fb7cdb77 --- /dev/null +++ b/src/PhpDoc/Tag/TypeAliasImportTag.php @@ -0,0 +1,36 @@ +importedAlias = $importedAlias; + $this->importedFrom = $importedFrom; + $this->importedAs = $importedAs; + } + + public function getImportedAlias(): string + { + return $this->importedAlias; + } + + public function getImportedFrom(): string + { + return $this->importedFrom; + } + + public function getImportedAs(): ?string + { + return $this->importedAs; + } + +} diff --git a/src/PhpDoc/Tag/TypeAliasTag.php b/src/PhpDoc/Tag/TypeAliasTag.php new file mode 100644 index 0000000000..d5ed9646cf --- /dev/null +++ b/src/PhpDoc/Tag/TypeAliasTag.php @@ -0,0 +1,41 @@ +aliasName = $aliasName; + $this->typeNode = $typeNode; + $this->nameScope = $nameScope; + } + + public function getAliasName(): string + { + return $this->aliasName; + } + + public function getTypeAlias(): \PHPStan\Type\TypeAlias + { + return new \PHPStan\Type\TypeAlias( + $this->typeNode, + $this->nameScope + ); + } + +} diff --git a/src/PhpDoc/TypeAlias/TypeAliasesTypeNodeResolverExtension.php b/src/PhpDoc/TypeAlias/TypeAliasesTypeNodeResolverExtension.php deleted file mode 100644 index cb940f021d..0000000000 --- a/src/PhpDoc/TypeAlias/TypeAliasesTypeNodeResolverExtension.php +++ /dev/null @@ -1,78 +0,0 @@ - */ - private array $aliases; - - /** @var array */ - private array $resolvedTypes = []; - - /** @var array */ - private array $inProcess = []; - - /** - * @param TypeStringResolver $typeStringResolver - * @param ReflectionProvider $reflectionProvider - * @param array $aliases - */ - public function __construct( - TypeStringResolver $typeStringResolver, - ReflectionProvider $reflectionProvider, - array $aliases - ) - { - $this->typeStringResolver = $typeStringResolver; - $this->reflectionProvider = $reflectionProvider; - $this->aliases = $aliases; - } - - public function resolve(TypeNode $typeNode, NameScope $nameScope): ?Type - { - if ($typeNode instanceof IdentifierTypeNode) { - $aliasName = $typeNode->name; - if (array_key_exists($aliasName, $this->resolvedTypes)) { - return $this->resolvedTypes[$aliasName]; - } - if (!array_key_exists($aliasName, $this->aliases)) { - return null; - } - - if ($this->reflectionProvider->hasClass($aliasName)) { - throw new \PHPStan\ShouldNotHappenException(sprintf('Type alias %s already exists as a class.', $aliasName)); - } - - if (array_key_exists($aliasName, $this->inProcess)) { - throw new \PHPStan\ShouldNotHappenException(sprintf('Circular definition for type alias %s.', $aliasName)); - } - - $this->inProcess[$aliasName] = true; - - $aliasTypeString = $this->aliases[$aliasName]; - $aliasType = $this->typeStringResolver->resolve($aliasTypeString); - $this->resolvedTypes[$aliasName] = $aliasType; - - unset($this->inProcess[$aliasName]); - - return $aliasType; - } - return null; - } - -} diff --git a/src/PhpDoc/TypeNodeResolver.php b/src/PhpDoc/TypeNodeResolver.php index 96b9d9dc53..2552f50c41 100644 --- a/src/PhpDoc/TypeNodeResolver.php +++ b/src/PhpDoc/TypeNodeResolver.php @@ -60,6 +60,7 @@ use PHPStan\Type\StringType; use PHPStan\Type\ThisType; use PHPStan\Type\Type; +use PHPStan\Type\TypeAliasResolver; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeWithClassName; use PHPStan\Type\UnionType; @@ -293,6 +294,14 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco } } + if (!$nameScope->shouldBypassTypeAliases()) { + $aliasName = $typeNode->name; + $typeAlias = $this->getTypeAliasResolver()->resolveTypeAlias($aliasName, $nameScope); + if ($typeAlias !== null) { + return $typeAlias; + } + } + $templateType = $nameScope->resolveTemplateTypeName($typeNode->name); if ($templateType !== null) { return $templateType; @@ -704,4 +713,9 @@ private function getReflectionProvider(): ReflectionProvider return $this->container->getByType(ReflectionProvider::class); } + private function getTypeAliasResolver(): TypeAliasResolver + { + return $this->container->getByType(TypeAliasResolver::class); + } + } diff --git a/src/Reflection/ClassReflection.php b/src/Reflection/ClassReflection.php index 4952bb1938..a549a55879 100644 --- a/src/Reflection/ClassReflection.php +++ b/src/Reflection/ClassReflection.php @@ -12,6 +12,8 @@ use PHPStan\PhpDoc\Tag\MixinTag; use PHPStan\PhpDoc\Tag\PropertyTag; use PHPStan\PhpDoc\Tag\TemplateTag; +use PHPStan\PhpDoc\Tag\TypeAliasImportTag; +use PHPStan\PhpDoc\Tag\TypeAliasTag; use PHPStan\Reflection\Php\PhpClassReflectionExtension; use PHPStan\Reflection\Php\PhpPropertyReflection; use PHPStan\Type\ErrorType; @@ -22,6 +24,7 @@ use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\Type; +use PHPStan\Type\TypeAlias; use PHPStan\Type\VerbosityLevel; use ReflectionMethod; @@ -98,6 +101,12 @@ class ClassReflection implements ReflectionWithFilename /** @var \PHPStan\Reflection\ClassReflection|false|null */ private $cachedParentClass = null; + /** @var array|null */ + private ?array $typeAliases = null; + + /** @var array */ + private static array $resolvingTypeAliasImports = []; + /** * @param \PHPStan\Reflection\ReflectionProvider $reflectionProvider * @param \PHPStan\Type\FileTypeMapper $fileTypeMapper @@ -752,6 +761,67 @@ private function getTraitNames(): array return $traitNames; } + /** + * @return array + */ + public function getTypeAliases(): array + { + if ($this->typeAliases === null) { + $resolvedPhpDoc = $this->getResolvedPhpDoc(); + if ($resolvedPhpDoc === null) { + return $this->typeAliases = []; + } + + $typeAliasImportTags = $resolvedPhpDoc->getTypeAliasImportTags(); + $typeAliasTags = $resolvedPhpDoc->getTypeAliasTags(); + + // prevent circular imports + if (array_key_exists($this->getName(), self::$resolvingTypeAliasImports)) { + throw new \PHPStan\Type\CircularTypeAliasDefinitionException(); + } + + self::$resolvingTypeAliasImports[$this->getName()] = true; + + $importedAliases = array_map(function (TypeAliasImportTag $typeAliasImportTag): ?TypeAlias { + $importedAlias = $typeAliasImportTag->getImportedAlias(); + $importedFromClassName = $typeAliasImportTag->getImportedFrom(); + + if (!$this->reflectionProvider->hasClass($importedFromClassName)) { + return null; + } + + $importedFromReflection = $this->reflectionProvider->getClass($importedFromClassName); + + try { + $typeAliases = $importedFromReflection->getTypeAliases(); + } catch (\PHPStan\Type\CircularTypeAliasDefinitionException $e) { + return TypeAlias::invalid(); + } + + if (!array_key_exists($importedAlias, $typeAliases)) { + return null; + } + + return $typeAliases[$importedAlias]; + }, $typeAliasImportTags); + + unset(self::$resolvingTypeAliasImports[$this->getName()]); + + $localAliases = array_map(static function (TypeAliasTag $typeAliasTag): TypeAlias { + return $typeAliasTag->getTypeAlias(); + }, $typeAliasTags); + + $this->typeAliases = array_filter( + array_merge($importedAliases, $localAliases), + static function (?TypeAlias $typeAlias): bool { + return $typeAlias !== null; + } + ); + } + + return $this->typeAliases; + } + public function getDeprecatedDescription(): ?string { if ($this->deprecatedDescription === null && $this->isDeprecated()) { diff --git a/src/Reflection/SignatureMap/SignatureMapParser.php b/src/Reflection/SignatureMap/SignatureMapParser.php index 2a4e203d85..66432fda22 100644 --- a/src/Reflection/SignatureMap/SignatureMapParser.php +++ b/src/Reflection/SignatureMap/SignatureMapParser.php @@ -49,7 +49,7 @@ private function getTypeFromString(string $typeString, ?string $className): Type return new MixedType(true); } - return $this->typeStringResolver->resolve($typeString, new NameScope(null, [], $className)); + return $this->typeStringResolver->resolve($typeString, (new NameScope(null, [], $className))->bypassTypeAliases()); } /** diff --git a/src/Rules/Classes/LocalTypeAliasesRule.php b/src/Rules/Classes/LocalTypeAliasesRule.php new file mode 100644 index 0000000000..d87fd12b46 --- /dev/null +++ b/src/Rules/Classes/LocalTypeAliasesRule.php @@ -0,0 +1,138 @@ + + */ +class LocalTypeAliasesRule implements Rule +{ + + /** @var array */ + private array $globalTypeAliases; + + private ReflectionProvider $reflectionProvider; + + private TypeNodeResolver $typeNodeResolver; + + /** + * @param array $globalTypeAliases + */ + public function __construct( + array $globalTypeAliases, + ReflectionProvider $reflectionProvider, + TypeNodeResolver $typeNodeResolver + ) + { + $this->globalTypeAliases = $globalTypeAliases; + $this->reflectionProvider = $reflectionProvider; + $this->typeNodeResolver = $typeNodeResolver; + } + + public function getNodeType(): string + { + return InClassNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $reflection = $node->getClassReflection(); + $phpDoc = $reflection->getResolvedPhpDoc(); + if ($phpDoc === null) { + return []; + } + + $nameScope = $phpDoc->getNullableNameScope(); + $resolveName = static function (string $name) use ($nameScope): string { + if ($nameScope === null) { + return $name; + } + + return $nameScope->resolveStringName($name); + }; + + $errors = []; + $className = $reflection->getName(); + + $importedAliases = []; + + foreach ($phpDoc->getTypeAliasImportTags() as $typeAliasImportTag) { + $aliasName = $typeAliasImportTag->getImportedAs() ?? $typeAliasImportTag->getImportedAlias(); + $importedAlias = $typeAliasImportTag->getImportedAlias(); + $importedFromClassName = $typeAliasImportTag->getImportedFrom(); + + if (!$this->reflectionProvider->hasClass($importedFromClassName)) { + $errors[] = RuleErrorBuilder::message(sprintf('Cannot import type alias %s: class %s does not exist.', $importedAlias, $importedFromClassName))->build(); + continue; + } + + $importedFromReflection = $this->reflectionProvider->getClass($importedFromClassName); + $typeAliases = $importedFromReflection->getTypeAliases(); + + if (!array_key_exists($importedAlias, $typeAliases)) { + $errors[] = RuleErrorBuilder::message(sprintf('Cannot import type alias %s: type alias does not exist in %s.', $importedAlias, $importedFromClassName))->build(); + continue; + } + + if ($this->reflectionProvider->hasClass($resolveName($aliasName))) { + $errors[] = RuleErrorBuilder::message(sprintf('Type alias %s already exists as a class in scope of %s.', $aliasName, $className))->build(); + continue; + } + + if (array_key_exists($aliasName, $this->globalTypeAliases)) { + $errors[] = RuleErrorBuilder::message(sprintf('Type alias %s already exists as a global type alias.', $aliasName))->build(); + continue; + } + + $importedAliases[] = $aliasName; + } + + foreach ($phpDoc->getTypeAliasTags() as $typeAliasTag) { + $aliasName = $typeAliasTag->getAliasName(); + + if (in_array($aliasName, $importedAliases, true)) { + $errors[] = RuleErrorBuilder::message(sprintf('Type alias %s overwrites an imported type alias of the same name.', $aliasName))->build(); + continue; + } + + if ($this->reflectionProvider->hasClass($resolveName($aliasName))) { + $errors[] = RuleErrorBuilder::message(sprintf('Type alias %s already exists as a class in scope of %s.', $aliasName, $className))->build(); + continue; + } + + if (array_key_exists($aliasName, $this->globalTypeAliases)) { + $errors[] = RuleErrorBuilder::message(sprintf('Type alias %s already exists as a global type alias.', $aliasName))->build(); + continue; + } + + $resolvedType = $typeAliasTag->getTypeAlias()->resolve($this->typeNodeResolver); + $foundError = false; + TypeTraverser::map($resolvedType, static function (\PHPStan\Type\Type $type, callable $traverse) use (&$errors, &$foundError, $aliasName): \PHPStan\Type\Type { + if ($foundError) { + return $type; + } + + if ($type instanceof ErrorType) { + $errors[] = RuleErrorBuilder::message(sprintf('Circular definition detected in type alias %s.', $aliasName))->build(); + $foundError = true; + return $type; + } + + return $traverse($type); + }); + } + + return $errors; + } + +} diff --git a/src/Rules/Generics/ClassTemplateTypeRule.php b/src/Rules/Generics/ClassTemplateTypeRule.php index e4b92bff73..ec726ce894 100644 --- a/src/Rules/Generics/ClassTemplateTypeRule.php +++ b/src/Rules/Generics/ClassTemplateTypeRule.php @@ -6,6 +6,7 @@ use PHPStan\Analyser\Scope; use PHPStan\Node\InClassNode; use PHPStan\Rules\Rule; +use PHPStan\Type\Generic\TemplateTypeScope; /** * @implements \PHPStan\Rules\Rule @@ -33,6 +34,7 @@ public function processNode(Node $node, Scope $scope): array return []; } $classReflection = $scope->getClassReflection(); + $className = $classReflection->getName(); if ($classReflection->isAnonymous()) { $displayName = 'anonymous class'; } else { @@ -41,6 +43,7 @@ public function processNode(Node $node, Scope $scope): array return $this->templateTypeCheck->check( $node, + TemplateTypeScope::createWithClass($className), $classReflection->getTemplateTags(), sprintf('PHPDoc tag @template for %s cannot have existing class %%s as its name.', $displayName), sprintf('PHPDoc tag @template for %s cannot have existing type alias %%s as its name.', $displayName), diff --git a/src/Rules/Generics/FunctionTemplateTypeRule.php b/src/Rules/Generics/FunctionTemplateTypeRule.php index 1d7d7bf831..2a6d2bc921 100644 --- a/src/Rules/Generics/FunctionTemplateTypeRule.php +++ b/src/Rules/Generics/FunctionTemplateTypeRule.php @@ -6,6 +6,7 @@ use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; use PHPStan\Type\FileTypeMapper; +use PHPStan\Type\Generic\TemplateTypeScope; /** * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Function_> @@ -53,6 +54,7 @@ public function processNode(Node $node, Scope $scope): array return $this->templateTypeCheck->check( $node, + TemplateTypeScope::createWithFunction($functionName), $resolvedPhpDoc->getTemplateTags(), sprintf('PHPDoc tag @template for function %s() cannot have existing class %%s as its name.', $functionName), sprintf('PHPDoc tag @template for function %s() cannot have existing type alias %%s as its name.', $functionName), diff --git a/src/Rules/Generics/InterfaceTemplateTypeRule.php b/src/Rules/Generics/InterfaceTemplateTypeRule.php index 046de07a51..87f8b8cded 100644 --- a/src/Rules/Generics/InterfaceTemplateTypeRule.php +++ b/src/Rules/Generics/InterfaceTemplateTypeRule.php @@ -6,6 +6,7 @@ use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; use PHPStan\Type\FileTypeMapper; +use PHPStan\Type\Generic\TemplateTypeScope; /** * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Interface_> @@ -53,6 +54,7 @@ public function processNode(Node $node, Scope $scope): array return $this->templateTypeCheck->check( $node, + TemplateTypeScope::createWithClass($interfaceName), $resolvedPhpDoc->getTemplateTags(), sprintf('PHPDoc tag @template for interface %s cannot have existing class %%s as its name.', $interfaceName), sprintf('PHPDoc tag @template for interface %s cannot have existing type alias %%s as its name.', $interfaceName), diff --git a/src/Rules/Generics/MethodTemplateTypeRule.php b/src/Rules/Generics/MethodTemplateTypeRule.php index f9e0ba4255..57ac147a2c 100644 --- a/src/Rules/Generics/MethodTemplateTypeRule.php +++ b/src/Rules/Generics/MethodTemplateTypeRule.php @@ -7,6 +7,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\FileTypeMapper; +use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\VerbosityLevel; /** @@ -58,6 +59,7 @@ public function processNode(Node $node, Scope $scope): array $methodTemplateTags = $resolvedPhpDoc->getTemplateTags(); $messages = $this->templateTypeCheck->check( $node, + TemplateTypeScope::createWithMethod($className, $methodName), $methodTemplateTags, sprintf('PHPDoc tag @template for method %s::%s() cannot have existing class %%s as its name.', $className, $methodName), sprintf('PHPDoc tag @template for method %s::%s() cannot have existing type alias %%s as its name.', $className, $methodName), diff --git a/src/Rules/Generics/TemplateTypeCheck.php b/src/Rules/Generics/TemplateTypeCheck.php index 45bc1ee86e..057623f0da 100644 --- a/src/Rules/Generics/TemplateTypeCheck.php +++ b/src/Rules/Generics/TemplateTypeCheck.php @@ -9,16 +9,17 @@ use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\Generic\TemplateType; +use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\IntegerType; use PHPStan\Type\MixedType; use PHPStan\Type\ObjectType; use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\StringType; use PHPStan\Type\Type; +use PHPStan\Type\TypeAliasResolver; use PHPStan\Type\TypeTraverser; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; -use function array_key_exists; use function array_map; class TemplateTypeCheck @@ -30,40 +31,34 @@ class TemplateTypeCheck private GenericObjectTypeCheck $genericObjectTypeCheck; - /** @var array */ - private array $typeAliases; + private TypeAliasResolver $typeAliasResolver; private bool $checkClassCaseSensitivity; - /** - * @param ReflectionProvider $reflectionProvider - * @param ClassCaseSensitivityCheck $classCaseSensitivityCheck - * @param GenericObjectTypeCheck $genericObjectTypeCheck - * @param array $typeAliases - * @param bool $checkClassCaseSensitivity - */ public function __construct( ReflectionProvider $reflectionProvider, ClassCaseSensitivityCheck $classCaseSensitivityCheck, GenericObjectTypeCheck $genericObjectTypeCheck, - array $typeAliases, + TypeAliasResolver $typeAliasResolver, bool $checkClassCaseSensitivity ) { $this->reflectionProvider = $reflectionProvider; $this->classCaseSensitivityCheck = $classCaseSensitivityCheck; $this->genericObjectTypeCheck = $genericObjectTypeCheck; - $this->typeAliases = $typeAliases; + $this->typeAliasResolver = $typeAliasResolver; $this->checkClassCaseSensitivity = $checkClassCaseSensitivity; } /** * @param \PhpParser\Node $node + * @param TemplateTypeScope $templateTypeScope * @param array $templateTags * @return \PHPStan\Rules\RuleError[] */ public function check( Node $node, + TemplateTypeScope $templateTypeScope, array $templateTags, string $sameTemplateTypeNameAsClassMessage, string $sameTemplateTypeNameAsTypeMessage, @@ -80,7 +75,7 @@ public function check( $templateTagName ))->build(); } - if (array_key_exists($templateTagName, $this->typeAliases)) { + if ($this->typeAliasResolver->hasTypeAlias($templateTagName, $templateTypeScope->getClassName())) { $messages[] = RuleErrorBuilder::message(sprintf( $sameTemplateTypeNameAsTypeMessage, $templateTagName diff --git a/src/Rules/Generics/TraitTemplateTypeRule.php b/src/Rules/Generics/TraitTemplateTypeRule.php index d06ffc9f4e..21b89339cd 100644 --- a/src/Rules/Generics/TraitTemplateTypeRule.php +++ b/src/Rules/Generics/TraitTemplateTypeRule.php @@ -6,6 +6,7 @@ use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; use PHPStan\Type\FileTypeMapper; +use PHPStan\Type\Generic\TemplateTypeScope; /** * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Trait_> @@ -53,6 +54,7 @@ public function processNode(Node $node, Scope $scope): array return $this->templateTypeCheck->check( $node, + TemplateTypeScope::createWithClass($traitName), $resolvedPhpDoc->getTemplateTags(), sprintf('PHPDoc tag @template for trait %s cannot have existing class %%s as its name.', $traitName), sprintf('PHPDoc tag @template for trait %s cannot have existing type alias %%s as its name.', $traitName), diff --git a/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php b/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php index 2d9d1a2fdb..f17d2df84e 100644 --- a/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php +++ b/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php @@ -31,6 +31,8 @@ class InvalidPHPStanDocTagRule implements \PHPStan\Rules\Rule '@phpstan-method', '@phpstan-pure', '@phpstan-impure', + '@phpstan-type', + '@phpstan-import-type', ]; private Lexer $phpDocLexer; diff --git a/src/Testing/TestCase.php b/src/Testing/TestCase.php index fc73b7aed0..1eafaa2b05 100644 --- a/src/Testing/TestCase.php +++ b/src/Testing/TestCase.php @@ -41,6 +41,8 @@ use PHPStan\PhpDoc\PhpDocNodeResolver; use PHPStan\PhpDoc\PhpDocStringResolver; use PHPStan\PhpDoc\StubPhpDocProvider; +use PHPStan\PhpDoc\TypeNodeResolver; +use PHPStan\PhpDoc\TypeStringResolver; use PHPStan\Reflection\Annotations\AnnotationsMethodsClassReflectionExtension; use PHPStan\Reflection\Annotations\AnnotationsPropertiesClassReflectionExtension; use PHPStan\Reflection\BetterReflection\BetterReflectionProvider; @@ -70,6 +72,7 @@ use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Php\SimpleXMLElementClassPropertyReflectionExtension; use PHPStan\Type\Type; +use PHPStan\Type\TypeAliasResolver; abstract class TestCase extends \PHPUnit\Framework\TestCase { @@ -570,6 +573,21 @@ public function createScopeFactory(Broker $broker, TypeSpecifier $typeSpecifier) ); } + /** + * @param array $globalTypeAliases + */ + public function createTypeAliasResolver(array $globalTypeAliases, ReflectionProvider $reflectionProvider): TypeAliasResolver + { + $container = self::getContainer(); + + return new TypeAliasResolver( + $globalTypeAliases, + $container->getByType(TypeStringResolver::class), + $container->getByType(TypeNodeResolver::class), + $reflectionProvider + ); + } + protected function shouldTreatPhpDocTypesAsCertain(): bool { return true; diff --git a/src/Type/CircularTypeAliasDefinitionException.php b/src/Type/CircularTypeAliasDefinitionException.php new file mode 100644 index 0000000000..62a946a020 --- /dev/null +++ b/src/Type/CircularTypeAliasDefinitionException.php @@ -0,0 +1,8 @@ +typeNode = $typeNode; + $this->nameScope = $nameScope; + } + + public static function invalid(): self + { + $self = new self(new IdentifierTypeNode('*ERROR*'), new NameScope(null, [])); + $self->resolvedType = new ErrorType(); + return $self; + } + + public function resolve(TypeNodeResolver $typeNodeResolver): Type + { + if ($this->resolvedType === null) { + $this->resolvedType = $typeNodeResolver->resolve( + $this->typeNode, + $this->nameScope + ); + } + + return $this->resolvedType; + } + +} diff --git a/src/Type/TypeAliasResolver.php b/src/Type/TypeAliasResolver.php new file mode 100644 index 0000000000..a87dbc6f9b --- /dev/null +++ b/src/Type/TypeAliasResolver.php @@ -0,0 +1,164 @@ + */ + private array $globalTypeAliases; + + private TypeStringResolver $typeStringResolver; + + private TypeNodeResolver $typeNodeResolver; + + private ReflectionProvider $reflectionProvider; + + /** @var array */ + private array $resolvedGlobalTypeAliases = []; + + /** @var array */ + private array $resolvedLocalTypeAliases = []; + + /** @var array */ + private array $resolvingClassTypeAliases = []; + + /** @var array */ + private array $inProcess = []; + + /** + * @param array $globalTypeAliases + */ + public function __construct( + array $globalTypeAliases, + TypeStringResolver $typeStringResolver, + TypeNodeResolver $typeNodeResolver, + ReflectionProvider $reflectionProvider + ) + { + $this->globalTypeAliases = $globalTypeAliases; + $this->typeStringResolver = $typeStringResolver; + $this->typeNodeResolver = $typeNodeResolver; + $this->reflectionProvider = $reflectionProvider; + } + + public function hasTypeAlias(string $aliasName, ?string $classNameScope): bool + { + $hasGlobalTypeAlias = array_key_exists($aliasName, $this->globalTypeAliases); + if ($hasGlobalTypeAlias) { + return true; + } + + if ($classNameScope === null || !$this->reflectionProvider->hasClass($classNameScope)) { + return false; + } + + $classReflection = $this->reflectionProvider->getClass($classNameScope); + $localTypeAliases = $classReflection->getTypeAliases(); + return array_key_exists($aliasName, $localTypeAliases); + } + + public function resolveTypeAlias(string $aliasName, NameScope $nameScope): ?Type + { + return $this->resolveLocalTypeAlias($aliasName, $nameScope) + ?? $this->resolveGlobalTypeAlias($aliasName, $nameScope); + } + + private function resolveLocalTypeAlias(string $aliasName, NameScope $nameScope): ?Type + { + $className = $nameScope->getClassName(); + if ($className === null) { + return null; + } + + $aliasNameInClassScope = $className . '::' . $aliasName; + + if (array_key_exists($aliasNameInClassScope, $this->resolvedLocalTypeAliases)) { + return $this->resolvedLocalTypeAliases[$aliasNameInClassScope]; + } + + // prevent infinite recursion + if (array_key_exists($className, $this->resolvingClassTypeAliases)) { + return null; + } + + $this->resolvingClassTypeAliases[$className] = true; + + if (!$this->reflectionProvider->hasClass($className)) { + unset($this->resolvingClassTypeAliases[$className]); + return null; + } + + $classReflection = $this->reflectionProvider->getClass($className); + $localTypeAliases = $classReflection->getTypeAliases(); + + unset($this->resolvingClassTypeAliases[$className]); + + if (!array_key_exists($aliasName, $localTypeAliases)) { + return null; + } + + if ($this->reflectionProvider->hasClass($nameScope->resolveStringName($aliasName))) { + return null; + } + + if (array_key_exists($aliasName, $this->globalTypeAliases)) { + return null; + } + + if (array_key_exists($aliasNameInClassScope, $this->inProcess)) { + // resolve circular reference as ErrorType to make it easier to detect + throw new \PHPStan\Type\CircularTypeAliasDefinitionException(); + } + + $this->inProcess[$aliasNameInClassScope] = true; + + try { + $unresolvedAlias = $localTypeAliases[$aliasName]; + $resolvedAliasType = $unresolvedAlias->resolve($this->typeNodeResolver); + } catch (\PHPStan\Type\CircularTypeAliasDefinitionException $e) { + $resolvedAliasType = new ErrorType(); + } + + $this->resolvedLocalTypeAliases[$aliasNameInClassScope] = $resolvedAliasType; + unset($this->inProcess[$aliasNameInClassScope]); + + return $resolvedAliasType; + } + + private function resolveGlobalTypeAlias(string $aliasName, NameScope $nameScope): ?Type + { + if (!array_key_exists($aliasName, $this->globalTypeAliases)) { + return null; + } + + if (array_key_exists($aliasName, $this->resolvedGlobalTypeAliases)) { + return $this->resolvedGlobalTypeAliases[$aliasName]; + } + + if ($this->reflectionProvider->hasClass($nameScope->resolveStringName($aliasName))) { + throw new \PHPStan\ShouldNotHappenException(sprintf('Type alias %s already exists as a class.', $aliasName)); + } + + if (array_key_exists($aliasName, $this->inProcess)) { + throw new \PHPStan\ShouldNotHappenException(sprintf('Circular definition for type alias %s.', $aliasName)); + } + + $this->inProcess[$aliasName] = true; + + $aliasTypeString = $this->globalTypeAliases[$aliasName]; + $aliasType = $this->typeStringResolver->resolve($aliasTypeString); + $this->resolvedGlobalTypeAliases[$aliasName] = $aliasType; + + unset($this->inProcess[$aliasName]); + + return $aliasType; + } + +} diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index f7c0295663..0b0021d99c 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -10341,6 +10341,10 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/multi-assign.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/generics-reduce-types-first.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4803.php'); + + require_once __DIR__ . '/data/type-aliases.php'; + + yield from $this->gatherAssertTypes(__DIR__ . '/data/type-aliases.php'); } /** @@ -10995,6 +10999,11 @@ private function processFile( ); } + public static function getAdditionalConfigFiles(): array + { + return [__DIR__ . '/typeAliases.neon']; + } + public function dataDeclareStrictTypes(): array { return [ diff --git a/tests/PHPStan/Analyser/data/type-aliases.php b/tests/PHPStan/Analyser/data/type-aliases.php new file mode 100644 index 0000000000..188a8ea6c1 --- /dev/null +++ b/tests/PHPStan/Analyser/data/type-aliases.php @@ -0,0 +1,161 @@ +baz); + assertType('*ERROR*', $this->qux); + } + + } + + /** + * @phpstan-import-type CircularTypeAliasImport1 from Baz + * @phpstan-type CircularTypeAliasImport2 CircularTypeAliasImport1 + */ + class Qux + { + } + + /** + * @phpstan-type LocalTypeAlias callable(string $value): (string|false) + * @phpstan-type NestedLocalTypeAlias LocalTypeAlias[] + * @phpstan-import-type ExportedTypeAlias from Bar as ImportedTypeAlias + * @phpstan-import-type ReexportedTypeAlias from Baz + * @phpstan-type NestedImportedTypeAlias iterable + * @phpstan-import-type ScopedAlias from SubScope\Bar + * @phpstan-import-type ImportedAliasFromNonClass from int + * @phpstan-import-type ImportedAliasFromUnknownClass from UnknownClass + * @phpstan-import-type ImportedUknownAlias from SubScope\Bar + * @phpstan-type Baz never + * @phpstan-type GlobalTypeAlias never + * @phpstan-type RecursiveTypeAlias RecursiveTypeAlias[] + * @phpstan-type CircularTypeAlias1 CircularTypeAlias2 + * @phpstan-type CircularTypeAlias2 CircularTypeAlias1 + * @phpstan-type int ShouldNotHappen + * @property GlobalTypeAlias $globalAliasProperty + * @property LocalTypeAlias $localAliasProperty + * @property ImportedTypeAlias $importedAliasProperty + * @property ReexportedTypeAlias $reexportedAliasProperty + * @property ScopedAlias $scopedAliasProperty + * @property RecursiveTypeAlias $recursiveAliasProperty + * @property CircularTypeAlias1 $circularAliasProperty + */ + class Foo + { + + /** + * @param GlobalTypeAlias $parameter + */ + public function globalAlias($parameter) + { + assertType('int|string', $parameter); + } + + /** + * @param LocalTypeAlias $parameter + */ + public function localAlias($parameter) + { + assertType('callable(string): string|false', $parameter); + } + + /** + * @param NestedLocalTypeAlias $parameter + */ + public function nestedLocalAlias($parameter) + { + assertType('array', $parameter); + } + + /** + * @param ImportedTypeAlias $parameter + */ + public function importedAlias($parameter) + { + assertType('Countable&Traversable', $parameter); + } + + /** + * @param NestedImportedTypeAlias $parameter + */ + public function nestedImportedAlias($parameter) + { + assertType('iterable', $parameter); + } + + /** + * @param ImportedAliasFromNonClass $parameter1 + * @param ImportedAliasFromUnknownClass $parameter2 + * @param ImportedUknownAlias $parameter3 + */ + public function invalidImports($parameter1, $parameter2, $parameter3) + { + assertType('TypeAliasesDataset\ImportedAliasFromNonClass', $parameter1); + assertType('TypeAliasesDataset\ImportedAliasFromUnknownClass', $parameter2); + assertType('TypeAliasesDataset\ImportedUknownAlias', $parameter3); + } + + /** + * @param Baz $parameter + */ + public function conflictingAlias($parameter) + { + assertType('TypeAliasesDataset\Baz', $parameter); + } + + public function __get(string $name) + { + return null; + } + + /** @param int $int */ + public function testIntAlias($int) + { + assertType('int', $int); + } + + } + + assertType('int|string', (new Foo)->globalAliasProperty); + assertType('callable(string): string|false', (new Foo)->localAliasProperty); + assertType('Countable&Traversable', (new Foo)->importedAliasProperty); + assertType('Countable&Traversable', (new Foo)->reexportedAliasProperty); + assertType('TypeAliasesDataset\SubScope\Foo', (new Foo)->scopedAliasProperty); + assertType('*ERROR*', (new Foo)->recursiveAliasProperty); + assertType('*ERROR*', (new Foo)->circularAliasProperty); + +} diff --git a/tests/PHPStan/Analyser/typeAliases.neon b/tests/PHPStan/Analyser/typeAliases.neon new file mode 100644 index 0000000000..9f2daa753a --- /dev/null +++ b/tests/PHPStan/Analyser/typeAliases.neon @@ -0,0 +1,3 @@ +parameters: + typeAliases: + GlobalTypeAlias: int|string diff --git a/tests/PHPStan/Rules/Classes/LocalTypeAliasesRuleTest.php b/tests/PHPStan/Rules/Classes/LocalTypeAliasesRuleTest.php new file mode 100644 index 0000000000..76213ff049 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/LocalTypeAliasesRuleTest.php @@ -0,0 +1,82 @@ + + */ +class LocalTypeAliasesRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new LocalTypeAliasesRule( + ['GlobalTypeAlias' => 'int|string'], + $this->createReflectionProvider(), + self::getContainer()->getByType(TypeNodeResolver::class) + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/local-type-aliases.php'], [ + [ + 'Type alias ExistingClassAlias already exists as a class in scope of LocalTypeAliases\Bar.', + 22, + ], + [ + 'Type alias GlobalTypeAlias already exists as a global type alias.', + 22, + ], + [ + 'Circular definition detected in type alias RecursiveTypeAlias.', + 22, + ], + [ + 'Circular definition detected in type alias CircularTypeAlias1.', + 22, + ], + [ + 'Circular definition detected in type alias CircularTypeAlias2.', + 22, + ], + [ + 'Cannot import type alias ImportedAliasFromNonClass: class LocalTypeAliases\int does not exist.', + 37, + ], + [ + 'Cannot import type alias ImportedAliasFromUnknownClass: class LocalTypeAliases\UnknownClass does not exist.', + 37, + ], + [ + 'Cannot import type alias ImportedUnknownAlias: type alias does not exist in LocalTypeAliases\Foo.', + 37, + ], + [ + 'Type alias ExistingClassAlias already exists as a class in scope of LocalTypeAliases\Baz.', + 37, + ], + [ + 'Type alias GlobalTypeAlias already exists as a global type alias.', + 37, + ], + [ + 'Type alias OverwrittenTypeAlias overwrites an imported type alias of the same name.', + 37, + ], + [ + 'Circular definition detected in type alias CircularTypeAliasImport2.', + 37, + ], + [ + 'Circular definition detected in type alias CircularTypeAliasImport1.', + 45, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Classes/data/local-type-aliases.php b/tests/PHPStan/Rules/Classes/data/local-type-aliases.php new file mode 100644 index 0000000000..3d6c367f14 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/local-type-aliases.php @@ -0,0 +1,47 @@ +createReflectionProvider(); + $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $broker); return new ClassTemplateTypeRule( new TemplateTypeCheck( $broker, new ClassCaseSensitivityCheck($broker), new GenericObjectTypeCheck(), - ['TypeAlias' => 'int'], + $typeAliasResolver, true ) ); @@ -29,6 +30,8 @@ protected function getRule(): Rule public function testRule(): void { + require_once __DIR__ . '/data/class-template.php'; + $this->analyse([__DIR__ . '/data/class-template.php'], [ [ 'PHPDoc tag @template for class ClassTemplateType\Foo cannot have existing class stdClass as its name.', @@ -48,27 +51,35 @@ public function testRule(): void ], [ 'PHPDoc tag @template for class ClassTemplateType\Ipsum cannot have existing type alias TypeAlias as its name.', - 40, + 41, + ], + [ + 'PHPDoc tag @template for class ClassTemplateType\Dolor cannot have existing type alias LocalAlias as its name.', + 53, + ], + [ + 'PHPDoc tag @template for class ClassTemplateType\Dolor cannot have existing type alias ImportedAlias as its name.', + 53, ], [ 'PHPDoc tag @template for anonymous class cannot have existing class stdClass as its name.', - 45, + 58, ], [ 'PHPDoc tag @template T for anonymous class has invalid bound type ClassTemplateType\Zazzzu.', - 50, + 63, ], [ 'PHPDoc tag @template T for anonymous class with bound type float is not supported.', - 55, + 68, ], [ 'Class ClassTemplateType\Baz referenced with incorrect case: ClassTemplateType\baz.', - 60, + 73, ], [ 'PHPDoc tag @template for anonymous class cannot have existing type alias TypeAlias as its name.', - 65, + 78, ], ]); } diff --git a/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php index fa5420ebc7..7f3975cf63 100644 --- a/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php @@ -16,9 +16,11 @@ class FunctionTemplateTypeRuleTest extends RuleTestCase protected function getRule(): Rule { $broker = $this->createReflectionProvider(); + $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $broker); + return new FunctionTemplateTypeRule( self::getContainer()->getByType(FileTypeMapper::class), - new TemplateTypeCheck($broker, new ClassCaseSensitivityCheck($broker), new GenericObjectTypeCheck(), ['TypeAlias' => 'int'], true) + new TemplateTypeCheck($broker, new ClassCaseSensitivityCheck($broker), new GenericObjectTypeCheck(), $typeAliasResolver, true) ); } diff --git a/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php index 06cc10f96f..231e2eb0c1 100644 --- a/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php @@ -16,14 +16,18 @@ class InterfaceTemplateTypeRuleTest extends RuleTestCase protected function getRule(): Rule { $broker = $this->createReflectionProvider(); + $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $broker); + return new InterfaceTemplateTypeRule( self::getContainer()->getByType(FileTypeMapper::class), - new TemplateTypeCheck($broker, new ClassCaseSensitivityCheck($broker), new GenericObjectTypeCheck(), ['TypeAlias' => 'int'], true) + new TemplateTypeCheck($broker, new ClassCaseSensitivityCheck($broker), new GenericObjectTypeCheck(), $typeAliasResolver, true) ); } public function testRule(): void { + require_once __DIR__ . '/data/interface-template.php'; + $this->analyse([__DIR__ . '/data/interface-template.php'], [ [ 'PHPDoc tag @template for interface InterfaceTemplateType\Foo cannot have existing class stdClass as its name.', @@ -39,7 +43,15 @@ public function testRule(): void ], [ 'PHPDoc tag @template for interface InterfaceTemplateType\Lorem cannot have existing type alias TypeAlias as its name.', - 32, + 33, + ], + [ + 'PHPDoc tag @template for interface InterfaceTemplateType\Ipsum cannot have existing type alias LocalAlias as its name.', + 45, + ], + [ + 'PHPDoc tag @template for interface InterfaceTemplateType\Ipsum cannot have existing type alias ImportedAlias as its name.', + 45, ], ]); } diff --git a/tests/PHPStan/Rules/Generics/MethodTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/MethodTemplateTypeRuleTest.php index 585bec3697..d8f2c90422 100644 --- a/tests/PHPStan/Rules/Generics/MethodTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/MethodTemplateTypeRuleTest.php @@ -16,14 +16,18 @@ class MethodTemplateTypeRuleTest extends RuleTestCase protected function getRule(): Rule { $broker = $this->createReflectionProvider(); + $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $broker); + return new MethodTemplateTypeRule( self::getContainer()->getByType(FileTypeMapper::class), - new TemplateTypeCheck($broker, new ClassCaseSensitivityCheck($broker), new GenericObjectTypeCheck(), ['TypeAlias' => 'int'], true) + new TemplateTypeCheck($broker, new ClassCaseSensitivityCheck($broker), new GenericObjectTypeCheck(), $typeAliasResolver, true) ); } public function testRule(): void { + require_once __DIR__ . '/data/method-template.php'; + $this->analyse([__DIR__ . '/data/method-template.php'], [ [ 'PHPDoc tag @template for method MethodTemplateType\Foo::doFoo() cannot have existing class stdClass as its name.', @@ -43,7 +47,15 @@ public function testRule(): void ], [ 'PHPDoc tag @template for method MethodTemplateType\Lorem::doFoo() cannot have existing type alias TypeAlias as its name.', - 63, + 66, + ], + [ + 'PHPDoc tag @template for method MethodTemplateType\Ipsum::doFoo() cannot have existing type alias LocalAlias as its name.', + 85, + ], + [ + 'PHPDoc tag @template for method MethodTemplateType\Ipsum::doFoo() cannot have existing type alias ImportedAlias as its name.', + 85, ], ]); } diff --git a/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php index 19a533f784..5ae1a3b1aa 100644 --- a/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php @@ -16,14 +16,18 @@ class TraitTemplateTypeRuleTest extends RuleTestCase protected function getRule(): Rule { $broker = $this->createReflectionProvider(); + $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $broker); + return new TraitTemplateTypeRule( self::getContainer()->getByType(FileTypeMapper::class), - new TemplateTypeCheck($broker, new ClassCaseSensitivityCheck($broker), new GenericObjectTypeCheck(), ['TypeAlias' => 'int'], true) + new TemplateTypeCheck($broker, new ClassCaseSensitivityCheck($broker), new GenericObjectTypeCheck(), $typeAliasResolver, true) ); } public function testRule(): void { + require_once __DIR__ . '/data/trait-template.php'; + $this->analyse([__DIR__ . '/data/trait-template.php'], [ [ 'PHPDoc tag @template for trait TraitTemplateType\Foo cannot have existing class stdClass as its name.', @@ -39,7 +43,15 @@ public function testRule(): void ], [ 'PHPDoc tag @template for trait TraitTemplateType\Lorem cannot have existing type alias TypeAlias as its name.', - 32, + 33, + ], + [ + 'PHPDoc tag @template for trait TraitTemplateType\Ipsum cannot have existing type alias LocalAlias as its name.', + 45, + ], + [ + 'PHPDoc tag @template for trait TraitTemplateType\Ipsum cannot have existing type alias ImportedAlias as its name.', + 45, ], ]); } diff --git a/tests/PHPStan/Rules/Generics/data/class-template.php b/tests/PHPStan/Rules/Generics/data/class-template.php index 771edc5c00..752983b26a 100644 --- a/tests/PHPStan/Rules/Generics/data/class-template.php +++ b/tests/PHPStan/Rules/Generics/data/class-template.php @@ -35,6 +35,7 @@ class Lorem } /** + * @phpstan-type ExportedAlias string * @template TypeAlias */ class Ipsum @@ -42,6 +43,18 @@ class Ipsum } +/** + * @phpstan-type LocalAlias string + * @phpstan-import-type ExportedAlias from Ipsum as ImportedAlias + * @template LocalAlias + * @template ExportedAlias + * @template ImportedAlias + */ +class Dolor +{ + +} + new /** @template stdClass */ class { diff --git a/tests/PHPStan/Rules/Generics/data/interface-template.php b/tests/PHPStan/Rules/Generics/data/interface-template.php index a29f5dc702..ed7f3ef667 100644 --- a/tests/PHPStan/Rules/Generics/data/interface-template.php +++ b/tests/PHPStan/Rules/Generics/data/interface-template.php @@ -27,6 +27,7 @@ interface Baz } /** + * @phpstan-type ExportedAlias string * @template TypeAlias */ interface Lorem @@ -34,6 +35,18 @@ interface Lorem } +/** + * @phpstan-type LocalAlias string + * @phpstan-import-type ExportedAlias from Lorem as ImportedAlias + * @template LocalAlias + * @template ExportedAlias + * @template ImportedAlias + */ +interface Ipsum +{ + +} + /** @template T */ interface NormalT { diff --git a/tests/PHPStan/Rules/Generics/data/method-template.php b/tests/PHPStan/Rules/Generics/data/method-template.php index a5fb337100..fc6c4c87e2 100644 --- a/tests/PHPStan/Rules/Generics/data/method-template.php +++ b/tests/PHPStan/Rules/Generics/data/method-template.php @@ -54,6 +54,9 @@ public function doFoo() } +/** + * @phpstan-type ExportedAlias string + */ class Lorem { @@ -66,3 +69,22 @@ public function doFoo() } } + +/** + * @phpstan-type LocalAlias string + * @phpstan-import-type ExportedAlias from Lorem as ImportedAlias + */ +class Ipsum +{ + + /** + * @template LocalAlias + * @template ExportedAlias + * @template ImportedAlias + */ + public function doFoo() + { + + } + +} diff --git a/tests/PHPStan/Rules/Generics/data/trait-template.php b/tests/PHPStan/Rules/Generics/data/trait-template.php index c5f3526edd..8870b4dd98 100644 --- a/tests/PHPStan/Rules/Generics/data/trait-template.php +++ b/tests/PHPStan/Rules/Generics/data/trait-template.php @@ -27,9 +27,22 @@ trait Baz } /** + * @phpstan-type ExportedAlias string * @template TypeAlias */ trait Lorem { } + +/** + * @phpstan-type LocalAlias string + * @phpstan-import-type ExportedAlias from Lorem as ImportedAlias + * @template LocalAlias + * @template ExportedAlias + * @template ImportedAlias + */ +trait Ipsum +{ + +}