diff --git a/src/PhpDoc/PhpDocNodeResolver.php b/src/PhpDoc/PhpDocNodeResolver.php index 845b9d3149..4aa2730423 100644 --- a/src/PhpDoc/PhpDocNodeResolver.php +++ b/src/PhpDoc/PhpDocNodeResolver.php @@ -279,7 +279,7 @@ public function resolveTemplateTags(PhpDocNode $phpDocNode, NameScope $nameScope $resolved[$valueNode->name] = new TemplateTag( $valueNode->name, - $valueNode->bound !== null ? $this->typeNodeResolver->resolve($valueNode->bound, $nameScope->unsetTemplateType($valueNode->name)) : new MixedType(), + $valueNode->bound !== null ? $this->typeNodeResolver->resolve($valueNode->bound, $nameScope->unsetTemplateType($valueNode->name)) : new MixedType(true), $variance, ); $resolvedPrefix[$valueNode->name] = $prefix; diff --git a/src/Reflection/ClassReflection.php b/src/Reflection/ClassReflection.php index 73f33b9a56..695b46dff9 100644 --- a/src/Reflection/ClassReflection.php +++ b/src/Reflection/ClassReflection.php @@ -181,7 +181,8 @@ public function getParentClass(): ?ClassReflection if ($this->isGeneric()) { $extendedType = TemplateTypeHelper::resolveTemplateTypes( $extendedType, - $this->getActiveTemplateTypeMap(), + $this->getPossiblyIncompleteActiveTemplateTypeMap(), + true, ); } @@ -195,7 +196,7 @@ public function getParentClass(): ?ClassReflection $parentReflection = $this->reflectionProvider->getClass($parentClass->getName()); if ($parentReflection->isGeneric()) { return $parentReflection->withTypes( - array_values($parentReflection->getTemplateTypeMap()->resolveToBounds()->getTypes()), + array_values($parentReflection->getTemplateTypeMap()->map(static fn (): Type => new ErrorType())->getTypes()), ); } @@ -224,7 +225,7 @@ public function getDisplayName(bool $withTemplateTypes = true): string return $name; } - return $name . '<' . implode(',', array_map(static fn (Type $type): string => $type->describe(VerbosityLevel::typeOnly()), $this->resolvedTemplateTypeMap->getTypes())) . '>'; + return $name . '<' . implode(',', array_map(static fn (Type $type): string => $type->describe(VerbosityLevel::typeOnly()), $this->getActiveTemplateTypeMap()->getTypes())) . '>'; } public function getCacheKey(): string @@ -728,7 +729,8 @@ public function getImmediateInterfaces(): array if ($this->isGeneric()) { $implementedType = TemplateTypeHelper::resolveTemplateTypes( $implementedType, - $this->getActiveTemplateTypeMap(), + $this->getPossiblyIncompleteActiveTemplateTypeMap(), + true, ); } @@ -743,7 +745,7 @@ public function getImmediateInterfaces(): array if ($immediateInterface->isGeneric()) { $immediateInterfaces[$immediateInterface->getName()] = $immediateInterface->withTypes( - array_values($immediateInterface->getTemplateTypeMap()->resolveToBounds()->getTypes()), + array_values($immediateInterface->getTemplateTypeMap()->map(static fn (): Type => new ErrorType())->getTypes()), ); continue; } @@ -1057,6 +1059,26 @@ public function getTemplateTypeMap(): TemplateTypeMap } public function getActiveTemplateTypeMap(): TemplateTypeMap + { + $resolved = $this->resolvedTemplateTypeMap; + if ($resolved !== null) { + $templateTypeMap = $this->getTemplateTypeMap(); + return $resolved->map(static function (string $name, Type $type) use ($templateTypeMap): Type { + if ($type instanceof ErrorType) { + $templateType = $templateTypeMap->getType($name); + if ($templateType !== null) { + return TemplateTypeHelper::resolveToBounds($templateType); + } + } + + return $type; + }); + } + + return $this->getTemplateTypeMap(); + } + + public function getPossiblyIncompleteActiveTemplateTypeMap(): TemplateTypeMap { return $this->resolvedTemplateTypeMap ?? $this->getTemplateTypeMap(); } diff --git a/src/Type/FileTypeMapper.php b/src/Type/FileTypeMapper.php index a72aa670ee..228adf69e5 100644 --- a/src/Type/FileTypeMapper.php +++ b/src/Type/FileTypeMapper.php @@ -180,7 +180,7 @@ private function resolvePhpDocStringToDocNode(string $phpDocString): PhpDocNode private function getNameScopeMap(string $fileName): array { if (!isset($this->memoryCache[$fileName])) { - $cacheKey = sprintf('%s-phpdocstring-v20-template-tags', $fileName); + $cacheKey = sprintf('%s-phpdocstring-v21-explicit-mixed', $fileName); $variableCacheKey = sprintf('%s-%s', implode(',', array_map(static fn (array $file): string => sprintf('%s-%d', $file['filename'], $file['modifiedTime']), $this->getCachedDependentFilesWithTimestamps($fileName))), $this->phpVersion->getVersionString()); $map = $this->cache->load($cacheKey, $variableCacheKey); diff --git a/src/Type/Generic/TemplateTypeHelper.php b/src/Type/Generic/TemplateTypeHelper.php index b8d15d47e1..30ec16a547 100644 --- a/src/Type/Generic/TemplateTypeHelper.php +++ b/src/Type/Generic/TemplateTypeHelper.php @@ -16,16 +16,16 @@ class TemplateTypeHelper /** * Replaces template types with standin types */ - public static function resolveTemplateTypes(Type $type, TemplateTypeMap $standins): Type + public static function resolveTemplateTypes(Type $type, TemplateTypeMap $standins, bool $keepErrorTypes = false): Type { - return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($standins): Type { + return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($standins, $keepErrorTypes): Type { if ($type instanceof TemplateType && !$type->isArgument()) { $newType = $standins->getType($type->getName()); if ($newType === null) { return $traverse($type); } - if ($newType instanceof ErrorType) { + if ($newType instanceof ErrorType && !$keepErrorTypes) { return $traverse($type->getBound()); } diff --git a/src/Type/Generic/TemplateTypeMap.php b/src/Type/Generic/TemplateTypeMap.php index b298c9c8c2..f807e3d65e 100644 --- a/src/Type/Generic/TemplateTypeMap.php +++ b/src/Type/Generic/TemplateTypeMap.php @@ -2,7 +2,6 @@ namespace PHPStan\Type\Generic; -use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; @@ -16,6 +15,8 @@ class TemplateTypeMap private static ?TemplateTypeMap $empty = null; + private ?TemplateTypeMap $resolvedToBounds = null; + /** * @api * @param array $types @@ -204,14 +205,10 @@ public function map(callable $cb): self public function resolveToBounds(): self { - return $this->map(static function (string $name, Type $type): Type { - $type = TemplateTypeHelper::resolveToBounds($type); - if ($type instanceof MixedType && $type->isExplicitMixed()) { - return new MixedType(false); - } - - return $type; - }); + if ($this->resolvedToBounds !== null) { + return $this->resolvedToBounds; + } + return $this->resolvedToBounds = $this->map(static fn (string $name, Type $type): Type => TemplateTypeHelper::resolveToBounds($type)); } /** diff --git a/src/Type/GenericTypeVariableResolver.php b/src/Type/GenericTypeVariableResolver.php index 76551343e0..17cabf4bd7 100644 --- a/src/Type/GenericTypeVariableResolver.php +++ b/src/Type/GenericTypeVariableResolver.php @@ -2,6 +2,8 @@ namespace PHPStan\Type; +use PHPStan\Type\Generic\TemplateTypeHelper; + /** @api */ class GenericTypeVariableResolver { @@ -12,19 +14,37 @@ public static function getType( string $typeVariableName, ): ?Type { - $ancestor = $type->getAncestorWithClassName($genericClassName); - if ($ancestor === null) { + $classReflection = $type->getClassReflection(); + if ($classReflection === null) { return null; } - - $classReflection = $ancestor->getClassReflection(); - if ($classReflection === null) { + $ancestorClassReflection = $classReflection->getAncestorWithClassName($genericClassName); + if ($ancestorClassReflection === null) { return null; } - $templateTypeMap = $classReflection->getActiveTemplateTypeMap(); + $activeTemplateTypeMap = $ancestorClassReflection->getPossiblyIncompleteActiveTemplateTypeMap(); + + // todo if type is not defined, return the bound + // in case of mixed bound, return implicit mixed + + $type = $activeTemplateTypeMap->getType($typeVariableName); + if ($type instanceof ErrorType) { + $templateTypeMap = $ancestorClassReflection->getTemplateTypeMap(); + $templateType = $templateTypeMap->getType($typeVariableName); + if ($templateType === null) { + return $type; + } + + $bound = TemplateTypeHelper::resolveToBounds($templateType); + if ($bound instanceof MixedType && $bound->isExplicitMixed()) { + return new MixedType(false); + } + + return $bound; + } - return $templateTypeMap->getType($typeVariableName); + return $type; } } diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index 671bb56205..bc288695d2 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -1108,7 +1108,7 @@ public function getClassReflection(): ?ClassReflection $classReflection = $reflectionProvider->getClass($this->className); if ($classReflection->isGeneric()) { - return $classReflection->withTypes(array_values($classReflection->getTemplateTypeMap()->resolveToBounds()->getTypes())); + return $classReflection->withTypes(array_values($classReflection->getTemplateTypeMap()->map(static fn (): Type => new ErrorType())->getTypes())); } return $classReflection; diff --git a/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php b/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php index 45729c7ba7..28be69c2fa 100644 --- a/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php @@ -121,4 +121,9 @@ public function testBug6472(): void $this->analyse([__DIR__ . '/data/bug-6472.php'], []); } + public function testFilterIteratorChildClass(): void + { + $this->analyse([__DIR__ . '/data/filter-iterator-child-class.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/filter-iterator-child-class.php b/tests/PHPStan/Rules/Methods/data/filter-iterator-child-class.php new file mode 100644 index 0000000000..64dad86b42 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/filter-iterator-child-class.php @@ -0,0 +1,27 @@ +