From 6a1214d36e30dc8f3657ef084a1b997efe5d5871 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Fri, 4 Feb 2022 15:20:02 +0100 Subject: [PATCH] Delegate most of TypeCombinator::remove() to Type::tryRemove() --- .../Accessory/AccessoryLiteralStringType.php | 2 + .../Accessory/AccessoryNonEmptyStringType.php | 2 + .../Accessory/AccessoryNumericStringType.php | 2 + src/Type/Accessory/HasMethodType.php | 2 + src/Type/Accessory/HasOffsetType.php | 2 + src/Type/Accessory/HasPropertyType.php | 2 + src/Type/Accessory/NonEmptyArrayType.php | 2 + src/Type/ArrayType.php | 18 ++++ src/Type/BooleanType.php | 10 ++ src/Type/CallableType.php | 2 + src/Type/ClosureType.php | 2 + src/Type/Enum/EnumCaseObjectType.php | 9 ++ src/Type/FloatType.php | 2 + src/Type/Generic/TemplateTypeTrait.php | 3 + src/Type/IntegerType.php | 21 ++++ src/Type/IntersectionType.php | 3 + src/Type/IterableType.php | 19 ++++ src/Type/MixedType.php | 9 ++ src/Type/NeverType.php | 2 + src/Type/NonexistentParentClassType.php | 2 + src/Type/NullType.php | 2 + src/Type/ObjectType.php | 22 +++++ src/Type/ObjectWithoutClassType.php | 9 ++ src/Type/ResourceType.php | 2 + src/Type/StaticType.php | 9 ++ src/Type/StrictMixedType.php | 2 + src/Type/StringType.php | 14 +++ src/Type/Traits/NonRemoveableTypeTrait.php | 15 +++ src/Type/Type.php | 7 ++ src/Type/TypeCombinator.php | 95 +------------------ src/Type/UnionType.php | 10 ++ src/Type/VoidType.php | 2 + .../data/class-implements-out-of-phpstan.php | 5 + .../Rules/Methods/ReturnTypeRuleTest.php | 6 ++ tests/PHPStan/Rules/Methods/data/bug-6438.php | 44 +++++++++ tests/PHPStan/Type/TypeCombinatorTest.php | 12 +++ 36 files changed, 278 insertions(+), 94 deletions(-) create mode 100644 src/Type/Traits/NonRemoveableTypeTrait.php create mode 100644 tests/PHPStan/Rules/Methods/data/bug-6438.php diff --git a/src/Type/Accessory/AccessoryLiteralStringType.php b/src/Type/Accessory/AccessoryLiteralStringType.php index 67e5dd5134..a271bb2a07 100644 --- a/src/Type/Accessory/AccessoryLiteralStringType.php +++ b/src/Type/Accessory/AccessoryLiteralStringType.php @@ -17,6 +17,7 @@ use PHPStan\Type\Traits\NonGenericTypeTrait; use PHPStan\Type\Traits\NonIterableTypeTrait; use PHPStan\Type\Traits\NonObjectTypeTrait; +use PHPStan\Type\Traits\NonRemoveableTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; use PHPStan\Type\Type; use PHPStan\Type\UnionType; @@ -30,6 +31,7 @@ class AccessoryLiteralStringType implements CompoundType, AccessoryType use NonIterableTypeTrait; use UndecidedComparisonCompoundTypeTrait; use NonGenericTypeTrait; + use NonRemoveableTypeTrait; /** @api */ public function __construct() diff --git a/src/Type/Accessory/AccessoryNonEmptyStringType.php b/src/Type/Accessory/AccessoryNonEmptyStringType.php index 85de5aed71..b8f50792e1 100644 --- a/src/Type/Accessory/AccessoryNonEmptyStringType.php +++ b/src/Type/Accessory/AccessoryNonEmptyStringType.php @@ -15,6 +15,7 @@ use PHPStan\Type\Traits\NonGenericTypeTrait; use PHPStan\Type\Traits\NonIterableTypeTrait; use PHPStan\Type\Traits\NonObjectTypeTrait; +use PHPStan\Type\Traits\NonRemoveableTypeTrait; use PHPStan\Type\Traits\TruthyBooleanTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; use PHPStan\Type\Type; @@ -30,6 +31,7 @@ class AccessoryNonEmptyStringType implements CompoundType, AccessoryType use TruthyBooleanTypeTrait; use UndecidedComparisonCompoundTypeTrait; use NonGenericTypeTrait; + use NonRemoveableTypeTrait; /** @api */ public function __construct() diff --git a/src/Type/Accessory/AccessoryNumericStringType.php b/src/Type/Accessory/AccessoryNumericStringType.php index 74768efa0d..75d044bb7e 100644 --- a/src/Type/Accessory/AccessoryNumericStringType.php +++ b/src/Type/Accessory/AccessoryNumericStringType.php @@ -15,6 +15,7 @@ use PHPStan\Type\Traits\NonGenericTypeTrait; use PHPStan\Type\Traits\NonIterableTypeTrait; use PHPStan\Type\Traits\NonObjectTypeTrait; +use PHPStan\Type\Traits\NonRemoveableTypeTrait; use PHPStan\Type\Traits\UndecidedBooleanTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; use PHPStan\Type\Type; @@ -30,6 +31,7 @@ class AccessoryNumericStringType implements CompoundType, AccessoryType use UndecidedBooleanTypeTrait; use UndecidedComparisonCompoundTypeTrait; use NonGenericTypeTrait; + use NonRemoveableTypeTrait; /** @api */ public function __construct() diff --git a/src/Type/Accessory/HasMethodType.php b/src/Type/Accessory/HasMethodType.php index b1af1797b4..1ab34073c9 100644 --- a/src/Type/Accessory/HasMethodType.php +++ b/src/Type/Accessory/HasMethodType.php @@ -12,6 +12,7 @@ use PHPStan\Type\CompoundType; use PHPStan\Type\IntersectionType; use PHPStan\Type\Traits\NonGenericTypeTrait; +use PHPStan\Type\Traits\NonRemoveableTypeTrait; use PHPStan\Type\Traits\ObjectTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; use PHPStan\Type\Type; @@ -26,6 +27,7 @@ class HasMethodType implements AccessoryType, CompoundType use ObjectTypeTrait; use NonGenericTypeTrait; use UndecidedComparisonCompoundTypeTrait; + use NonRemoveableTypeTrait; /** @api */ public function __construct(private string $methodName) diff --git a/src/Type/Accessory/HasOffsetType.php b/src/Type/Accessory/HasOffsetType.php index 8f3da7ff03..edf56ef6b0 100644 --- a/src/Type/Accessory/HasOffsetType.php +++ b/src/Type/Accessory/HasOffsetType.php @@ -12,6 +12,7 @@ use PHPStan\Type\Traits\MaybeIterableTypeTrait; use PHPStan\Type\Traits\MaybeObjectTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; +use PHPStan\Type\Traits\NonRemoveableTypeTrait; use PHPStan\Type\Traits\TruthyBooleanTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; use PHPStan\Type\Type; @@ -28,6 +29,7 @@ class HasOffsetType implements CompoundType, AccessoryType use TruthyBooleanTypeTrait; use NonGenericTypeTrait; use UndecidedComparisonCompoundTypeTrait; + use NonRemoveableTypeTrait; /** @api */ public function __construct(private Type $offsetType) diff --git a/src/Type/Accessory/HasPropertyType.php b/src/Type/Accessory/HasPropertyType.php index 38791dd728..d581085125 100644 --- a/src/Type/Accessory/HasPropertyType.php +++ b/src/Type/Accessory/HasPropertyType.php @@ -8,6 +8,7 @@ use PHPStan\Type\CompoundType; use PHPStan\Type\IntersectionType; use PHPStan\Type\Traits\NonGenericTypeTrait; +use PHPStan\Type\Traits\NonRemoveableTypeTrait; use PHPStan\Type\Traits\ObjectTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; use PHPStan\Type\Type; @@ -21,6 +22,7 @@ class HasPropertyType implements AccessoryType, CompoundType use ObjectTypeTrait; use NonGenericTypeTrait; use UndecidedComparisonCompoundTypeTrait; + use NonRemoveableTypeTrait; /** @api */ public function __construct(private string $propertyName) diff --git a/src/Type/Accessory/NonEmptyArrayType.php b/src/Type/Accessory/NonEmptyArrayType.php index 42bccc3008..57094d492f 100644 --- a/src/Type/Accessory/NonEmptyArrayType.php +++ b/src/Type/Accessory/NonEmptyArrayType.php @@ -12,6 +12,7 @@ use PHPStan\Type\Traits\MaybeCallableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; use PHPStan\Type\Traits\NonObjectTypeTrait; +use PHPStan\Type\Traits\NonRemoveableTypeTrait; use PHPStan\Type\Traits\TruthyBooleanTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; use PHPStan\Type\Type; @@ -26,6 +27,7 @@ class NonEmptyArrayType implements CompoundType, AccessoryType use TruthyBooleanTypeTrait; use NonGenericTypeTrait; use UndecidedComparisonCompoundTypeTrait; + use NonRemoveableTypeTrait; /** @api */ public function __construct() diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php index 9909d41f43..cf6dda2f9f 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -7,6 +7,7 @@ use PHPStan\Reflection\TrivialParametersAcceptor; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\HasOffsetType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantFloatType; @@ -420,6 +421,23 @@ public function traverse(callable $cb): Type return $this; } + public function tryRemove(Type $typeToRemove): ?Type + { + if ($typeToRemove instanceof ConstantArrayType && $typeToRemove->isIterableAtLeastOnce()->no()) { + return TypeCombinator::intersect($this, new NonEmptyArrayType()); + } + + if ($typeToRemove instanceof NonEmptyArrayType) { + return new ConstantArrayType([], []); + } + + if ($this instanceof ConstantArrayType && $typeToRemove instanceof HasOffsetType) { + return $this->unsetOffset($typeToRemove->getOffsetType()); + } + + return null; + } + /** * @param mixed[] $properties */ diff --git a/src/Type/BooleanType.php b/src/Type/BooleanType.php index 755376be6d..70bf387db5 100644 --- a/src/Type/BooleanType.php +++ b/src/Type/BooleanType.php @@ -3,6 +3,7 @@ namespace PHPStan\Type; use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; @@ -75,6 +76,15 @@ public function toArray(): Type ); } + public function tryRemove(Type $typeToRemove): ?Type + { + if ($typeToRemove instanceof ConstantBooleanType) { + return new ConstantBooleanType(!$typeToRemove->getValue()); + } + + return null; + } + /** * @param mixed[] $properties */ diff --git a/src/Type/CallableType.php b/src/Type/CallableType.php index af281fbead..5b96b285bc 100644 --- a/src/Type/CallableType.php +++ b/src/Type/CallableType.php @@ -16,6 +16,7 @@ use PHPStan\Type\Traits\MaybeIterableTypeTrait; use PHPStan\Type\Traits\MaybeObjectTypeTrait; use PHPStan\Type\Traits\MaybeOffsetAccessibleTypeTrait; +use PHPStan\Type\Traits\NonRemoveableTypeTrait; use PHPStan\Type\Traits\TruthyBooleanTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; use function array_map; @@ -32,6 +33,7 @@ class CallableType implements CompoundType, ParametersAcceptor use MaybeOffsetAccessibleTypeTrait; use TruthyBooleanTypeTrait; use UndecidedComparisonCompoundTypeTrait; + use NonRemoveableTypeTrait; /** @var array */ private array $parameters; diff --git a/src/Type/ClosureType.php b/src/Type/ClosureType.php index fcdc03a5c4..f2ccf89e93 100644 --- a/src/Type/ClosureType.php +++ b/src/Type/ClosureType.php @@ -24,6 +24,7 @@ use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Traits\NonGenericTypeTrait; use PHPStan\Type\Traits\NonOffsetAccessibleTypeTrait; +use PHPStan\Type\Traits\NonRemoveableTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonTypeTrait; use function array_map; use function array_merge; @@ -37,6 +38,7 @@ class ClosureType implements TypeWithClassName, ParametersAcceptor use NonGenericTypeTrait; use UndecidedComparisonTypeTrait; use NonOffsetAccessibleTypeTrait; + use NonRemoveableTypeTrait; private ObjectType $objectType; diff --git a/src/Type/Enum/EnumCaseObjectType.php b/src/Type/Enum/EnumCaseObjectType.php index 0785184ae1..714ad1219c 100644 --- a/src/Type/Enum/EnumCaseObjectType.php +++ b/src/Type/Enum/EnumCaseObjectType.php @@ -91,6 +91,15 @@ public function getSubtractedType(): ?Type return null; } + public function tryRemove(Type $typeToRemove): ?Type + { + if ($this->isSuperTypeOf($typeToRemove)->yes()) { + return $this->subtract($typeToRemove); + } + + return null; + } + public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection { $classReflection = $this->getClassReflection(); diff --git a/src/Type/FloatType.php b/src/Type/FloatType.php index b519b38090..9b14721832 100644 --- a/src/Type/FloatType.php +++ b/src/Type/FloatType.php @@ -11,6 +11,7 @@ use PHPStan\Type\Traits\NonIterableTypeTrait; use PHPStan\Type\Traits\NonObjectTypeTrait; use PHPStan\Type\Traits\NonOffsetAccessibleTypeTrait; +use PHPStan\Type\Traits\NonRemoveableTypeTrait; use PHPStan\Type\Traits\UndecidedBooleanTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonTypeTrait; use function get_class; @@ -26,6 +27,7 @@ class FloatType implements Type use UndecidedComparisonTypeTrait; use NonGenericTypeTrait; use NonOffsetAccessibleTypeTrait; + use NonRemoveableTypeTrait; /** @api */ public function __construct() diff --git a/src/Type/Generic/TemplateTypeTrait.php b/src/Type/Generic/TemplateTypeTrait.php index 387587c5a6..911b2bc1ff 100644 --- a/src/Type/Generic/TemplateTypeTrait.php +++ b/src/Type/Generic/TemplateTypeTrait.php @@ -5,6 +5,7 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; +use PHPStan\Type\Traits\NonRemoveableTypeTrait; use PHPStan\Type\Type; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; @@ -16,6 +17,8 @@ trait TemplateTypeTrait { + use NonRemoveableTypeTrait; + private string $name; private TemplateTypeScope $scope; diff --git a/src/Type/IntegerType.php b/src/Type/IntegerType.php index 1922e7a079..b60c1ae9f8 100644 --- a/src/Type/IntegerType.php +++ b/src/Type/IntegerType.php @@ -76,4 +76,25 @@ public function toArray(): Type ); } + public function tryRemove(Type $typeToRemove): ?Type + { + if ($typeToRemove instanceof IntegerRangeType || $typeToRemove instanceof ConstantIntegerType) { + if ($typeToRemove instanceof IntegerRangeType) { + $removeValueMin = $typeToRemove->getMin(); + $removeValueMax = $typeToRemove->getMax(); + } else { + $removeValueMin = $typeToRemove->getValue(); + $removeValueMax = $typeToRemove->getValue(); + } + $lowerPart = $removeValueMin !== null ? IntegerRangeType::fromInterval(null, $removeValueMin, -1) : null; + $upperPart = $removeValueMax !== null ? IntegerRangeType::fromInterval($removeValueMax, null, +1) : null; + if ($lowerPart !== null && $upperPart !== null) { + return new UnionType([$lowerPart, $upperPart]); + } + return $lowerPart ?? $upperPart ?? new NeverType(); + } + + return null; + } + } diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index 956d385ccf..87c80d8b6f 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -22,6 +22,7 @@ use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\Traits\NonRemoveableTypeTrait; use function array_map; use function count; use function implode; @@ -34,6 +35,8 @@ class IntersectionType implements CompoundType { + use NonRemoveableTypeTrait; + /** @var Type[] */ private array $types; diff --git a/src/Type/IterableType.php b/src/Type/IterableType.php index 421eec91eb..8eeb62d74a 100644 --- a/src/Type/IterableType.php +++ b/src/Type/IterableType.php @@ -4,6 +4,7 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\Generic\TemplateMixedType; use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeMap; @@ -278,6 +279,24 @@ public function traverse(callable $cb): Type return $this; } + public function tryRemove(Type $typeToRemove): ?Type + { + $arrayType = new ArrayType(new MixedType(), new MixedType()); + if ($typeToRemove->isSuperTypeOf($arrayType)->yes()) { + return new GenericObjectType(Traversable::class, [ + $this->getIterableKeyType(), + $this->getIterableValueType(), + ]); + } + + $traversableType = new ObjectType(Traversable::class); + if ($typeToRemove->isSuperTypeOf($traversableType)->yes()) { + return new ArrayType($this->getIterableKeyType(), $this->getIterableValueType()); + } + + return null; + } + /** * @param mixed[] $properties */ diff --git a/src/Type/MixedType.php b/src/Type/MixedType.php index c11e525e9a..7c130546a3 100644 --- a/src/Type/MixedType.php +++ b/src/Type/MixedType.php @@ -420,6 +420,15 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function tryRemove(Type $typeToRemove): ?Type + { + if ($this->isSuperTypeOf($typeToRemove)->yes()) { + return $this->subtract($typeToRemove); + } + + return null; + } + /** * @param mixed[] $properties */ diff --git a/src/Type/NeverType.php b/src/Type/NeverType.php index ef2c90575a..0fa866a387 100644 --- a/src/Type/NeverType.php +++ b/src/Type/NeverType.php @@ -13,6 +13,7 @@ use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Traits\NonGenericTypeTrait; +use PHPStan\Type\Traits\NonRemoveableTypeTrait; use PHPStan\Type\Traits\UndecidedBooleanTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; @@ -23,6 +24,7 @@ class NeverType implements CompoundType use UndecidedBooleanTypeTrait; use NonGenericTypeTrait; use UndecidedComparisonCompoundTypeTrait; + use NonRemoveableTypeTrait; /** @api */ public function __construct(private bool $isExplicit = false) diff --git a/src/Type/NonexistentParentClassType.php b/src/Type/NonexistentParentClassType.php index e43f5df6ee..9de84a4c96 100644 --- a/src/Type/NonexistentParentClassType.php +++ b/src/Type/NonexistentParentClassType.php @@ -14,6 +14,7 @@ use PHPStan\Type\Traits\NonGenericTypeTrait; use PHPStan\Type\Traits\NonIterableTypeTrait; use PHPStan\Type\Traits\NonOffsetAccessibleTypeTrait; +use PHPStan\Type\Traits\NonRemoveableTypeTrait; use PHPStan\Type\Traits\TruthyBooleanTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonTypeTrait; @@ -27,6 +28,7 @@ class NonexistentParentClassType implements Type use TruthyBooleanTypeTrait; use NonGenericTypeTrait; use UndecidedComparisonTypeTrait; + use NonRemoveableTypeTrait; public function describe(VerbosityLevel $level): string { diff --git a/src/Type/NullType.php b/src/Type/NullType.php index 3822dcc403..29f15f0b20 100644 --- a/src/Type/NullType.php +++ b/src/Type/NullType.php @@ -13,6 +13,7 @@ use PHPStan\Type\Traits\NonGenericTypeTrait; use PHPStan\Type\Traits\NonIterableTypeTrait; use PHPStan\Type\Traits\NonObjectTypeTrait; +use PHPStan\Type\Traits\NonRemoveableTypeTrait; /** @api */ class NullType implements ConstantScalarType @@ -23,6 +24,7 @@ class NullType implements ConstantScalarType use NonObjectTypeTrait; use FalseyBooleanTypeTrait; use NonGenericTypeTrait; + use NonRemoveableTypeTrait; /** @api */ public function __construct() diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index 146569ea25..a615e24977 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -4,6 +4,9 @@ use ArrayAccess; use Closure; +use DateTime; +use DateTimeImmutable; +use DateTimeInterface; use Iterator; use IteratorAggregate; use PHPStan\Analyser\OutOfClassScope; @@ -1185,4 +1188,23 @@ private function getInterfaces(): array return $this->cachedInterfaces = array_map(static fn (ClassReflection $interfaceReflection): self => self::createFromReflection($interfaceReflection), $thisReflection->getInterfaces()); } + public function tryRemove(Type $typeToRemove): ?Type + { + if ($this->getClassName() === DateTimeInterface::class) { + if ($typeToRemove instanceof ObjectType && $typeToRemove->getClassName() === DateTimeImmutable::class) { + return new ObjectType(DateTime::class); + } + + if ($typeToRemove instanceof ObjectType && $typeToRemove->getClassName() === DateTime::class) { + return new ObjectType(DateTimeImmutable::class); + } + } + + if ($this->isSuperTypeOf($typeToRemove)->yes()) { + return $this->subtract($typeToRemove); + } + + return null; + } + } diff --git a/src/Type/ObjectWithoutClassType.php b/src/Type/ObjectWithoutClassType.php index 51232b7d76..5aa5fe3fec 100644 --- a/src/Type/ObjectWithoutClassType.php +++ b/src/Type/ObjectWithoutClassType.php @@ -156,6 +156,15 @@ public function traverse(callable $cb): Type return $this; } + public function tryRemove(Type $typeToRemove): ?Type + { + if ($this->isSuperTypeOf($typeToRemove)->yes()) { + return $this->subtract($typeToRemove); + } + + return null; + } + /** * @param mixed[] $properties */ diff --git a/src/Type/ResourceType.php b/src/Type/ResourceType.php index afc4879b72..afd0005665 100644 --- a/src/Type/ResourceType.php +++ b/src/Type/ResourceType.php @@ -9,6 +9,7 @@ use PHPStan\Type\Traits\NonIterableTypeTrait; use PHPStan\Type\Traits\NonObjectTypeTrait; use PHPStan\Type\Traits\NonOffsetAccessibleTypeTrait; +use PHPStan\Type\Traits\NonRemoveableTypeTrait; use PHPStan\Type\Traits\TruthyBooleanTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonTypeTrait; @@ -24,6 +25,7 @@ class ResourceType implements Type use NonGenericTypeTrait; use UndecidedComparisonTypeTrait; use NonOffsetAccessibleTypeTrait; + use NonRemoveableTypeTrait; /** @api */ public function __construct() diff --git a/src/Type/StaticType.php b/src/Type/StaticType.php index eab5c9fa14..6d681bd720 100644 --- a/src/Type/StaticType.php +++ b/src/Type/StaticType.php @@ -450,6 +450,15 @@ public function getSubtractedType(): ?Type return $this->subtractedType; } + public function tryRemove(Type $typeToRemove): ?Type + { + if ($this->isSuperTypeOf($typeToRemove)->yes()) { + return $this->subtract($typeToRemove); + } + + return null; + } + /** * @param mixed[] $properties */ diff --git a/src/Type/StrictMixedType.php b/src/Type/StrictMixedType.php index f009a07f0e..f98580057f 100644 --- a/src/Type/StrictMixedType.php +++ b/src/Type/StrictMixedType.php @@ -13,12 +13,14 @@ use PHPStan\Type\Generic\TemplateMixedType; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\Traits\NonRemoveableTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; class StrictMixedType implements CompoundType { use UndecidedComparisonCompoundTypeTrait; + use NonRemoveableTypeTrait; public function getReferencedClasses(): array { diff --git a/src/Type/StringType.php b/src/Type/StringType.php index 807ba4a355..cf47790a72 100644 --- a/src/Type/StringType.php +++ b/src/Type/StringType.php @@ -4,8 +4,10 @@ use PHPStan\Reflection\ReflectionProviderStaticAccessor; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Traits\MaybeCallableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; use PHPStan\Type\Traits\NonIterableTypeTrait; @@ -146,6 +148,18 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function tryRemove(Type $typeToRemove): ?Type + { + if ($typeToRemove instanceof ConstantStringType && $typeToRemove->getValue() === '') { + return TypeCombinator::intersect($this, new AccessoryNonEmptyStringType()); + } + if ($typeToRemove instanceof AccessoryNonEmptyStringType) { + return new ConstantStringType(''); + } + + return null; + } + /** * @param mixed[] $properties */ diff --git a/src/Type/Traits/NonRemoveableTypeTrait.php b/src/Type/Traits/NonRemoveableTypeTrait.php new file mode 100644 index 0000000000..1eb40ea378 --- /dev/null +++ b/src/Type/Traits/NonRemoveableTypeTrait.php @@ -0,0 +1,15 @@ +getTypes() as $innerType) { - $innerTypes[] = self::remove($innerType, $typeToRemove); - } - - return self::union(...$innerTypes); - } - $isSuperType = $typeToRemove->isSuperTypeOf($fromType); if ($isSuperType->yes()) { return new NeverType(); @@ -76,85 +61,7 @@ public static function remove(Type $fromType, Type $typeToRemove): Type } } - if ($fromType instanceof BooleanType) { - if ($typeToRemove instanceof ConstantBooleanType) { - return new ConstantBooleanType(!$typeToRemove->getValue()); - } - } elseif ($fromType instanceof IterableType) { - $arrayType = new ArrayType(new MixedType(), new MixedType()); - if ($typeToRemove->isSuperTypeOf($arrayType)->yes()) { - return new GenericObjectType(Traversable::class, [ - $fromType->getIterableKeyType(), - $fromType->getIterableValueType(), - ]); - } - - $traversableType = new ObjectType(Traversable::class); - if ($typeToRemove->isSuperTypeOf($traversableType)->yes()) { - return new ArrayType($fromType->getIterableKeyType(), $fromType->getIterableValueType()); - } - } elseif ($fromType instanceof IntegerRangeType) { - $type = $fromType->tryRemove($typeToRemove); - if ($type !== null) { - return $type; - } - } elseif ($fromType instanceof IntegerType) { - if ($typeToRemove instanceof IntegerRangeType || $typeToRemove instanceof ConstantIntegerType) { - if ($typeToRemove instanceof IntegerRangeType) { - $removeValueMin = $typeToRemove->getMin(); - $removeValueMax = $typeToRemove->getMax(); - } else { - $removeValueMin = $typeToRemove->getValue(); - $removeValueMax = $typeToRemove->getValue(); - } - $lowerPart = $removeValueMin !== null ? IntegerRangeType::fromInterval(null, $removeValueMin, -1) : null; - $upperPart = $removeValueMax !== null ? IntegerRangeType::fromInterval($removeValueMax, null, +1) : null; - if ($lowerPart !== null && $upperPart !== null) { - return new UnionType([$lowerPart, $upperPart]); - } - return $lowerPart ?? $upperPart ?? new NeverType(); - } - } elseif ($fromType->isArray()->yes()) { - if ($typeToRemove instanceof ConstantArrayType && $typeToRemove->isIterableAtLeastOnce()->no()) { - return self::intersect($fromType, new NonEmptyArrayType()); - } - - if ($typeToRemove instanceof NonEmptyArrayType) { - return new ConstantArrayType([], []); - } - - if ($fromType instanceof ConstantArrayType && $typeToRemove instanceof HasOffsetType) { - return $fromType->unsetOffset($typeToRemove->getOffsetType()); - } - } elseif ($fromType instanceof StringType) { - if ($typeToRemove instanceof ConstantStringType && $typeToRemove->getValue() === '') { - return self::intersect($fromType, new AccessoryNonEmptyStringType()); - } - if ($typeToRemove instanceof AccessoryNonEmptyStringType) { - return new ConstantStringType(''); - } - } elseif ($fromType instanceof ObjectType && $fromType->getClassName() === DateTimeInterface::class) { - if ($typeToRemove instanceof ObjectType && $typeToRemove->getClassName() === DateTimeImmutable::class) { - return new ObjectType(DateTime::class); - } - - if ($typeToRemove instanceof ObjectType && $typeToRemove->getClassName() === DateTime::class) { - return new ObjectType(DateTimeImmutable::class); - } - } - - if ($fromType instanceof SubtractableType) { - $typeToSubtractFrom = $fromType; - if ($fromType instanceof TemplateType) { - $typeToSubtractFrom = $fromType->getBound(); - } - - if ($typeToSubtractFrom->isSuperTypeOf($typeToRemove)->yes()) { - return $fromType->subtract($typeToRemove); - } - } - - return $fromType; + return $fromType->tryRemove($typeToRemove) ?? $fromType; } public static function removeNull(Type $type): Type diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index cbad84bc79..cd18394120 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -634,6 +634,16 @@ public function traverse(callable $cb): Type return $this; } + public function tryRemove(Type $typeToRemove): ?Type + { + $innerTypes = []; + foreach ($this->getTypes() as $innerType) { + $innerTypes[] = TypeCombinator::remove($innerType, $typeToRemove); + } + + return TypeCombinator::union(...$innerTypes); + } + /** * @param mixed[] $properties */ diff --git a/src/Type/VoidType.php b/src/Type/VoidType.php index 7863913572..0c40218abe 100644 --- a/src/Type/VoidType.php +++ b/src/Type/VoidType.php @@ -9,6 +9,7 @@ use PHPStan\Type\Traits\NonIterableTypeTrait; use PHPStan\Type\Traits\NonObjectTypeTrait; use PHPStan\Type\Traits\NonOffsetAccessibleTypeTrait; +use PHPStan\Type\Traits\NonRemoveableTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonTypeTrait; /** @api */ @@ -22,6 +23,7 @@ class VoidType implements Type use FalseyBooleanTypeTrait; use NonGenericTypeTrait; use UndecidedComparisonTypeTrait; + use NonRemoveableTypeTrait; /** @api */ public function __construct() diff --git a/tests/PHPStan/Rules/Api/data/class-implements-out-of-phpstan.php b/tests/PHPStan/Rules/Api/data/class-implements-out-of-phpstan.php index 0804ed77dd..91dbb321c5 100644 --- a/tests/PHPStan/Rules/Api/data/class-implements-out-of-phpstan.php +++ b/tests/PHPStan/Rules/Api/data/class-implements-out-of-phpstan.php @@ -284,6 +284,11 @@ public function traverse(callable $cb): \PHPStan\Type\Type // TODO: Implement traverse() method. } + public function tryRemove(Type $typeToRemove): ?Type + { + // TODO: Implement tryRemove() method. + } + public static function __set_state(array $properties): \PHPStan\Type\Type { // TODO: Implement __set_state() method. diff --git a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php index 78cb5edf89..3f9b8ea5d3 100644 --- a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php @@ -580,4 +580,10 @@ public function testBug6053(): void $this->analyse([__DIR__ . '/data/bug-6053.php'], []); } + public function testBug6438(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-6438.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/bug-6438.php b/tests/PHPStan/Rules/Methods/data/bug-6438.php new file mode 100644 index 0000000000..826725d665 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-6438.php @@ -0,0 +1,44 @@ + $value, 'description' => $description]; + } + + /** + * @phpstan-return array{value: int, description: string}|null + */ + public function testInteger() + { + return $this->getValueDescription(5, 'Description'); + } + + /** + * @phpstan-return array{value: bool, description: string}|null + */ + public function testBooleanTrue() + { + return $this->getValueDescription(true, 'Description'); + } + + /** + * @phpstan-return array{value: bool, description: string}|null + */ + public function testBooleanFalse() + { + return $this->getValueDescription(false, 'Description'); + } +} diff --git a/tests/PHPStan/Type/TypeCombinatorTest.php b/tests/PHPStan/Type/TypeCombinatorTest.php index b31dc6c3e2..145c7d080b 100644 --- a/tests/PHPStan/Type/TypeCombinatorTest.php +++ b/tests/PHPStan/Type/TypeCombinatorTest.php @@ -28,6 +28,7 @@ use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\Generic\TemplateBenevolentUnionType; +use PHPStan\Type\Generic\TemplateBooleanType; use PHPStan\Type\Generic\TemplateMixedType; use PHPStan\Type\Generic\TemplateObjectType; use PHPStan\Type\Generic\TemplateObjectWithoutClassType; @@ -3905,6 +3906,17 @@ public function dataRemove(): array ObjectType::class, 'stdClass', ], + [ + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new BooleanType(), + TemplateTypeVariance::createInvariant(), + ), + new ConstantBooleanType(false), + TemplateBooleanType::class, + 'T of bool (class Foo, parameter)', + ], ]; }