Skip to content

Commit e0e7ba4

Browse files
authored
Implement conditional types slightly better
1 parent 9240f16 commit e0e7ba4

12 files changed

+757
-46
lines changed

src/PhpDoc/TypeNodeResolver.php

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@
4444
use PHPStan\Type\CallableType;
4545
use PHPStan\Type\ClassStringType;
4646
use PHPStan\Type\ClosureType;
47+
use PHPStan\Type\ConditionalType;
48+
use PHPStan\Type\ConditionalTypeForParameter;
4749
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
4850
use PHPStan\Type\Constant\ConstantBooleanType;
4951
use PHPStan\Type\Constant\ConstantIntegerType;
@@ -123,9 +125,12 @@ public function resolve(TypeNode $typeNode, NameScope $nameScope): Type
123125
} elseif ($typeNode instanceof IntersectionTypeNode) {
124126
return $this->resolveIntersectionTypeNode($typeNode, $nameScope);
125127

126-
} elseif ($typeNode instanceof ConditionalTypeNode || $typeNode instanceof ConditionalTypeForParameterNode) {
128+
} elseif ($typeNode instanceof ConditionalTypeNode) {
127129
return $this->resolveConditionalTypeNode($typeNode, $nameScope);
128130

131+
} elseif ($typeNode instanceof ConditionalTypeForParameterNode) {
132+
return $this->resolveConditionalTypeForParameterNode($typeNode, $nameScope);
133+
129134
} elseif ($typeNode instanceof ArrayTypeNode) {
130135
return $this->resolveArrayTypeNode($typeNode, $nameScope);
131136

@@ -443,10 +448,26 @@ private function resolveIntersectionTypeNode(IntersectionTypeNode $typeNode, Nam
443448
return TypeCombinator::intersect(...$types);
444449
}
445450

446-
private function resolveConditionalTypeNode(ConditionalTypeNode|ConditionalTypeForParameterNode $typeNode, NameScope $nameScope): Type
451+
private function resolveConditionalTypeNode(ConditionalTypeNode $typeNode, NameScope $nameScope): Type
452+
{
453+
return ConditionalType::create(
454+
$this->resolve($typeNode->subjectType, $nameScope),
455+
$this->resolve($typeNode->targetType, $nameScope),
456+
$this->resolve($typeNode->if, $nameScope),
457+
$this->resolve($typeNode->else, $nameScope),
458+
$typeNode->negated,
459+
);
460+
}
461+
462+
private function resolveConditionalTypeForParameterNode(ConditionalTypeForParameterNode $typeNode, NameScope $nameScope): Type
447463
{
448-
$types = $this->resolveMultiple([$typeNode->if, $typeNode->else], $nameScope);
449-
return TypeCombinator::union(...$types);
464+
return new ConditionalTypeForParameter(
465+
$typeNode->parameterName,
466+
$this->resolve($typeNode->targetType, $nameScope),
467+
$this->resolve($typeNode->if, $nameScope),
468+
$this->resolve($typeNode->else, $nameScope),
469+
$typeNode->negated,
470+
);
450471
}
451472

452473
private function resolveArrayTypeNode(ArrayTypeNode $typeNode, NameScope $nameScope): Type

src/Reflection/GenericParametersAcceptorResolver.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class GenericParametersAcceptorResolver
1717
public static function resolve(array $argTypes, ParametersAcceptor $parametersAcceptor): ParametersAcceptor
1818
{
1919
$typeMap = TemplateTypeMap::createEmpty();
20+
$passedArgs = [];
2021

2122
foreach ($parametersAcceptor->getParameters() as $i => $param) {
2223
if (isset($argTypes[$i])) {
@@ -31,6 +32,7 @@ public static function resolve(array $argTypes, ParametersAcceptor $parametersAc
3132

3233
$paramType = $param->getType();
3334
$typeMap = $typeMap->union($paramType->inferTemplateTypes($argType));
35+
$passedArgs['$' . $param->getName()] = $argType;
3436
}
3537

3638
return new ResolvedFunctionVariant(
@@ -39,6 +41,7 @@ public static function resolve(array $argTypes, ParametersAcceptor $parametersAc
3941
$parametersAcceptor->getTemplateTypeMap()->map(static fn (string $name, Type $type): Type => new ErrorType())->getTypes(),
4042
$typeMap->getTypes(),
4143
)),
44+
$passedArgs,
4245
);
4346
}
4447

src/Reflection/ResolvedFunctionVariant.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
33
namespace PHPStan\Reflection;
44

55
use PHPStan\Reflection\Php\DummyParameter;
6+
use PHPStan\Type\ConditionalTypeForParameter;
67
use PHPStan\Type\Generic\TemplateTypeHelper;
78
use PHPStan\Type\Generic\TemplateTypeMap;
89
use PHPStan\Type\Type;
10+
use PHPStan\Type\TypeTraverser;
11+
use function array_key_exists;
912
use function array_map;
1013

1114
class ResolvedFunctionVariant implements ParametersAcceptor
@@ -16,9 +19,13 @@ class ResolvedFunctionVariant implements ParametersAcceptor
1619

1720
private ?Type $returnType = null;
1821

22+
/**
23+
* @param array<string, Type> $passedArgs
24+
*/
1925
public function __construct(
2026
private ParametersAcceptor $parametersAcceptor,
2127
private TemplateTypeMap $resolvedTemplateTypeMap,
28+
private array $passedArgs,
2229
)
2330
{
2431
}
@@ -73,10 +80,23 @@ public function getReturnType(): Type
7380
$this->resolvedTemplateTypeMap,
7481
);
7582

83+
$type = $this->resolveConditionalTypes($type);
84+
7685
$this->returnType = $type;
7786
}
7887

7988
return $type;
8089
}
8190

91+
private function resolveConditionalTypes(Type $type): Type
92+
{
93+
return TypeTraverser::map($type, function (Type $type, callable $traverse): Type {
94+
while ($type instanceof ConditionalTypeForParameter && array_key_exists($type->getParameterName(), $this->passedArgs)) {
95+
$type = $type->toConditional($this->passedArgs[$type->getParameterName()]);
96+
}
97+
98+
return $traverse($type);
99+
});
100+
}
101+
82102
}

src/Reflection/ResolvedMethodReflection.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ public function getVariants(): array
4242
$variants[] = new ResolvedFunctionVariant(
4343
$variant,
4444
$this->resolvedTemplateTypeMap,
45+
[],
4546
);
4647
}
4748

src/Type/ConditionalType.php

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type;
4+
5+
use PHPStan\Type\Generic\TemplateType;
6+
use PHPStan\Type\Generic\TemplateTypeVariance;
7+
use PHPStan\Type\Traits\ConditionalTypeTrait;
8+
use PHPStan\Type\Traits\NonGeneralizableTypeTrait;
9+
use function array_merge;
10+
use function sprintf;
11+
12+
/** @api */
13+
final class ConditionalType implements CompoundType
14+
{
15+
16+
use ConditionalTypeTrait;
17+
use NonGeneralizableTypeTrait;
18+
19+
private function __construct(
20+
private Type $subject,
21+
private Type $target,
22+
Type $if,
23+
Type $else,
24+
private bool $negated,
25+
)
26+
{
27+
$this->if = $if;
28+
$this->else = $else;
29+
}
30+
31+
public function getReferencedClasses(): array
32+
{
33+
return array_merge(
34+
$this->subject->getReferencedClasses(),
35+
$this->target->getReferencedClasses(),
36+
$this->if->getReferencedClasses(),
37+
$this->else->getReferencedClasses(),
38+
);
39+
}
40+
41+
public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array
42+
{
43+
return array_merge(
44+
$this->subject->getReferencedTemplateTypes($positionVariance),
45+
$this->target->getReferencedTemplateTypes($positionVariance),
46+
$this->if->getReferencedTemplateTypes($positionVariance),
47+
$this->else->getReferencedTemplateTypes($positionVariance),
48+
);
49+
}
50+
51+
public function equals(Type $type): bool
52+
{
53+
return $type instanceof self
54+
&& $this->subject->equals($type->subject)
55+
&& $this->target->equals($type->target)
56+
&& $this->if->equals($type->if)
57+
&& $this->else->equals($type->else);
58+
}
59+
60+
public function describe(VerbosityLevel $level): string
61+
{
62+
return sprintf(
63+
'(%s %s %s ? %s : %s)',
64+
$this->subject->describe($level),
65+
$this->negated ? 'is not' : 'is',
66+
$this->target->describe($level),
67+
$this->if->describe($level),
68+
$this->else->describe($level),
69+
);
70+
}
71+
72+
public static function create(
73+
Type $subject,
74+
Type $target,
75+
Type $if,
76+
Type $else,
77+
bool $negated,
78+
): Type
79+
{
80+
return (new self($subject, $target, $if, $else, $negated))->resolve();
81+
}
82+
83+
private function resolve(): Type
84+
{
85+
$isSuperType = $this->target->isSuperTypeOf($this->subject);
86+
87+
if ($isSuperType->yes()) {
88+
return !$this->negated ? $this->if : $this->else;
89+
} elseif ($isSuperType->no()) {
90+
return !$this->negated ? $this->else : $this->if;
91+
}
92+
93+
if ($this->isResolved()) {
94+
return TypeCombinator::union($this->if, $this->else);
95+
}
96+
97+
return $this;
98+
}
99+
100+
public function traverse(callable $cb): Type
101+
{
102+
$subject = $cb($this->subject);
103+
$target = $cb($this->target);
104+
$if = $cb($this->if);
105+
$else = $cb($this->else);
106+
107+
if ($this->subject === $subject && $this->target === $target && $this->if === $if && $this->else === $else) {
108+
return $this;
109+
}
110+
111+
return self::create($subject, $target, $if, $else, $this->negated);
112+
}
113+
114+
private function isResolved(): bool
115+
{
116+
return !$this->containsTemplate($this->subject) && !$this->containsTemplate($this->target);
117+
}
118+
119+
private function containsTemplate(Type $type): bool
120+
{
121+
$containsTemplate = false;
122+
TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$containsTemplate): Type {
123+
if ($type instanceof TemplateType) {
124+
$containsTemplate = true;
125+
}
126+
127+
return $containsTemplate ? $type : $traverse($type);
128+
});
129+
130+
return $containsTemplate;
131+
}
132+
133+
/**
134+
* @param mixed[] $properties
135+
*/
136+
public static function __set_state(array $properties): Type
137+
{
138+
return new self(
139+
$properties['subject'],
140+
$properties['target'],
141+
$properties['if'],
142+
$properties['else'],
143+
$properties['negated'],
144+
);
145+
}
146+
147+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type;
4+
5+
use PHPStan\Type\Generic\TemplateTypeVariance;
6+
use PHPStan\Type\Traits\ConditionalTypeTrait;
7+
use PHPStan\Type\Traits\NonGeneralizableTypeTrait;
8+
use function array_merge;
9+
use function sprintf;
10+
11+
/** @api */
12+
final class ConditionalTypeForParameter implements CompoundType
13+
{
14+
15+
use ConditionalTypeTrait;
16+
use NonGeneralizableTypeTrait;
17+
18+
public function __construct(
19+
private string $parameterName,
20+
private Type $target,
21+
Type $if,
22+
Type $else,
23+
private bool $negated,
24+
)
25+
{
26+
$this->if = $if;
27+
$this->else = $else;
28+
}
29+
30+
public function getParameterName(): string
31+
{
32+
return $this->parameterName;
33+
}
34+
35+
public function toConditional(Type $subject): Type
36+
{
37+
return ConditionalType::create(
38+
$subject,
39+
$this->target,
40+
$this->if,
41+
$this->else,
42+
$this->negated,
43+
);
44+
}
45+
46+
public function getReferencedClasses(): array
47+
{
48+
return array_merge(
49+
$this->target->getReferencedClasses(),
50+
$this->if->getReferencedClasses(),
51+
$this->else->getReferencedClasses(),
52+
);
53+
}
54+
55+
public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array
56+
{
57+
return array_merge(
58+
$this->target->getReferencedTemplateTypes($positionVariance),
59+
$this->if->getReferencedTemplateTypes($positionVariance),
60+
$this->else->getReferencedTemplateTypes($positionVariance),
61+
);
62+
}
63+
64+
public function equals(Type $type): bool
65+
{
66+
return $type instanceof self
67+
&& $this->parameterName === $type->parameterName
68+
&& $this->target->equals($type->target)
69+
&& $this->if->equals($type->if)
70+
&& $this->else->equals($type->else);
71+
}
72+
73+
public function describe(VerbosityLevel $level): string
74+
{
75+
return sprintf(
76+
'(%s %s %s ? %s : %s)',
77+
$this->parameterName,
78+
$this->negated ? 'is not' : 'is',
79+
$this->target->describe($level),
80+
$this->if->describe($level),
81+
$this->else->describe($level),
82+
);
83+
}
84+
85+
/**
86+
* @param callable(Type): Type $cb
87+
*/
88+
public function traverse(callable $cb): Type
89+
{
90+
$target = $cb($this->target);
91+
$if = $cb($this->if);
92+
$else = $cb($this->else);
93+
94+
if ($this->target === $target && $this->if === $if && $this->else === $else) {
95+
return $this;
96+
}
97+
98+
return new ConditionalTypeForParameter($this->parameterName, $target, $if, $else, $this->negated);
99+
}
100+
101+
/**
102+
* @param mixed[] $properties
103+
*/
104+
public static function __set_state(array $properties): Type
105+
{
106+
return new self(
107+
$properties['parameterName'],
108+
$properties['target'],
109+
$properties['if'],
110+
$properties['else'],
111+
$properties['negated'],
112+
);
113+
}
114+
115+
}

0 commit comments

Comments
 (0)