From d909fb5d3f02479a91330bc6d2db984d58390428 Mon Sep 17 00:00:00 2001 From: VincentLanglet <9052536+VincentLanglet@users.noreply.github.com> Date: Mon, 11 May 2026 22:04:03 +0000 Subject: [PATCH 1/3] Check inner union types before delegating to `isSubTypeOf` for `LateResolvableType` in `UnionType::isSuperTypeOf()` - When a `ConditionalTypeForParameter` (or other `LateResolvableType`) was inside a `UnionType`, `isSuperTypeOf()` immediately delegated to `isSubTypeOf()`, which resolved the conditional and lost precision (anding with Maybe for non-resolvable types) - Now the inner types of the union are checked first via `isSuperTypeOf()`, which allows matching conditional types to find each other directly (via the `$type instanceof self` branch in `ConditionalTypeForParameter::isSuperTypeOf()`) before falling back to the resolve-based `isSubTypeOf()` path - This fixes false positive "should be contravariant/covariant" errors when a child method overrides a parent method with the exact same conditional parameter/return type in a union --- src/Type/UnionType.php | 10 ++++- .../Rules/Methods/MethodSignatureRuleTest.php | 8 ++++ .../PHPStan/Rules/Methods/data/bug-10942.php | 39 +++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Rules/Methods/data/bug-10942.php diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index f69dff57684..d427c36fa69 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -268,12 +268,20 @@ public function isSuperTypeOf(Type $otherType): IsSuperTypeOfResult ($otherType instanceof self && !$otherType instanceof TemplateUnionType) || ($otherType instanceof IterableType && !$otherType instanceof TemplateIterableType) || $otherType instanceof NeverType - || ($otherType instanceof LateResolvableType && $otherType instanceof CompoundType && !$otherType instanceof TemplateType) || $otherType instanceof IntegerRangeType ) { return $otherType->isSubTypeOf($this); } + if ($otherType instanceof LateResolvableType && $otherType instanceof CompoundType && !$otherType instanceof TemplateType) { + foreach ($this->types as $innerType) { + if ($innerType->isSuperTypeOf($otherType)->yes()) { + return IsSuperTypeOfResult::createYes(); + } + } + return $otherType->isSubTypeOf($this); + } + $results = []; foreach ($this->types as $innerType) { $result = $innerType->isSuperTypeOf($otherType); diff --git a/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php b/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php index b016afd0a48..51b42d8d00c 100644 --- a/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php @@ -630,4 +630,12 @@ public function testBug14563Trait(): void ]); } + #[RequiresPhp('>= 8.0.0')] + public function testBug10942(): void + { + $this->reportMaybes = true; + $this->reportStatic = true; + $this->analyse([__DIR__ . '/data/bug-10942.php'], []); + } + } 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 00000000000..34ea640cbc6 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-10942.php @@ -0,0 +1,39 @@ += 8.0 + +namespace Bug10942; + +class A +{ + /** + * @param string|($x is int ? float : bool) $y + */ + public function foo(mixed $x, mixed $y): void + { + } + + /** + * @return string|($x is int ? float : bool) + */ + public function bar(mixed $x): mixed + { + return ''; + } +} + +class B extends A +{ + /** + * @param string|($x is int ? float : bool) $y + */ + public function foo(mixed $x, mixed $y): void + { + } + + /** + * @return string|($x is int ? float : bool) + */ + public function bar(mixed $x): mixed + { + return ''; + } +} From b457e50f937a985d6472a87628c18f158e203b0e Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Tue, 12 May 2026 05:32:59 +0000 Subject: [PATCH 2/3] Add 1:1 reproducer from issue #10942 to regression test The existing test used a simplified snippet. Add the exact code from the issue's playground link (with inherited conditional param type and no repeated PHPDoc in the child class) as the primary test case, and keep the original explicit-PHPDoc variant as a secondary case. Co-Authored-By: Claude Opus 4.6 --- .../PHPStan/Rules/Methods/data/bug-10942.php | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/tests/PHPStan/Rules/Methods/data/bug-10942.php b/tests/PHPStan/Rules/Methods/data/bug-10942.php index 34ea640cbc6..810bda5d518 100644 --- a/tests/PHPStan/Rules/Methods/data/bug-10942.php +++ b/tests/PHPStan/Rules/Methods/data/bug-10942.php @@ -3,6 +3,25 @@ namespace Bug10942; class A +{ + /** + * @param string|($operator is 'in' ? int : never) $sqlRight + */ + protected function _renderConditionBinary(string $operator, string $sqlLeft, $sqlRight): string + { + return 'x'; + } +} + +class B extends A +{ + protected function _renderConditionBinary(string $operator, string $sqlLeft, $sqlRight): string + { + return 'y'; + } +} + +class C { /** * @param string|($x is int ? float : bool) $y @@ -20,7 +39,7 @@ public function bar(mixed $x): mixed } } -class B extends A +class D extends C { /** * @param string|($x is int ? float : bool) $y From 559d8051b5905250769d03c04497326335aacaa5 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 12 May 2026 07:36:12 +0200 Subject: [PATCH 3/3] simplify --- src/Type/UnionType.php | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index d427c36fa69..c1296d2f8f3 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -273,15 +273,6 @@ public function isSuperTypeOf(Type $otherType): IsSuperTypeOfResult return $otherType->isSubTypeOf($this); } - if ($otherType instanceof LateResolvableType && $otherType instanceof CompoundType && !$otherType instanceof TemplateType) { - foreach ($this->types as $innerType) { - if ($innerType->isSuperTypeOf($otherType)->yes()) { - return IsSuperTypeOfResult::createYes(); - } - } - return $otherType->isSubTypeOf($this); - } - $results = []; foreach ($this->types as $innerType) { $result = $innerType->isSuperTypeOf($otherType); @@ -292,7 +283,10 @@ public function isSuperTypeOf(Type $otherType): IsSuperTypeOfResult } $result = IsSuperTypeOfResult::createNo()->or(...$results); - if ($otherType instanceof TemplateUnionType) { + if ( + $otherType instanceof TemplateUnionType + || ($otherType instanceof LateResolvableType && $otherType instanceof CompoundType && !$otherType instanceof TemplateType) + ) { return $result->or($otherType->isSubTypeOf($this)); }