Skip to content

Commit ca00199

Browse files
authored
Resolve ConditionalType when subject-target relationship is deterministic despite containing template types (#5631)
1 parent 5679c36 commit ca00199

9 files changed

Lines changed: 332 additions & 1 deletion

File tree

src/Type/ConditionalType.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,13 @@ public function describe(VerbosityLevel $level): string
113113

114114
public function isResolvable(): bool
115115
{
116-
return !TypeUtils::containsTemplateType($this->subject) && !TypeUtils::containsTemplateType($this->target);
116+
if (!TypeUtils::containsTemplateType($this->subject) && !TypeUtils::containsTemplateType($this->target)) {
117+
return true;
118+
}
119+
120+
$isSuperType = $this->target->isSuperTypeOf($this->subject);
121+
122+
return $isSuperType->yes() || $isSuperType->no();
117123
}
118124

119125
protected function getResult(): Type
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php // lint >= 8.0
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug11894Nsrt;
6+
7+
use function PHPStan\Testing\assertType;
8+
9+
/**
10+
* @template T
11+
* @param T $a
12+
* @return (T is string ? string : T)
13+
*/
14+
function conditionalReturn(mixed $a): mixed
15+
{
16+
if (!is_string($a)) {
17+
return $a;
18+
}
19+
return trim($a);
20+
}
21+
22+
/**
23+
* @template T of string|null
24+
* @param T $a
25+
*/
26+
function testNarrowedToString(mixed $a): void
27+
{
28+
if (!is_string($a)) {
29+
return;
30+
}
31+
assertType('string', conditionalReturn($a));
32+
}
33+
34+
/**
35+
* @template T of int|null
36+
* @param T $a
37+
*/
38+
function testNarrowedToNonMatchingType(mixed $a): void
39+
{
40+
if (!is_int($a)) {
41+
return;
42+
}
43+
assertType('T of int (function Bug11894Nsrt\testNarrowedToNonMatchingType(), argument)', conditionalReturn($a));
44+
}
45+
46+
/**
47+
* @template T of string|int
48+
* @param T $a
49+
*/
50+
function testNotFullyNarrowable(mixed $a): void
51+
{
52+
assertType('string|T of int (function Bug11894Nsrt\testNotFullyNarrowable(), argument)', conditionalReturn($a));
53+
}
54+
55+
abstract class ConditionalArrayKeys
56+
{
57+
/**
58+
* @template TKey of array-key
59+
* @template TArray of array<TKey, mixed>
60+
* @param TArray $array
61+
* @return (TArray is non-empty-array ? non-empty-list<TKey> : list<TKey>)
62+
*/
63+
abstract public function arrayKeys(array $array): array;
64+
65+
/** @param non-empty-array<int, int> $nonEmpty */
66+
public function testMaybeStaysUnresolved(array $nonEmpty): void
67+
{
68+
assertType('non-empty-list<int>', $this->arrayKeys($nonEmpty));
69+
}
70+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Bug8048Nsrt;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
interface CustomResponseInterface {}
8+
9+
class CustomResponse implements CustomResponseInterface {}
10+
11+
class ApiService
12+
{
13+
/**
14+
* @template T of CustomResponseInterface
15+
*
16+
* @param class-string<T>|null $responseType
17+
*
18+
* @return ($responseType is class-string<T> ? T : null)
19+
*/
20+
public function request(?string $responseType = null): ?CustomResponseInterface
21+
{
22+
if ($responseType === null) {
23+
return null;
24+
}
25+
26+
return new CustomResponse();
27+
}
28+
}
29+
30+
function (): void {
31+
assertType('null', (new ApiService())->request(null));
32+
assertType('Bug8048Nsrt\CustomResponse', (new ApiService())->request(CustomResponse::class));
33+
$x = rand(0, 1) ? CustomResponse::class : null;
34+
assertType('Bug8048Nsrt\CustomResponse|null', (new ApiService())->request($x));
35+
};

tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2871,4 +2871,9 @@ public function testBug3842(): void
28712871
$this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-3842.php'], []);
28722872
}
28732873

2874+
public function testBug11894(): void
2875+
{
2876+
$this->analyse([__DIR__ . '/data/bug-11894.php'], []);
2877+
}
2878+
28742879
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug11894;
4+
5+
/**
6+
* @template T of string|null
7+
* @param T $a
8+
*/
9+
function test(mixed $a): mixed
10+
{
11+
if (!is_string($a)) {
12+
return $a;
13+
}
14+
15+
return conditionalReturn($a);
16+
}
17+
18+
/**
19+
* @template T
20+
* @param T $a
21+
* @return (T is string ? string : T)
22+
*/
23+
function conditionalReturn(mixed $a): mixed
24+
{
25+
if (!is_string($a)) {
26+
return $a;
27+
}
28+
29+
return trim($a);
30+
}
31+
32+
/**
33+
* @template T of string|null
34+
* @param T $a
35+
*/
36+
function testNegated(mixed $a): mixed
37+
{
38+
if (!is_string($a)) {
39+
return $a;
40+
}
41+
42+
return conditionalReturnNegated($a);
43+
}
44+
45+
/**
46+
* @template T
47+
* @param T $a
48+
* @return (T is not string ? T : string)
49+
*/
50+
function conditionalReturnNegated(mixed $a): mixed
51+
{
52+
if (!is_string($a)) {
53+
return $a;
54+
}
55+
56+
return trim($a);
57+
}
58+
59+
/**
60+
* @template T of int|null
61+
* @param T $a
62+
*/
63+
function testNoRelation(mixed $a): mixed
64+
{
65+
if (!is_int($a)) {
66+
return $a;
67+
}
68+
69+
return conditionalReturn($a);
70+
}
71+
72+
/**
73+
* @template T of string|int
74+
* @param T $a
75+
*/
76+
function testMaybeRelation(mixed $a): mixed
77+
{
78+
return conditionalReturn($a);
79+
}

tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4062,4 +4062,20 @@ public function testBug14549(): void
40624062
]);
40634063
}
40644064

4065+
public function testBug11894(): void
4066+
{
4067+
$this->checkThisOnly = false;
4068+
$this->checkNullables = true;
4069+
$this->checkUnionTypes = true;
4070+
$this->analyse([__DIR__ . '/data/bug-11894.php'], []);
4071+
}
4072+
4073+
public function testBug8048(): void
4074+
{
4075+
$this->checkThisOnly = false;
4076+
$this->checkNullables = true;
4077+
$this->checkUnionTypes = true;
4078+
$this->analyse([__DIR__ . '/data/bug-8048.php'], []);
4079+
}
4080+
40654081
}

tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1015,4 +1015,10 @@ public function testPipeOperator(): void
10151015
]);
10161016
}
10171017

1018+
public function testBug11894(): void
1019+
{
1020+
$this->checkThisOnly = false;
1021+
$this->analyse([__DIR__ . '/data/bug-11894.php'], []);
1022+
}
1023+
10181024
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug11894Methods;
4+
5+
class Converter
6+
{
7+
/**
8+
* @template T
9+
* @param T $a
10+
* @return (T is string ? string : T)
11+
*/
12+
public function conditionalReturn(mixed $a): mixed
13+
{
14+
if (!is_string($a)) {
15+
return $a;
16+
}
17+
return trim($a);
18+
}
19+
20+
/**
21+
* @template T
22+
* @param T $a
23+
* @return (T is string ? string : T)
24+
*/
25+
public static function conditionalReturnStatic(mixed $a): mixed
26+
{
27+
if (!is_string($a)) {
28+
return $a;
29+
}
30+
return trim($a);
31+
}
32+
}
33+
34+
class Consumer
35+
{
36+
/**
37+
* @template T of string|null
38+
* @param T $a
39+
*/
40+
public function testMethod(mixed $a): mixed
41+
{
42+
if (!is_string($a)) {
43+
return $a;
44+
}
45+
46+
$c = new Converter();
47+
return $c->conditionalReturn($a);
48+
}
49+
50+
/**
51+
* @template T of string|null
52+
* @param T $a
53+
*/
54+
public function testStaticMethod(mixed $a): mixed
55+
{
56+
if (!is_string($a)) {
57+
return $a;
58+
}
59+
60+
return Converter::conditionalReturnStatic($a);
61+
}
62+
63+
/**
64+
* @template T of string|int
65+
* @param T $a
66+
*/
67+
public function testMaybeMethod(mixed $a): mixed
68+
{
69+
$c = new Converter();
70+
return $c->conditionalReturn($a);
71+
}
72+
73+
/**
74+
* @template T of string|int
75+
* @param T $a
76+
*/
77+
public function testMaybeStaticMethod(mixed $a): mixed
78+
{
79+
return Converter::conditionalReturnStatic($a);
80+
}
81+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Bug8048;
4+
5+
interface CustomResponseInterface {}
6+
7+
class CustomResponse implements CustomResponseInterface {}
8+
9+
class ApiService
10+
{
11+
/**
12+
* @template T of CustomResponseInterface
13+
*
14+
* @param class-string<T>|null $responseType
15+
*
16+
* @return ($responseType is class-string<T> ? T : null)
17+
*/
18+
public function request(?string $responseType = null): ?CustomResponseInterface
19+
{
20+
if ($responseType === null) {
21+
return null;
22+
}
23+
24+
return new CustomResponse();
25+
}
26+
}
27+
28+
function (): void {
29+
(new ApiService())->request(null);
30+
(new ApiService())->request(CustomResponse::class);
31+
$x = rand(0, 1) ? CustomResponse::class : null;
32+
(new ApiService())->request($x);
33+
};

0 commit comments

Comments
 (0)