From e90d916150e6f76eb2524ea2d1cdc9a7fb228e7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pudil?= Date: Wed, 30 Dec 2020 16:03:50 +0100 Subject: [PATCH 01/25] type aliases: extract TypeAliasResolver --- conf/config.neon | 8 +- .../TypeAliasesTypeNodeResolverExtension.php | 55 ++------------ src/Rules/Generics/TemplateTypeCheck.php | 13 ++-- src/Testing/TestCase.php | 16 ++++ src/Type/TypeAliasResolver.php | 74 +++++++++++++++++++ .../Generics/ClassTemplateTypeRuleTest.php | 6 +- .../Generics/FunctionTemplateTypeRuleTest.php | 4 +- .../InterfaceTemplateTypeRuleTest.php | 4 +- .../Generics/MethodTemplateTypeRuleTest.php | 4 +- .../Generics/TraitTemplateTypeRuleTest.php | 4 +- 10 files changed, 123 insertions(+), 65 deletions(-) create mode 100644 src/Type/TypeAliasResolver.php diff --git a/conf/config.neon b/conf/config.neon index fa8ff89021..9abab1fde4 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -368,8 +368,6 @@ services: - class: PHPStan\PhpDoc\TypeAlias\TypeAliasesTypeNodeResolverExtension - arguments: - aliases: %typeAliases% tags: - phpstan.phpDoc.typeNodeResolverExtension @@ -747,7 +745,6 @@ services: class: PHPStan\Rules\Generics\TemplateTypeCheck arguments: checkClassCaseSensitivity: %checkClassCaseSensitivity% - typeAliases: %typeAliases% - class: PHPStan\Rules\Generics\VarianceCheck @@ -802,6 +799,11 @@ services: - class: PHPStan\Type\FileTypeMapper + - + class: PHPStan\Type\TypeAliasResolver + arguments: + aliases: %typeAliases% + - class: PHPStan\Type\Php\ArgumentBasedFunctionReturnTypeExtension tags: diff --git a/src/PhpDoc/TypeAlias/TypeAliasesTypeNodeResolverExtension.php b/src/PhpDoc/TypeAlias/TypeAliasesTypeNodeResolverExtension.php index cb940f021d..fb8f42fab6 100644 --- a/src/PhpDoc/TypeAlias/TypeAliasesTypeNodeResolverExtension.php +++ b/src/PhpDoc/TypeAlias/TypeAliasesTypeNodeResolverExtension.php @@ -4,73 +4,28 @@ use PHPStan\Analyser\NameScope; use PHPStan\PhpDoc\TypeNodeResolverExtension; -use PHPStan\PhpDoc\TypeStringResolver; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; -use PHPStan\Reflection\ReflectionProvider; use PHPStan\Type\Type; -use function array_key_exists; +use PHPStan\Type\TypeAliasResolver; class TypeAliasesTypeNodeResolverExtension implements TypeNodeResolverExtension { - private TypeStringResolver $typeStringResolver; + private TypeAliasResolver $typeAliasResolver; - private ReflectionProvider $reflectionProvider; - - /** @var array */ - 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 + TypeAliasResolver $typeAliasResolver ) { - $this->typeStringResolver = $typeStringResolver; - $this->reflectionProvider = $reflectionProvider; - $this->aliases = $aliases; + $this->typeAliasResolver = $typeAliasResolver; } 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 $this->typeAliasResolver->resolveTypeAlias($aliasName, $nameScope); } return null; } diff --git a/src/Rules/Generics/TemplateTypeCheck.php b/src/Rules/Generics/TemplateTypeCheck.php index 45bc1ee86e..70fe2c6df5 100644 --- a/src/Rules/Generics/TemplateTypeCheck.php +++ b/src/Rules/Generics/TemplateTypeCheck.php @@ -15,10 +15,10 @@ 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,8 +30,7 @@ class TemplateTypeCheck private GenericObjectTypeCheck $genericObjectTypeCheck; - /** @var array */ - private array $typeAliases; + private TypeAliasResolver $typeAliasResolver; private bool $checkClassCaseSensitivity; @@ -39,21 +38,21 @@ class TemplateTypeCheck * @param ReflectionProvider $reflectionProvider * @param ClassCaseSensitivityCheck $classCaseSensitivityCheck * @param GenericObjectTypeCheck $genericObjectTypeCheck - * @param array $typeAliases + * @param TypeAliasResolver $typeAliasResolver * @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; } @@ -80,7 +79,7 @@ public function check( $templateTagName ))->build(); } - if (array_key_exists($templateTagName, $this->typeAliases)) { + if ($this->typeAliasResolver->hasTypeAlias($templateTagName)) { $messages[] = RuleErrorBuilder::message(sprintf( $sameTemplateTypeNameAsTypeMessage, $templateTagName diff --git a/src/Testing/TestCase.php b/src/Testing/TestCase.php index fc73b7aed0..8550fdb08d 100644 --- a/src/Testing/TestCase.php +++ b/src/Testing/TestCase.php @@ -41,6 +41,7 @@ use PHPStan\PhpDoc\PhpDocNodeResolver; use PHPStan\PhpDoc\PhpDocStringResolver; use PHPStan\PhpDoc\StubPhpDocProvider; +use PHPStan\PhpDoc\TypeStringResolver; use PHPStan\Reflection\Annotations\AnnotationsMethodsClassReflectionExtension; use PHPStan\Reflection\Annotations\AnnotationsPropertiesClassReflectionExtension; use PHPStan\Reflection\BetterReflection\BetterReflectionProvider; @@ -70,6 +71,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 +572,20 @@ 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), + $reflectionProvider, + ); + } + protected function shouldTreatPhpDocTypesAsCertain(): bool { return true; diff --git a/src/Type/TypeAliasResolver.php b/src/Type/TypeAliasResolver.php new file mode 100644 index 0000000000..1e0ef37679 --- /dev/null +++ b/src/Type/TypeAliasResolver.php @@ -0,0 +1,74 @@ + */ + private array $aliases; + + private TypeStringResolver $typeStringResolver; + + private ReflectionProvider $reflectionProvider; + + /** @var array */ + private array $resolvedTypes = []; + + /** @var array */ + private array $inProcess = []; + + /** + * @param array $aliases + */ + public function __construct( + array $aliases, + TypeStringResolver $typeStringResolver, + ReflectionProvider $reflectionProvider + ) + { + $this->aliases = $aliases; + $this->typeStringResolver = $typeStringResolver; + $this->reflectionProvider = $reflectionProvider; + } + + public function hasTypeAlias(string $aliasName): bool + { + return array_key_exists($aliasName, $this->aliases); + } + + public function resolveTypeAlias(string $aliasName, NameScope $nameScope): ?Type + { + if (!array_key_exists($aliasName, $this->aliases)) { + return null; + } + + if (array_key_exists($aliasName, $this->resolvedTypes)) { + return $this->resolvedTypes[$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->aliases[$aliasName]; + $aliasType = $this->typeStringResolver->resolve($aliasTypeString); + $this->resolvedTypes[$aliasName] = $aliasType; + + unset($this->inProcess[$aliasName]); + + return $aliasType; + } + +} diff --git a/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php index 62c8227a5c..d7e419d9cb 100644 --- a/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php @@ -2,9 +2,12 @@ namespace PHPStan\Rules\Generics; +use PHPStan\PhpDoc\TypeStringResolver; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPStan\Type\TypeAliasResolver; + /** * @extends \PHPStan\Testing\RuleTestCase @@ -15,13 +18,14 @@ class ClassTemplateTypeRuleTest extends RuleTestCase protected function getRule(): Rule { $broker = $this->createReflectionProvider(); + $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $broker); return new ClassTemplateTypeRule( new TemplateTypeCheck( $broker, new ClassCaseSensitivityCheck($broker), new GenericObjectTypeCheck(), - ['TypeAlias' => 'int'], + $typeAliasResolver, true ) ); 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..f44832e28e 100644 --- a/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php @@ -16,9 +16,11 @@ 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) ); } diff --git a/tests/PHPStan/Rules/Generics/MethodTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/MethodTemplateTypeRuleTest.php index 585bec3697..b9da9f97fb 100644 --- a/tests/PHPStan/Rules/Generics/MethodTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/MethodTemplateTypeRuleTest.php @@ -16,9 +16,11 @@ 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) ); } diff --git a/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php index 19a533f784..44d851ca7c 100644 --- a/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php @@ -16,9 +16,11 @@ 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) ); } From 1cf6303a12aa224ca3fab2685420289db19c1286 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pudil?= Date: Sat, 27 Feb 2021 15:44:09 +0100 Subject: [PATCH 02/25] type aliases: add test --- .../PHPStan/Analyser/NodeScopeResolverTest.php | 16 ++++++++++++++++ tests/PHPStan/Analyser/data/type-aliases.php | 18 ++++++++++++++++++ tests/PHPStan/Analyser/typeAliases.neon | 3 +++ 3 files changed, 37 insertions(+) create mode 100644 tests/PHPStan/Analyser/data/type-aliases.php create mode 100644 tests/PHPStan/Analyser/typeAliases.neon diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index f7c0295663..da082cb3b3 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'); } /** @@ -10376,6 +10380,13 @@ public function testFileAsserts( } } + public function dataTypeAliases(): array + { + require_once __DIR__ . '/data/type-aliases.php'; + + return $this->gatherAssertTypes(__DIR__ . '/data/type-aliases.php'); + } + /** * @param string $file * @return array @@ -10995,6 +11006,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..cc02b28c73 --- /dev/null +++ b/tests/PHPStan/Analyser/data/type-aliases.php @@ -0,0 +1,18 @@ + Date: Sat, 27 Feb 2021 16:46:00 +0100 Subject: [PATCH 03/25] type aliases: support class-scoped local aliases --- conf/config.neon | 2 +- src/PhpDoc/PhpDocNodeResolver.php | 19 ++++ src/PhpDoc/ResolvedPhpDocBlock.php | 22 ++++ src/PhpDoc/Tag/TypeAliasTag.php | 40 +++++++ src/Reflection/ClassReflection.php | 20 ++++ src/Rules/Generics/ClassTemplateTypeRule.php | 3 + .../Generics/FunctionTemplateTypeRule.php | 2 + .../Generics/InterfaceTemplateTypeRule.php | 2 + src/Rules/Generics/MethodTemplateTypeRule.php | 2 + src/Rules/Generics/TemplateTypeCheck.php | 5 +- src/Rules/Generics/TraitTemplateTypeRule.php | 2 + src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php | 2 + src/Type/TypeAliasResolver.php | 103 ++++++++++++++++-- tests/PHPStan/Analyser/data/type-aliases.php | 11 ++ 14 files changed, 221 insertions(+), 14 deletions(-) create mode 100644 src/PhpDoc/Tag/TypeAliasTag.php diff --git a/conf/config.neon b/conf/config.neon index 9abab1fde4..1eafd3269f 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -802,7 +802,7 @@ services: - class: PHPStan\Type\TypeAliasResolver arguments: - aliases: %typeAliases% + globalTypeAliases: %typeAliases% - class: PHPStan\Type\Php\ArgumentBasedFunctionReturnTypeExtension diff --git a/src/PhpDoc/PhpDocNodeResolver.php b/src/PhpDoc/PhpDocNodeResolver.php index ecc757874e..c5b9f9d076 100644 --- a/src/PhpDoc/PhpDocNodeResolver.php +++ b/src/PhpDoc/PhpDocNodeResolver.php @@ -14,6 +14,7 @@ use PHPStan\PhpDoc\Tag\ReturnTag; use PHPStan\PhpDoc\Tag\TemplateTag; use PHPStan\PhpDoc\Tag\ThrowsTag; +use PHPStan\PhpDoc\Tag\TypeAliasTag; use PHPStan\PhpDoc\Tag\UsesTag; use PHPStan\PhpDoc\Tag\VarTag; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprNullNode; @@ -365,6 +366,24 @@ public function resolveMixinTags(PhpDocNode $phpDocNode, NameScope $nameScope): }, $phpDocNode->getMixinTagValues()); } + /** + * @return array + */ + public function resolveTypeAliasTags(PhpDocNode $phpDocNode): array + { + $resolved = []; + + foreach (['@phpstan-type', '@psalm-type'] as $tagName) { + foreach ($phpDocNode->getTypeAliasTagValues($tagName) as $typeAliasTagValue) { + $alias = $typeAliasTagValue->alias; + $type = (string) $typeAliasTagValue->type; + $resolved[$alias] = new TypeAliasTag($alias, $type); + } + } + + 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..ccab7f892f 100644 --- a/src/PhpDoc/ResolvedPhpDocBlock.php +++ b/src/PhpDoc/ResolvedPhpDocBlock.php @@ -7,6 +7,7 @@ use PHPStan\PhpDoc\Tag\ParamTag; use PHPStan\PhpDoc\Tag\ReturnTag; use PHPStan\PhpDoc\Tag\ThrowsTag; +use PHPStan\PhpDoc\Tag\TypeAliasTag; use PHPStan\PhpDoc\Tag\TypedTag; use PHPStan\PhpDoc\Tag\VarTag; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode; @@ -64,6 +65,9 @@ class ResolvedPhpDocBlock /** @var array|false */ private $mixinTags = false; + /** @var array|false */ + private $typeAliasTags = false; + /** @var \PHPStan\PhpDoc\Tag\DeprecatedTag|false|null */ private $deprecatedTag = false; @@ -133,6 +137,7 @@ public static function createEmpty(): self $self->returnTag = null; $self->throwsTag = null; $self->mixinTags = []; + $self->typeAliasTags = []; $self->deprecatedTag = null; $self->isDeprecated = false; $self->isInternal = false; @@ -176,6 +181,7 @@ 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->deprecatedTag = $this->getDeprecatedTag(); $result->isDeprecated = $result->deprecatedTag !== null; $result->isInternal = $this->isInternal(); @@ -219,6 +225,8 @@ 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->deprecatedTag = $this->deprecatedTag; $self->isDeprecated = $this->isDeprecated; $self->isInternal = $this->isInternal; @@ -399,6 +407,20 @@ public function getMixinTags(): array return $this->mixinTags; } + /** + * @return array + */ + public function getTypeAliasTags(): array + { + if ($this->typeAliasTags === false) { + $this->typeAliasTags = $this->phpDocNodeResolver->resolveTypeAliasTags( + $this->phpDocNode + ); + } + + return $this->typeAliasTags; + } + public function getDeprecatedTag(): ?\PHPStan\PhpDoc\Tag\DeprecatedTag { if ($this->deprecatedTag === false) { diff --git a/src/PhpDoc/Tag/TypeAliasTag.php b/src/PhpDoc/Tag/TypeAliasTag.php new file mode 100644 index 0000000000..e7a2f8ab01 --- /dev/null +++ b/src/PhpDoc/Tag/TypeAliasTag.php @@ -0,0 +1,40 @@ +alias = $alias; + $this->type = $type; + } + + public function getAlias(): string + { + return $this->alias; + } + + public function getType(): string + { + return $this->type; + } + + /** + * @param mixed[] $properties + * @return TypeAliasTag + */ + public static function __set_state(array $properties): self + { + return new self( + $properties['alias'], + $properties['type'] + ); + } + +} diff --git a/src/Reflection/ClassReflection.php b/src/Reflection/ClassReflection.php index 4952bb1938..9c3077ae19 100644 --- a/src/Reflection/ClassReflection.php +++ b/src/Reflection/ClassReflection.php @@ -12,6 +12,7 @@ use PHPStan\PhpDoc\Tag\MixinTag; use PHPStan\PhpDoc\Tag\PropertyTag; use PHPStan\PhpDoc\Tag\TemplateTag; +use PHPStan\PhpDoc\Tag\TypeAliasTag; use PHPStan\Reflection\Php\PhpClassReflectionExtension; use PHPStan\Reflection\Php\PhpPropertyReflection; use PHPStan\Type\ErrorType; @@ -98,6 +99,9 @@ class ClassReflection implements ReflectionWithFilename /** @var \PHPStan\Reflection\ClassReflection|false|null */ private $cachedParentClass = null; + /** @var array|null */ + private ?array $typeAliases = null; + /** * @param \PHPStan\Reflection\ReflectionProvider $reflectionProvider * @param \PHPStan\Type\FileTypeMapper $fileTypeMapper @@ -752,6 +756,22 @@ private function getTraitNames(): array return $traitNames; } + /** + * @return array + */ + public function getTypeAliases(): array + { + if ($this->typeAliases === null) { + $resolvedPhpDoc = $this->getResolvedPhpDoc(); + $typeAliasTags = $resolvedPhpDoc !== null ? $resolvedPhpDoc->getTypeAliasTags() : []; + $this->typeAliases = array_map(function (TypeAliasTag $typeAliasTag): string { + return $typeAliasTag->getType(); + }, $typeAliasTags); + } + + return $this->typeAliases; + } + public function getDeprecatedDescription(): ?string { if ($this->deprecatedDescription === null && $this->isDeprecated()) { 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 70fe2c6df5..7e47023d73 100644 --- a/src/Rules/Generics/TemplateTypeCheck.php +++ b/src/Rules/Generics/TemplateTypeCheck.php @@ -9,6 +9,7 @@ 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; @@ -58,11 +59,13 @@ public function __construct( /** * @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, @@ -79,7 +82,7 @@ public function check( $templateTagName ))->build(); } - if ($this->typeAliasResolver->hasTypeAlias($templateTagName)) { + 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/Type/TypeAliasResolver.php b/src/Type/TypeAliasResolver.php index 1e0ef37679..3e441a505d 100644 --- a/src/Type/TypeAliasResolver.php +++ b/src/Type/TypeAliasResolver.php @@ -11,45 +11,124 @@ class TypeAliasResolver { /** @var array */ - private array $aliases; + private array $globalTypeAliases; private TypeStringResolver $typeStringResolver; private ReflectionProvider $reflectionProvider; /** @var array */ - private array $resolvedTypes = []; + private array $resolvedGlobalTypeAliases = []; + + /** @var array */ + private array $resolvedLocalTypeAliases = []; + + /** @var array */ + private array $resolvingClassTypeAliases = []; /** @var array */ private array $inProcess = []; /** - * @param array $aliases + * @param array $globalTypeAliases */ public function __construct( - array $aliases, + array $globalTypeAliases, TypeStringResolver $typeStringResolver, ReflectionProvider $reflectionProvider ) { - $this->aliases = $aliases; + $this->globalTypeAliases = $globalTypeAliases; $this->typeStringResolver = $typeStringResolver; $this->reflectionProvider = $reflectionProvider; } - public function hasTypeAlias(string $aliasName): bool + public function hasTypeAlias(string $aliasName, ?string $classNameScope): bool { - return array_key_exists($aliasName, $this->aliases); + $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 { - if (!array_key_exists($aliasName, $this->aliases)) { + 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; + } + + // prevent infinite recursion + if (array_key_exists($className, $this->resolvingClassTypeAliases)) { + return null; + } + + $this->resolvingClassTypeAliases[$className] = true; + + if (!$this->reflectionProvider->hasClass($className)) { + return null; + } + + $classReflection = $this->reflectionProvider->getClass($className); + $localTypeAliases = $classReflection->getTypeAliases(); + + unset($this->resolvingClassTypeAliases[$className]); + + if (!array_key_exists($aliasName, $localTypeAliases)) { + return null; + } + + $aliasNameInClassScope = $className . '::' . $aliasName; + + if (array_key_exists($aliasNameInClassScope, $this->resolvedLocalTypeAliases)) { + return $this->resolvedLocalTypeAliases[$aliasNameInClassScope]; + } + + if ($this->reflectionProvider->hasClass($nameScope->resolveStringName($aliasName))) { + throw new \PHPStan\ShouldNotHappenException(sprintf('Type alias %s already exists as a class in scope of %s.', $aliasName, $className)); + } + + if (array_key_exists($aliasName, $this->globalTypeAliases)) { + throw new \PHPStan\ShouldNotHappenException(sprintf('Type alias %s used in scope of %s already exists as a global type alias.', $aliasName, $className)); + } + + if (array_key_exists($aliasNameInClassScope, $this->inProcess)) { + throw new \PHPStan\ShouldNotHappenException(sprintf('Circular definition for type alias %s in scope of %s.', $aliasName, $className)); + } + + $this->inProcess[$aliasNameInClassScope] = true; + + $aliasTypeString = $localTypeAliases[$aliasName]; + $aliasType = $this->typeStringResolver->resolve($aliasTypeString, $nameScope); + $this->resolvedLocalTypeAliases[$aliasNameInClassScope] = $aliasType; + + unset($this->inProcess[$aliasNameInClassScope]); + + return $aliasType; + } + + private function resolveGlobalTypeAlias(string $aliasName, NameScope $nameScope): ?Type + { + if (!array_key_exists($aliasName, $this->globalTypeAliases)) { return null; } - if (array_key_exists($aliasName, $this->resolvedTypes)) { - return $this->resolvedTypes[$aliasName]; + if (array_key_exists($aliasName, $this->resolvedGlobalTypeAliases)) { + return $this->resolvedGlobalTypeAliases[$aliasName]; } if ($this->reflectionProvider->hasClass($nameScope->resolveStringName($aliasName))) { @@ -62,9 +141,9 @@ public function resolveTypeAlias(string $aliasName, NameScope $nameScope): ?Type $this->inProcess[$aliasName] = true; - $aliasTypeString = $this->aliases[$aliasName]; + $aliasTypeString = $this->globalTypeAliases[$aliasName]; $aliasType = $this->typeStringResolver->resolve($aliasTypeString); - $this->resolvedTypes[$aliasName] = $aliasType; + $this->resolvedGlobalTypeAliases[$aliasName] = $aliasType; unset($this->inProcess[$aliasName]); diff --git a/tests/PHPStan/Analyser/data/type-aliases.php b/tests/PHPStan/Analyser/data/type-aliases.php index cc02b28c73..1e9bc40d96 100644 --- a/tests/PHPStan/Analyser/data/type-aliases.php +++ b/tests/PHPStan/Analyser/data/type-aliases.php @@ -4,6 +4,9 @@ use function PHPStan\Analyser\assertType; +/** + * @phpstan-type LocalTypeAlias callable(string $value): (string|false) + */ class Foo { @@ -15,4 +18,12 @@ public function globalAlias($parameter) assertType('int|string', $parameter); } + /** + * @param LocalTypeAlias $parameter + */ + public function localAlias($parameter) + { + assertType('callable(string): string|false', $parameter); + } + } From f54f80e89d52798f7549f27c27f3ec37a472ec1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pudil?= Date: Sat, 27 Feb 2021 18:15:21 +0100 Subject: [PATCH 04/25] type aliases: add test for nested aliases --- tests/PHPStan/Analyser/data/type-aliases.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/PHPStan/Analyser/data/type-aliases.php b/tests/PHPStan/Analyser/data/type-aliases.php index 1e9bc40d96..a513265e49 100644 --- a/tests/PHPStan/Analyser/data/type-aliases.php +++ b/tests/PHPStan/Analyser/data/type-aliases.php @@ -6,6 +6,7 @@ /** * @phpstan-type LocalTypeAlias callable(string $value): (string|false) + * @phpstan-type NestedLocalTypeAlias LocalTypeAlias[] */ class Foo { @@ -26,4 +27,12 @@ public function localAlias($parameter) assertType('callable(string): string|false', $parameter); } + /** + * @param NestedLocalTypeAlias $parameter + */ + public function nestedLocalAlias($parameter) + { + assertType('array', $parameter); + } + } From 293d9b33c7ef06d804806584563774164fa70172 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pudil?= Date: Sat, 27 Feb 2021 19:19:39 +0100 Subject: [PATCH 05/25] type aliases: support type alias imports --- src/PhpDoc/PhpDocNodeResolver.php | 20 ++++++++ src/PhpDoc/ResolvedPhpDocBlock.php | 22 +++++++++ src/PhpDoc/Tag/TypeAliasImportTag.php | 51 ++++++++++++++++++++ src/Reflection/ClassReflection.php | 32 +++++++++++- tests/PHPStan/Analyser/data/type-aliases.php | 25 ++++++++++ 5 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 src/PhpDoc/Tag/TypeAliasImportTag.php diff --git a/src/PhpDoc/PhpDocNodeResolver.php b/src/PhpDoc/PhpDocNodeResolver.php index c5b9f9d076..647c26aae3 100644 --- a/src/PhpDoc/PhpDocNodeResolver.php +++ b/src/PhpDoc/PhpDocNodeResolver.php @@ -14,6 +14,7 @@ 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; @@ -384,6 +385,25 @@ public function resolveTypeAliasTags(PhpDocNode $phpDocNode): array return $resolved; } + /** + * @return array + */ + public function resolveTypeAliasImportTags(PhpDocNode $phpDocNode, NameScope $nameScope): array + { + $resolved = []; + + foreach (['@phpstan-import-type', '@psalm-import-type'] as $tagName) { + foreach ($phpDocNode->getTypeAliasImportTagValues($tagName) as $typeAliasImportTagValue) { + $importedAlias = $typeAliasImportTagValue->importedAlias; + $importedFrom = $this->typeNodeResolver->resolve($typeAliasImportTagValue->importedFrom, $nameScope); + $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 ccab7f892f..1775407d04 100644 --- a/src/PhpDoc/ResolvedPhpDocBlock.php +++ b/src/PhpDoc/ResolvedPhpDocBlock.php @@ -7,6 +7,7 @@ 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; @@ -68,6 +69,9 @@ class ResolvedPhpDocBlock /** @var array|false */ private $typeAliasTags = false; + /** @var array|false */ + private $typeAliasImportTags = false; + /** @var \PHPStan\PhpDoc\Tag\DeprecatedTag|false|null */ private $deprecatedTag = false; @@ -138,6 +142,7 @@ public static function createEmpty(): self $self->throwsTag = null; $self->mixinTags = []; $self->typeAliasTags = []; + $self->typeAliasImportTags = []; $self->deprecatedTag = null; $self->isDeprecated = false; $self->isInternal = false; @@ -182,6 +187,7 @@ public function merge(array $parents, array $parentPhpDocBlocks): self $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(); @@ -227,6 +233,7 @@ public function changeParameterNamesByMapping(array $parameterNameMapping): self $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; @@ -421,6 +428,21 @@ public function getTypeAliasTags(): array 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..e57780c5a0 --- /dev/null +++ b/src/PhpDoc/Tag/TypeAliasImportTag.php @@ -0,0 +1,51 @@ +importedAlias = $importedAlias; + $this->importedFrom = $importedFrom; + $this->importedAs = $importedAs; + } + + public function getImportedAlias(): string + { + return $this->importedAlias; + } + + public function getImportedFrom(): \PHPStan\Type\Type + { + return $this->importedFrom; + } + + public function getImportedAs(): ?string + { + return $this->importedAs; + } + + /** + * @param mixed[] $properties + * @return self + */ + public static function __set_state(array $properties): self + { + return new self( + $properties['importedAlias'], + $properties['importedFrom'], + $properties['importedAs'], + ); + } + +} diff --git a/src/Reflection/ClassReflection.php b/src/Reflection/ClassReflection.php index 9c3077ae19..816547a5f3 100644 --- a/src/Reflection/ClassReflection.php +++ b/src/Reflection/ClassReflection.php @@ -12,6 +12,7 @@ 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; @@ -22,6 +23,7 @@ use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeScope; +use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; use ReflectionMethod; @@ -763,10 +765,38 @@ public function getTypeAliases(): array { if ($this->typeAliases === null) { $resolvedPhpDoc = $this->getResolvedPhpDoc(); + $typeAliasImportTags = $resolvedPhpDoc !== null ? $resolvedPhpDoc->getTypeAliasImportTags() : []; $typeAliasTags = $resolvedPhpDoc !== null ? $resolvedPhpDoc->getTypeAliasTags() : []; - $this->typeAliases = array_map(function (TypeAliasTag $typeAliasTag): string { + + $importedAliases = array_map(function (TypeAliasImportTag $typeAliasImportTag): string { + $importedAlias = $typeAliasImportTag->getImportedAlias(); + $importedFrom = $typeAliasImportTag->getImportedFrom(); + + if (!($importedFrom instanceof ObjectType)) { + throw new \PHPStan\ShouldNotHappenException(sprintf('Cannot import type alias %s in scope of %s from non-class type %s.', $typeAliasImportTag->getImportedAlias(), $this->getName(), $typeAliasImportTag->getImportedFrom()->describe(VerbosityLevel::typeOnly()))); + } + + $importedFromClassName = $importedFrom->getClassName(); + $importedFromReflection = $this->reflectionProvider->getClass($importedFromClassName); + + $typeAliases = $importedFromReflection->getTypeAliases(); + + if (!array_key_exists($importedAlias, $typeAliases)) { + throw new \PHPStan\ShouldNotHappenException(sprintf('Type alias %s imported in %s does not exist on %s.', $importedAlias, $this->getName(), $importedFromClassName)); + } + + return $typeAliases[$importedAlias]; + }, $typeAliasImportTags); + + $localAliases = array_map(function (TypeAliasTag $typeAliasTag) use ($importedAliases): string { + if (array_key_exists($typeAliasTag->getAlias(), $importedAliases)) { + throw new \PHPStan\ShouldNotHappenException(sprintf('Type alias %s overwrites imported type alias of the same name in scope of %s.', $typeAliasTag->getAlias(), $this->getName())); + } + return $typeAliasTag->getType(); }, $typeAliasTags); + + $this->typeAliases = array_merge($importedAliases, $localAliases); } return $this->typeAliases; diff --git a/tests/PHPStan/Analyser/data/type-aliases.php b/tests/PHPStan/Analyser/data/type-aliases.php index a513265e49..060c860702 100644 --- a/tests/PHPStan/Analyser/data/type-aliases.php +++ b/tests/PHPStan/Analyser/data/type-aliases.php @@ -4,9 +4,18 @@ use function PHPStan\Analyser\assertType; +/** + * @phpstan-type ExportedTypeAlias \Countable&\Traversable + */ +class Bar +{ +} + /** * @phpstan-type LocalTypeAlias callable(string $value): (string|false) * @phpstan-type NestedLocalTypeAlias LocalTypeAlias[] + * @phpstan-import-type ExportedTypeAlias from Bar as ImportedTypeAlias + * @phpstan-type NestedImportedTypeAlias iterable */ class Foo { @@ -35,4 +44,20 @@ 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); + } + } From 390cb96b451a131588e53020cc9285b9ee9e1491 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pudil?= Date: Sat, 27 Feb 2021 21:34:17 +0100 Subject: [PATCH 06/25] fix cs and phpstan baseline --- build/baseline-7.4.neon | 2 +- src/PhpDoc/Tag/TypeAliasImportTag.php | 6 ++---- src/Reflection/ClassReflection.php | 2 +- src/Rules/Generics/TemplateTypeCheck.php | 7 ------- src/Testing/TestCase.php | 2 +- tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php | 3 --- 6 files changed, 5 insertions(+), 17 deletions(-) diff --git a/build/baseline-7.4.neon b/build/baseline-7.4.neon index 9217c32c19..15b6db799a 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\\:269 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/src/PhpDoc/Tag/TypeAliasImportTag.php b/src/PhpDoc/Tag/TypeAliasImportTag.php index e57780c5a0..eb003a70c0 100644 --- a/src/PhpDoc/Tag/TypeAliasImportTag.php +++ b/src/PhpDoc/Tag/TypeAliasImportTag.php @@ -1,6 +1,4 @@ -getTypeAliasImportTags() : []; $typeAliasTags = $resolvedPhpDoc !== null ? $resolvedPhpDoc->getTypeAliasTags() : []; - $importedAliases = array_map(function (TypeAliasImportTag $typeAliasImportTag): string { + $importedAliases = array_map(function (TypeAliasImportTag $typeAliasImportTag): string { $importedAlias = $typeAliasImportTag->getImportedAlias(); $importedFrom = $typeAliasImportTag->getImportedFrom(); diff --git a/src/Rules/Generics/TemplateTypeCheck.php b/src/Rules/Generics/TemplateTypeCheck.php index 7e47023d73..057623f0da 100644 --- a/src/Rules/Generics/TemplateTypeCheck.php +++ b/src/Rules/Generics/TemplateTypeCheck.php @@ -35,13 +35,6 @@ class TemplateTypeCheck private bool $checkClassCaseSensitivity; - /** - * @param ReflectionProvider $reflectionProvider - * @param ClassCaseSensitivityCheck $classCaseSensitivityCheck - * @param GenericObjectTypeCheck $genericObjectTypeCheck - * @param TypeAliasResolver $typeAliasResolver - * @param bool $checkClassCaseSensitivity - */ public function __construct( ReflectionProvider $reflectionProvider, ClassCaseSensitivityCheck $classCaseSensitivityCheck, diff --git a/src/Testing/TestCase.php b/src/Testing/TestCase.php index 8550fdb08d..d1b9dacdec 100644 --- a/src/Testing/TestCase.php +++ b/src/Testing/TestCase.php @@ -582,7 +582,7 @@ public function createTypeAliasResolver(array $globalTypeAliases, ReflectionProv return new TypeAliasResolver( $globalTypeAliases, $container->getByType(TypeStringResolver::class), - $reflectionProvider, + $reflectionProvider ); } diff --git a/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php index d7e419d9cb..54213801f5 100644 --- a/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php @@ -2,12 +2,9 @@ namespace PHPStan\Rules\Generics; -use PHPStan\PhpDoc\TypeStringResolver; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use PHPStan\Type\TypeAliasResolver; - /** * @extends \PHPStan\Testing\RuleTestCase From a4e48fea207f941f58e93babcb06f6b33e435174 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 28 Feb 2021 15:41:12 +0100 Subject: [PATCH 07/25] Correct prefix priority --- src/PhpDoc/PhpDocNodeResolver.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PhpDoc/PhpDocNodeResolver.php b/src/PhpDoc/PhpDocNodeResolver.php index 647c26aae3..5aa0e33abd 100644 --- a/src/PhpDoc/PhpDocNodeResolver.php +++ b/src/PhpDoc/PhpDocNodeResolver.php @@ -374,7 +374,7 @@ public function resolveTypeAliasTags(PhpDocNode $phpDocNode): array { $resolved = []; - foreach (['@phpstan-type', '@psalm-type'] as $tagName) { + foreach (['@psalm-type', '@phpstan-type'] as $tagName) { foreach ($phpDocNode->getTypeAliasTagValues($tagName) as $typeAliasTagValue) { $alias = $typeAliasTagValue->alias; $type = (string) $typeAliasTagValue->type; @@ -392,7 +392,7 @@ public function resolveTypeAliasImportTags(PhpDocNode $phpDocNode, NameScope $na { $resolved = []; - foreach (['@phpstan-import-type', '@psalm-import-type'] as $tagName) { + foreach (['@psalm-import-type', '@phpstan-import-type'] as $tagName) { foreach ($phpDocNode->getTypeAliasImportTagValues($tagName) as $typeAliasImportTagValue) { $importedAlias = $typeAliasImportTagValue->importedAlias; $importedFrom = $this->typeNodeResolver->resolve($typeAliasImportTagValue->importedFrom, $nameScope); From 9ed65ccead12acdc0d324388c9326c59aa5b04e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pudil?= Date: Sat, 27 Mar 2021 17:24:29 +0100 Subject: [PATCH 08/25] type aliases: resolve type aliases lazily and in correct NameScope This adds support for nested type aliases and helps preserve scope when importing aliases. Aliases are resolved lazily because eager resolution would create a chicken-and-egg problem where aliases need to be resolved while they are already being resolved, thus not being resolved at all as a result of recursion protection. --- src/PhpDoc/PhpDocNodeResolver.php | 6 +- src/PhpDoc/ResolvedPhpDocBlock.php | 3 +- src/PhpDoc/Tag/TypeAliasTag.php | 22 +++- src/Reflection/ClassReflection.php | 11 +- src/Type/TypeAlias.php | 38 +++++++ src/Type/TypeAliasResolver.php | 8 +- tests/PHPStan/Analyser/data/type-aliases.php | 114 +++++++++++++------ 7 files changed, 147 insertions(+), 55 deletions(-) create mode 100644 src/Type/TypeAlias.php diff --git a/src/PhpDoc/PhpDocNodeResolver.php b/src/PhpDoc/PhpDocNodeResolver.php index 5aa0e33abd..b3271cefc5 100644 --- a/src/PhpDoc/PhpDocNodeResolver.php +++ b/src/PhpDoc/PhpDocNodeResolver.php @@ -370,15 +370,15 @@ public function resolveMixinTags(PhpDocNode $phpDocNode, NameScope $nameScope): /** * @return array */ - public function resolveTypeAliasTags(PhpDocNode $phpDocNode): 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; - $type = (string) $typeAliasTagValue->type; - $resolved[$alias] = new TypeAliasTag($alias, $type); + $typeString = (string) $typeAliasTagValue->type; + $resolved[$alias] = new TypeAliasTag($alias, $typeString, $nameScope); } } diff --git a/src/PhpDoc/ResolvedPhpDocBlock.php b/src/PhpDoc/ResolvedPhpDocBlock.php index 1775407d04..d40ce8fa7d 100644 --- a/src/PhpDoc/ResolvedPhpDocBlock.php +++ b/src/PhpDoc/ResolvedPhpDocBlock.php @@ -421,7 +421,8 @@ public function getTypeAliasTags(): array { if ($this->typeAliasTags === false) { $this->typeAliasTags = $this->phpDocNodeResolver->resolveTypeAliasTags( - $this->phpDocNode + $this->phpDocNode, + $this->getNameScope() ); } diff --git a/src/PhpDoc/Tag/TypeAliasTag.php b/src/PhpDoc/Tag/TypeAliasTag.php index e7a2f8ab01..abb51fb134 100644 --- a/src/PhpDoc/Tag/TypeAliasTag.php +++ b/src/PhpDoc/Tag/TypeAliasTag.php @@ -2,17 +2,26 @@ namespace PHPStan\PhpDoc\Tag; +use PHPStan\Analyser\NameScope; + class TypeAliasTag { private string $alias; - private string $type; + private string $typeString; + + private NameScope $nameScope; - public function __construct(string $alias, string $type) + public function __construct( + string $alias, + string $typeString, + NameScope $nameScope + ) { $this->alias = $alias; - $this->type = $type; + $this->typeString = $typeString; + $this->nameScope = $nameScope; } public function getAlias(): string @@ -20,9 +29,12 @@ public function getAlias(): string return $this->alias; } - public function getType(): string + public function getTypeAlias(): \PHPStan\Type\TypeAlias { - return $this->type; + return new \PHPStan\Type\TypeAlias( + $this->typeString, + $this->nameScope + ); } /** diff --git a/src/Reflection/ClassReflection.php b/src/Reflection/ClassReflection.php index 3297cbbbc8..380be105cc 100644 --- a/src/Reflection/ClassReflection.php +++ b/src/Reflection/ClassReflection.php @@ -25,6 +25,7 @@ use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; +use PHPStan\Type\TypeAlias; use PHPStan\Type\VerbosityLevel; use ReflectionMethod; @@ -101,7 +102,7 @@ class ClassReflection implements ReflectionWithFilename /** @var \PHPStan\Reflection\ClassReflection|false|null */ private $cachedParentClass = null; - /** @var array|null */ + /** @var array|null */ private ?array $typeAliases = null; /** @@ -759,7 +760,7 @@ private function getTraitNames(): array } /** - * @return array + * @return array */ public function getTypeAliases(): array { @@ -768,7 +769,7 @@ public function getTypeAliases(): array $typeAliasImportTags = $resolvedPhpDoc !== null ? $resolvedPhpDoc->getTypeAliasImportTags() : []; $typeAliasTags = $resolvedPhpDoc !== null ? $resolvedPhpDoc->getTypeAliasTags() : []; - $importedAliases = array_map(function (TypeAliasImportTag $typeAliasImportTag): string { + $importedAliases = array_map(function (TypeAliasImportTag $typeAliasImportTag): TypeAlias { $importedAlias = $typeAliasImportTag->getImportedAlias(); $importedFrom = $typeAliasImportTag->getImportedFrom(); @@ -788,12 +789,12 @@ public function getTypeAliases(): array return $typeAliases[$importedAlias]; }, $typeAliasImportTags); - $localAliases = array_map(function (TypeAliasTag $typeAliasTag) use ($importedAliases): string { + $localAliases = array_map(function (TypeAliasTag $typeAliasTag) use ($importedAliases): TypeAlias { if (array_key_exists($typeAliasTag->getAlias(), $importedAliases)) { throw new \PHPStan\ShouldNotHappenException(sprintf('Type alias %s overwrites imported type alias of the same name in scope of %s.', $typeAliasTag->getAlias(), $this->getName())); } - return $typeAliasTag->getType(); + return $typeAliasTag->getTypeAlias(); }, $typeAliasTags); $this->typeAliases = array_merge($importedAliases, $localAliases); diff --git a/src/Type/TypeAlias.php b/src/Type/TypeAlias.php new file mode 100644 index 0000000000..16a97e74b1 --- /dev/null +++ b/src/Type/TypeAlias.php @@ -0,0 +1,38 @@ +aliasString = $typeString; + $this->nameScope = $nameScope; + } + + public function resolve(TypeStringResolver $typeStringResolver): Type + { + if ($this->resolvedType === null) { + $this->resolvedType = $typeStringResolver->resolve( + $this->aliasString, + $this->nameScope + ); + } + + return $this->resolvedType; + } + +} diff --git a/src/Type/TypeAliasResolver.php b/src/Type/TypeAliasResolver.php index 3e441a505d..70c8731e94 100644 --- a/src/Type/TypeAliasResolver.php +++ b/src/Type/TypeAliasResolver.php @@ -112,13 +112,13 @@ private function resolveLocalTypeAlias(string $aliasName, NameScope $nameScope): $this->inProcess[$aliasNameInClassScope] = true; - $aliasTypeString = $localTypeAliases[$aliasName]; - $aliasType = $this->typeStringResolver->resolve($aliasTypeString, $nameScope); - $this->resolvedLocalTypeAliases[$aliasNameInClassScope] = $aliasType; + $unresolvedAlias = $localTypeAliases[$aliasName]; + $resolvedAliasType = $unresolvedAlias->resolve($this->typeStringResolver); + $this->resolvedLocalTypeAliases[$aliasNameInClassScope] = $resolvedAliasType; unset($this->inProcess[$aliasNameInClassScope]); - return $aliasType; + return $resolvedAliasType; } private function resolveGlobalTypeAlias(string $aliasName, NameScope $nameScope): ?Type diff --git a/tests/PHPStan/Analyser/data/type-aliases.php b/tests/PHPStan/Analyser/data/type-aliases.php index 060c860702..12a67402b1 100644 --- a/tests/PHPStan/Analyser/data/type-aliases.php +++ b/tests/PHPStan/Analyser/data/type-aliases.php @@ -1,63 +1,103 @@ - */ -class Foo -{ - - /** - * @param GlobalTypeAlias $parameter - */ - public function globalAlias($parameter) +namespace TypeAliasesDataset\SubScope { + class Foo { - assertType('int|string', $parameter); } /** - * @param LocalTypeAlias $parameter + * @phpstan-type ScopedAlias Foo */ - public function localAlias($parameter) + class Bar { - assertType('callable(string): string|false', $parameter); } +} + +namespace TypeAliasesDataset { + + use function PHPStan\Analyser\assertType; /** - * @param NestedLocalTypeAlias $parameter + * @phpstan-type ExportedTypeAlias \Countable&\Traversable */ - public function nestedLocalAlias($parameter) + class Bar { - assertType('array', $parameter); } /** - * @param ImportedTypeAlias $parameter + * @phpstan-import-type ExportedTypeAlias from Bar as ReexportedTypeAlias */ - public function importedAlias($parameter) + class Baz { - assertType('Countable&Traversable', $parameter); } /** - * @param NestedImportedTypeAlias $parameter + * @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 + * @property GlobalTypeAlias $globalAliasProperty + * @property LocalTypeAlias $localAliasProperty + * @property ImportedTypeAlias $importedAliasProperty + * @property ReexportedTypeAlias $reexportedAliasProperty + * @property ScopedAlias $scopedAliasProperty */ - public function nestedImportedAlias($parameter) + class Foo { - assertType('iterable', $parameter); + + /** + * @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); + } + + public function __get(string $name) + { + return null; + } + } + 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); + } From de4ff0cd209e3554fb38dce162edcb67b361eba2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pudil?= Date: Sat, 27 Mar 2021 17:25:41 +0100 Subject: [PATCH 09/25] type aliases: remove __set_state from PhpDoc\Tag classes as they are no longer stored in cache --- src/PhpDoc/Tag/TypeAliasImportTag.php | 13 ------------- src/PhpDoc/Tag/TypeAliasTag.php | 12 ------------ 2 files changed, 25 deletions(-) diff --git a/src/PhpDoc/Tag/TypeAliasImportTag.php b/src/PhpDoc/Tag/TypeAliasImportTag.php index eb003a70c0..6acefe718d 100644 --- a/src/PhpDoc/Tag/TypeAliasImportTag.php +++ b/src/PhpDoc/Tag/TypeAliasImportTag.php @@ -33,17 +33,4 @@ public function getImportedAs(): ?string return $this->importedAs; } - /** - * @param mixed[] $properties - * @return self - */ - public static function __set_state(array $properties): self - { - return new self( - $properties['importedAlias'], - $properties['importedFrom'], - $properties['importedAs'] - ); - } - } diff --git a/src/PhpDoc/Tag/TypeAliasTag.php b/src/PhpDoc/Tag/TypeAliasTag.php index abb51fb134..47d2d9e0cd 100644 --- a/src/PhpDoc/Tag/TypeAliasTag.php +++ b/src/PhpDoc/Tag/TypeAliasTag.php @@ -37,16 +37,4 @@ public function getTypeAlias(): \PHPStan\Type\TypeAlias ); } - /** - * @param mixed[] $properties - * @return TypeAliasTag - */ - public static function __set_state(array $properties): self - { - return new self( - $properties['alias'], - $properties['type'] - ); - } - } From b8941b648816800375470944ff20459ab8d9c39c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pudil?= Date: Sat, 27 Mar 2021 17:52:36 +0100 Subject: [PATCH 10/25] type aliases: add TemplateTypeCheck tests for local aliases --- .../Generics/ClassTemplateTypeRuleTest.php | 22 ++++++++++++++----- .../InterfaceTemplateTypeRuleTest.php | 12 +++++++++- .../Generics/MethodTemplateTypeRuleTest.php | 12 +++++++++- .../Generics/TraitTemplateTypeRuleTest.php | 12 +++++++++- .../Rules/Generics/data/class-template.php | 13 +++++++++++ .../Generics/data/interface-template.php | 13 +++++++++++ .../Rules/Generics/data/method-template.php | 22 +++++++++++++++++++ .../Rules/Generics/data/trait-template.php | 13 +++++++++++ 8 files changed, 110 insertions(+), 9 deletions(-) diff --git a/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php index 54213801f5..db13da5a03 100644 --- a/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php @@ -30,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.', @@ -49,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/InterfaceTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php index f44832e28e..231e2eb0c1 100644 --- a/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php @@ -26,6 +26,8 @@ protected function getRule(): Rule 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.', @@ -41,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 b9da9f97fb..d8f2c90422 100644 --- a/tests/PHPStan/Rules/Generics/MethodTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/MethodTemplateTypeRuleTest.php @@ -26,6 +26,8 @@ protected function getRule(): Rule 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.', @@ -45,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 44d851ca7c..5ae1a3b1aa 100644 --- a/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php @@ -26,6 +26,8 @@ protected function getRule(): Rule 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.', @@ -41,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 +{ + +} From 7f066e5fb9ccc7f4106829e775f45ecde86c649d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pudil?= Date: Sun, 11 Apr 2021 12:00:03 +0200 Subject: [PATCH 11/25] type aliases: keep the TypeNode already resolved by phpdoc-parser --- build/baseline-7.4.neon | 2 +- src/PhpDoc/PhpDocNodeResolver.php | 4 ++-- src/PhpDoc/Tag/TypeAliasTag.php | 19 ++++++++++--------- src/Testing/TestCase.php | 2 ++ src/Type/TypeAlias.php | 15 ++++++++------- src/Type/TypeAliasResolver.php | 7 ++++++- 6 files changed, 29 insertions(+), 20 deletions(-) diff --git a/build/baseline-7.4.neon b/build/baseline-7.4.neon index 15b6db799a..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\\:269 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/src/PhpDoc/PhpDocNodeResolver.php b/src/PhpDoc/PhpDocNodeResolver.php index b3271cefc5..bdd5cf3b84 100644 --- a/src/PhpDoc/PhpDocNodeResolver.php +++ b/src/PhpDoc/PhpDocNodeResolver.php @@ -377,8 +377,8 @@ public function resolveTypeAliasTags(PhpDocNode $phpDocNode, NameScope $nameScop foreach (['@psalm-type', '@phpstan-type'] as $tagName) { foreach ($phpDocNode->getTypeAliasTagValues($tagName) as $typeAliasTagValue) { $alias = $typeAliasTagValue->alias; - $typeString = (string) $typeAliasTagValue->type; - $resolved[$alias] = new TypeAliasTag($alias, $typeString, $nameScope); + $typeNode = $typeAliasTagValue->type; + $resolved[$alias] = new TypeAliasTag($alias, $typeNode, $nameScope); } } diff --git a/src/PhpDoc/Tag/TypeAliasTag.php b/src/PhpDoc/Tag/TypeAliasTag.php index 47d2d9e0cd..d5ed9646cf 100644 --- a/src/PhpDoc/Tag/TypeAliasTag.php +++ b/src/PhpDoc/Tag/TypeAliasTag.php @@ -3,36 +3,37 @@ namespace PHPStan\PhpDoc\Tag; use PHPStan\Analyser\NameScope; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; class TypeAliasTag { - private string $alias; + private string $aliasName; - private string $typeString; + private TypeNode $typeNode; private NameScope $nameScope; public function __construct( - string $alias, - string $typeString, + string $aliasName, + TypeNode $typeNode, NameScope $nameScope ) { - $this->alias = $alias; - $this->typeString = $typeString; + $this->aliasName = $aliasName; + $this->typeNode = $typeNode; $this->nameScope = $nameScope; } - public function getAlias(): string + public function getAliasName(): string { - return $this->alias; + return $this->aliasName; } public function getTypeAlias(): \PHPStan\Type\TypeAlias { return new \PHPStan\Type\TypeAlias( - $this->typeString, + $this->typeNode, $this->nameScope ); } diff --git a/src/Testing/TestCase.php b/src/Testing/TestCase.php index d1b9dacdec..1eafaa2b05 100644 --- a/src/Testing/TestCase.php +++ b/src/Testing/TestCase.php @@ -41,6 +41,7 @@ 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; @@ -582,6 +583,7 @@ public function createTypeAliasResolver(array $globalTypeAliases, ReflectionProv return new TypeAliasResolver( $globalTypeAliases, $container->getByType(TypeStringResolver::class), + $container->getByType(TypeNodeResolver::class), $reflectionProvider ); } diff --git a/src/Type/TypeAlias.php b/src/Type/TypeAlias.php index 16a97e74b1..d831590ac5 100644 --- a/src/Type/TypeAlias.php +++ b/src/Type/TypeAlias.php @@ -3,31 +3,32 @@ namespace PHPStan\Type; use PHPStan\Analyser\NameScope; -use PHPStan\PhpDoc\TypeStringResolver; +use PHPStan\PhpDoc\TypeNodeResolver; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; class TypeAlias { - private string $aliasString; + private TypeNode $typeNode; private NameScope $nameScope; private ?Type $resolvedType = null; public function __construct( - string $typeString, + TypeNode $typeNode, NameScope $nameScope ) { - $this->aliasString = $typeString; + $this->typeNode = $typeNode; $this->nameScope = $nameScope; } - public function resolve(TypeStringResolver $typeStringResolver): Type + public function resolve(TypeNodeResolver $typeNodeResolver): Type { if ($this->resolvedType === null) { - $this->resolvedType = $typeStringResolver->resolve( - $this->aliasString, + $this->resolvedType = $typeNodeResolver->resolve( + $this->typeNode, $this->nameScope ); } diff --git a/src/Type/TypeAliasResolver.php b/src/Type/TypeAliasResolver.php index 70c8731e94..9954a902ee 100644 --- a/src/Type/TypeAliasResolver.php +++ b/src/Type/TypeAliasResolver.php @@ -3,6 +3,7 @@ namespace PHPStan\Type; use PHPStan\Analyser\NameScope; +use PHPStan\PhpDoc\TypeNodeResolver; use PHPStan\PhpDoc\TypeStringResolver; use PHPStan\Reflection\ReflectionProvider; use function array_key_exists; @@ -15,6 +16,8 @@ class TypeAliasResolver private TypeStringResolver $typeStringResolver; + private TypeNodeResolver $typeNodeResolver; + private ReflectionProvider $reflectionProvider; /** @var array */ @@ -35,11 +38,13 @@ class TypeAliasResolver public function __construct( array $globalTypeAliases, TypeStringResolver $typeStringResolver, + TypeNodeResolver $typeNodeResolver, ReflectionProvider $reflectionProvider ) { $this->globalTypeAliases = $globalTypeAliases; $this->typeStringResolver = $typeStringResolver; + $this->typeNodeResolver = $typeNodeResolver; $this->reflectionProvider = $reflectionProvider; } @@ -113,7 +118,7 @@ private function resolveLocalTypeAlias(string $aliasName, NameScope $nameScope): $this->inProcess[$aliasNameInClassScope] = true; $unresolvedAlias = $localTypeAliases[$aliasName]; - $resolvedAliasType = $unresolvedAlias->resolve($this->typeStringResolver); + $resolvedAliasType = $unresolvedAlias->resolve($this->typeNodeResolver); $this->resolvedLocalTypeAliases[$aliasNameInClassScope] = $resolvedAliasType; unset($this->inProcess[$aliasNameInClassScope]); From 3519a52cc99046deec8a721a39ccdc0ddd167ebb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pudil?= Date: Thu, 15 Apr 2021 11:12:10 +0200 Subject: [PATCH 12/25] type aliases: report invalid alias definitions via a Rule instead of failing the resolution --- conf/config.level0.neon | 1 + src/Reflection/ClassReflection.php | 39 +++-- src/Rules/Classes/LocalTypeAliasesRule.php | 150 ++++++++++++++++++ .../CircularTypeAliasDefinitionException.php | 8 + src/Type/TypeAlias.php | 8 + src/Type/TypeAliasResolver.php | 17 +- tests/PHPStan/Analyser/data/type-aliases.php | 51 ++++++ .../Classes/LocalTypeAliasesRuleTest.php | 86 ++++++++++ .../Rules/Classes/data/local-type-aliases.php | 47 ++++++ 9 files changed, 392 insertions(+), 15 deletions(-) create mode 100644 src/Rules/Classes/LocalTypeAliasesRule.php create mode 100644 src/Type/CircularTypeAliasDefinitionException.php create mode 100644 tests/PHPStan/Rules/Classes/LocalTypeAliasesRuleTest.php create mode 100644 tests/PHPStan/Rules/Classes/data/local-type-aliases.php diff --git a/conf/config.level0.neon b/conf/config.level0.neon index a615d9401f..4fa8630ac3 100644 --- a/conf/config.level0.neon +++ b/conf/config.level0.neon @@ -30,6 +30,7 @@ rules: - PHPStan\Rules\Classes\ExistingClassInTraitUseRule - PHPStan\Rules\Classes\InstantiationRule - PHPStan\Rules\Classes\InvalidPromotedPropertiesRule + #- PHPStan\Rules\Classes\LocalTypeAliasesRule - PHPStan\Rules\Classes\NewStaticRule - PHPStan\Rules\Classes\NonClassAttributeClassRule - PHPStan\Rules\Classes\TraitAttributeClassRule diff --git a/src/Reflection/ClassReflection.php b/src/Reflection/ClassReflection.php index 380be105cc..8d07684590 100644 --- a/src/Reflection/ClassReflection.php +++ b/src/Reflection/ClassReflection.php @@ -105,6 +105,9 @@ class ClassReflection implements ReflectionWithFilename /** @var array|null */ private ?array $typeAliases = null; + /** @var array */ + private static array $resolvingTypeAliasImports = []; + /** * @param \PHPStan\Reflection\ReflectionProvider $reflectionProvider * @param \PHPStan\Type\FileTypeMapper $fileTypeMapper @@ -769,35 +772,53 @@ public function getTypeAliases(): array $typeAliasImportTags = $resolvedPhpDoc !== null ? $resolvedPhpDoc->getTypeAliasImportTags() : []; $typeAliasTags = $resolvedPhpDoc !== null ? $resolvedPhpDoc->getTypeAliasTags() : []; - $importedAliases = array_map(function (TypeAliasImportTag $typeAliasImportTag): TypeAlias { + // 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(); $importedFrom = $typeAliasImportTag->getImportedFrom(); if (!($importedFrom instanceof ObjectType)) { - throw new \PHPStan\ShouldNotHappenException(sprintf('Cannot import type alias %s in scope of %s from non-class type %s.', $typeAliasImportTag->getImportedAlias(), $this->getName(), $typeAliasImportTag->getImportedFrom()->describe(VerbosityLevel::typeOnly()))); + return null; } $importedFromClassName = $importedFrom->getClassName(); + if (!$this->reflectionProvider->hasClass($importedFromClassName)) { + return null; + } + $importedFromReflection = $this->reflectionProvider->getClass($importedFromClassName); - $typeAliases = $importedFromReflection->getTypeAliases(); + try { + $typeAliases = $importedFromReflection->getTypeAliases(); + } catch (\PHPStan\Type\CircularTypeAliasDefinitionException $e) { + return TypeAlias::invalid(); + } if (!array_key_exists($importedAlias, $typeAliases)) { - throw new \PHPStan\ShouldNotHappenException(sprintf('Type alias %s imported in %s does not exist on %s.', $importedAlias, $this->getName(), $importedFromClassName)); + return null; } return $typeAliases[$importedAlias]; }, $typeAliasImportTags); - $localAliases = array_map(function (TypeAliasTag $typeAliasTag) use ($importedAliases): TypeAlias { - if (array_key_exists($typeAliasTag->getAlias(), $importedAliases)) { - throw new \PHPStan\ShouldNotHappenException(sprintf('Type alias %s overwrites imported type alias of the same name in scope of %s.', $typeAliasTag->getAlias(), $this->getName())); - } + unset(self::$resolvingTypeAliasImports[$this->getName()]); + $localAliases = array_map(static function (TypeAliasTag $typeAliasTag): TypeAlias { return $typeAliasTag->getTypeAlias(); }, $typeAliasTags); - $this->typeAliases = array_merge($importedAliases, $localAliases); + $this->typeAliases = array_filter( + array_merge($importedAliases, $localAliases), + static function (?TypeAlias $typeAlias): bool { + return $typeAlias !== null; + } + ); } return $this->typeAliases; diff --git a/src/Rules/Classes/LocalTypeAliasesRule.php b/src/Rules/Classes/LocalTypeAliasesRule.php new file mode 100644 index 0000000000..a47ceb7786 --- /dev/null +++ b/src/Rules/Classes/LocalTypeAliasesRule.php @@ -0,0 +1,150 @@ + + */ +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 Node\Stmt\ClassLike::class; + } + + /** @param Node\Stmt\ClassLike $node */ + public function processNode(Node $node, Scope $scope): array + { + if ($node->name === null) { + return []; + } + + $reflection = $this->reflectionProvider->getClass((string) $node->namespacedName); + $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(); + $importedFrom = $typeAliasImportTag->getImportedFrom(); + + if (!($importedFrom instanceof ObjectType)) { + $errors[] = RuleErrorBuilder::message(sprintf('Cannot import type alias %s from a non-class type %s.', $importedAlias, $importedFrom->describe(VerbosityLevel::typeOnly())))->build(); + continue; + } + + $importedFromClassName = $importedFrom->getClassName(); + 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/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 @@ +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) { diff --git a/src/Type/TypeAliasResolver.php b/src/Type/TypeAliasResolver.php index 9954a902ee..f51faca8b3 100644 --- a/src/Type/TypeAliasResolver.php +++ b/src/Type/TypeAliasResolver.php @@ -104,23 +104,28 @@ private function resolveLocalTypeAlias(string $aliasName, NameScope $nameScope): } if ($this->reflectionProvider->hasClass($nameScope->resolveStringName($aliasName))) { - throw new \PHPStan\ShouldNotHappenException(sprintf('Type alias %s already exists as a class in scope of %s.', $aliasName, $className)); + return null; } if (array_key_exists($aliasName, $this->globalTypeAliases)) { - throw new \PHPStan\ShouldNotHappenException(sprintf('Type alias %s used in scope of %s already exists as a global type alias.', $aliasName, $className)); + return null; } if (array_key_exists($aliasNameInClassScope, $this->inProcess)) { - throw new \PHPStan\ShouldNotHappenException(sprintf('Circular definition for type alias %s in scope of %s.', $aliasName, $className)); + // resolve circular reference as ErrorType to make it easier to detect + throw new \PHPStan\Type\CircularTypeAliasDefinitionException(); } $this->inProcess[$aliasNameInClassScope] = true; - $unresolvedAlias = $localTypeAliases[$aliasName]; - $resolvedAliasType = $unresolvedAlias->resolve($this->typeNodeResolver); - $this->resolvedLocalTypeAliases[$aliasNameInClassScope] = $resolvedAliasType; + 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; diff --git a/tests/PHPStan/Analyser/data/type-aliases.php b/tests/PHPStan/Analyser/data/type-aliases.php index 12a67402b1..b8311a7d53 100644 --- a/tests/PHPStan/Analyser/data/type-aliases.php +++ b/tests/PHPStan/Analyser/data/type-aliases.php @@ -26,8 +26,27 @@ class Bar /** * @phpstan-import-type ExportedTypeAlias from Bar as ReexportedTypeAlias + * @phpstan-import-type CircularTypeAliasImport2 from Qux + * @phpstan-type CircularTypeAliasImport1 CircularTypeAliasImport2 + * @property CircularTypeAliasImport1 $baz + * @property CircularTypeAliasImport2 $qux */ class Baz + { + + public function circularAlias() + { + assertType('*ERROR*', $this->baz); + assertType('*ERROR*', $this->qux); + } + + } + + /** + * @phpstan-import-type CircularTypeAliasImport1 from Baz + * @phpstan-type CircularTypeAliasImport2 CircularTypeAliasImport1 + */ + class Qux { } @@ -38,11 +57,21 @@ class Baz * @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 * @property GlobalTypeAlias $globalAliasProperty * @property LocalTypeAlias $localAliasProperty * @property ImportedTypeAlias $importedAliasProperty * @property ReexportedTypeAlias $reexportedAliasProperty * @property ScopedAlias $scopedAliasProperty + * @property RecursiveTypeAlias $recursiveAliasProperty + * @property CircularTypeAlias1 $circularAliasProperty */ class Foo { @@ -87,6 +116,26 @@ 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; @@ -99,5 +148,7 @@ public function __get(string $name) 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/Rules/Classes/LocalTypeAliasesRuleTest.php b/tests/PHPStan/Rules/Classes/LocalTypeAliasesRuleTest.php new file mode 100644 index 0000000000..b92159d66c --- /dev/null +++ b/tests/PHPStan/Rules/Classes/LocalTypeAliasesRuleTest.php @@ -0,0 +1,86 @@ + + */ +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 + { + if (!self::$useStaticReflectionProvider) { + self::markTestSkipped('Test requires static reflection.'); + } + + $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 from a non-class type int.', + 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 @@ + Date: Sun, 18 Apr 2021 18:32:59 +0200 Subject: [PATCH 13/25] Microoptimization --- src/Reflection/ClassReflection.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Reflection/ClassReflection.php b/src/Reflection/ClassReflection.php index 8d07684590..088b2f4662 100644 --- a/src/Reflection/ClassReflection.php +++ b/src/Reflection/ClassReflection.php @@ -769,8 +769,12 @@ public function getTypeAliases(): array { if ($this->typeAliases === null) { $resolvedPhpDoc = $this->getResolvedPhpDoc(); - $typeAliasImportTags = $resolvedPhpDoc !== null ? $resolvedPhpDoc->getTypeAliasImportTags() : []; - $typeAliasTags = $resolvedPhpDoc !== null ? $resolvedPhpDoc->getTypeAliasTags() : []; + if ($resolvedPhpDoc === null) { + return $this->typeAliases = []; + } + + $typeAliasImportTags = $resolvedPhpDoc->getTypeAliasImportTags(); + $typeAliasTags = $resolvedPhpDoc->getTypeAliasTags(); // prevent circular imports if (array_key_exists($this->getName(), self::$resolvingTypeAliasImports)) { From ce5a49ab0cb54d8a7d4b0fc84299b911a2ea3827 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 18 Apr 2021 18:33:15 +0200 Subject: [PATCH 14/25] resolvingClassTypeAliases should be unset here as well --- src/Type/TypeAliasResolver.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Type/TypeAliasResolver.php b/src/Type/TypeAliasResolver.php index f51faca8b3..41fd81dc18 100644 --- a/src/Type/TypeAliasResolver.php +++ b/src/Type/TypeAliasResolver.php @@ -85,6 +85,7 @@ private function resolveLocalTypeAlias(string $aliasName, NameScope $nameScope): $this->resolvingClassTypeAliases[$className] = true; if (!$this->reflectionProvider->hasClass($className)) { + unset($this->resolvingClassTypeAliases[$className]); return null; } From e2cfe2099617eda5ef9dab9f0c6b44b332623f2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pudil?= Date: Sun, 18 Apr 2021 18:42:04 +0200 Subject: [PATCH 15/25] fix LocalTypeAliasesRule registration --- conf/config.level0.neon | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/conf/config.level0.neon b/conf/config.level0.neon index 4fa8630ac3..728ddc8ec3 100644 --- a/conf/config.level0.neon +++ b/conf/config.level0.neon @@ -30,7 +30,6 @@ rules: - PHPStan\Rules\Classes\ExistingClassInTraitUseRule - PHPStan\Rules\Classes\InstantiationRule - PHPStan\Rules\Classes\InvalidPromotedPropertiesRule - #- PHPStan\Rules\Classes\LocalTypeAliasesRule - PHPStan\Rules\Classes\NewStaticRule - PHPStan\Rules\Classes\NonClassAttributeClassRule - PHPStan\Rules\Classes\TraitAttributeClassRule @@ -201,3 +200,10 @@ services: - class: PHPStan\Rules\Whitespace\FileWhitespaceRule + + - + class: PHPStan\Rules\Classes\LocalTypeAliasesRule + arguments: + globalTypeAliases: %typeAliases% + tags: + - phpstan.rules.rule From ec36298ccca95006e3b5e8529bc02034d38a7642 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pudil?= Date: Sun, 18 Apr 2021 18:42:25 +0200 Subject: [PATCH 16/25] remove obsolete data provider --- tests/PHPStan/Analyser/NodeScopeResolverTest.php | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index da082cb3b3..0b0021d99c 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -10380,13 +10380,6 @@ public function testFileAsserts( } } - public function dataTypeAliases(): array - { - require_once __DIR__ . '/data/type-aliases.php'; - - return $this->gatherAssertTypes(__DIR__ . '/data/type-aliases.php'); - } - /** * @param string $file * @return array From 10704635368ae8b6776e14794f53dd18b3c2b8ac Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 18 Apr 2021 18:50:03 +0200 Subject: [PATCH 17/25] Allow type aliases to be bypassed in SignatureMapParser --- src/Analyser/NameScope.php | 15 ++++++++++++++- .../TypeAliasesTypeNodeResolverExtension.php | 4 ++++ .../SignatureMap/SignatureMapParser.php | 2 +- 3 files changed, 19 insertions(+), 2 deletions(-) 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/TypeAlias/TypeAliasesTypeNodeResolverExtension.php b/src/PhpDoc/TypeAlias/TypeAliasesTypeNodeResolverExtension.php index fb8f42fab6..9dc8371715 100644 --- a/src/PhpDoc/TypeAlias/TypeAliasesTypeNodeResolverExtension.php +++ b/src/PhpDoc/TypeAlias/TypeAliasesTypeNodeResolverExtension.php @@ -23,6 +23,10 @@ public function __construct( public function resolve(TypeNode $typeNode, NameScope $nameScope): ?Type { + if ($nameScope->shouldBypassTypeAliases()) { + return null; + } + if ($typeNode instanceof IdentifierTypeNode) { $aliasName = $typeNode->name; return $this->typeAliasResolver->resolveTypeAlias($aliasName, $nameScope); 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()); } /** From 4fc1534b96b240452ecb53adee6fa35065c1b52f Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 18 Apr 2021 19:06:38 +0200 Subject: [PATCH 18/25] Fix LocalTypeAliasesRule --- src/Rules/Classes/LocalTypeAliasesRule.php | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/Rules/Classes/LocalTypeAliasesRule.php b/src/Rules/Classes/LocalTypeAliasesRule.php index a47ceb7786..8bbecf1ba7 100644 --- a/src/Rules/Classes/LocalTypeAliasesRule.php +++ b/src/Rules/Classes/LocalTypeAliasesRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Node\InClassNode; use PHPStan\PhpDoc\TypeNodeResolver; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; @@ -14,7 +15,7 @@ use PHPStan\Type\VerbosityLevel; /** - * @implements Rule + * @implements Rule */ class LocalTypeAliasesRule implements Rule { @@ -42,17 +43,12 @@ public function __construct( public function getNodeType(): string { - return Node\Stmt\ClassLike::class; + return InClassNode::class; } - /** @param Node\Stmt\ClassLike $node */ public function processNode(Node $node, Scope $scope): array { - if ($node->name === null) { - return []; - } - - $reflection = $this->reflectionProvider->getClass((string) $node->namespacedName); + $reflection = $node->getClassReflection(); $phpDoc = $reflection->getResolvedPhpDoc(); if ($phpDoc === null) { return []; From 22eb90a09b3b3fe587237f914645a537582aebe4 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 18 Apr 2021 19:08:32 +0200 Subject: [PATCH 19/25] LocalTypeAliasesRuleTest does not require static reflection --- tests/PHPStan/Rules/Classes/LocalTypeAliasesRuleTest.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/PHPStan/Rules/Classes/LocalTypeAliasesRuleTest.php b/tests/PHPStan/Rules/Classes/LocalTypeAliasesRuleTest.php index b92159d66c..368b7983f1 100644 --- a/tests/PHPStan/Rules/Classes/LocalTypeAliasesRuleTest.php +++ b/tests/PHPStan/Rules/Classes/LocalTypeAliasesRuleTest.php @@ -23,10 +23,6 @@ protected function getRule(): Rule public function testRule(): void { - if (!self::$useStaticReflectionProvider) { - self::markTestSkipped('Test requires static reflection.'); - } - $this->analyse([__DIR__ . '/data/local-type-aliases.php'], [ [ 'Type alias ExistingClassAlias already exists as a class in scope of LocalTypeAliases\Bar.', From b3fa3744f6cbe2d4b5fc12762f7e21d32ffaf848 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pudil?= Date: Sun, 18 Apr 2021 19:16:04 +0200 Subject: [PATCH 20/25] treat TypeAliasImportTag::$importedFrom as a class name string --- src/PhpDoc/PhpDocNodeResolver.php | 2 +- src/PhpDoc/Tag/TypeAliasImportTag.php | 6 +++--- src/Reflection/ClassReflection.php | 13 +++++-------- src/Rules/Classes/LocalTypeAliasesRule.php | 10 +--------- .../Rules/Classes/LocalTypeAliasesRuleTest.php | 2 +- 5 files changed, 11 insertions(+), 22 deletions(-) diff --git a/src/PhpDoc/PhpDocNodeResolver.php b/src/PhpDoc/PhpDocNodeResolver.php index bdd5cf3b84..15a67782df 100644 --- a/src/PhpDoc/PhpDocNodeResolver.php +++ b/src/PhpDoc/PhpDocNodeResolver.php @@ -395,7 +395,7 @@ public function resolveTypeAliasImportTags(PhpDocNode $phpDocNode, NameScope $na foreach (['@psalm-import-type', '@phpstan-import-type'] as $tagName) { foreach ($phpDocNode->getTypeAliasImportTagValues($tagName) as $typeAliasImportTagValue) { $importedAlias = $typeAliasImportTagValue->importedAlias; - $importedFrom = $this->typeNodeResolver->resolve($typeAliasImportTagValue->importedFrom, $nameScope); + $importedFrom = $typeAliasImportTagValue->importedFrom->name; $importedAs = $typeAliasImportTagValue->importedAs; $resolved[$importedAs ?? $importedAlias] = new TypeAliasImportTag($importedAlias, $importedFrom, $importedAs); } diff --git a/src/PhpDoc/Tag/TypeAliasImportTag.php b/src/PhpDoc/Tag/TypeAliasImportTag.php index 6acefe718d..62fb7cdb77 100644 --- a/src/PhpDoc/Tag/TypeAliasImportTag.php +++ b/src/PhpDoc/Tag/TypeAliasImportTag.php @@ -7,11 +7,11 @@ final class TypeAliasImportTag private string $importedAlias; - private \PHPStan\Type\Type $importedFrom; + private string $importedFrom; private ?string $importedAs; - public function __construct(string $importedAlias, \PHPStan\Type\Type $importedFrom, ?string $importedAs) + public function __construct(string $importedAlias, string $importedFrom, ?string $importedAs) { $this->importedAlias = $importedAlias; $this->importedFrom = $importedFrom; @@ -23,7 +23,7 @@ public function getImportedAlias(): string return $this->importedAlias; } - public function getImportedFrom(): \PHPStan\Type\Type + public function getImportedFrom(): string { return $this->importedFrom; } diff --git a/src/Reflection/ClassReflection.php b/src/Reflection/ClassReflection.php index 088b2f4662..9006f67773 100644 --- a/src/Reflection/ClassReflection.php +++ b/src/Reflection/ClassReflection.php @@ -3,6 +3,7 @@ namespace PHPStan\Reflection; use Attribute; +use PHPStan\Analyser\NameScope; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionClass; use PHPStan\Php\PhpVersion; use PHPStan\PhpDoc\ResolvedPhpDocBlock; @@ -23,7 +24,6 @@ use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeScope; -use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\TypeAlias; use PHPStan\Type\VerbosityLevel; @@ -776,6 +776,8 @@ public function getTypeAliases(): array $typeAliasImportTags = $resolvedPhpDoc->getTypeAliasImportTags(); $typeAliasTags = $resolvedPhpDoc->getTypeAliasTags(); + $nameScope = $resolvedPhpDoc->getNullableNameScope() ?? new NameScope(null, []); + // prevent circular imports if (array_key_exists($this->getName(), self::$resolvingTypeAliasImports)) { throw new \PHPStan\Type\CircularTypeAliasDefinitionException(); @@ -783,15 +785,10 @@ public function getTypeAliases(): array self::$resolvingTypeAliasImports[$this->getName()] = true; - $importedAliases = array_map(function (TypeAliasImportTag $typeAliasImportTag): ?TypeAlias { + $importedAliases = array_map(function (TypeAliasImportTag $typeAliasImportTag) use ($nameScope): ?TypeAlias { $importedAlias = $typeAliasImportTag->getImportedAlias(); - $importedFrom = $typeAliasImportTag->getImportedFrom(); - - if (!($importedFrom instanceof ObjectType)) { - return null; - } + $importedFromClassName = $nameScope->resolveStringName($typeAliasImportTag->getImportedFrom()); - $importedFromClassName = $importedFrom->getClassName(); if (!$this->reflectionProvider->hasClass($importedFromClassName)) { return null; } diff --git a/src/Rules/Classes/LocalTypeAliasesRule.php b/src/Rules/Classes/LocalTypeAliasesRule.php index 8bbecf1ba7..ab86adecd3 100644 --- a/src/Rules/Classes/LocalTypeAliasesRule.php +++ b/src/Rules/Classes/LocalTypeAliasesRule.php @@ -10,9 +10,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\ErrorType; -use PHPStan\Type\ObjectType; use PHPStan\Type\TypeTraverser; -use PHPStan\Type\VerbosityLevel; /** * @implements Rule @@ -71,14 +69,8 @@ public function processNode(Node $node, Scope $scope): array foreach ($phpDoc->getTypeAliasImportTags() as $typeAliasImportTag) { $aliasName = $typeAliasImportTag->getImportedAs() ?? $typeAliasImportTag->getImportedAlias(); $importedAlias = $typeAliasImportTag->getImportedAlias(); - $importedFrom = $typeAliasImportTag->getImportedFrom(); + $importedFromClassName = $resolveName($typeAliasImportTag->getImportedFrom()); - if (!($importedFrom instanceof ObjectType)) { - $errors[] = RuleErrorBuilder::message(sprintf('Cannot import type alias %s from a non-class type %s.', $importedAlias, $importedFrom->describe(VerbosityLevel::typeOnly())))->build(); - continue; - } - - $importedFromClassName = $importedFrom->getClassName(); if (!$this->reflectionProvider->hasClass($importedFromClassName)) { $errors[] = RuleErrorBuilder::message(sprintf('Cannot import type alias %s: class %s does not exist.', $importedAlias, $importedFromClassName))->build(); continue; diff --git a/tests/PHPStan/Rules/Classes/LocalTypeAliasesRuleTest.php b/tests/PHPStan/Rules/Classes/LocalTypeAliasesRuleTest.php index 368b7983f1..76213ff049 100644 --- a/tests/PHPStan/Rules/Classes/LocalTypeAliasesRuleTest.php +++ b/tests/PHPStan/Rules/Classes/LocalTypeAliasesRuleTest.php @@ -45,7 +45,7 @@ public function testRule(): void 22, ], [ - 'Cannot import type alias ImportedAliasFromNonClass from a non-class type int.', + 'Cannot import type alias ImportedAliasFromNonClass: class LocalTypeAliases\int does not exist.', 37, ], [ From c8f3f4f8813e322c2d85a19fa52de945dd5b54e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pudil?= Date: Sun, 18 Apr 2021 19:26:18 +0200 Subject: [PATCH 21/25] resolve TypeAliasImportTag::$importedFrom in the name scope when resolving the doc block --- src/PhpDoc/PhpDocNodeResolver.php | 2 +- src/Reflection/ClassReflection.php | 7 ++----- src/Rules/Classes/LocalTypeAliasesRule.php | 2 +- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/PhpDoc/PhpDocNodeResolver.php b/src/PhpDoc/PhpDocNodeResolver.php index 15a67782df..aa188fe918 100644 --- a/src/PhpDoc/PhpDocNodeResolver.php +++ b/src/PhpDoc/PhpDocNodeResolver.php @@ -395,7 +395,7 @@ public function resolveTypeAliasImportTags(PhpDocNode $phpDocNode, NameScope $na foreach (['@psalm-import-type', '@phpstan-import-type'] as $tagName) { foreach ($phpDocNode->getTypeAliasImportTagValues($tagName) as $typeAliasImportTagValue) { $importedAlias = $typeAliasImportTagValue->importedAlias; - $importedFrom = $typeAliasImportTagValue->importedFrom->name; + $importedFrom = $nameScope->resolveStringName($typeAliasImportTagValue->importedFrom->name); $importedAs = $typeAliasImportTagValue->importedAs; $resolved[$importedAs ?? $importedAlias] = new TypeAliasImportTag($importedAlias, $importedFrom, $importedAs); } diff --git a/src/Reflection/ClassReflection.php b/src/Reflection/ClassReflection.php index 9006f67773..a549a55879 100644 --- a/src/Reflection/ClassReflection.php +++ b/src/Reflection/ClassReflection.php @@ -3,7 +3,6 @@ namespace PHPStan\Reflection; use Attribute; -use PHPStan\Analyser\NameScope; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionClass; use PHPStan\Php\PhpVersion; use PHPStan\PhpDoc\ResolvedPhpDocBlock; @@ -776,8 +775,6 @@ public function getTypeAliases(): array $typeAliasImportTags = $resolvedPhpDoc->getTypeAliasImportTags(); $typeAliasTags = $resolvedPhpDoc->getTypeAliasTags(); - $nameScope = $resolvedPhpDoc->getNullableNameScope() ?? new NameScope(null, []); - // prevent circular imports if (array_key_exists($this->getName(), self::$resolvingTypeAliasImports)) { throw new \PHPStan\Type\CircularTypeAliasDefinitionException(); @@ -785,9 +782,9 @@ public function getTypeAliases(): array self::$resolvingTypeAliasImports[$this->getName()] = true; - $importedAliases = array_map(function (TypeAliasImportTag $typeAliasImportTag) use ($nameScope): ?TypeAlias { + $importedAliases = array_map(function (TypeAliasImportTag $typeAliasImportTag): ?TypeAlias { $importedAlias = $typeAliasImportTag->getImportedAlias(); - $importedFromClassName = $nameScope->resolveStringName($typeAliasImportTag->getImportedFrom()); + $importedFromClassName = $typeAliasImportTag->getImportedFrom(); if (!$this->reflectionProvider->hasClass($importedFromClassName)) { return null; diff --git a/src/Rules/Classes/LocalTypeAliasesRule.php b/src/Rules/Classes/LocalTypeAliasesRule.php index ab86adecd3..d87fd12b46 100644 --- a/src/Rules/Classes/LocalTypeAliasesRule.php +++ b/src/Rules/Classes/LocalTypeAliasesRule.php @@ -69,7 +69,7 @@ public function processNode(Node $node, Scope $scope): array foreach ($phpDoc->getTypeAliasImportTags() as $typeAliasImportTag) { $aliasName = $typeAliasImportTag->getImportedAs() ?? $typeAliasImportTag->getImportedAlias(); $importedAlias = $typeAliasImportTag->getImportedAlias(); - $importedFromClassName = $resolveName($typeAliasImportTag->getImportedFrom()); + $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(); From 25013e6e1fbfa4ccbd65bd10b1268d30a7c3afa4 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 18 Apr 2021 20:10:07 +0200 Subject: [PATCH 22/25] Resolve type aliases directly in resolveIdentifierTypeNode, not through an extension --- conf/config.neon | 5 --- .../TypeAliasesTypeNodeResolverExtension.php | 37 ------------------- src/PhpDoc/TypeNodeResolver.php | 14 +++++++ 3 files changed, 14 insertions(+), 42 deletions(-) delete mode 100644 src/PhpDoc/TypeAlias/TypeAliasesTypeNodeResolverExtension.php diff --git a/conf/config.neon b/conf/config.neon index 1eafd3269f..0464df88d3 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -366,11 +366,6 @@ services: - class: PHPStan\PhpDoc\ConstExprNodeResolver - - - class: PHPStan\PhpDoc\TypeAlias\TypeAliasesTypeNodeResolverExtension - tags: - - phpstan.phpDoc.typeNodeResolverExtension - - class: PHPStan\PhpDoc\TypeNodeResolver diff --git a/src/PhpDoc/TypeAlias/TypeAliasesTypeNodeResolverExtension.php b/src/PhpDoc/TypeAlias/TypeAliasesTypeNodeResolverExtension.php deleted file mode 100644 index 9dc8371715..0000000000 --- a/src/PhpDoc/TypeAlias/TypeAliasesTypeNodeResolverExtension.php +++ /dev/null @@ -1,37 +0,0 @@ -typeAliasResolver = $typeAliasResolver; - } - - public function resolve(TypeNode $typeNode, NameScope $nameScope): ?Type - { - if ($nameScope->shouldBypassTypeAliases()) { - return null; - } - - if ($typeNode instanceof IdentifierTypeNode) { - $aliasName = $typeNode->name; - return $this->typeAliasResolver->resolveTypeAlias($aliasName, $nameScope); - } - 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); + } + } From 334960c94d19b03a91c1d2a5ecb34d5432133c17 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 18 Apr 2021 20:24:47 +0200 Subject: [PATCH 23/25] Bump memory limit for tests-fast-static-reflection a bit --- build.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.xml b/build.xml index cd530ab15d..91632acb53 100644 --- a/build.xml +++ b/build.xml @@ -221,7 +221,7 @@ checkreturn="true" > - + From 9a8738dbe769bde0e8becf2ca1a4d885dd6be467 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 18 Apr 2021 20:26:47 +0200 Subject: [PATCH 24/25] Microoptimization --- src/Type/TypeAliasResolver.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Type/TypeAliasResolver.php b/src/Type/TypeAliasResolver.php index 41fd81dc18..a87dbc6f9b 100644 --- a/src/Type/TypeAliasResolver.php +++ b/src/Type/TypeAliasResolver.php @@ -77,6 +77,12 @@ private function resolveLocalTypeAlias(string $aliasName, NameScope $nameScope): 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; @@ -98,12 +104,6 @@ private function resolveLocalTypeAlias(string $aliasName, NameScope $nameScope): return null; } - $aliasNameInClassScope = $className . '::' . $aliasName; - - if (array_key_exists($aliasNameInClassScope, $this->resolvedLocalTypeAliases)) { - return $this->resolvedLocalTypeAliases[$aliasNameInClassScope]; - } - if ($this->reflectionProvider->hasClass($nameScope->resolveStringName($aliasName))) { return null; } From 31ea9a21042a224c964052ac07892e4697dfc4f0 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 18 Apr 2021 20:30:09 +0200 Subject: [PATCH 25/25] Regression test --- tests/PHPStan/Analyser/data/type-aliases.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/PHPStan/Analyser/data/type-aliases.php b/tests/PHPStan/Analyser/data/type-aliases.php index b8311a7d53..188a8ea6c1 100644 --- a/tests/PHPStan/Analyser/data/type-aliases.php +++ b/tests/PHPStan/Analyser/data/type-aliases.php @@ -65,6 +65,7 @@ class Qux * @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 @@ -141,6 +142,12 @@ public function __get(string $name) return null; } + /** @param int $int */ + public function testIntAlias($int) + { + assertType('int', $int); + } + } assertType('int|string', (new Foo)->globalAliasProperty);