From 0808a89469a0d28bebd0b8d7b14af1639e441215 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 26 Feb 2022 12:34:05 +0100 Subject: [PATCH 1/8] Template tag without bound is explicit mixed --- src/PhpDoc/PhpDocNodeResolver.php | 2 +- src/Reflection/ClassReflection.php | 32 ++++++++++++++--- src/Type/FileTypeMapper.php | 2 +- src/Type/Generic/TemplateTypeHelper.php | 6 ++-- src/Type/Generic/TemplateTypeMap.php | 15 ++++---- src/Type/GenericTypeVariableResolver.php | 34 +++++++++++++++---- src/Type/ObjectType.php | 2 +- ...MissingMethodParameterTypehintRuleTest.php | 5 +++ .../data/filter-iterator-child-class.php | 27 +++++++++++++++ 9 files changed, 98 insertions(+), 27 deletions(-) create mode 100644 tests/PHPStan/Rules/Methods/data/filter-iterator-child-class.php 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..695b46dff9f 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 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 b8d15d47e1a..30ec16a5472 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 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 671bb56205a..bc288695d27 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 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..64dad86b42f --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/filter-iterator-child-class.php @@ -0,0 +1,27 @@ + Date: Sun, 27 Feb 2022 13:14:17 +0100 Subject: [PATCH 2/8] Tests --- .../Analyser/NodeScopeResolverTest.php | 1 + .../data/filter-iterator-child-class.php | 48 +++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index bcf743e8454..2b150ae17b9 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -722,6 +722,7 @@ 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'); } /** diff --git a/tests/PHPStan/Rules/Methods/data/filter-iterator-child-class.php b/tests/PHPStan/Rules/Methods/data/filter-iterator-child-class.php index 64dad86b42f..43d6a49c2e7 100644 --- a/tests/PHPStan/Rules/Methods/data/filter-iterator-child-class.php +++ b/tests/PHPStan/Rules/Methods/data/filter-iterator-child-class.php @@ -2,6 +2,8 @@ namespace FilterIteratorChild; +use function PHPStan\Testing\assertType; + class ArchivableFilesFinder extends \FilterIterator { @@ -21,7 +23,53 @@ class ArchivableFilesFinderTest public function doFoo(ArchivableFilesFinder $finder): void { + foreach ($finder as $f) { + assertType('mixed', $f); + } + } + +} + +interface IteratorChild extends \Iterator +{ + + /** @return int */ + public function key(); + + /** @return int */ + public function current(); + +} + +/** @extends \Iterator */ +interface IteratorChild2 extends \Iterator +{ + + /** @return int */ + public function key(); + + /** @return int */ + 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); + } } } From 036134c911f435a4d6e10d12e8520d64e9e982ab Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 27 Feb 2022 13:15:43 +0100 Subject: [PATCH 3/8] Fix --- tests/PHPStan/Analyser/data/bug-2676.php | 2 +- .../Rules/Methods/data/filter-iterator-child-class.php | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) 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/Rules/Methods/data/filter-iterator-child-class.php b/tests/PHPStan/Rules/Methods/data/filter-iterator-child-class.php index 43d6a49c2e7..4141372f8f1 100644 --- a/tests/PHPStan/Rules/Methods/data/filter-iterator-child-class.php +++ b/tests/PHPStan/Rules/Methods/data/filter-iterator-child-class.php @@ -34,9 +34,11 @@ interface IteratorChild extends \Iterator { /** @return int */ + #[\ReturnTypeWillChange] public function key(); /** @return int */ + #[\ReturnTypeWillChange] public function current(); } @@ -46,9 +48,11 @@ interface IteratorChild2 extends \Iterator { /** @return int */ + #[\ReturnTypeWillChange] public function key(); /** @return int */ + #[\ReturnTypeWillChange] public function current(); } From 7f55bdbeb92f5142a11f8947f224976ba16c49f9 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 27 Feb 2022 13:19:46 +0100 Subject: [PATCH 4/8] Test --- .../PHPStan/Rules/Generics/ClassAncestorsRuleTest.php | 5 +++++ .../Rules/Generics/data/class-ancestors-extends.php | 10 ++++++++++ 2 files changed, 15 insertions(+) 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; + } + +} From bedcc2db8616d195f4cf698d3731cc69d634c2b7 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 27 Feb 2022 13:50:03 +0100 Subject: [PATCH 5/8] Test --- .../Methods/data/filter-iterator-child-class.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/PHPStan/Rules/Methods/data/filter-iterator-child-class.php b/tests/PHPStan/Rules/Methods/data/filter-iterator-child-class.php index 4141372f8f1..ee92479f4df 100644 --- a/tests/PHPStan/Rules/Methods/data/filter-iterator-child-class.php +++ b/tests/PHPStan/Rules/Methods/data/filter-iterator-child-class.php @@ -77,3 +77,18 @@ public function doFoo2(IteratorChild2 $c) } } + +interface IteratorChild3 extends \Iterator +{ + +} + +class IteratorChildTest +{ + + public function doFoo(IteratorChild3 $c) + { + + } + +} From a2e63adbc6c3f725ad4f7daabdef2aee00fff435 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 27 Feb 2022 15:38:28 +0100 Subject: [PATCH 6/8] Fix binary operations for benevolent unions --- src/Analyser/MutatingScope.php | 8 +++----- src/Type/Accessory/AccessoryLiteralStringType.php | 5 +---- src/Type/Accessory/AccessoryNonEmptyStringType.php | 5 +---- tests/PHPStan/Rules/Operators/data/invalid-binary.php | 6 ++++++ 4 files changed, 11 insertions(+), 13 deletions(-) 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/Type/Accessory/AccessoryLiteralStringType.php b/src/Type/Accessory/AccessoryLiteralStringType.php index ad874199e68..52b32d66031 100644 --- a/src/Type/Accessory/AccessoryLiteralStringType.php +++ b/src/Type/Accessory/AccessoryLiteralStringType.php @@ -129,10 +129,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 048a1d946ea..822acdb7284 100644 --- a/src/Type/Accessory/AccessoryNonEmptyStringType.php +++ b/src/Type/Accessory/AccessoryNonEmptyStringType.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/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; + } +}; From d9932a1e1204f47fb7b14f221731831ee4d9873a Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 27 Feb 2022 15:47:51 +0100 Subject: [PATCH 7/8] Optimization --- src/Reflection/ClassReflection.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Reflection/ClassReflection.php b/src/Reflection/ClassReflection.php index 695b46dff9f..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; @@ -1060,10 +1062,13 @@ 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 $resolved->map(static function (string $name, Type $type) use ($templateTypeMap): Type { + 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) { @@ -1075,7 +1080,7 @@ public function getActiveTemplateTypeMap(): TemplateTypeMap }); } - return $this->getTemplateTypeMap(); + return $this->activeTemplateTypeMap = $this->getTemplateTypeMap(); } public function getPossiblyIncompleteActiveTemplateTypeMap(): TemplateTypeMap From dd5633f1835d58f67edb95858185b7791a13220c Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 27 Feb 2022 16:46:57 +0100 Subject: [PATCH 8/8] Fix isSmallerThan etc. in BenevolentUnionType --- src/Type/UnionType.php | 16 ++++++--- .../Analyser/NodeScopeResolverTest.php | 1 + .../Analyser/data/smaller-than-benevolent.php | 35 +++++++++++++++++++ 3 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 tests/PHPStan/Analyser/data/smaller-than-benevolent.php diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index 77740ed0538..76b54472f8f 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -478,12 +478,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 @@ -508,12 +508,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 @@ -668,6 +668,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 2b150ae17b9..9b239daabf0 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -723,6 +723,7 @@ public function dataFileAsserts(): iterable 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/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; + }); + } + +}