Skip to content

Commit c24eef4

Browse files
committed
Merge branch 2.2.x into 2.3.x
2 parents 9c0e0dc + 51b4d48 commit c24eef4

5 files changed

Lines changed: 161 additions & 9 deletions

File tree

‎src/Type/ConditionalType.php‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ public function traverse(callable $cb): Type
155155
return $this;
156156
}
157157

158-
return new self($subject, $target, $if, $else, $this->negated);
158+
return $this->withNormalizedBranches($subject, $target, $if, $else);
159159
}
160160

161161
public function traverseSimultaneously(Type $right, callable $cb): Type
@@ -178,7 +178,7 @@ public function traverseSimultaneously(Type $right, callable $cb): Type
178178
return $this;
179179
}
180180

181-
return new self($subject, $target, $if, $else, $this->negated);
181+
return $this->withNormalizedBranches($subject, $target, $if, $else);
182182
}
183183

184184
public function toPhpDocNode(): TypeNode
@@ -192,6 +192,21 @@ public function toPhpDocNode(): TypeNode
192192
);
193193
}
194194

195+
/**
196+
* The branches passed in are images of normalized branches, so the references to the
197+
* subject in them have already been narrowed, and follow whatever the subject maps to.
198+
* Normalizing them again against the mapped subject would narrow unrelated types that
199+
* happen to equal it, and re-traverse every conditional type nested in the branches.
200+
*/
201+
private function withNormalizedBranches(Type $subject, Type $target, Type $if, Type $else): self
202+
{
203+
$type = new self($subject, $target, $if, $else, $this->negated);
204+
$type->normalizedIf = $if;
205+
$type->normalizedElse = $else;
206+
207+
return $type;
208+
}
209+
195210
private function getNormalizedIf(): Type
196211
{
197212
return $this->normalizedIf ??= $this->narrowSubjectIn($this->if, !$this->negated);

‎src/Type/ConditionalTypeForParameter.php‎

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -117,21 +117,40 @@ public static function resolveInType(Type $type, callable $getSubjectType): Type
117117
if ($type instanceof self) {
118118
$subjectType = $getSubjectType($type->getParameterName());
119119
if ($subjectType !== null) {
120-
// Traverse children first, then convert — avoids infinite loop when
120+
// Resolve the branches first, then convert — avoids infinite loop when
121121
// the subject contains a ConditionalTypeForParameter with a colliding parameter name.
122-
$type = $traverse($type);
123-
if ($type instanceof self) {
124-
return $type->toConditional($subjectType);
125-
}
126-
127-
return $type;
122+
return $type->resolveWithSubject($subjectType, $getSubjectType);
128123
}
129124
}
130125

131126
return $traverse($type);
132127
});
133128
}
134129

130+
/**
131+
* A condition on the same parameter nested in a branch sees the subject as the branch
132+
* knows it: `subject & target` where this condition holds, `subject ~ target` where it
133+
* does not. `($key is Model ? A : ($key is Arrayable ? B : C))` asks whether the
134+
* non-Model part of the argument is Arrayable.
135+
*
136+
* @param callable(string): ?Type $getSubjectType
137+
*/
138+
private function resolveWithSubject(Type $subject, callable $getSubjectType): Type
139+
{
140+
$target = self::resolveInType($this->target, $getSubjectType);
141+
$getNarrowedSubjectType = fn (bool $conditionHolds): callable => fn (string $parameterName): ?Type => $parameterName === $this->parameterName
142+
? ($conditionHolds ? TypeCombinator::intersect($subject, $target) : TypeCombinator::remove($subject, $target))
143+
: $getSubjectType($parameterName);
144+
145+
return new ConditionalType(
146+
$subject,
147+
$target,
148+
self::resolveInType($this->getNormalizedIf(), $getNarrowedSubjectType(!$this->negated)),
149+
self::resolveInType($this->getNormalizedElse(), $getNarrowedSubjectType($this->negated)),
150+
$this->negated,
151+
);
152+
}
153+
135154
public function toConditional(Type $subject): Type
136155
{
137156
return new ConditionalType(

‎tests/PHPStan/Analyser/AnalyserIntegrationTest.php‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1792,6 +1792,15 @@ public function testBug15252AnalysedFileWithInvalidClassName(): void
17921792
$this->assertSame(10, $errors[0]->getLine());
17931793
}
17941794

1795+
#[RequiresPhp('>= 8.1.0')]
1796+
public function testBug15310(): void
1797+
{
1798+
$errors = $this->runAnalyse(__DIR__ . '/data/bug-15310.php');
1799+
$this->assertCount(1, $errors);
1800+
$this->assertSame('Function Bug15310\\reproduce() return type with generic class Bug15310\\ExampleType does not specify its types: TValue', $errors[0]->getMessage());
1801+
$this->assertSame(39, $errors[0]->getLine());
1802+
}
1803+
17951804
/**
17961805
* @param string[]|null $allAnalysedFiles
17971806
* @return list<Error>
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php // lint >= 8.1
2+
3+
namespace Bug15310;
4+
5+
enum ExampleIdentifier { case A; case B; case C; case D; case E; case F; case G; case H; case I; case J; case K; case L; case M; case N; case O; }
6+
7+
/** @template-covariant TValue */
8+
abstract class ExampleType
9+
{
10+
}
11+
12+
/**
13+
* @template T of ExampleIdentifier
14+
* @extends ExampleType<(
15+
* T is ExampleIdentifier::A ? array :
16+
* T is ExampleIdentifier::B ? bool :
17+
* T is ExampleIdentifier::C ? callable :
18+
* T is ExampleIdentifier::D ? false :
19+
* T is ExampleIdentifier::E ? float :
20+
* T is ExampleIdentifier::F ? int :
21+
* T is ExampleIdentifier::G ? iterable :
22+
* T is ExampleIdentifier::H ? mixed :
23+
* T is ExampleIdentifier::I ? null :
24+
* T is ExampleIdentifier::J ? object :
25+
* T is ExampleIdentifier::K ? resource :
26+
* T is ExampleIdentifier::L ? string :
27+
* T is ExampleIdentifier::M ? true :
28+
* T is ExampleIdentifier::N ? never :
29+
* T is ExampleIdentifier::O ? void :
30+
* mixed
31+
* )>
32+
*/
33+
final class ExampleBuiltinType extends ExampleType
34+
{
35+
/** @param T $identifier */
36+
public function __construct(public ExampleIdentifier $identifier) {}
37+
}
38+
39+
function reproduce(): ExampleType
40+
{
41+
return new ExampleBuiltinType(ExampleIdentifier::A);
42+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php // lint >= 8.0
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug15296;
6+
7+
use function PHPStan\Testing\assertType;
8+
9+
interface Arrayable {}
10+
class Model implements Arrayable {}
11+
final class User extends Model {}
12+
13+
/** @template TModel of Model */
14+
final class Collection
15+
{
16+
/**
17+
* @return ($key is Model ? TModel|null : ($key is (Arrayable|array<mixed>) ? static : TModel|null))
18+
*/
19+
public function find(mixed $key): mixed
20+
{
21+
return null;
22+
}
23+
}
24+
25+
/** @param Collection<User> $collection */
26+
function test(Collection $collection, ?User $user): void
27+
{
28+
assertType('Bug15296\User|null', $collection->find($user));
29+
}
30+
31+
class Variants
32+
{
33+
34+
/**
35+
* @return ($key is not Model ? ($key is Arrayable ? 'arrayable' : 'other') : 'model')
36+
*/
37+
public function negated(mixed $key): string
38+
{
39+
return 'other';
40+
}
41+
42+
/**
43+
* @return ($key is Model ? ($key is User ? 'user' : 'model') : 'other')
44+
*/
45+
public function inIf(mixed $key): string
46+
{
47+
return 'other';
48+
}
49+
50+
/**
51+
* @return ($key is User ? 'user' : ($key is Model ? 'model' : ($key is null ? 'null' : 'other')))
52+
*/
53+
public function deep(mixed $key): string
54+
{
55+
return 'other';
56+
}
57+
58+
}
59+
60+
function testVariants(Variants $v, ?User $user, Model|int $modelOrInt, User|Model|null $any): void
61+
{
62+
assertType("'model'|'other'", $v->negated($modelOrInt));
63+
assertType("'model'", $v->negated($user ?? new User()));
64+
assertType("'other'|'user'", $v->inIf($user));
65+
assertType("'model'|'other'|'user'", $v->inIf($modelOrInt));
66+
assertType("'model'|'null'|'user'", $v->deep($any));
67+
}

0 commit comments

Comments
 (0)