diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 7a3ab4c970b..26280cdf852 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -1406,13 +1406,11 @@ private function resolveType(Expr $node): Type return new ErrorType(); } - if (($leftType->isString()->yes() && !$leftType->isNumericString()->yes()) - || ($rightType->isString()->yes() && !$rightType->isNumericString()->yes())) { - return new ErrorType(); - } - $leftNumberType = $leftType->toNumber(); $rightNumberType = $rightType->toNumber(); + if ($leftNumberType instanceof ErrorType || $rightNumberType instanceof ErrorType) { + return new ErrorType(); + } if ( (new FloatType())->isSuperTypeOf($leftNumberType)->yes() diff --git a/src/PhpDoc/PhpDocNodeResolver.php b/src/PhpDoc/PhpDocNodeResolver.php index 845b9d31499..4aa27304236 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 73f33b9a562..7be2d1651a8 100644 --- a/src/Reflection/ClassReflection.php +++ b/src/Reflection/ClassReflection.php @@ -82,6 +82,8 @@ class ClassReflection private ?TemplateTypeMap $templateTypeMap = null; + private ?TemplateTypeMap $activeTemplateTypeMap = null; + /** @var array|null */ private ?array $ancestors = null; @@ -181,7 +183,8 @@ public function getParentClass(): ?ClassReflection if ($this->isGeneric()) { $extendedType = TemplateTypeHelper::resolveTemplateTypes( $extendedType, - $this->getActiveTemplateTypeMap(), + $this->getPossiblyIncompleteActiveTemplateTypeMap(), + true, ); } @@ -195,7 +198,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 +227,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 +731,8 @@ public function getImmediateInterfaces(): array if ($this->isGeneric()) { $implementedType = TemplateTypeHelper::resolveTemplateTypes( $implementedType, - $this->getActiveTemplateTypeMap(), + $this->getPossiblyIncompleteActiveTemplateTypeMap(), + true, ); } @@ -743,7 +747,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 +1061,29 @@ public function getTemplateTypeMap(): TemplateTypeMap } public function getActiveTemplateTypeMap(): TemplateTypeMap + { + if ($this->activeTemplateTypeMap !== null) { + return $this->activeTemplateTypeMap; + } + $resolved = $this->resolvedTemplateTypeMap; + if ($resolved !== null) { + $templateTypeMap = $this->getTemplateTypeMap(); + return $this->activeTemplateTypeMap = $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->activeTemplateTypeMap = $this->getTemplateTypeMap(); + } + + public function getPossiblyIncompleteActiveTemplateTypeMap(): TemplateTypeMap { return $this->resolvedTemplateTypeMap ?? $this->getTemplateTypeMap(); } diff --git a/src/Type/Accessory/AccessoryLiteralStringType.php b/src/Type/Accessory/AccessoryLiteralStringType.php index 9eb9e3d6cd7..1c8a9bc81d7 100644 --- a/src/Type/Accessory/AccessoryLiteralStringType.php +++ b/src/Type/Accessory/AccessoryLiteralStringType.php @@ -130,10 +130,7 @@ public function isArray(): TrinaryLogic public function toNumber(): Type { - return new UnionType([ - $this->toInteger(), - $this->toFloat(), - ]); + return new ErrorType(); } public function toInteger(): Type diff --git a/src/Type/Accessory/AccessoryNonEmptyStringType.php b/src/Type/Accessory/AccessoryNonEmptyStringType.php index 0219866213b..7e17a4f5248 100644 --- a/src/Type/Accessory/AccessoryNonEmptyStringType.php +++ b/src/Type/Accessory/AccessoryNonEmptyStringType.php @@ -131,10 +131,7 @@ public function isArray(): TrinaryLogic public function toNumber(): Type { - return new UnionType([ - $this->toInteger(), - $this->toFloat(), - ]); + return new ErrorType(); } public function toInteger(): Type diff --git a/src/Type/FileTypeMapper.php b/src/Type/FileTypeMapper.php index a72aa670ee4..228adf69e55 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 6d5d6335745..f8e72514cf6 100644 --- a/src/Type/Generic/TemplateTypeHelper.php +++ b/src/Type/Generic/TemplateTypeHelper.php @@ -12,16 +12,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 b298c9c8c2d..f807e3d65e2 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 76551343e0c..17cabf4bd77 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 920631de840..eb0f9265989 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -1110,7 +1110,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/src/Type/UnionType.php b/src/Type/UnionType.php index fb76f4d1128..95c12bb7562 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -481,12 +481,12 @@ public function isCloneable(): TrinaryLogic public function isSmallerThan(Type $otherType): TrinaryLogic { - return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThan($otherType)); + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThan($otherType)); } public function isSmallerThanOrEqual(Type $otherType): TrinaryLogic { - return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThanOrEqual($otherType)); + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThanOrEqual($otherType)); } public function getSmallerType(): Type @@ -511,12 +511,12 @@ public function getGreaterOrEqualType(): Type public function isGreaterThan(Type $otherType): TrinaryLogic { - return $this->unionResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThan($type)); + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThan($type)); } public function isGreaterThanOrEqual(Type $otherType): TrinaryLogic { - return $this->unionResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThanOrEqual($type)); + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThanOrEqual($type)); } public function toBoolean(): BooleanType @@ -671,6 +671,14 @@ protected function unionResults(callable $getResult): TrinaryLogic return TrinaryLogic::extremeIdentity(...array_map($getResult, $this->types)); } + /** + * @param callable(Type $type): TrinaryLogic $getResult + */ + private function notBenevolentUnionResults(callable $getResult): TrinaryLogic + { + return TrinaryLogic::extremeIdentity(...array_map($getResult, $this->types)); + } + /** * @param callable(Type $type): Type $getType */ diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index f27e9107b67..6e411d150c9 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -726,6 +726,8 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/countable.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6696.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/filter-iterator-child-class.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/smaller-than-benevolent.php'); } /** diff --git a/tests/PHPStan/Analyser/data/bug-2676.php b/tests/PHPStan/Analyser/data/bug-2676.php index 4daa2b55525..30723db8a9d 100644 --- a/tests/PHPStan/Analyser/data/bug-2676.php +++ b/tests/PHPStan/Analyser/data/bug-2676.php @@ -39,7 +39,7 @@ function (Wallet $wallet): void assertType('DoctrineIntersectionTypeIsSupertypeOf\Collection&iterable', $bankAccounts); foreach ($bankAccounts as $key => $bankAccount) { - assertType('(int|string)', $key); + assertType('mixed', $key); assertType('Bug2676\BankAccount', $bankAccount); } }; diff --git a/tests/PHPStan/Analyser/data/smaller-than-benevolent.php b/tests/PHPStan/Analyser/data/smaller-than-benevolent.php new file mode 100644 index 00000000000..6a43d008b9c --- /dev/null +++ b/tests/PHPStan/Analyser/data/smaller-than-benevolent.php @@ -0,0 +1,35 @@ +|int<32, max>|string)', $x); + assertType('(int|int<32, max>|string)', $y); + + assertType('bool', $x < $y); + assertType('bool', $x <= $y); + assertType('bool', $x > $y); + assertType('bool', $x >= $y); + + return $x < $y ? 1 : -1; + }); + } + +} diff --git a/tests/PHPStan/Rules/Generics/ClassAncestorsRuleTest.php b/tests/PHPStan/Rules/Generics/ClassAncestorsRuleTest.php index 3f9ed11fec0..fda1c25d6f7 100644 --- a/tests/PHPStan/Rules/Generics/ClassAncestorsRuleTest.php +++ b/tests/PHPStan/Rules/Generics/ClassAncestorsRuleTest.php @@ -95,6 +95,11 @@ public function testRuleExtends(): void 'Template type T is declared as covariant, but occurs in invariant position in extended type ClassAncestorsExtends\FooGeneric8 of class ClassAncestorsExtends\FooGeneric9.', 192, ], + [ + 'Class ClassAncestorsExtends\FilterIteratorChild extends generic class FilterIterator but does not specify its types: TKey, TValue, TIterator', + 197, + 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', + ], ]); } diff --git a/tests/PHPStan/Rules/Generics/data/class-ancestors-extends.php b/tests/PHPStan/Rules/Generics/data/class-ancestors-extends.php index 62a1cd93034..caa906001a5 100644 --- a/tests/PHPStan/Rules/Generics/data/class-ancestors-extends.php +++ b/tests/PHPStan/Rules/Generics/data/class-ancestors-extends.php @@ -193,3 +193,13 @@ class FooGeneric9 extends FooGeneric8 { } + +class FilterIteratorChild extends \FilterIterator +{ + + public function accept() + { + return true; + } + +} diff --git a/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php b/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php index 45729c7ba72..28be69c2fa3 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 00000000000..ee92479f4df --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/filter-iterator-child-class.php @@ -0,0 +1,94 @@ + */ +interface IteratorChild2 extends \Iterator +{ + + /** @return int */ + #[\ReturnTypeWillChange] + public function key(); + + /** @return int */ + #[\ReturnTypeWillChange] + public function current(); + +} + +class Foo +{ + + public function doFoo(IteratorChild $c) + { + foreach ($c as $k => $v) { + assertType('int', $k); + assertType('int', $v); + } + } + + public function doFoo2(IteratorChild2 $c) + { + foreach ($c as $k => $v) { + assertType('mixed', $k); + assertType('mixed', $v); + } + } + +} + +interface IteratorChild3 extends \Iterator +{ + +} + +class IteratorChildTest +{ + + public function doFoo(IteratorChild3 $c) + { + + } + +} diff --git a/tests/PHPStan/Rules/Operators/data/invalid-binary.php b/tests/PHPStan/Rules/Operators/data/invalid-binary.php index c144298b6d0..3069c82ce8d 100644 --- a/tests/PHPStan/Rules/Operators/data/invalid-binary.php +++ b/tests/PHPStan/Rules/Operators/data/invalid-binary.php @@ -248,3 +248,9 @@ function bug6624_no_error($numericString) { echo (10 * $numericLiteral); echo (10 / $numericLiteral); } + +function benevolentPlus(array $a, int $i): void { + foreach ($a as $k => $v) { + echo $k + $i; + } +};