Improve intersection of ConstantArray and AccessoryIsList#5067
Improve intersection of ConstantArray and AccessoryIsList#5067VincentLanglet wants to merge 10 commits intophpstan:2.1.xfrom
Conversation
| continue 2; | ||
| } | ||
|
|
||
| if ($types[$i] instanceof ConstantArrayType && $types[$j] instanceof AccessoryArrayListType) { |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
I wonder why these 2 types of "isList" exist in the first place. could this always be using AccessoryArrayListType?
There was a problem hiding this comment.
ConstantArray need to implement isList, if you return Maybe for this method, you'll end up with too many test failure ; having to intersect with AccessoryArrayListType every time, even when doing a basic $a = [].
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.
| ); | ||
| } | ||
|
|
||
| private function shouldBeDescribedAsAList(): bool |
There was a problem hiding this comment.
Since ConstantArray&AccessoryArrayIsList is simplified into ConstantArray, I always get a description like
array{...}
This is annoying when having too much optional keys.
I tried to find the formula to change as few description as possible:
- array like
array{0: string, 1: string}without optional keys are untouched because they are always a list - same for array like
array{0: string, 1: string, 2?: string}where the optional keys is the last one list{0: string, 1?: string, 2: string}should stay a list (in the next PR I'll make the offset 1 required)list{0: string, 1?: string, 2?: string}with 2+ optional keys should stay a list.
So we cannot only rely on IntersectionType to add the list keyWord.
| $kind = str_starts_with($description, 'list') ? 'list' : 'array'; | ||
| $descriptionWithoutKind = substr($description, strlen($kind)); |
There was a problem hiding this comment.
Now constant array is not always a array{...} and strlen of list and array are different so I need to check it.
| new IntersectionType([ | ||
| $constantArrayWithOptionalKeys, | ||
| new AccessoryArrayListType(), | ||
| ]), |
There was a problem hiding this comment.
This is technically not a valid intersection anymore as it will be simplified into a constant array with isList = Yes by TypeCombinator::intersect.
So the test was failing because
- this type was transformed into
list{0: string, 1: string, 2?: string, 3?: string}. - this string was parsed into a ConstantArray (isList = Yes) without the need of an intersectionType.
I updated the test to reproduce the "new real case".
| } | ||
|
|
||
| public function unsetOffset(Type $offsetType): Type | ||
| public function unsetOffset(Type $offsetType, bool $preserveListCertainty = false): Type |
There was a problem hiding this comment.
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:
- when unsetting the value
- during something like
if (array_key_exist(...)) {
} else {
// Here we're trying to remove the array key, and calling unsetOffset
}
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'.
if ($typeToRemove instanceof HasOffsetType) {
return $this->unsetOffset($typeToRemove->getOffsetType(), true);
}
There was a problem hiding this comment.
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.
I added
/**
* 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.
*/
93ff38c to
65797b9
Compare
|
This pull request has been marked as ready for review. |
|
This solve phpstan/phpstan#11602 (comment) but not the whole issue. |
4e14478 to
355d824
Compare
| 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)); |
There was a problem hiding this comment.
This is some bonus of this PR.
Before a ConstantArray(isList = true) was described as array in every occasion.
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 array{0: 0, 1?: 1|2, 2?: 2} but this would not have been clear for the user why.
See https://phpstan.org/r/a82c93cb-e834-4fd9-8c01-6bd06446ec04
staabm
left a comment
There was a problem hiding this comment.
I think this makes sense. Ondrej should have another look though.
Proof of the gain in the next PR #5069