From 5a42acfab1da770b887d351e87e047bfd1dfdf76 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sun, 3 Aug 2025 14:40:30 +0200 Subject: [PATCH 1/3] Improve TypeCombinator::unionWithSubtractedType --- src/Type/TypeCombinator.php | 12 ++++++++++ tests/PHPStan/Analyser/nsrt/subtracted.php | 27 ++++++++++++++++++++++ tests/PHPStan/Type/TypeCombinatorTest.php | 8 +++++++ 3 files changed, 47 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/subtracted.php diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index 103e88278a..780cb6e8c8 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -540,6 +540,18 @@ private static function unionWithSubtractedType( return $type; } + if ($subtractedType instanceof SubtractableType) { + $withoutSubtracted = $subtractedType->getTypeWithoutSubtractedType(); + if ($withoutSubtracted->isSuperTypeOf($type)->yes()) { + $subtractedSubtractedType = $subtractedType->getSubtractedType(); + if ($subtractedSubtractedType === null) { + return new NeverType(); + } + + return self::intersect($type, $subtractedSubtractedType); + } + } + if ($type instanceof SubtractableType) { $subtractedType = $type->getSubtractedType() === null ? $subtractedType diff --git a/tests/PHPStan/Analyser/nsrt/subtracted.php b/tests/PHPStan/Analyser/nsrt/subtracted.php new file mode 100644 index 0000000000..903cf0b485 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/subtracted.php @@ -0,0 +1,27 @@ + Date: Sat, 30 Aug 2025 12:14:37 +0200 Subject: [PATCH 2/3] Fix --- tests/PHPStan/Analyser/nsrt/subtracted.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/PHPStan/Analyser/nsrt/subtracted.php b/tests/PHPStan/Analyser/nsrt/subtracted.php index 903cf0b485..0f7979223c 100644 --- a/tests/PHPStan/Analyser/nsrt/subtracted.php +++ b/tests/PHPStan/Analyser/nsrt/subtracted.php @@ -7,7 +7,11 @@ class HelloWorld { - public function sayHello(mixed $date, bool $foo): void + /** + * @param mixed $date + * @param bool $foo + */ + public function sayHello($date, $foo): void { if(is_object($date)){ From ed8df0ea1a4349b2b31847cc424f9f422b7179e9 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Tue, 2 Sep 2025 12:25:29 +0200 Subject: [PATCH 3/3] Update describe --- src/Type/MixedType.php | 20 +++------------ src/Type/ObjectType.php | 19 +++----------- src/Type/ObjectWithoutClassType.php | 14 +++------- src/Type/Traits/SubstractableTypeTrait.php | 30 ++++++++++++++++++++++ tests/PHPStan/Analyser/nsrt/subtracted.php | 2 +- tests/PHPStan/Type/TypeCombinatorTest.php | 2 +- 6 files changed, 43 insertions(+), 44 deletions(-) create mode 100644 src/Type/Traits/SubstractableTypeTrait.php diff --git a/src/Type/MixedType.php b/src/Type/MixedType.php index 1245743947..11d60685b5 100644 --- a/src/Type/MixedType.php +++ b/src/Type/MixedType.php @@ -36,6 +36,7 @@ use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; +use PHPStan\Type\Traits\SubstractableTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; use function get_class; use function sprintf; @@ -47,6 +48,7 @@ class MixedType implements CompoundType, SubtractableType use NonGenericTypeTrait; use UndecidedComparisonCompoundTypeTrait; use NonGeneralizableTypeTrait; + use SubstractableTypeTrait; private ?Type $subtractedType; @@ -471,23 +473,9 @@ public function describe(VerbosityLevel $level): string return $level->handle( static fn (): string => 'mixed', static fn (): string => 'mixed', + fn (): string => 'mixed' . $this->describeSubtractedType($this->subtractedType, $level), function () use ($level): string { - $description = 'mixed'; - if ($this->subtractedType !== null) { - $description .= $this->subtractedType instanceof UnionType - ? sprintf('~(%s)', $this->subtractedType->describe($level)) - : sprintf('~%s', $this->subtractedType->describe($level)); - } - - return $description; - }, - function () use ($level): string { - $description = 'mixed'; - if ($this->subtractedType !== null) { - $description .= $this->subtractedType instanceof UnionType - ? sprintf('~(%s)', $this->subtractedType->describe($level)) - : sprintf('~%s', $this->subtractedType->describe($level)); - } + $description = 'mixed' . $this->describeSubtractedType($this->subtractedType, $level); if ($this->isExplicitMixed) { $description .= '=explicit'; diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index 78e4473410..cdb01e7d31 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -46,6 +46,7 @@ use PHPStan\Type\Traits\NonArrayTypeTrait; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; +use PHPStan\Type\Traits\SubstractableTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonTypeTrait; use Stringable; use Throwable; @@ -68,6 +69,7 @@ class ObjectType implements TypeWithClassName, SubtractableType use NonGenericTypeTrait; use UndecidedComparisonTypeTrait; use NonGeneralizableTypeTrait; + use SubstractableTypeTrait; private const EXTRA_OFFSET_CLASSES = [ 'DOMNamedNodeMap', // Only read and existence @@ -504,16 +506,7 @@ public function describe(VerbosityLevel $level): string return $reflectionProvider->getClassName($this->className); }; - $preciseWithSubtracted = function () use ($level): string { - $description = $this->className; - if ($this->subtractedType !== null) { - $description .= $this->subtractedType instanceof UnionType - ? sprintf('~(%s)', $this->subtractedType->describe($level)) - : sprintf('~%s', $this->subtractedType->describe($level)); - } - - return $description; - }; + $preciseWithSubtracted = fn (): string => $this->className . $this->describeSubtractedType($this->subtractedType, $level); return $level->handle( $preciseNameCallback, @@ -559,11 +552,7 @@ private function describeCache(): string $description .= '<' . implode(', ', $typeDescriptions) . '>'; } - if ($this->subtractedType !== null) { - $description .= $this->subtractedType instanceof UnionType - ? sprintf('~(%s)', $this->subtractedType->describe(VerbosityLevel::cache())) - : sprintf('~%s', $this->subtractedType->describe(VerbosityLevel::cache())); - } + $description .= $this->describeSubtractedType($this->subtractedType, VerbosityLevel::cache()); $reflection = $this->classReflection; if ($reflection !== null) { diff --git a/src/Type/ObjectWithoutClassType.php b/src/Type/ObjectWithoutClassType.php index a3b47c7686..9823dcaf80 100644 --- a/src/Type/ObjectWithoutClassType.php +++ b/src/Type/ObjectWithoutClassType.php @@ -7,8 +7,8 @@ use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; use PHPStan\Type\Traits\ObjectTypeTrait; +use PHPStan\Type\Traits\SubstractableTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonTypeTrait; -use function sprintf; /** @api */ class ObjectWithoutClassType implements SubtractableType @@ -18,6 +18,7 @@ class ObjectWithoutClassType implements SubtractableType use NonGenericTypeTrait; use UndecidedComparisonTypeTrait; use NonGeneralizableTypeTrait; + use SubstractableTypeTrait; private ?Type $subtractedType; @@ -120,16 +121,7 @@ public function describe(VerbosityLevel $level): string return $level->handle( static fn (): string => 'object', static fn (): string => 'object', - function () use ($level): string { - $description = 'object'; - if ($this->subtractedType !== null) { - $description .= $this->subtractedType instanceof UnionType - ? sprintf('~(%s)', $this->subtractedType->describe($level)) - : sprintf('~%s', $this->subtractedType->describe($level)); - } - - return $description; - }, + fn (): string => 'object' . $this->describeSubtractedType($this->subtractedType, $level), ); } diff --git a/src/Type/Traits/SubstractableTypeTrait.php b/src/Type/Traits/SubstractableTypeTrait.php new file mode 100644 index 0000000000..bf563eb899 --- /dev/null +++ b/src/Type/Traits/SubstractableTypeTrait.php @@ -0,0 +1,30 @@ +getSubtractedType() !== null) + ) { + return sprintf('~(%s)', $subtractedType->describe($level)); + } + + return sprintf('~%s', $subtractedType->describe($level)); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/subtracted.php b/tests/PHPStan/Analyser/nsrt/subtracted.php index 0f7979223c..0bb82c342c 100644 --- a/tests/PHPStan/Analyser/nsrt/subtracted.php +++ b/tests/PHPStan/Analyser/nsrt/subtracted.php @@ -21,7 +21,7 @@ public function sayHello($date, $foo): void if ($foo) { $date = new \stdClass(); } - assertType('mixed~object~stdClass', $date); + assertType('mixed~(object~stdClass)', $date); if (is_object($date)) { assertType('stdClass', $date); diff --git a/tests/PHPStan/Type/TypeCombinatorTest.php b/tests/PHPStan/Type/TypeCombinatorTest.php index 695a2a1bc6..7cfe7237ab 100644 --- a/tests/PHPStan/Type/TypeCombinatorTest.php +++ b/tests/PHPStan/Type/TypeCombinatorTest.php @@ -1140,7 +1140,7 @@ public static function dataUnion(): iterable new ObjectType('InvalidArgumentException'), ], MixedType::class, - 'mixed~Exception~InvalidArgumentException=implicit', + 'mixed~(Exception~InvalidArgumentException)=implicit', ], [ [