diff --git a/src/Analyser/NameScope.php b/src/Analyser/NameScope.php index e23497a085..ad0c2bcab7 100644 --- a/src/Analyser/NameScope.php +++ b/src/Analyser/NameScope.php @@ -23,13 +23,15 @@ class NameScope /** @var array */ private array $typeAliasesMap; + private bool $bypassTypeAliases; + /** * @param string|null $namespace * @param array $uses alias(string) => fullName(string) * @param string|null $className * @param array $typeAliasesMap */ - public function __construct(?string $namespace, array $uses, ?string $className = null, ?string $functionName = null, ?TemplateTypeMap $templateTypeMap = null, array $typeAliasesMap = []) + public function __construct(?string $namespace, array $uses, ?string $className = null, ?string $functionName = null, ?TemplateTypeMap $templateTypeMap = null, array $typeAliasesMap = [], bool $bypassTypeAliases = false) { $this->namespace = $namespace; $this->uses = $uses; @@ -37,6 +39,7 @@ public function __construct(?string $namespace, array $uses, ?string $className $this->functionName = $functionName; $this->templateTypeMap = $templateTypeMap ?? TemplateTypeMap::createEmpty(); $this->typeAliasesMap = $typeAliasesMap; + $this->bypassTypeAliases = $bypassTypeAliases; } public function getNamespace(): ?string @@ -148,6 +151,16 @@ public function unsetTemplateType(string $name): self ); } + public function bypassTypeAliases(): self + { + return new self($this->namespace, $this->uses, $this->className, $this->functionName, $this->templateTypeMap, $this->typeAliasesMap, true); + } + + public function shouldBypassTypeAliases(): bool + { + return $this->bypassTypeAliases; + } + public function hasTypeAlias(string $alias): bool { return array_key_exists($alias, $this->typeAliasesMap); diff --git a/src/PhpDoc/TypeNodeResolver.php b/src/PhpDoc/TypeNodeResolver.php index fd38987958..3c6d6ce47e 100644 --- a/src/PhpDoc/TypeNodeResolver.php +++ b/src/PhpDoc/TypeNodeResolver.php @@ -294,9 +294,11 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco } } - $typeAlias = $this->getTypeAliasResolver()->resolveTypeAlias($typeNode->name, $nameScope); - if ($typeAlias !== null) { - return $typeAlias; + if (!$nameScope->shouldBypassTypeAliases()) { + $typeAlias = $this->getTypeAliasResolver()->resolveTypeAlias($typeNode->name, $nameScope); + if ($typeAlias !== null) { + return $typeAlias; + } } $templateType = $nameScope->resolveTemplateTypeName($typeNode->name); diff --git a/src/Rules/Classes/LocalTypeAliasesRule.php b/src/Rules/Classes/LocalTypeAliasesRule.php index d87fd12b46..9773b79dc6 100644 --- a/src/Rules/Classes/LocalTypeAliasesRule.php +++ b/src/Rules/Classes/LocalTypeAliasesRule.php @@ -3,13 +3,17 @@ namespace PHPStan\Rules\Classes; use PhpParser\Node; +use PHPStan\Analyser\NameScope; use PHPStan\Analyser\Scope; use PHPStan\Node\InClassNode; use PHPStan\PhpDoc\TypeNodeResolver; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\ErrorType; +use PHPStan\Type\Generic\TemplateType; +use PHPStan\Type\ObjectType; use PHPStan\Type\TypeTraverser; /** @@ -94,6 +98,12 @@ public function processNode(Node $node, Scope $scope): array continue; } + $importedAs = $typeAliasImportTag->getImportedAs(); + if ($importedAs !== null && !$this->isAliasNameValid($importedAs, $nameScope)) { + $errors[] = RuleErrorBuilder::message(sprintf('Imported type alias %s has an invalid name: %s.', $importedAlias, $importedAs))->build(); + continue; + } + $importedAliases[] = $aliasName; } @@ -115,6 +125,11 @@ public function processNode(Node $node, Scope $scope): array continue; } + if (!$this->isAliasNameValid($aliasName, $nameScope)) { + $errors[] = RuleErrorBuilder::message(sprintf('Type alias has an invalid name: %s.', $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 { @@ -135,4 +150,15 @@ public function processNode(Node $node, Scope $scope): array return $errors; } + private function isAliasNameValid(string $aliasName, ?NameScope $nameScope): bool + { + if ($nameScope === null) { + return true; + } + + $aliasNameResolvedType = $this->typeNodeResolver->resolve(new IdentifierTypeNode($aliasName), $nameScope->bypassTypeAliases()); + return ($aliasNameResolvedType instanceof ObjectType && !in_array($aliasName, ['self', 'parent'], true)) + || $aliasNameResolvedType instanceof TemplateType; // aliases take precedence over type parameters, this is reported by other rules using TemplateTypeCheck + } + } diff --git a/tests/PHPStan/Rules/Classes/LocalTypeAliasesRuleTest.php b/tests/PHPStan/Rules/Classes/LocalTypeAliasesRuleTest.php index 76213ff049..11262e5187 100644 --- a/tests/PHPStan/Rules/Classes/LocalTypeAliasesRuleTest.php +++ b/tests/PHPStan/Rules/Classes/LocalTypeAliasesRuleTest.php @@ -26,55 +26,63 @@ 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, + 23, ], [ 'Type alias GlobalTypeAlias already exists as a global type alias.', - 22, + 23, + ], + [ + 'Type alias has an invalid name: int.', + 23, ], [ 'Circular definition detected in type alias RecursiveTypeAlias.', - 22, + 23, ], [ 'Circular definition detected in type alias CircularTypeAlias1.', - 22, + 23, ], [ 'Circular definition detected in type alias CircularTypeAlias2.', - 22, + 23, ], [ 'Cannot import type alias ImportedAliasFromNonClass: class LocalTypeAliases\int does not exist.', - 37, + 39, ], [ 'Cannot import type alias ImportedAliasFromUnknownClass: class LocalTypeAliases\UnknownClass does not exist.', - 37, + 39, ], [ 'Cannot import type alias ImportedUnknownAlias: type alias does not exist in LocalTypeAliases\Foo.', - 37, + 39, ], [ 'Type alias ExistingClassAlias already exists as a class in scope of LocalTypeAliases\Baz.', - 37, + 39, ], [ 'Type alias GlobalTypeAlias already exists as a global type alias.', - 37, + 39, + ], + [ + 'Imported type alias ExportedTypeAlias has an invalid name: int.', + 39, ], [ 'Type alias OverwrittenTypeAlias overwrites an imported type alias of the same name.', - 37, + 39, ], [ 'Circular definition detected in type alias CircularTypeAliasImport2.', - 37, + 39, ], [ 'Circular definition detected in type alias CircularTypeAliasImport1.', - 45, + 47, ], ]); } diff --git a/tests/PHPStan/Rules/Classes/data/local-type-aliases.php b/tests/PHPStan/Rules/Classes/data/local-type-aliases.php index 3d6c367f14..316a52c323 100644 --- a/tests/PHPStan/Rules/Classes/data/local-type-aliases.php +++ b/tests/PHPStan/Rules/Classes/data/local-type-aliases.php @@ -15,6 +15,7 @@ class Foo * @phpstan-type LocalTypeAlias int * @phpstan-type ExistingClassAlias \stdClass * @phpstan-type GlobalTypeAlias bool + * @phpstan-type int \stdClass * @phpstan-type RecursiveTypeAlias RecursiveTypeAlias[] * @phpstan-type CircularTypeAlias1 CircularTypeAlias2 * @phpstan-type CircularTypeAlias2 CircularTypeAlias1 @@ -30,6 +31,7 @@ class Bar * @phpstan-import-type ExportedTypeAlias from Foo as ExistingClassAlias * @phpstan-import-type ExportedTypeAlias from Foo as GlobalTypeAlias * @phpstan-import-type ExportedTypeAlias from Foo as OverwrittenTypeAlias + * @phpstan-import-type ExportedTypeAlias from Foo as int * @phpstan-type OverwrittenTypeAlias string * @phpstan-import-type CircularTypeAliasImport1 from Qux * @phpstan-type CircularTypeAliasImport2 CircularTypeAliasImport1 @@ -45,3 +47,11 @@ class Baz class Qux { } + +/** + * @phpstan-template T + * @phpstan-type T never + */ +class Generic +{ +}