diff --git a/src/Analyser/IntermediaryNameScope.php b/src/Analyser/IntermediaryNameScope.php index ef7aa4c285..1ac0c1a348 100644 --- a/src/Analyser/IntermediaryNameScope.php +++ b/src/Analyser/IntermediaryNameScope.php @@ -3,6 +3,7 @@ namespace PHPStan\Analyser; use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; final class IntermediaryNameScope { @@ -13,7 +14,7 @@ final class IntermediaryNameScope * @param array $uses alias(string) => fullName(string) * @param array $templatePhpDocNodes * @param array $constUses alias(string) => fullName(string) - * @param array $typeAliasesMap + * @param array $typeAliasesMap * @param array{string, string, string, string|null, string|null}|null $traitData */ public function __construct( @@ -129,7 +130,7 @@ public function getParent(): ?self } /** - * @return array + * @return array */ public function getTypeAliasesMap(): array { diff --git a/src/Analyser/NameScope.php b/src/Analyser/NameScope.php index 685927eb27..41ebee3f10 100644 --- a/src/Analyser/NameScope.php +++ b/src/Analyser/NameScope.php @@ -3,6 +3,7 @@ namespace PHPStan\Analyser; use PHPStan\PhpDoc\Tag\TemplateTag; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\Type; @@ -31,7 +32,7 @@ final class NameScope * @param array $uses alias(string) => fullName(string) * @param array $constUses alias(string) => fullName(string) * @param array $templateTags - * @param array $typeAliasesMap + * @param array $typeAliasesMap */ public function __construct( private ?string $namespace, @@ -264,4 +265,13 @@ public function hasTypeAlias(string $alias): bool return array_key_exists($alias, $this->typeAliasesMap); } + public function getTypeAlias(string $alias): Type + { + if (!$this->hasTypeAlias($alias)) { + throw new ShouldNotHappenException(sprintf('Type alias %s not in NameScope', $alias)); + } + + return $this->typeAliasesMap[$alias]; + } + } diff --git a/src/PhpDoc/Tag/TypeAliasTag.php b/src/PhpDoc/Tag/TypeAliasTag.php index d5cd10e5d6..edd71d29e6 100644 --- a/src/PhpDoc/Tag/TypeAliasTag.php +++ b/src/PhpDoc/Tag/TypeAliasTag.php @@ -25,6 +25,11 @@ public function getAliasName(): string return $this->aliasName; } + public function getTypeNode(): TypeNode + { + return $this->typeNode; + } + public function getTypeAlias(): TypeAlias { return new TypeAlias( diff --git a/src/Testing/PHPStanTestCase.php b/src/Testing/PHPStanTestCase.php index 89ee716a19..52d7b983aa 100644 --- a/src/Testing/PHPStanTestCase.php +++ b/src/Testing/PHPStanTestCase.php @@ -23,7 +23,6 @@ use PHPStan\Parser\Parser; use PHPStan\Php\ComposerPhpVersionFactory; use PHPStan\Php\PhpVersion; -use PHPStan\PhpDoc\TypeNodeResolver; use PHPStan\PhpDoc\TypeStringResolver; use PHPStan\Reflection\AttributeReflectionFactory; use PHPStan\Reflection\InitializerExprTypeResolver; @@ -183,7 +182,6 @@ public static function createTypeAliasResolver(array $globalTypeAliases, Reflect return new UsefulTypeAliasResolver( $globalTypeAliases, $container->getByType(TypeStringResolver::class), - $container->getByType(TypeNodeResolver::class), $reflectionProvider, ); } diff --git a/src/Type/FileTypeMapper.php b/src/Type/FileTypeMapper.php index f86923f24b..f7844809c9 100644 --- a/src/Type/FileTypeMapper.php +++ b/src/Type/FileTypeMapper.php @@ -19,9 +19,11 @@ use PHPStan\PhpDoc\PhpDocStringResolver; use PHPStan\PhpDoc\ResolvedPhpDocBlock; use PHPStan\PhpDoc\Tag\TemplateTag; +use PHPStan\PhpDoc\TypeNodeResolver; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode; use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Reflection\ReflectionProvider\ReflectionProviderProvider; use PHPStan\ShouldNotHappenException; use PHPStan\Type\Generic\GenericObjectType; @@ -76,6 +78,7 @@ public function __construct( private Parser $phpParser, private PhpDocStringResolver $phpDocStringResolver, private PhpDocNodeResolver $phpDocNodeResolver, + private TypeNodeResolver $typeNodeResolver, private AnonymousClassNameHelper $anonymousClassNameHelper, private FileHelper $fileHelper, private Cache $cache, @@ -217,6 +220,7 @@ public function getNameScope( $phpDocTemplateTypes = []; $templateTags = []; + $typeAliases = []; $reflectionProvider = $this->reflectionProviderProvider->getReflectionProvider(); foreach (array_reverse($parents) as $parent) { $nameScope = new NameScope( @@ -226,7 +230,21 @@ public function getNameScope( $parent->getFunctionName(), new TemplateTypeMap($phpDocTemplateTypes), $templateTags, - $parent->getTypeAliasesMap(), + $typeAliases, + $parent->shouldBypassTypeAliases(), + $parent->getConstUses(), + $parent->getClassNameForTypeAlias(), + ); + $resolvedTypeAliases = $this->resolveTypeAliases($parent->getTypeAliasesMap(), $nameScope); + $typeAliases = array_merge($typeAliases, $resolvedTypeAliases); + $nameScope = new NameScope( + $parent->getNamespace(), + $parent->getUses(), + $parent->getClassName(), + $parent->getFunctionName(), + new TemplateTypeMap($phpDocTemplateTypes), + $templateTags, + $typeAliases, $parent->shouldBypassTypeAliases(), $parent->getConstUses(), $parent->getClassNameForTypeAlias(), @@ -307,7 +325,7 @@ public function getNameScope( $intermediaryNameScope->getFunctionName(), new TemplateTypeMap($phpDocTemplateTypes), $templateTags, - $intermediaryNameScope->getTypeAliasesMap(), + $typeAliases, $intermediaryNameScope->shouldBypassTypeAliases(), $intermediaryNameScope->getConstUses(), $intermediaryNameScope->getClassNameForTypeAlias(), @@ -317,6 +335,36 @@ public function getNameScope( } } + /** + * @param array $typeAliasesMap + * @return array + */ + private function resolveTypeAliases(array $typeAliasesMap, NameScope $nameScope): array + { + $aliases = []; + foreach ($typeAliasesMap as $localAliasName => $alias) { + if (is_array($alias)) { + [$aliasName, $importedFrom] = $alias; + $importedFrom = $nameScope->resolveStringName($importedFrom); + $reflectionProvider = $this->reflectionProviderProvider->getReflectionProvider(); + if (!$reflectionProvider->hasClass($importedFrom)) { + continue; + } + $importedFromClassReflection = $reflectionProvider->getClass($importedFrom); + $classTypeAliaseses = $importedFromClassReflection->getTypeAliases(); + if (!array_key_exists($aliasName, $classTypeAliaseses)) { + continue; + } + + $aliases[$localAliasName] = $classTypeAliaseses[$aliasName]->resolve($this->typeNodeResolver); + continue; + } + + $aliases[$localAliasName] = $this->typeNodeResolver->resolve($alias, $nameScope); + } + return $aliases; + } + /** * @return array{array} */ @@ -324,7 +372,7 @@ private function getNameScopeMap(string $fileName): array { if (!isset($this->memoryCache[$fileName])) { $cacheKey = sprintf('ftm-%s', $fileName); - $variableCacheKey = 'v2'; + $variableCacheKey = 'v3'; $cached = $this->loadCachedPhpDocNodeMap($cacheKey, $variableCacheKey); if ($cached === null) { [$nameScopeMap, $files] = $this->createPhpDocNodeMap($fileName, null, null, [], $fileName); @@ -399,7 +447,7 @@ private function createPhpDocNodeMap(string $fileName, ?string $lookForTrait, ?s /** @var array $typeMapStack */ $typeMapStack = []; - /** @var array> $typeAliasStack */ + /** @var array> $typeAliasStack */ $typeAliasStack = []; /** @var string[] $classStack */ @@ -735,19 +783,19 @@ private function chooseTemplateTagValueNodesByPriority(array $tags): array } /** - * @return array + * @return array */ private function getTypeAliasesMap(PhpDocNode $phpDocNode): array { $nameScope = new NameScope(null, []); $aliasesMap = []; - foreach (array_keys($this->phpDocNodeResolver->resolveTypeAliasImportTags($phpDocNode, $nameScope)) as $key) { - $aliasesMap[$key] = true; + foreach ($this->phpDocNodeResolver->resolveTypeAliasImportTags($phpDocNode, $nameScope) as $key => $typeAliasImportTag) { + $aliasesMap[$key] = [$typeAliasImportTag->getImportedAlias(), $typeAliasImportTag->getImportedFrom()]; } - foreach (array_keys($this->phpDocNodeResolver->resolveTypeAliasTags($phpDocNode, $nameScope)) as $key) { - $aliasesMap[$key] = true; + foreach ($this->phpDocNodeResolver->resolveTypeAliasTags($phpDocNode, $nameScope) as $key => $typeAlias) { + $aliasesMap[$key] = $typeAlias->getTypeNode(); } return $aliasesMap; diff --git a/src/Type/UsefulTypeAliasResolver.php b/src/Type/UsefulTypeAliasResolver.php index 6577289ff9..e2b44070dd 100644 --- a/src/Type/UsefulTypeAliasResolver.php +++ b/src/Type/UsefulTypeAliasResolver.php @@ -5,7 +5,6 @@ use PHPStan\Analyser\NameScope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\PhpDoc\TypeNodeResolver; use PHPStan\PhpDoc\TypeStringResolver; use PHPStan\Reflection\ReflectionProvider; use PHPStan\ShouldNotHappenException; @@ -19,12 +18,6 @@ final class UsefulTypeAliasResolver implements TypeAliasResolver /** @var array */ private array $resolvedGlobalTypeAliases = []; - /** @var array */ - private array $resolvedLocalTypeAliases = []; - - /** @var array */ - private array $resolvingClassTypeAliases = []; - /** @var array */ private array $inProcess = []; @@ -35,7 +28,6 @@ public function __construct( #[AutowiredParameter(ref: '%typeAliases%')] private array $globalTypeAliases, private TypeStringResolver $typeStringResolver, - private TypeNodeResolver $typeNodeResolver, private ReflectionProvider $reflectionProvider, ) { @@ -73,56 +65,7 @@ private function resolveLocalTypeAlias(string $aliasName, NameScope $nameScope): return null; } - $className = $nameScope->getClassNameForTypeAlias(); - if ($className === null) { - return null; - } - - $aliasNameInClassScope = $className . '::' . $aliasName; - - if (array_key_exists($aliasNameInClassScope, $this->resolvedLocalTypeAliases)) { - return $this->resolvedLocalTypeAliases[$aliasNameInClassScope]; - } - - // prevent infinite recursion - if (array_key_exists($className, $this->resolvingClassTypeAliases)) { - return null; - } - - $this->resolvingClassTypeAliases[$className] = true; - - if (!$this->reflectionProvider->hasClass($className)) { - unset($this->resolvingClassTypeAliases[$className]); - return null; - } - - $classReflection = $this->reflectionProvider->getClass($className); - $localTypeAliases = $classReflection->getTypeAliases(); - - unset($this->resolvingClassTypeAliases[$className]); - - if (!array_key_exists($aliasName, $localTypeAliases)) { - return null; - } - - if (array_key_exists($aliasNameInClassScope, $this->inProcess)) { - // resolve circular reference as ErrorType to make it easier to detect - throw new CircularTypeAliasDefinitionException(); - } - - $this->inProcess[$aliasNameInClassScope] = true; - - try { - $unresolvedAlias = $localTypeAliases[$aliasName]; - $resolvedAliasType = $unresolvedAlias->resolve($this->typeNodeResolver); - } catch (CircularTypeAliasDefinitionException) { - $resolvedAliasType = new CircularTypeAliasErrorType(); - } - - $this->resolvedLocalTypeAliases[$aliasNameInClassScope] = $resolvedAliasType; - unset($this->inProcess[$aliasNameInClassScope]); - - return $resolvedAliasType; + return $nameScope->getTypeAlias($aliasName); } private function resolveGlobalTypeAlias(string $aliasName, NameScope $nameScope): ?Type diff --git a/tests/PHPStan/Analyser/nsrt/bug-11314.php b/tests/PHPStan/Analyser/nsrt/bug-11314.php new file mode 100644 index 0000000000..67731a56b4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11314.php @@ -0,0 +1,106 @@ +breed); + } +} + +$cat = new Cat(); +assertType("'British Shorthair'|'Maine Coon'|'Siamese'", $cat->breed); + +/** + * @phpstan-import-type Breed from Cat + * + * @template T of Breed + */ +class Cat2 +{ + /** + * @var Breed + */ + public string $breed; // Should be of type Breed, but "@template T of Breed" removes the type + + public function doFoo(): void + { + assertType("'British Shorthair'|'Maine Coon'|'Siamese'", $this->breed); + } +} + +$cat2 = new Cat2(); +assertType("'British Shorthair'|'Maine Coon'|'Siamese'", $cat2->breed); + +/** + * @phpstan-import-type Breed from Cat + */ +class Cat3 +{ + /** + * @var Breed + */ + public string $breed; // Here it works without the "@template" + + public function doFoo(): void + { + assertType("'British Shorthair'|'Maine Coon'|'Siamese'", $this->breed); + } +} + +$cat3 = new Cat3(); +assertType("'British Shorthair'|'Maine Coon'|'Siamese'", $cat3->breed); + +/** + * @phpstan-type Breed 'Siamese'|'British Shorthair'|'Maine Coon' + * + * @template T of Breed + */ +class Cat4 +{ + /** + * @var Breed + */ + public string $breed; // Should be of type Breed, but "@template T of Breed" removes the type + + public function doFoo(): void + { + assertType("'British Shorthair'|'Maine Coon'|'Siamese'", $this->breed); + } +} + +$cat4 = new Cat4(); +assertType("'British Shorthair'|'Maine Coon'|'Siamese'", $cat4->breed); + +/** + * @phpstan-type Breed 'Siamese'|'British Shorthair'|'Maine Coon' + * + * @template T of Breed + */ +class Cat5 +{ + /** + * @var T + */ + public string $breed; // Should be of type Breed, but "@template T of Breed" removes the type + + public function doFoo(): void + { + assertType("T of 'British Shorthair'|'Maine Coon'|'Siamese' (class Bug11314\Cat5, argument)", $this->breed); + } +} + +$cat5 = new Cat5(); +assertType("'British Shorthair'|'Maine Coon'|'Siamese'", $cat5->breed); diff --git a/tests/PHPStan/Analyser/nsrt/bug-7152.php b/tests/PHPStan/Analyser/nsrt/bug-7152.php new file mode 100644 index 0000000000..9c3a2d9df7 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7152.php @@ -0,0 +1,29 @@ + + */ +class Root +{ +} + +/** + * @phpstan-type Foo array + * @template T of Foo + * @extends Root + */ +class Middle extends Root { + + /** @var T */ + public $t; + + public function doFoo(): void + { + assertType('T of array (class Bug7152\Middle, argument)', $this->t); + } + +}