diff --git a/src/PhpDoc/ResolvedPhpDocBlock.php b/src/PhpDoc/ResolvedPhpDocBlock.php index e1026770cef..4c7b6acd797 100644 --- a/src/PhpDoc/ResolvedPhpDocBlock.php +++ b/src/PhpDoc/ResolvedPhpDocBlock.php @@ -30,6 +30,7 @@ use PHPStan\Reflection\ReflectionProvider; use PHPStan\ShouldNotHappenException; use PHPStan\Turbo\ReferencedByTurboExtension; +use PHPStan\Type\ConditionalType; use PHPStan\Type\ConditionalTypeForParameter; use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateTypeMap; @@ -1249,7 +1250,32 @@ private static function resolveTemplateTypeInTag( $classReflection->getCallSiteVarianceMap(), $positionVariance, ); - return $tag->withType($type); + return $tag->withType(self::resolveConditionalTypes($type)); + } + + /** + * A conditional type on the parent's template type can be decided by the child's + * type arguments. It stays when its subject is static, which depends on the called + * class, or when it resolves to another conditional type, including one on a + * parameter, which the parent's conditional type is compared with branch by branch. + */ + private static function resolveConditionalTypes(Type $type): Type + { + if (!$type->hasTemplateOrLateResolvableType()) { + return $type; + } + + return TypeTraverser::map($type, static function (Type $type, callable $traverse): Type { + $type = $traverse($type); + if ($type instanceof ConditionalType && !$type->getSubject() instanceof StaticType && $type->isResolvable()) { + $resolved = $type->resolve(); + if (!$resolved instanceof ConditionalType && !$resolved instanceof ConditionalTypeForParameter) { + return $resolved; + } + } + + return $type; + }); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-15128.php b/tests/PHPStan/Analyser/nsrt/bug-15128.php new file mode 100644 index 00000000000..b4bd97e0b7b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-15128.php @@ -0,0 +1,94 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug15128Nsrt; + +use function PHPStan\Testing\assertType; + +/** + * @template T of int|string + */ +class A +{ + + /** + * @return (T is int ? string : int) + */ + public function foo(): int|string + { + return 1; + } + +} + +/** + * @extends A + */ +class B extends A +{ + + public function foo(): int + { + return 1; + } + +} + +/** + * @template U of int|string + * @extends A + */ +class C extends A +{ + + public function foo(): int|string + { + return 1; + } + +} + +interface Foo +{ + +} + +class Base +{ + + /** + * @return (static is Foo ? int : string) + */ + public function bar(): int|string + { + return 1; + } + +} + +class Child extends Base +{ + + public function bar(): int|string + { + return 1; + } + +} + +class GrandChild extends Child implements Foo +{ + +} + +/** + * @param C $c + */ +function doFoo(B $b, C $c, Child $child, GrandChild $grandChild): void +{ + assertType('int', $b->foo()); + assertType('string', $c->foo()); + assertType('int|string', $child->bar()); + assertType('int', $grandChild->bar()); +} diff --git a/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php b/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php index f24317d6b61..27a35e979cc 100644 --- a/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php @@ -676,4 +676,11 @@ public function testPureUnlessParameterPassedOverride(): void ]); } + public function testBug15128(): void + { + $this->reportMaybes = true; + $this->reportStatic = true; + $this->analyse([__DIR__ . '/data/bug-15128.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/bug-15128.php b/tests/PHPStan/Rules/Methods/data/bug-15128.php new file mode 100644 index 00000000000..503267bda04 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-15128.php @@ -0,0 +1,42 @@ + : (TValue is int ? int : string)) + */ + public function get(); + + /** + * @param int|string $key + * @return (TKey is int ? list : ($key is int ? int : string)) + */ + public function find($key); + +} + +/** + * @template TValue of int|string + * @implements Store + */ +class StringStore implements Store +{ + + public function get() + { + return 1; + } + + public function find($key) + { + return 1; + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/MethodConditionalReturnTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/MethodConditionalReturnTypeRuleTest.php index 5a86e06f2df..5cadc2a03ee 100644 --- a/tests/PHPStan/Rules/PhpDoc/MethodConditionalReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/MethodConditionalReturnTypeRuleTest.php @@ -112,4 +112,9 @@ public function testBug10938(): void $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-10938.php'], []); } + public function testBug15128(): void + { + $this->analyse([__DIR__ . '/data/bug-15128.php'], []); + } + } diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-15128.php b/tests/PHPStan/Rules/PhpDoc/data/bug-15128.php new file mode 100644 index 00000000000..1236753ab1a --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-15128.php @@ -0,0 +1,68 @@ += 8.0 + +namespace Bug15128; + +use Override; + +/** + * @template T of int|string + */ +class A { + /** + * @param T $val + */ + public function __construct( + protected int|string $val, + ) { + } + + /** + * @return (T is int ? string : int) + */ + public function foo(): int|string { + return is_string($this->val) ? intval($this->val) : (string)$this->val; + } + + /** + * @param (T is int ? string : int) $x + */ + public function bar(int|string $x): void { + } + + /** + * @return (T is int ? string : (T is 'a' ? int : string)) + */ + public function baz(): int|string { + return 1; + } +} + +/** + * @extends A + */ +class B extends A { + #[Override] + public function foo(): int { + return intval($this->val); + } + + #[Override] + public function bar(int|string $x): void { + } + + #[Override] + public function baz(): int|string { + return 1; + } +} + +/** + * @template U of int + * @extends A + */ +class C extends A { + #[Override] + public function foo(): string { + return (string)$this->val; + } +}