diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index 3e9c9ae862..92d0d4b415 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -268,11 +268,15 @@ public function isSuperTypeOf(Type $type): TrinaryLogic return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createMaybe(); } + $transformResult = static fn (TrinaryLogic $result) => $result; if ($this->subtractedType !== null) { $isSuperType = $this->subtractedType->isSuperTypeOf($type); if ($isSuperType->yes()) { return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createNo(); } + if ($isSuperType->maybe()) { + $transformResult = static fn (TrinaryLogic $result) => $result->and(TrinaryLogic::createMaybe()); + } } if ( @@ -289,7 +293,7 @@ public function isSuperTypeOf(Type $type): TrinaryLogic $thatClassName = $type->getClassName(); if ($thatClassName === $thisClassName) { - return TrinaryLogic::createYes(); + return $transformResult(TrinaryLogic::createYes()); } $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); @@ -302,11 +306,11 @@ public function isSuperTypeOf(Type $type): TrinaryLogic $thatClassReflection = $reflectionProvider->getClass($thatClassName); if ($thisClassReflection->getName() === $thatClassReflection->getName()) { - return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createYes(); + return self::$superTypes[$thisDescription][$description] = $transformResult(TrinaryLogic::createYes()); } if ($thatClassReflection->isSubclassOf($thisClassName)) { - return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createYes(); + return self::$superTypes[$thisDescription][$description] = $transformResult(TrinaryLogic::createYes()); } if ($thisClassReflection->isSubclassOf($thatClassName)) { diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index e18a75cf1b..8244bf601f 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -459,7 +459,36 @@ private static function intersectWithSubtractedType( return $a; } - if ($b instanceof SubtractableType) { + if ($b instanceof IntersectionType) { + $subtractableTypes = []; + foreach ($b->getTypes() as $innerType) { + if (!$innerType instanceof SubtractableType) { + continue; + } + + $subtractableTypes[] = $innerType; + } + + if (count($subtractableTypes) === 0) { + return $a->getTypeWithoutSubtractedType(); + } + + $subtractedTypes = []; + foreach ($subtractableTypes as $subtractableType) { + if ($subtractableType->getSubtractedType() === null) { + continue; + } + + $subtractedTypes[] = $subtractableType->getSubtractedType(); + } + + if (count($subtractedTypes) === 0) { + return $a->getTypeWithoutSubtractedType(); + + } + + $subtractedType = self::union(...$subtractedTypes); + } elseif ($b instanceof SubtractableType) { $subtractedType = $b->getSubtractedType(); if ($subtractedType === null) { return $a->getTypeWithoutSubtractedType(); diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 6f7f2b09d5..acb1b6c4c8 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -925,6 +925,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/collected-data.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7550.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7580.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/this-subtractable.php'); } /** diff --git a/tests/PHPStan/Analyser/data/bug-4117.php b/tests/PHPStan/Analyser/data/bug-4117.php index 4e5c2f90bf..4b5cbcb847 100644 --- a/tests/PHPStan/Analyser/data/bug-4117.php +++ b/tests/PHPStan/Analyser/data/bug-4117.php @@ -30,13 +30,14 @@ public function getIterator(): ArrayIterator public function broken(int $key) { $item = $this->items[$key] ?? null; + assertType('T (class Bug4117Types\GenericList, argument)|null', $item); if ($item) { assertType("T of mixed~0|0.0|''|'0'|array{}|false|null (class Bug4117Types\GenericList, argument)", $item); } else { assertType("(array{}&T (class Bug4117Types\GenericList, argument))|(0.0&T (class Bug4117Types\GenericList, argument))|(0&T (class Bug4117Types\GenericList, argument))|(''&T (class Bug4117Types\GenericList, argument))|('0'&T (class Bug4117Types\GenericList, argument))|(T (class Bug4117Types\GenericList, argument)&false)|null", $item); } - assertType("T of mixed~0|0.0|''|'0'|array{}|false|null (class Bug4117Types\GenericList, argument)|null", $item); + assertType('T (class Bug4117Types\GenericList, argument)|null', $item); return $item; } diff --git a/tests/PHPStan/Analyser/data/instanceof.php b/tests/PHPStan/Analyser/data/instanceof.php index bf1d12d2d2..098b74cb47 100644 --- a/tests/PHPStan/Analyser/data/instanceof.php +++ b/tests/PHPStan/Analyser/data/instanceof.php @@ -156,7 +156,7 @@ public function testExprInstanceof($subject, string $classString, $union, $inter assertType('object', $subject); assertType('bool', $subject instanceof $string); } else { - assertType('mixed~MixedT (method InstanceOfNamespace\Foo::testExprInstanceof(), argument)', $subject); + assertType('mixed', $subject); assertType('bool', $subject instanceof $string); } diff --git a/tests/PHPStan/Analyser/data/this-subtractable.php b/tests/PHPStan/Analyser/data/this-subtractable.php new file mode 100644 index 0000000000..a087e3468b --- /dev/null +++ b/tests/PHPStan/Analyser/data/this-subtractable.php @@ -0,0 +1,109 @@ +returnStatic(); + assertType('static(ThisSubtractable\Foo)', $s); + + if (!$s instanceof Bar && !$s instanceof Baz) { + assertType('static(ThisSubtractable\Foo~ThisSubtractable\Bar|ThisSubtractable\Baz)', $s); + } else { + assertType('(static(ThisSubtractable\Foo)&ThisSubtractable\Bar)|(static(ThisSubtractable\Foo)&ThisSubtractable\Baz)', $s); + } + + assertType('static(ThisSubtractable\Foo)', $s); + } + + public function doBaz(self $s) + { + assertType('ThisSubtractable\Foo', $s); + + if (!$s instanceof Lorem && !$s instanceof Ipsum) { + assertType('ThisSubtractable\Foo', $s); + } else { + assertType('(ThisSubtractable\Foo&ThisSubtractable\Ipsum)|(ThisSubtractable\Foo&ThisSubtractable\Lorem)', $s); + } + + assertType('ThisSubtractable\Foo', $s); + } + + public function doBazz(self $s) + { + assertType('ThisSubtractable\Foo', $s); + + if (!$s instanceof Bar && !$s instanceof Baz) { + assertType('ThisSubtractable\Foo~ThisSubtractable\Bar|ThisSubtractable\Baz', $s); + } else { + assertType('ThisSubtractable\Bar|ThisSubtractable\Baz', $s); + } + + assertType('ThisSubtractable\Foo', $s); + } + + public function doBazzz(self $s) + { + assertType('ThisSubtractable\Foo', $s); + if (!method_exists($s, 'test123', $s)) { + return; + } + + assertType('ThisSubtractable\Foo&hasMethod(test123)', $s); + + if (!$s instanceof Bar && !$s instanceof Baz) { + assertType('ThisSubtractable\Foo~ThisSubtractable\Bar|ThisSubtractable\Baz&hasMethod(test123)', $s); + } else { + assertType('(ThisSubtractable\Bar&hasMethod(test123))|(ThisSubtractable\Baz&hasMethod(test123))', $s); + } + + assertType('(ThisSubtractable\Bar&hasMethod(test123))|(ThisSubtractable\Baz&hasMethod(test123))|(ThisSubtractable\Foo~ThisSubtractable\Bar|ThisSubtractable\Baz&hasMethod(test123))', $s); + } + + /** + * @return static + */ + public function returnStatic() + { + return $this; + } + +} + +class Bar extends Foo +{ + +} + +class Baz extends Foo +{ + +} + +interface Lorem +{ + +} + +interface Ipsum +{ + +} diff --git a/tests/PHPStan/Type/Generic/GenericClassStringTypeTest.php b/tests/PHPStan/Type/Generic/GenericClassStringTypeTest.php index 4cc5157d87..3efd727f4c 100644 --- a/tests/PHPStan/Type/Generic/GenericClassStringTypeTest.php +++ b/tests/PHPStan/Type/Generic/GenericClassStringTypeTest.php @@ -9,7 +9,9 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\ClassStringType; +use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; use PHPStan\Type\ObjectType; use PHPStan\Type\ObjectWithoutClassType; @@ -145,6 +147,14 @@ public function dataIsSuperTypeOf(): array new ConstantStringType(Exception::class), TrinaryLogic::createYes(), ], + 18 => [ + new GenericClassStringType(new ObjectType(Type::class, new UnionType([ + new ObjectType(ConstantIntegerType::class), + new ObjectType(IntegerRangeType::class), + ]))), + new ConstantStringType(IntegerType::class), + TrinaryLogic::createMaybe(), + ], ]; } diff --git a/tests/PHPStan/Type/ObjectTypeTest.php b/tests/PHPStan/Type/ObjectTypeTest.php index b435768d57..2af650e6bb 100644 --- a/tests/PHPStan/Type/ObjectTypeTest.php +++ b/tests/PHPStan/Type/ObjectTypeTest.php @@ -10,6 +10,7 @@ use DateTime; use DateTimeImmutable; use DateTimeInterface; +use Exception; use Generator; use InvalidArgumentException; use Iterator; @@ -18,6 +19,7 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\HasMethodType; use PHPStan\Type\Accessory\HasPropertyType; +use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\Generic\TemplateTypeFactory; @@ -26,6 +28,7 @@ use SimpleXMLElement; use stdClass; use Throwable; +use ThrowPoints\TryCatch\MyInvalidArgumentException; use Traversable; use function sprintf; @@ -298,7 +301,7 @@ public function dataIsSuperTypeOf(): array 39 => [ new ObjectType(Throwable::class, new ObjectType(InvalidArgumentException::class)), new ObjectType('Exception'), - TrinaryLogic::createYes(), + TrinaryLogic::createMaybe(), ], 40 => [ new ObjectType(Throwable::class, new ObjectType('Exception')), @@ -313,7 +316,7 @@ public function dataIsSuperTypeOf(): array 42 => [ new ObjectType(Throwable::class, new ObjectType('Exception')), new ObjectType(Throwable::class), - TrinaryLogic::createYes(), + TrinaryLogic::createMaybe(), ], 43 => [ new ObjectType(Throwable::class), @@ -360,6 +363,49 @@ public function dataIsSuperTypeOf(): array ), TrinaryLogic::createMaybe(), ], + 49 => [ + new ObjectType(Exception::class, new ObjectType(InvalidArgumentException::class)), + new ObjectType(InvalidArgumentException::class), + TrinaryLogic::createNo(), + ], + 50 => [ + new ObjectType(Exception::class, new ObjectType(InvalidArgumentException::class)), + new ObjectType(MyInvalidArgumentException::class), + TrinaryLogic::createNo(), + ], + 51 => [ + new ObjectType(Exception::class, new ObjectType(InvalidArgumentException::class)), + new ObjectType(LogicException::class), + TrinaryLogic::createMaybe(), + ], + 52 => [ + new ObjectType(InvalidArgumentException::class, new ObjectType(MyInvalidArgumentException::class)), + new ObjectType(Exception::class), + TrinaryLogic::createMaybe(), + ], + 53 => [ + new ObjectType(InvalidArgumentException::class, new ObjectType(MyInvalidArgumentException::class)), + new ObjectType(Exception::class, new ObjectType(InvalidArgumentException::class)), + TrinaryLogic::createNo(), + ], + 54 => [ + new ObjectType(InvalidArgumentException::class), + new ObjectType(Exception::class, new ObjectType(InvalidArgumentException::class)), + TrinaryLogic::createNo(), + ], + 55 => [ + new ObjectType(stdClass::class, new ObjectType(Throwable::class)), + new ObjectType(Throwable::class), + TrinaryLogic::createNo(), + ], + 56 => [ + new ObjectType(Type::class, new UnionType([ + new ObjectType(ConstantIntegerType::class), + new ObjectType(IntegerRangeType::class), + ])), + new ObjectType(IntegerType::class), + TrinaryLogic::createMaybe(), + ], ]; } diff --git a/tests/PHPStan/Type/StaticTypeTest.php b/tests/PHPStan/Type/StaticTypeTest.php index 670d42aaf7..a462bda476 100644 --- a/tests/PHPStan/Type/StaticTypeTest.php +++ b/tests/PHPStan/Type/StaticTypeTest.php @@ -253,6 +253,23 @@ public function dataIsSuperTypeOf(): array new ObjectType(FinalChild::class), TrinaryLogic::createYes(), ], + [ + new ThisType( + $reflectionProvider->getClass(\ThisSubtractable\Foo::class), // phpcs:ignore + new UnionType([new ObjectType(\ThisSubtractable\Bar::class), new ObjectType(\ThisSubtractable\Baz::class)]), // phpcs:ignore + ), + new UnionType([ + new IntersectionType([ + new ThisType($reflectionProvider->getClass(\ThisSubtractable\Foo::class)), // phpcs:ignore + new ObjectType(\ThisSubtractable\Bar::class), // phpcs:ignore + ]), + new IntersectionType([ + new ThisType($reflectionProvider->getClass(\ThisSubtractable\Foo::class)), // phpcs:ignore + new ObjectType(\ThisSubtractable\Baz::class), // phpcs:ignore + ]), + ]), + TrinaryLogic::createNo(), + ], ]; } diff --git a/tests/PHPStan/Type/TypeCombinatorTest.php b/tests/PHPStan/Type/TypeCombinatorTest.php index 59f19c40b6..84d7f24372 100644 --- a/tests/PHPStan/Type/TypeCombinatorTest.php +++ b/tests/PHPStan/Type/TypeCombinatorTest.php @@ -2119,6 +2119,27 @@ public function dataUnion(): iterable UnionType::class, '$this(stdClass)|stdClass::foo', ]; + + yield [ + [ + new ThisType( + $reflectionProvider->getClass(\ThisSubtractable\Foo::class), // phpcs:ignore + new UnionType([new ObjectType(\ThisSubtractable\Bar::class), new ObjectType(\ThisSubtractable\Baz::class)]), // phpcs:ignore + ), + new UnionType([ + new IntersectionType([ + new ThisType($reflectionProvider->getClass(\ThisSubtractable\Foo::class)), // phpcs:ignore + new ObjectType(\ThisSubtractable\Bar::class), // phpcs:ignore + ]), + new IntersectionType([ + new ThisType($reflectionProvider->getClass(\ThisSubtractable\Foo::class)), // phpcs:ignore + new ObjectType(\ThisSubtractable\Baz::class), // phpcs:ignore + ]), + ]), + ], + ThisType::class, + '$this(ThisSubtractable\Foo)', + ]; } /**