diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index d91eb9b78b..2bf54f9c9e 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1680,7 +1680,7 @@ parameters: - rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantArrayType is error-prone and deprecated. Use Type::getConstantArrays() instead.' identifier: phpstanApi.instanceofType - count: 16 + count: 18 path: src/Type/TypeCombinator.php - diff --git a/src/Type/Constant/ConstantArrayType.php b/src/Type/Constant/ConstantArrayType.php index a7e0c62a92..3ea57af380 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -715,7 +715,7 @@ public function setExistingOffsetValueType(Type $offsetType, Type $valueType): T return $builder->getArray(); } - public function unsetOffset(Type $offsetType): Type + public function unsetOffset(Type $offsetType, bool $preserveListCertainty = false): Type { $offsetType = $offsetType->toArrayKey(); if ($offsetType instanceof ConstantIntegerType || $offsetType instanceof ConstantStringType) { @@ -749,6 +749,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 +796,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 +826,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 +866,7 @@ private static function isListAfterUnset(array $newKeyTypes, array $newOptionalK } } - return TrinaryLogic::createMaybe(); + return $arrayIsList; } public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type @@ -1531,7 +1546,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 +1587,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 + { + 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 +1678,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; @@ -1805,24 +1840,50 @@ public function makeOffsetRequired(Type $offsetType): self { $offsetType = $offsetType->toArrayKey(); $optionalKeys = $this->optionalKeys; + $isList = $this->isList->yes(); foreach ($this->keyTypes as $i => $keyType) { if (!$keyType->equals($offsetType)) { continue; } + $keyValue = $keyType->getValue(); foreach ($optionalKeys as $j => $key) { - if ($i === $key) { + if ( + $i === $key + || ( + $isList + && is_int($keyValue) + && is_int($this->keyTypes[$key]->getValue()) + && $this->keyTypes[$key]->getValue() < $keyValue + ) + ) { unset($optionalKeys[$j]); - return new self($this->keyTypes, $this->valueTypes, $this->nextAutoIndexes, array_values($optionalKeys), $this->isList); } } + if (count($this->optionalKeys) !== count($optionalKeys)) { + return new self($this->keyTypes, $this->valueTypes, $this->nextAutoIndexes, array_values($optionalKeys), $this->isList); + } + break; } 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 +1924,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 diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index 06e3d781ef..f68ccb5b64 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -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)); $begin = $isList ? 'list' : 'array'; if ($isNonEmptyArray && !$type->isIterableAtLeastOnce()->yes()) { $begin = 'non-empty-' . $begin; diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index aa5f933532..6f564875a1 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -1331,6 +1331,20 @@ public static function intersect(Type ...$types): Type continue 2; } + if ($types[$i] instanceof ConstantArrayType && $types[$j] instanceof AccessoryArrayListType) { + $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 diff --git a/tests/PHPStan/Analyser/nsrt/array-chunk.php b/tests/PHPStan/Analyser/nsrt/array-chunk.php index cedb50ddb7..645a982dff 100644 --- a/tests/PHPStan/Analyser/nsrt/array-chunk.php +++ b/tests/PHPStan/Analyser/nsrt/array-chunk.php @@ -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)); 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>', array_chunk($arr, $tooBig)); } diff --git a/tests/PHPStan/Analyser/nsrt/array-column.php b/tests/PHPStan/Analyser/nsrt/array-column.php index 4f830b96d9..7049a5130b 100644 --- a/tests/PHPStan/Analyser/nsrt/array-column.php +++ b/tests/PHPStan/Analyser/nsrt/array-column.php @@ -158,8 +158,8 @@ public function testConstantArray12(array $array): void /** @param array{0?: array{column: 'foo1', key: 'bar1'}, 1?: array{column: 'foo2', key: 'bar2'}} $array */ public function testConstantArray13(array $array): void { - assertType("array{0?: 'foo1'|'foo2', 1?: 'foo2'}", array_column($array, 'column')); - assertType("array{0?: 'foo1'|'foo2', 1?: 'foo2'}", array_column($array, 'column', null)); + assertType("list{0?: 'foo1'|'foo2', 1?: 'foo2'}", array_column($array, 'column')); + assertType("list{0?: 'foo1'|'foo2', 1?: 'foo2'}", array_column($array, 'column', null)); assertType("array{bar1?: 'foo1', bar2?: 'foo2'}", array_column($array, 'column', 'key')); } diff --git a/tests/PHPStan/Analyser/nsrt/array-reverse.php b/tests/PHPStan/Analyser/nsrt/array-reverse.php index 86a3bb72cf..db05a1d57e 100644 --- a/tests/PHPStan/Analyser/nsrt/array-reverse.php +++ b/tests/PHPStan/Analyser/nsrt/array-reverse.php @@ -49,7 +49,7 @@ public function constantArrays(array $a, array $b, array $c): void assertType('array{\'bar\', \'foo\'}|array{bar: 19, foo: 17}', array_reverse($b)); assertType('array{19: \'bar\', 17: \'foo\'}|array{bar: 19, foo: 17}', array_reverse($b, true)); - assertType("array{0: 'A'|'B'|'C', 1?: 'A'|'B', 2?: 'A'}", array_reverse($c)); + assertType("list{0: 'A'|'B'|'C', 1?: 'A'|'B', 2?: 'A'}", array_reverse($c)); assertType("array{2?: 'C', 1?: 'B', 0: 'A'}", array_reverse($c, true)); } diff --git a/tests/PHPStan/Analyser/nsrt/array_keys.php b/tests/PHPStan/Analyser/nsrt/array_keys.php index 6808bf36b3..ddeebafddd 100644 --- a/tests/PHPStan/Analyser/nsrt/array_keys.php +++ b/tests/PHPStan/Analyser/nsrt/array_keys.php @@ -22,6 +22,6 @@ public function constantArrayType(): void [1 => 'a', 2 => 'b', 3 => 'c'], static fn ($value) => mt_rand(0, 1) === 0, ); - assertType("array{0?: 1|2|3, 1?: 2|3, 2?: 3}", array_keys($numbers)); + assertType("list{0?: 1|2|3, 1?: 2|3, 2?: 3}", array_keys($numbers)); } } diff --git a/tests/PHPStan/Analyser/nsrt/array_values.php b/tests/PHPStan/Analyser/nsrt/array_values.php index 18074963a4..16f22215d8 100644 --- a/tests/PHPStan/Analyser/nsrt/array_values.php +++ b/tests/PHPStan/Analyser/nsrt/array_values.php @@ -35,7 +35,7 @@ public function constantArrayType(): void [1 => 'a', 2 => 'b', 3 => 'c'], static fn ($value) => mt_rand(0, 1) === 0, ); - assertType("array{0?: 'a'|'b'|'c', 1?: 'b'|'c', 2?: 'c'}", array_values($numbers)); + assertType("list{0?: 'a'|'b'|'c', 1?: 'b'|'c', 2?: 'c'}", array_values($numbers)); } /** diff --git a/tests/PHPStan/Analyser/nsrt/bug-14177.php b/tests/PHPStan/Analyser/nsrt/bug-14177.php index 1610725170..c586b65429 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-14177.php +++ b/tests/PHPStan/Analyser/nsrt/bug-14177.php @@ -12,9 +12,9 @@ class HelloWorld public function testList(array $b): void { if (array_key_exists(3, $b)) { - assertType('list{0: string, 1: string, 2?: string, 3: string}', $b); + assertType('array{string, string, string, string}', $b); } else { - assertType('list{0: string, 1: string, 2?: string}', $b); + assertType('array{0: string, 1: string, 2?: string}', $b); } assertType('list{0: string, 1: string, 2?: string, 3?: string}', $b); } @@ -200,4 +200,25 @@ public function testUnsetInt(array $a, array $b, array $c, int $int): void assertType('bool', array_is_list($a)); assertType('false', array_is_list($b)); } + + /** + * @param list{0?: string, 1?: string, 2?: string} $l + */ + public function testFoo($l): void + { + if (array_key_exists(2, $l, true)) { + assertType('true', array_is_list($l)); + assertType('list{0?: string, 1?: string, 2: string}', $l); + if (array_key_exists(1, $l, true)) { + assertType('true', array_is_list($l)); + assertType('list{0?: string, 1: string, 2: string}', $l); + } else { + assertType('true', array_is_list($l)); + assertType('*NEVER*', $l); + } + } else { + assertType('true', array_is_list($l)); + assertType('list{0?: string, 1?: string}', $l); + } + } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-4700.php b/tests/PHPStan/Analyser/nsrt/bug-4700.php index 9d386b0c50..24a680e387 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-4700.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4700.php @@ -19,7 +19,7 @@ function(array $array, int $count): void { if (isset($array['e'])) $a[] = $array['e']; if (count($a) >= $count) { assertType('int<1, 5>', count($a)); - assertType('array{0: mixed~null, 1?: mixed~null, 2?: mixed~null, 3?: mixed~null, 4?: mixed~null}', $a); + assertType('list{0: mixed~null, 1?: mixed~null, 2?: mixed~null, 3?: mixed~null, 4?: mixed~null}', $a); } else { assertType('0', count($a)); assertType('array{}', $a); @@ -44,6 +44,6 @@ function(array $array, int $count): void { assertType('list{0: mixed~null, 1: mixed~null, 2?: mixed~null, 3?: mixed~null, 4?: mixed~null}', $a); } else { assertType('int<0, 5>', count($a)); // Could be int<0, 1> - assertType('array{}|array{0: mixed~null, 1?: mixed~null, 2?: mixed~null, 3?: mixed~null, 4?: mixed~null}', $a); // Could be array{}|array{0: mixed~null} + assertType('array{}|list{0: mixed~null, 1?: mixed~null, 2?: mixed~null, 3?: mixed~null, 4?: mixed~null}', $a); // Could be array{}|array{0: mixed~null} } }; diff --git a/tests/PHPStan/Analyser/nsrt/constant-array-optional-set.php b/tests/PHPStan/Analyser/nsrt/constant-array-optional-set.php index fe3512a45b..08579c67e4 100644 --- a/tests/PHPStan/Analyser/nsrt/constant-array-optional-set.php +++ b/tests/PHPStan/Analyser/nsrt/constant-array-optional-set.php @@ -17,15 +17,15 @@ public function doFoo() if (rand(0, 1)) { $a[] = 3; } - assertType('array{0: 1, 1?: 2|3, 2?: 3}', $a); + assertType('list{0: 1, 1?: 2|3, 2?: 3}', $a); if (rand(0, 1)) { $a[] = 4; } - assertType('array{0: 1, 1?: 2|3|4, 2?: 3|4, 3?: 4}', $a); + assertType('list{0: 1, 1?: 2|3|4, 2?: 3|4, 3?: 4}', $a); if (rand(0, 1)) { $a[] = 5; } - assertType('array{0: 1, 1?: 2|3|4|5, 2?: 3|4|5, 3?: 4|5, 4?: 5}', $a); + assertType('list{0: 1, 1?: 2|3|4|5, 2?: 3|4|5, 3?: 4|5, 4?: 5}', $a); } public function doBar() diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php index 0a4cc7f6e1..bf26bd5506 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php @@ -239,7 +239,7 @@ function doFoo(string $row): void assertType("array{0: non-falsy-string, 1: non-falsy-string, 2?: 'b'}", $matches); } if (preg_match('~^(a(b)?)?$~', $row, $matches) === 1) { - assertType("array{0: string, 1?: non-falsy-string, 2?: 'b'}", $matches); + assertType("list{0: string, 1?: non-falsy-string, 2?: 'b'}", $matches); } } @@ -286,7 +286,7 @@ function (string $size): void { if (preg_match('~^a\.b(c(\d+)?)?d~', $size, $matches) !== 1) { throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); } - assertType('array{0: non-falsy-string, 1?: non-falsy-string, 2?: numeric-string}', $matches); + assertType('list{0: non-falsy-string, 1?: non-falsy-string, 2?: numeric-string}', $matches); }; function (string $size): void { @@ -346,11 +346,11 @@ function bug11277b(string $value): void // https://3v4l.org/09qdT function bug11291(string $s): void { if (preg_match('/(?|(a)|(b)(c)|(d)(e)(f))/', $s, $matches)) { - assertType('array{0: non-empty-string, 1: non-empty-string, 2?: non-empty-string, 3?: non-empty-string}', $matches); + assertType('list{0: non-empty-string, 1: non-empty-string, 2?: non-empty-string, 3?: non-empty-string}', $matches); } else { assertType('array{}', $matches); } - assertType('array{}|array{0: non-empty-string, 1: non-empty-string, 2?: non-empty-string, 3?: non-empty-string}', $matches); + assertType('array{}|list{0: non-empty-string, 1: non-empty-string, 2?: non-empty-string, 3?: non-empty-string}', $matches); } function bug11323a(string $s): void diff --git a/tests/PHPStan/Rules/Variables/IssetRuleTest.php b/tests/PHPStan/Rules/Variables/IssetRuleTest.php index fac3234530..20fdeb99d2 100644 --- a/tests/PHPStan/Rules/Variables/IssetRuleTest.php +++ b/tests/PHPStan/Rules/Variables/IssetRuleTest.php @@ -499,6 +499,22 @@ public function testPr4374(): void ]); } + public function testIssetConstantArray(): void + { + $this->treatPhpDocTypesAsCertain = true; + + $this->analyse([__DIR__ . '/data/isset-constant-array.php'], [ + [ + 'Offset 2 on array{0: string, 1: string, 2: string, 3: string, 4?: string} in isset() always exists and is not nullable.', + 13, + ], + [ + 'Offset 3 on array{string, string, string, string, string} in isset() always exists and is not nullable.', + 17, + ], + ]); + } + public function testBug10640(): void { $this->treatPhpDocTypesAsCertain = true; diff --git a/tests/PHPStan/Rules/Variables/data/isset-constant-array.php b/tests/PHPStan/Rules/Variables/data/isset-constant-array.php new file mode 100644 index 0000000000..44047da47a --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/isset-constant-array.php @@ -0,0 +1,22 @@ +assertTrue($array3->isKeysSupersetOf($array2)); $array2MergedWith3 = $array3->mergeWith($array2); - $this->assertSame('array{0: 1, 1?: 2|3, 2?: 3}', $array2MergedWith3->describe(VerbosityLevel::precise())); + $this->assertSame('list{0: 1, 1?: 2|3, 2?: 3}', $array2MergedWith3->describe(VerbosityLevel::precise())); $this->assertSame([1, 2, 3], $array2MergedWith3->getNextAutoIndexes()); $builder->setOffsetValueType(null, new ConstantIntegerType(4)); @@ -95,10 +95,10 @@ public function testAppendingOptionalKeys(): void $this->assertSame('array{0?: bool}', $builder->getArray()->describe(VerbosityLevel::precise())); $builder->setOffsetValueType(null, new NullType(), true); - $this->assertSame('array{0?: bool|null, 1?: null}', $builder->getArray()->describe(VerbosityLevel::precise())); + $this->assertSame('list{0?: bool|null, 1?: null}', $builder->getArray()->describe(VerbosityLevel::precise())); $builder->setOffsetValueType(null, new ConstantIntegerType(17)); - $this->assertSame('array{0: 17|bool|null, 1?: 17|null, 2?: 17}', $builder->getArray()->describe(VerbosityLevel::precise())); + $this->assertSame('list{0: 17|bool|null, 1?: 17|null, 2?: 17}', $builder->getArray()->describe(VerbosityLevel::precise())); } public function testDegradedArrayIsNotAlwaysOversized(): void diff --git a/tests/PHPStan/Type/TypeToPhpDocNodeTest.php b/tests/PHPStan/Type/TypeToPhpDocNodeTest.php index 29bfe8f70a..aec8c53ea9 100644 --- a/tests/PHPStan/Type/TypeToPhpDocNodeTest.php +++ b/tests/PHPStan/Type/TypeToPhpDocNodeTest.php @@ -369,7 +369,8 @@ public static function dataToPhpDocNode(): iterable ]), 'non-empty-array', ]; - $constantArrayWithOptionalKeys = new ConstantArrayType([ + + $listWithOptionalKeys = new ConstantArrayType([ new ConstantIntegerType(0), new ConstantIntegerType(1), new ConstantIntegerType(2), @@ -379,25 +380,14 @@ public static function dataToPhpDocNode(): iterable new StringType(), new StringType(), new StringType(), - ], [3], [2, 3], TrinaryLogic::createMaybe()); - - yield [ - new IntersectionType([ - $constantArrayWithOptionalKeys, - new AccessoryArrayListType(), - ]), - 'list{0: string, 1: string, 2?: string, 3?: string}', - ]; + ], [3], [2, 3], TrinaryLogic::createYes()); yield [ - new IntersectionType([ - $constantArrayWithOptionalKeys, - new AccessoryArrayListType(), - ]), + $listWithOptionalKeys, 'list{0: string, 1: string, 2?: string, 3?: string}', ]; - $constantArrayWithAllOptionalKeys = new ConstantArrayType([ + $listArrayWithAllOptionalKeys = new ConstantArrayType([ new ConstantIntegerType(0), new ConstantIntegerType(1), new ConstantIntegerType(2), @@ -407,25 +397,33 @@ public static function dataToPhpDocNode(): iterable new StringType(), new StringType(), new StringType(), - ], [3], [0, 1, 2, 3], TrinaryLogic::createMaybe()); + ], [3], [0, 1, 2, 3], TrinaryLogic::createYes()); yield [ - new IntersectionType([ - $constantArrayWithAllOptionalKeys, - new AccessoryArrayListType(), - ]), + $listArrayWithAllOptionalKeys, 'list{0?: string, 1?: string, 2?: string, 3?: string}', ]; yield [ new IntersectionType([ - $constantArrayWithAllOptionalKeys, + $listArrayWithAllOptionalKeys, new NonEmptyArrayType(), - new AccessoryArrayListType(), ]), 'non-empty-list{0?: string, 1?: string, 2?: string, 3?: string}', ]; + $constantArrayWithAllOptionalKeys = new ConstantArrayType([ + new ConstantIntegerType(0), + new ConstantIntegerType(1), + new ConstantIntegerType(2), + new ConstantIntegerType(3), + ], [ + new StringType(), + new StringType(), + new StringType(), + new StringType(), + ], [3], [0, 1, 2, 3], TrinaryLogic::createMaybe()); + yield [ new IntersectionType([ $constantArrayWithAllOptionalKeys,