-
Notifications
You must be signed in to change notification settings - Fork 554
Improve intersection of ConstantArray and AccessoryIsList #5067
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 2.1.x
Are you sure you want to change the base?
Changes from all commits
49e476f
8df03dc
a355076
749303d
b9d204d
8c56fba
65797b9
2980730
9d88e75
355d824
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -715,7 +715,15 @@ public function setExistingOffsetValueType(Type $offsetType, Type $valueType): T | |
| return $builder->getArray(); | ||
| } | ||
|
|
||
| public function unsetOffset(Type $offsetType): Type | ||
| /** | ||
| * Removes or marks as optional the key(s) matching the given offset type from this constant array. | ||
| * | ||
| * By default, the method assumes an actual `unset()` call was made, which actively modifies the | ||
| * array and weakens its list certainty to "maybe". However, in some contexts, such as the else | ||
| * branch of an array_key_exists() check, the key is statically known to be absent without any | ||
| * modification, so list certainty should be preserved as-is. | ||
| */ | ||
| public function unsetOffset(Type $offsetType, bool $preserveListCertainty = false): Type | ||
| { | ||
| $offsetType = $offsetType->toArrayKey(); | ||
| if ($offsetType instanceof ConstantIntegerType || $offsetType instanceof ConstantStringType) { | ||
|
|
@@ -749,6 +757,11 @@ public function unsetOffset(Type $offsetType): Type | |
| $this->isList, | ||
| in_array($i, $this->optionalKeys, true), | ||
| ); | ||
| if (!$preserveListCertainty) { | ||
| $newIsList = $newIsList->and(TrinaryLogic::createMaybe()); | ||
| } elseif ($this->isList->yes() && $newIsList->no()) { | ||
| return new NeverType(); | ||
| } | ||
|
|
||
| return new self($newKeyTypes, $newValueTypes, $this->nextAutoIndexes, $newOptionalKeys, $newIsList); | ||
| } | ||
|
|
@@ -791,6 +804,11 @@ public function unsetOffset(Type $offsetType): Type | |
| $this->isList, | ||
| count($optionalKeys) === count($this->optionalKeys), | ||
| ); | ||
| if (!$preserveListCertainty) { | ||
| $newIsList = $newIsList->and(TrinaryLogic::createMaybe()); | ||
| } elseif ($this->isList->yes() && $newIsList->no()) { | ||
| return new NeverType(); | ||
| } | ||
|
|
||
| return new self($this->keyTypes, $this->valueTypes, $this->nextAutoIndexes, $optionalKeys, $newIsList); | ||
| } | ||
|
|
@@ -816,6 +834,11 @@ public function unsetOffset(Type $offsetType): Type | |
| $this->isList, | ||
| count($optionalKeys) === count($this->optionalKeys), | ||
| ); | ||
| if (!$preserveListCertainty) { | ||
| $newIsList = $newIsList->and(TrinaryLogic::createMaybe()); | ||
| } elseif ($this->isList->yes() && $newIsList->no()) { | ||
| return new NeverType(); | ||
| } | ||
|
|
||
| return new self($this->keyTypes, $this->valueTypes, $this->nextAutoIndexes, $optionalKeys, $newIsList); | ||
| } | ||
|
|
@@ -851,7 +874,7 @@ private static function isListAfterUnset(array $newKeyTypes, array $newOptionalK | |
| } | ||
| } | ||
|
|
||
| return TrinaryLogic::createMaybe(); | ||
| return $arrayIsList; | ||
| } | ||
|
|
||
| public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type | ||
|
|
@@ -1531,7 +1554,9 @@ private function getKeysOrValuesArray(array $types): self | |
|
|
||
| public function describe(VerbosityLevel $level): string | ||
| { | ||
| $describeValue = function (bool $truncate) use ($level): string { | ||
| $arrayName = $this->shouldBeDescribedAsAList() ? 'list' : 'array'; | ||
|
|
||
| $describeValue = function (bool $truncate) use ($level, $arrayName): string { | ||
| $items = []; | ||
| $values = []; | ||
| $exportValuesOnly = true; | ||
|
|
@@ -1570,18 +1595,36 @@ public function describe(VerbosityLevel $level): string | |
| } | ||
|
|
||
| return sprintf( | ||
| 'array{%s%s}', | ||
| '%s{%s%s}', | ||
| $arrayName, | ||
| implode(', ', $exportValuesOnly ? $values : $items), | ||
| $append, | ||
| ); | ||
| }; | ||
| return $level->handle( | ||
| fn (): string => $this->isIterableAtLeastOnce()->no() ? 'array' : sprintf('array<%s, %s>', $this->getIterableKeyType()->describe($level), $this->getIterableValueType()->describe($level)), | ||
| fn (): string => $this->isIterableAtLeastOnce()->no() ? $arrayName : sprintf('%s<%s, %s>', $arrayName, $this->getIterableKeyType()->describe($level), $this->getIterableValueType()->describe($level)), | ||
| static fn (): string => $describeValue(true), | ||
| static fn (): string => $describeValue(false), | ||
| ); | ||
| } | ||
|
|
||
| private function shouldBeDescribedAsAList(): bool | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since ConstantArray&AccessoryArrayIsList is simplified into ConstantArray, I always get a description like This is annoying when having too much optional keys. I tried to find the formula to change as few description as possible:
So we cannot only rely on IntersectionType to add the |
||
| { | ||
| if (!$this->isList->yes()) { | ||
| return false; | ||
| } | ||
|
|
||
| if (count($this->optionalKeys) === 0) { | ||
| return false; | ||
| } | ||
|
|
||
| if (count($this->optionalKeys) > 1) { | ||
| return true; | ||
| } | ||
|
|
||
| return $this->optionalKeys[0] !== count($this->keyTypes) - 1; | ||
| } | ||
|
|
||
| public function inferTemplateTypes(Type $receivedType): TemplateTypeMap | ||
| { | ||
| if ($receivedType instanceof UnionType || $receivedType instanceof IntersectionType) { | ||
|
|
@@ -1643,11 +1686,11 @@ public function tryRemove(Type $typeToRemove): ?Type | |
| } | ||
|
|
||
| if ($typeToRemove instanceof HasOffsetType) { | ||
| return $this->unsetOffset($typeToRemove->getOffsetType()); | ||
| return $this->unsetOffset($typeToRemove->getOffsetType(), true); | ||
| } | ||
|
|
||
| if ($typeToRemove instanceof HasOffsetValueType) { | ||
| return $this->unsetOffset($typeToRemove->getOffsetType()); | ||
| return $this->unsetOffset($typeToRemove->getOffsetType(), true); | ||
| } | ||
|
|
||
| return null; | ||
|
|
@@ -1823,6 +1866,19 @@ public function makeOffsetRequired(Type $offsetType): self | |
| return $this; | ||
| } | ||
|
|
||
| public function makeList(): Type | ||
| { | ||
| if ($this->isList->yes()) { | ||
| return $this; | ||
| } | ||
|
|
||
| if ($this->isList->no()) { | ||
| return new NeverType(); | ||
| } | ||
|
|
||
| return new self($this->keyTypes, $this->valueTypes, $this->nextAutoIndexes, $this->optionalKeys, TrinaryLogic::createYes()); | ||
| } | ||
|
|
||
| public function toPhpDocNode(): TypeNode | ||
| { | ||
| $items = []; | ||
|
|
@@ -1863,7 +1919,10 @@ public function toPhpDocNode(): TypeNode | |
| ); | ||
| } | ||
|
|
||
| return ArrayShapeNode::createSealed($exportValuesOnly ? $values : $items); | ||
| return ArrayShapeNode::createSealed( | ||
| $exportValuesOnly ? $values : $items, | ||
| $this->shouldBeDescribedAsAList() ? ArrayShapeNode::KIND_LIST : ArrayShapeNode::KIND_ARRAY, | ||
| ); | ||
| } | ||
|
|
||
| public static function isValidIdentifier(string $value): bool | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -55,6 +55,7 @@ | |
| use function is_int; | ||
| use function ksort; | ||
| use function sprintf; | ||
| use function str_starts_with; | ||
| use function strcasecmp; | ||
| use function strlen; | ||
| use function substr; | ||
|
|
@@ -448,7 +449,8 @@ private function describeItself(VerbosityLevel $level, bool $skipAccessoryTypes) | |
| continue; | ||
| } elseif ($type instanceof ConstantArrayType) { | ||
| $description = $type->describe($level); | ||
| $descriptionWithoutKind = substr($description, strlen('array')); | ||
| $kind = str_starts_with($description, 'list') ? 'list' : 'array'; | ||
| $descriptionWithoutKind = substr($description, strlen($kind)); | ||
|
Comment on lines
+452
to
+453
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Now constant array is not always a |
||
| $begin = $isList ? 'list' : 'array'; | ||
| if ($isNonEmptyArray && !$type->isIterableAtLeastOnce()->yes()) { | ||
| $begin = 'non-empty-' . $begin; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1331,6 +1331,20 @@ public static function intersect(Type ...$types): Type | |
| continue 2; | ||
| } | ||
|
|
||
| if ($types[$i] instanceof ConstantArrayType && $types[$j] instanceof AccessoryArrayListType) { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The main idea of this PR. Before, we had sometime a ConstantArray(isList = true) and sometimes ConstantArray(isList=maybe)&AccessoryArrayListType ; now I merged them to always have ConstantArray(isList = true).
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder why these 2 types of "isList" exist in the first place. could this always be using
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ConstantArray need to implement Also, there is some optimisation in the ConstantArray possible when we know it's a list. It would be harder to do such thing in the intersection type. And that's the whole purpose of this PR ; currently some optimisation are ignored because the array doesn't know it's a list. And some other optimisation are possible. |
||
| $types[$i] = $types[$i]->makeList(); | ||
| array_splice($types, $j--, 1); | ||
| $typesCount--; | ||
| continue; | ||
| } | ||
|
|
||
| if ($types[$j] instanceof ConstantArrayType && $types[$i] instanceof AccessoryArrayListType) { | ||
| $types[$j] = $types[$j]->makeList(); | ||
| array_splice($types, $i--, 1); | ||
| $typesCount--; | ||
| continue 2; | ||
| } | ||
|
|
||
| if ( | ||
| $types[$i] instanceof ConstantArrayType | ||
| && count($types[$i]->getKeyTypes()) === 1 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -49,9 +49,9 @@ public function constantArraysWithOptionalKeys(array $arr): void | |
| */ | ||
| public function chunkUnionTypeLength(array $arr, $positiveRange, $positiveUnion) { | ||
| /** @var array{a: 0, b?: 1, c: 2} $arr */ | ||
| assertType('array{0: array{0: 0, 1?: 1|2, 2?: 2}, 1?: array{0?: 2}}', array_chunk($arr, $positiveRange)); | ||
| assertType('array{0: list{0: 0, 1?: 1|2, 2?: 2}, 1?: array{0?: 2}}', array_chunk($arr, $positiveRange)); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is some bonus of this PR. Before a ConstantArray(isList = true) was described as Now I describe it as list in some conditions, which gives a meaningful type. As an example here array_is_list would have a return true for |
||
| assertType('array{0: array{a: 0, b?: 1, c?: 2}, 1?: array{c?: 2}}', array_chunk($arr, $positiveRange, true)); | ||
| assertType('array{0: array{0: 0, 1?: 1|2, 2?: 2}, 1?: array{0?: 2}}', array_chunk($arr, $positiveUnion)); | ||
| assertType('array{0: list{0: 0, 1?: 1|2, 2?: 2}, 1?: array{0?: 2}}', array_chunk($arr, $positiveUnion)); | ||
| assertType('array{0: array{a: 0, b?: 1, c?: 2}, 1?: array{c?: 2}}', array_chunk($arr, $positiveUnion, true)); | ||
| } | ||
|
|
||
|
|
@@ -70,7 +70,7 @@ public function lengthIntRanges(array $arr, int $positiveInt, int $bigger50) { | |
| */ | ||
| function testLimits(array $arr, int $oneToFour, int $tooBig) { | ||
| /** @var array{a: 0, b?: 1, c: 2, d: 3} $arr */ | ||
| assertType('array{0: array{0: 0, 1?: 1|2, 2?: 2|3, 3?: 3}, 1?: array{0?: 2|3, 1?: 3}}|array{array{0}, array{0?: 1|2, 1?: 2}, array{0?: 2|3, 1?: 3}, array{0?: 3}}', array_chunk($arr, $oneToFour)); | ||
| assertType('array{0: list{0: 0, 1?: 1|2, 2?: 2|3, 3?: 3}, 1?: list{0?: 2|3, 1?: 3}}|array{array{0}, list{0?: 1|2, 1?: 2}, list{0?: 2|3, 1?: 3}, array{0?: 3}}', array_chunk($arr, $oneToFour)); | ||
| assertType('non-empty-list<non-empty-list<0|1|2|3>>', array_chunk($arr, $tooBig)); | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Kinda related to #5029 ; this is an idea I already had in mind. Without this fix tests are failing because we don't have the AccessoryIsList anymore (already merged into the constant array).
This method is used in two different context:
For the second example, even if the key is "unset", shouldn't touch the list certainty since we didn't really modified the array. That's why tryRemove will call with 'true'.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this needs a phpdoc for this parameter, because intuitively unsetting on a list will always destroy the list
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added