Skip to content

Commit 9aaeccb

Browse files
canvuralondrejmirtes
authored andcommitted
Support template types as object shape keys
1 parent 7d0521a commit 9aaeccb

11 files changed

Lines changed: 1006 additions & 12 deletions

‎src/PhpDoc/TypeNodeResolver.php‎

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -93,12 +93,12 @@
9393
use PHPStan\Type\IterableType;
9494
use PHPStan\Type\KeyOfType;
9595
use PHPStan\Type\LateResolvableArrayShapeType;
96+
use PHPStan\Type\LateResolvableObjectShapeType;
9697
use PHPStan\Type\MixedType;
9798
use PHPStan\Type\NewObjectType;
9899
use PHPStan\Type\NonAcceptingNeverType;
99100
use PHPStan\Type\NonexistentParentClassType;
100101
use PHPStan\Type\NullType;
101-
use PHPStan\Type\ObjectShapeType;
102102
use PHPStan\Type\ObjectType;
103103
use PHPStan\Type\ObjectWithoutClassType;
104104
use PHPStan\Type\OffsetAccessType;
@@ -1272,23 +1272,19 @@ private function resolveArrayShapeOffsetType(ArrayShapeItemNode $itemNode, NameS
12721272

12731273
private function resolveObjectShapeNode(ObjectShapeNode $typeNode, NameScope $nameScope): Type
12741274
{
1275-
$properties = [];
1276-
$optionalProperties = [];
1275+
$items = [];
12771276
foreach ($typeNode->items as $itemNode) {
12781277
if ($itemNode->keyName instanceof IdentifierTypeNode) {
1279-
$propertyName = $itemNode->keyName->name;
1280-
} elseif ($itemNode->keyName instanceof ConstExprStringNode) {
1281-
$propertyName = $itemNode->keyName->value;
1282-
}
1283-
1284-
if ($itemNode->optional) {
1285-
$optionalProperties[] = $propertyName;
1278+
$keyType = $nameScope->resolveTemplateTypeName($itemNode->keyName->name)
1279+
?? new ConstantStringType($itemNode->keyName->name);
1280+
} else {
1281+
$keyType = new ConstantStringType($itemNode->keyName->value);
12861282
}
12871283

1288-
$properties[$propertyName] = $this->resolve($itemNode->valueType, $nameScope);
1284+
$items[] = [$keyType, $this->resolve($itemNode->valueType, $nameScope), $itemNode->optional];
12891285
}
12901286

1291-
return new ObjectShapeType($properties, $optionalProperties);
1287+
return LateResolvableObjectShapeType::create($items);
12921288
}
12931289

12941290
private function resolveConstTypeNode(ConstTypeNode $typeNode, NameScope $nameScope): Type
Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type;
4+
5+
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode;
6+
use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode;
7+
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
8+
use PHPStan\PhpDocParser\Ast\Type\ObjectShapeItemNode;
9+
use PHPStan\PhpDocParser\Ast\Type\ObjectShapeNode;
10+
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
11+
use PHPStan\Type\Constant\ConstantArrayType;
12+
use PHPStan\Type\Generic\TemplateTypeHelper;
13+
use PHPStan\Type\Generic\TemplateTypeVariance;
14+
use PHPStan\Type\Traits\LateResolvableTypeTrait;
15+
use PHPStan\Type\Traits\NonGeneralizableTypeTrait;
16+
use function array_keys;
17+
use function array_merge;
18+
use function count;
19+
use function sprintf;
20+
21+
/**
22+
* Object shape whose property names are not known yet because at least one of
23+
* them is a template type: `object{TKey: int}`. Once the template types are
24+
* resolved the shape collapses into an ObjectShapeType (or an ErrorType when
25+
* the resolved key cannot be a property name at all). Written shapes without
26+
* template keys go through it too and resolve right away.
27+
*/
28+
final class LateResolvableObjectShapeType implements CompoundType, LateResolvableType
29+
{
30+
31+
use LateResolvableTypeTrait;
32+
use NonGeneralizableTypeTrait;
33+
34+
/**
35+
* @param list<array{Type, Type, bool}> $items keyType, valueType, optional
36+
*/
37+
private function __construct(private array $items)
38+
{
39+
}
40+
41+
/**
42+
* Builds the object shape type, keeping it late-resolvable only for as long
43+
* as some of its keys still contain template types.
44+
*
45+
* @param list<array{Type, Type, bool}> $items keyType, valueType, optional
46+
*/
47+
public static function create(array $items): Type
48+
{
49+
$self = new self($items);
50+
if ($self->isResolvable()) {
51+
return $self->resolve();
52+
}
53+
54+
return $self;
55+
}
56+
57+
public function getReferencedClasses(): array
58+
{
59+
$classes = [];
60+
foreach ($this->items as [$keyType, $valueType]) {
61+
$classes = array_merge($classes, $keyType->getReferencedClasses(), $valueType->getReferencedClasses());
62+
}
63+
64+
return $classes;
65+
}
66+
67+
public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array
68+
{
69+
$references = [];
70+
foreach ($this->items as [$keyType, $valueType]) {
71+
$references = array_merge(
72+
$references,
73+
$keyType->getReferencedTemplateTypes($positionVariance),
74+
$valueType->getReferencedTemplateTypes($positionVariance),
75+
);
76+
}
77+
78+
return $references;
79+
}
80+
81+
public function equals(Type $type): bool
82+
{
83+
if (!$type instanceof self) {
84+
return false;
85+
}
86+
87+
if (count($this->items) !== count($type->items)) {
88+
return false;
89+
}
90+
91+
foreach ($this->items as $i => [$keyType, $valueType, $optional]) {
92+
[$otherKeyType, $otherValueType, $otherOptional] = $type->items[$i];
93+
if ($optional !== $otherOptional) {
94+
return false;
95+
}
96+
if (!$keyType->equals($otherKeyType)) {
97+
return false;
98+
}
99+
if (!$valueType->equals($otherValueType)) {
100+
return false;
101+
}
102+
}
103+
104+
return true;
105+
}
106+
107+
public function describe(VerbosityLevel $level): string
108+
{
109+
// a shape that can be resolved never stays late-resolvable, see create()
110+
return TypeNodeDescriber::describe($this->toPhpDocNode());
111+
}
112+
113+
public function isResolvable(): bool
114+
{
115+
foreach ($this->items as [$keyType]) {
116+
if (TypeUtils::containsTemplateType($keyType)) {
117+
return false;
118+
}
119+
}
120+
121+
return true;
122+
}
123+
124+
protected function getResult(): Type
125+
{
126+
/** @var array<int|string, Type> $properties */
127+
$properties = [];
128+
/** @var array<int|string, true> $writtenOptional */
129+
$writtenOptional = [];
130+
/** @var array<int|string, true> $uncertain */
131+
$uncertain = [];
132+
$hasUnknownName = false;
133+
134+
foreach ($this->items as [$keyType, $valueType, $optional]) {
135+
if (TypeUtils::containsTemplateType($keyType)) {
136+
// at this point the template types are never going to be
137+
// substituted anymore, so the key degrades to their bounds
138+
$keyType = TemplateTypeHelper::resolveToBounds($keyType);
139+
}
140+
141+
if ($keyType instanceof ErrorType) {
142+
return $keyType;
143+
}
144+
145+
$isString = $keyType->isString();
146+
if ($isString->no()) {
147+
return new ErrorType(sprintf(
148+
'Type %s cannot be used as an object shape key.',
149+
$keyType->describe(VerbosityLevel::typeOnly()),
150+
));
151+
}
152+
153+
if ($isString->maybe()) {
154+
// only the string part can name a property
155+
$keyType = TypeCombinator::intersect($keyType, new StringType());
156+
}
157+
158+
$constantStrings = $keyType->getConstantStrings();
159+
if (count($constantStrings) === 0) {
160+
// the property name is not known; object shapes allow
161+
// properties they do not list, so it is left out
162+
$hasUnknownName = true;
163+
continue;
164+
}
165+
166+
if (count($constantStrings) === 1 && $isString->yes()) {
167+
// a known name: the later one wins, like in a written shape
168+
$propertyName = $constantStrings[0]->getValue();
169+
$properties[$propertyName] = $valueType;
170+
unset($uncertain[$propertyName]);
171+
if ($optional) {
172+
$writtenOptional[$propertyName] = true;
173+
}
174+
continue;
175+
}
176+
177+
// any of several names: each of them only might be there
178+
foreach ($constantStrings as $constantString) {
179+
$propertyName = $constantString->getValue();
180+
if (isset($properties[$propertyName])) {
181+
$properties[$propertyName] = TypeCombinator::union($properties[$propertyName], $valueType);
182+
continue;
183+
}
184+
185+
$properties[$propertyName] = $valueType;
186+
$uncertain[$propertyName] = true;
187+
}
188+
}
189+
190+
if (count($properties) === 0 && $hasUnknownName) {
191+
return new ObjectWithoutClassType();
192+
}
193+
194+
$optionalProperties = [];
195+
foreach (array_keys($properties) as $propertyName) {
196+
if (!isset($writtenOptional[$propertyName]) && !isset($uncertain[$propertyName])) {
197+
continue;
198+
}
199+
200+
$optionalProperties[] = $propertyName;
201+
}
202+
203+
return new ObjectShapeType($properties, $optionalProperties);
204+
}
205+
206+
/**
207+
* @param callable(Type): Type $cb
208+
*/
209+
public function traverse(callable $cb): Type
210+
{
211+
$stillOriginal = true;
212+
$items = [];
213+
foreach ($this->items as [$keyType, $valueType, $optional]) {
214+
$newKeyType = $cb($keyType);
215+
$newValueType = $cb($valueType);
216+
if ($newKeyType !== $keyType || $newValueType !== $valueType) {
217+
$stillOriginal = false;
218+
}
219+
220+
$items[] = [$newKeyType, $newValueType, $optional];
221+
}
222+
223+
if ($stillOriginal) {
224+
return $this;
225+
}
226+
227+
return self::create($items);
228+
}
229+
230+
public function traverseSimultaneously(Type $right, callable $cb): Type
231+
{
232+
if (!$right instanceof self) {
233+
return $this;
234+
}
235+
236+
if (count($this->items) !== count($right->items)) {
237+
return $this;
238+
}
239+
240+
$stillOriginal = true;
241+
$items = [];
242+
foreach ($this->items as $i => [$keyType, $valueType, $optional]) {
243+
[$rightKeyType, $rightValueType] = $right->items[$i];
244+
$newKeyType = $cb($keyType, $rightKeyType);
245+
$newValueType = $cb($valueType, $rightValueType);
246+
if ($newKeyType !== $keyType || $newValueType !== $valueType) {
247+
$stillOriginal = false;
248+
}
249+
250+
$items[] = [$newKeyType, $newValueType, $optional];
251+
}
252+
253+
if ($stillOriginal) {
254+
return $this;
255+
}
256+
257+
return self::create($items);
258+
}
259+
260+
public function toPhpDocNode(): TypeNode
261+
{
262+
$items = [];
263+
foreach ($this->items as [$keyType, $valueType, $optional]) {
264+
$items[] = new ObjectShapeItemNode(
265+
self::keyNameNode($keyType),
266+
$optional,
267+
$valueType->toPhpDocNode(),
268+
);
269+
}
270+
271+
return new ObjectShapeNode($items);
272+
}
273+
274+
/**
275+
* @return ConstExprStringNode|IdentifierTypeNode
276+
*/
277+
private static function keyNameNode(Type $keyType)
278+
{
279+
$node = $keyType->toPhpDocNode();
280+
if ($node instanceof IdentifierTypeNode) {
281+
return $node;
282+
}
283+
284+
if ($node instanceof ConstTypeNode && $node->constExpr instanceof ConstExprStringNode) {
285+
if (ConstantArrayType::isValidIdentifier($node->constExpr->value)) {
286+
return new IdentifierTypeNode($node->constExpr->value);
287+
}
288+
289+
return $node->constExpr;
290+
}
291+
292+
return new IdentifierTypeNode($keyType->describe(VerbosityLevel::precise()));
293+
}
294+
295+
}

0 commit comments

Comments
 (0)