From d574aa7ab1d8ea0cd0b8ba225e0936411cadbc37 Mon Sep 17 00:00:00 2001 From: tajimayuki Date: Thu, 27 Nov 2025 01:21:59 +0900 Subject: [PATCH 1/3] Short-circuit identical late-resolvable conditionals in subtype checks --- src/Type/Traits/LateResolvableTypeTrait.php | 13 +++++++++ .../Rules/Methods/MethodSignatureRuleTest.php | 7 +++++ .../PHPStan/Rules/Methods/data/bug-10942.php | 27 +++++++++++++++++++ 3 files changed, 47 insertions(+) create mode 100644 tests/PHPStan/Rules/Methods/data/bug-10942.php diff --git a/src/Type/Traits/LateResolvableTypeTrait.php b/src/Type/Traits/LateResolvableTypeTrait.php index d35ee72461..023fb51187 100644 --- a/src/Type/Traits/LateResolvableTypeTrait.php +++ b/src/Type/Traits/LateResolvableTypeTrait.php @@ -18,6 +18,7 @@ use PHPStan\Type\LateResolvableType; use PHPStan\Type\NeverType; use PHPStan\Type\Type; +use PHPStan\Type\UnionType; trait LateResolvableTypeTrait { @@ -570,6 +571,18 @@ public function tryRemove(Type $typeToRemove): ?Type public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult { + if ($this->equals($otherType)) { + return IsSuperTypeOfResult::createYes(); + } + + if ($otherType instanceof UnionType) { + foreach ($otherType->getTypes() as $innerType) { + if ($this->equals($innerType)) { + return IsSuperTypeOfResult::createYes(); + } + } + } + $result = $this->resolve(); if ($result instanceof CompoundType) { diff --git a/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php b/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php index faa0a3080b..0af1b0e953 100644 --- a/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php @@ -550,6 +550,13 @@ public function testBug10488(): void $this->analyse([__DIR__ . '/data/bug-10488.php'], []); } + public function testBug10942(): void + { + $this->reportMaybes = true; + $this->reportStatic = true; + $this->analyse([__DIR__ . '/data/bug-10942.php'], []); + } + #[RequiresPhp('>= 8.0')] public function testBug12073(): void { diff --git a/tests/PHPStan/Rules/Methods/data/bug-10942.php b/tests/PHPStan/Rules/Methods/data/bug-10942.php new file mode 100644 index 0000000000..d4a6d5824f --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-10942.php @@ -0,0 +1,27 @@ + Date: Thu, 27 Nov 2025 23:40:14 +0900 Subject: [PATCH 2/3] Short-circuit identical late-resolvable types in isSuperTypeOf and add coverage --- src/Type/Traits/LateResolvableTypeTrait.php | 12 +++++++ .../Type/LateResolvableTypeTraitTest.php | 34 +++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 tests/PHPStan/Type/LateResolvableTypeTraitTest.php diff --git a/src/Type/Traits/LateResolvableTypeTrait.php b/src/Type/Traits/LateResolvableTypeTrait.php index 023fb51187..4c8bfd4c53 100644 --- a/src/Type/Traits/LateResolvableTypeTrait.php +++ b/src/Type/Traits/LateResolvableTypeTrait.php @@ -66,6 +66,18 @@ private function isSuperTypeOfDefault(Type $type): IsSuperTypeOfResult return IsSuperTypeOfResult::createYes(); } + if ($this->equals($type)) { + return IsSuperTypeOfResult::createYes(); + } + + if ($type instanceof UnionType) { + foreach ($type->getTypes() as $innerType) { + if ($this->equals($innerType)) { + return IsSuperTypeOfResult::createYes(); + } + } + } + if ($type instanceof LateResolvableType) { $type = $type->resolve(); } diff --git a/tests/PHPStan/Type/LateResolvableTypeTraitTest.php b/tests/PHPStan/Type/LateResolvableTypeTraitTest.php new file mode 100644 index 0000000000..bfb7bf9a8e --- /dev/null +++ b/tests/PHPStan/Type/LateResolvableTypeTraitTest.php @@ -0,0 +1,34 @@ +assertSame('Yes', $conditional->isSuperTypeOf($conditional)->describe()); + + $unionWithConditional = new UnionType([ + new StringType(), + $conditional, + ]); + + $this->assertSame('Yes', $conditional->isSuperTypeOf($unionWithConditional)->describe()); + } + +} From 28bd4ecf2d090ca7b0057a72dcb462c94707f81e Mon Sep 17 00:00:00 2001 From: tajimayuki Date: Fri, 28 Nov 2025 00:27:45 +0900 Subject: [PATCH 3/3] Add data-provider tests for late-resolvable isSuperTypeOf/isSubTypeOf --- .../Type/LateResolvableTypeTraitTest.php | 70 +++++++++++++++---- 1 file changed, 57 insertions(+), 13 deletions(-) diff --git a/tests/PHPStan/Type/LateResolvableTypeTraitTest.php b/tests/PHPStan/Type/LateResolvableTypeTraitTest.php index bfb7bf9a8e..5fcc06596f 100644 --- a/tests/PHPStan/Type/LateResolvableTypeTraitTest.php +++ b/tests/PHPStan/Type/LateResolvableTypeTraitTest.php @@ -3,32 +3,76 @@ namespace PHPStan\Type; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\TrinaryLogic; use PHPStan\Type\IntegerType; use PHPStan\Type\NeverType; +use PHPStan\Type\StringType; +use PHPStan\Type\Type; +use PHPStan\Type\ConditionalTypeForParameter; use PHPStan\Type\UnionType; use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\Attributes\DataProvider; class LateResolvableTypeTraitTest extends TestCase { + public static function dataIsSuperTypeOf(): array + { + return self::provideCases(); + } + + public static function dataIsSubTypeOf(): array + { + return self::provideCases(); + } - public function testIsSuperTypeOfForConditional(): void + private static function createConditional( + string $parameterName = '$operator', + string $targetLiteral = 'in', + ?Type $ifType = null, + ?Type $elseType = null, + bool $negated = false, + ): ConditionalTypeForParameter { - $conditional = new ConditionalTypeForParameter( - '$operator', - new ConstantStringType('in'), - new IntegerType(), - new NeverType(), - false, + return new ConditionalTypeForParameter( + $parameterName, + new ConstantStringType($targetLiteral), + $ifType ?? new IntegerType(), + $elseType ?? new NeverType(), + $negated, ); + } - $this->assertSame('Yes', $conditional->isSuperTypeOf($conditional)->describe()); + /** + * @return list + */ + private static function provideCases(): array + { + return [ + 'conditional vs same conditional' => [ + self::createConditional(), + self::createConditional(), + TrinaryLogic::createYes(), + ], + 'conditional vs union containing it' => [ + self::createConditional(), + new UnionType([new StringType(), self::createConditional()]), + TrinaryLogic::createYes(), + ], + ]; + } - $unionWithConditional = new UnionType([ - new StringType(), - $conditional, - ]); + #[DataProvider('dataIsSuperTypeOf')] + public function testIsSuperTypeOf(Type $left, Type $right, TrinaryLogic $expected): void + { + $actual = $left->isSuperTypeOf($right); + $this->assertSame($expected->describe(), $actual->describe()); + } - $this->assertSame('Yes', $conditional->isSuperTypeOf($unionWithConditional)->describe()); + #[DataProvider('dataIsSubTypeOf')] + public function testIsSubTypeOf(Type $left, Type $right, TrinaryLogic $expected): void + { + $actual = $left->isSubTypeOf($right); + $this->assertSame($expected->describe(), $actual->describe()); } }