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..76c2f948f7 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -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 + { + 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 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..53261d163e 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-14177.php +++ b/tests/PHPStan/Analyser/nsrt/bug-14177.php @@ -14,7 +14,7 @@ public function testList(array $b): void if (array_key_exists(3, $b)) { assertType('list{0: string, 1: string, 2?: string, 3: 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/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index f877c5dec7..6d55c1b0ed 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -1149,6 +1149,12 @@ public function testBug6209(): void $this->analyse([__DIR__ . '/data/bug-6209.php'], []); } + public function testBug11602(): void + { + $this->reportPossiblyNonexistentConstantArrayOffset = true; + $this->analyse([__DIR__ . '/data/bug-11602.php'], []); + } + public function testBug11276(): void { $this->reportPossiblyNonexistentConstantArrayOffset = true; diff --git a/tests/PHPStan/Rules/Arrays/data/bug-11602.php b/tests/PHPStan/Rules/Arrays/data/bug-11602.php new file mode 100644 index 0000000000..3eedbb0ed0 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-11602.php @@ -0,0 +1,23 @@ +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/TypeCombinatorTest.php b/tests/PHPStan/Type/TypeCombinatorTest.php index 136e43c51e..508768ca6d 100644 --- a/tests/PHPStan/Type/TypeCombinatorTest.php +++ b/tests/PHPStan/Type/TypeCombinatorTest.php @@ -19,6 +19,7 @@ use PHPStan\Reflection\Callables\SimpleImpurePoint; use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\AccessoryLiteralStringType; use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; @@ -4216,6 +4217,44 @@ public static function dataIntersect(): iterable StringType::class, 'string', ], + [ + [ + new ConstantArrayType( + [new ConstantStringType('a')], + [new ConstantStringType('foo')], + ), + new AccessoryArrayListType(), + ], + NeverType::class, + '*NEVER*=implicit', + ], + [ + [ + new ConstantArrayType( + [new ConstantIntegerType(0), new ConstantIntegerType(1)], + [new ConstantStringType('foo'), new ConstantStringType('bar')], + [2], + isList: TrinaryLogic::createMaybe(), + ), + new AccessoryArrayListType(), + ], + ConstantArrayType::class, + 'array{\'foo\', \'bar\'}', + ], + [ + [ + new ConstantArrayType( + [new ConstantIntegerType(0), new ConstantIntegerType(1)], + [new ConstantStringType('foo'), new ConstantStringType('bar')], + [2], + [0, 1], + TrinaryLogic::createMaybe(), + ), + new AccessoryArrayListType(), + ], + ConstantArrayType::class, + 'list{0?: \'foo\', 1?: \'bar\'}', + ], ]; if (PHP_VERSION_ID < 80100) { 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,