Skip to content

Commit 132fd1a

Browse files
committed
Initial implementation of unsealed array shapes
Array shapes like `array{a: int}` in PHPDocs are only sealed in Bleeding Edge. Without Bleeding edge, the goal is to match the current flawed behaviour as close as possible.
1 parent d8be8dd commit 132fd1a

File tree

12 files changed

+847
-69
lines changed

12 files changed

+847
-69
lines changed

src/PhpDoc/TypeNodeResolver.php

Lines changed: 62 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -704,24 +704,7 @@ static function (string $variance): TemplateTypeVariance {
704704
if (count($genericTypes) === 1) { // array<ValueType>
705705
$arrayType = new ArrayType((new BenevolentUnionType([new IntegerType(), new StringType()]))->toArrayKey(), $genericTypes[0]);
706706
} elseif (count($genericTypes) === 2) { // array<KeyType, ValueType>
707-
$originalKey = $genericTypes[0];
708-
if ($this->reportUnsafeArrayStringKeyCasting === ReportUnsafeArrayStringKeyCastingToggle::PREVENT) {
709-
$originalKey = TypeTraverser::map($originalKey, static function (Type $type, callable $traverse) {
710-
if ($type instanceof UnionType || $type instanceof IntersectionType) {
711-
return $traverse($type);
712-
}
713-
714-
if ($type instanceof StringType) {
715-
return TypeCombinator::intersect($type, new AccessoryDecimalIntegerStringType(inverse: true));
716-
}
717-
718-
return $type;
719-
});
720-
}
721-
$keyType = TypeCombinator::intersect($originalKey->toArrayKey(), new UnionType([
722-
new IntegerType(),
723-
new StringType(),
724-
]))->toArrayKey();
707+
$keyType = $this->transformUnsafeArrayKey($genericTypes[0]);
725708
$finiteTypes = $keyType->getFiniteTypes();
726709
if (
727710
count($finiteTypes) === 1
@@ -1001,6 +984,28 @@ static function (string $variance): TemplateTypeVariance {
1001984
return new ErrorType();
1002985
}
1003986

987+
private function transformUnsafeArrayKey(Type $keyType): Type
988+
{
989+
if ($this->reportUnsafeArrayStringKeyCasting === ReportUnsafeArrayStringKeyCastingToggle::PREVENT) {
990+
$keyType = TypeTraverser::map($keyType, static function (Type $type, callable $traverse) {
991+
if ($type instanceof UnionType || $type instanceof IntersectionType) {
992+
return $traverse($type);
993+
}
994+
995+
if ($type instanceof StringType) {
996+
return TypeCombinator::intersect($type, new AccessoryDecimalIntegerStringType(inverse: true));
997+
}
998+
999+
return $type;
1000+
});
1001+
}
1002+
1003+
return TypeCombinator::intersect($keyType->toArrayKey(), new UnionType([
1004+
new IntegerType(),
1005+
new StringType(),
1006+
]))->toArrayKey();
1007+
}
1008+
10041009
private function resolveCallableTypeNode(CallableTypeNode $typeNode, NameScope $nameScope): Type
10051010
{
10061011
$templateTags = [];
@@ -1100,13 +1105,48 @@ private function resolveArrayShapeNode(ArrayShapeNode $typeNode, NameScope $name
11001105
$builder->setOffsetValueType($offsetType, $this->resolve($itemNode->valueType, $nameScope), $itemNode->optional);
11011106
}
11021107

1108+
$isList = in_array($typeNode->kind, [
1109+
ArrayShapeNode::KIND_LIST,
1110+
ArrayShapeNode::KIND_NON_EMPTY_LIST,
1111+
], true);
1112+
1113+
if (!$typeNode->sealed) {
1114+
if ($typeNode->unsealedType === null) {
1115+
if ($isList) {
1116+
$unsealedKeyType = IntegerRangeType::createAllGreaterThanOrEqualTo(0);
1117+
} else {
1118+
$unsealedKeyType = (new BenevolentUnionType([new IntegerType(), new StringType()]))->toArrayKey();
1119+
}
1120+
$builder->makeUnsealed(
1121+
$unsealedKeyType,
1122+
new MixedType(),
1123+
);
1124+
} else {
1125+
if ($typeNode->unsealedType->keyType === null) {
1126+
if ($isList) {
1127+
$unsealedKeyType = IntegerRangeType::createAllGreaterThanOrEqualTo(0);
1128+
} else {
1129+
$unsealedKeyType = (new BenevolentUnionType([new IntegerType(), new StringType()]))->toArrayKey();
1130+
}
1131+
} else {
1132+
$unsealedKeyType = $this->transformUnsafeArrayKey($this->resolve($typeNode->unsealedType->keyType, $nameScope));
1133+
}
1134+
$unsealedKeyFiniteTypes = $unsealedKeyType->getFiniteTypes();
1135+
$unsealedValueType = $this->resolve($typeNode->unsealedType->valueType, $nameScope);
1136+
if (count($unsealedKeyFiniteTypes) > 0) {
1137+
foreach ($unsealedKeyFiniteTypes as $unsealedKeyFiniteType) {
1138+
$builder->setOffsetValueType($unsealedKeyFiniteType, $unsealedValueType, true);
1139+
}
1140+
} else {
1141+
$builder->makeUnsealed($unsealedKeyType, $unsealedValueType);
1142+
}
1143+
}
1144+
}
1145+
11031146
$arrayType = $builder->getArray();
11041147

11051148
$accessories = [];
1106-
if (in_array($typeNode->kind, [
1107-
ArrayShapeNode::KIND_LIST,
1108-
ArrayShapeNode::KIND_NON_EMPTY_LIST,
1109-
], true)) {
1149+
if ($isList) {
11101150
$accessories[] = new AccessoryArrayListType();
11111151
}
11121152

0 commit comments

Comments
 (0)