From 8ba8d069d6d7af5b982939d83329963bf67bc65f Mon Sep 17 00:00:00 2001 From: Martin Herndl Date: Sat, 19 Apr 2025 20:59:05 +0200 Subject: [PATCH 1/2] Make constant array degradation to general array more DRY --- src/Type/Constant/ConstantArrayType.php | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/Type/Constant/ConstantArrayType.php b/src/Type/Constant/ConstantArrayType.php index 8e76f0d08f..678510e87e 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -913,10 +913,7 @@ public function shiftArray(): Type public function shuffleArray(): Type { - $builder = ConstantArrayTypeBuilder::createFromConstantArray($this->getValuesArray()); - $builder->degradeToGeneralArray(); - - return $builder->getArray(); + return $this->getValuesArray()->degradeToGeneralArray(); } public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type @@ -937,10 +934,7 @@ public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $pre } if ($offset === null || $length === null) { - $builder = ConstantArrayTypeBuilder::createFromConstantArray($this); - $builder->degradeToGeneralArray(); - - return $builder->getArray() + return $this->degradeToGeneralArray() ->sliceArray($offsetType, $lengthType, $preserveKeys); } @@ -1252,6 +1246,14 @@ public function generalizeValues(): self return new self($this->keyTypes, $valueTypes, $this->nextAutoIndexes, $this->optionalKeys, $this->isList); } + private function degradeToGeneralArray(): Type + { + $builder = ConstantArrayTypeBuilder::createFromConstantArray($this); + $builder->degradeToGeneralArray(); + + return $builder->getArray(); + } + public function getKeysArray(): self { return $this->getKeysOrValuesArray($this->keyTypes); From 4ebff11ec0f1f84bc6fede4aebafdf739f3a6cc7 Mon Sep 17 00:00:00 2001 From: Martin Herndl Date: Sat, 19 Apr 2025 21:01:18 +0200 Subject: [PATCH 2/2] Add `Type::spliceArray()`, improve `splice_array()` array type narrowing --- src/Analyser/NodeScopeResolver.php | 17 +- src/Type/Accessory/AccessoryArrayListType.php | 5 + src/Type/Accessory/HasOffsetType.php | 9 + src/Type/Accessory/HasOffsetValueType.php | 9 + src/Type/Accessory/NonEmptyArrayType.php | 12 ++ src/Type/Accessory/OversizedArrayType.php | 5 + src/Type/ArrayType.php | 17 ++ src/Type/Constant/ConstantArrayType.php | 116 ++++++++++- src/Type/IntersectionType.php | 5 + src/Type/MixedType.php | 9 + src/Type/NeverType.php | 5 + src/Type/StaticType.php | 5 + src/Type/Traits/LateResolvableTypeTrait.php | 5 + src/Type/Traits/MaybeArrayTypeTrait.php | 5 + src/Type/Traits/NonArrayTypeTrait.php | 5 + src/Type/Type.php | 2 + src/Type/UnionType.php | 5 + tests/PHPStan/Analyser/nsrt/array_splice.php | 184 +++++++++++++++++- tests/PHPStan/Analyser/nsrt/bug-11917.php | 23 +++ tests/PHPStan/Analyser/nsrt/bug-5017.php | 8 +- .../Rules/Functions/ReturnTypeRuleTest.php | 7 + 21 files changed, 436 insertions(+), 22 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-11917.php diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 756ea8db31..a143d9f3be 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -2668,19 +2668,20 @@ static function (): void { if ( $functionReflection !== null && $functionReflection->getName() === 'array_splice' - && count($expr->getArgs()) >= 1 + && count($expr->getArgs()) >= 2 ) { $arrayArg = $expr->getArgs()[0]->value; $arrayArgType = $scope->getType($arrayArg); - $valueType = $arrayArgType->getIterableValueType(); - if (count($expr->getArgs()) >= 4) { - $replacementType = $scope->getType($expr->getArgs()[3]->value)->toArray(); - $valueType = TypeCombinator::union($valueType, $replacementType->getIterableValueType()); - } + $arrayArgNativeType = $scope->getNativeType($arrayArg); + + $offsetType = $scope->getType($expr->getArgs()[1]->value); + $lengthType = isset($expr->getArgs()[2]) ? $scope->getType($expr->getArgs()[2]->value) : new NullType(); + $replacementType = isset($expr->getArgs()[3]) ? $scope->getType($expr->getArgs()[3]->value) : new ConstantArrayType([], []); + $scope = $scope->invalidateExpression($arrayArg)->assignExpression( $arrayArg, - new ArrayType($arrayArgType->getIterableKeyType(), $valueType), - new ArrayType($arrayArgType->getIterableKeyType(), $valueType), + $arrayArgType->spliceArray($offsetType, $lengthType, $replacementType), + $arrayArgNativeType->spliceArray($offsetType, $lengthType, $replacementType), ); } diff --git a/src/Type/Accessory/AccessoryArrayListType.php b/src/Type/Accessory/AccessoryArrayListType.php index 5f60fe8eb7..4f2a5ab1aa 100644 --- a/src/Type/Accessory/AccessoryArrayListType.php +++ b/src/Type/Accessory/AccessoryArrayListType.php @@ -248,6 +248,11 @@ public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $pre return new MixedType(); } + public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type + { + return $this; + } + public function isIterable(): TrinaryLogic { return TrinaryLogic::createYes(); diff --git a/src/Type/Accessory/HasOffsetType.php b/src/Type/Accessory/HasOffsetType.php index 455f0de86e..42d0178921 100644 --- a/src/Type/Accessory/HasOffsetType.php +++ b/src/Type/Accessory/HasOffsetType.php @@ -214,6 +214,15 @@ public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $pre return new MixedType(); } + public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type + { + if ((new ConstantIntegerType(0))->isSuperTypeOf($lengthType)->yes()) { + return $this; + } + + return new MixedType(); + } + public function isIterableAtLeastOnce(): TrinaryLogic { return TrinaryLogic::createYes(); diff --git a/src/Type/Accessory/HasOffsetValueType.php b/src/Type/Accessory/HasOffsetValueType.php index ec6e822a31..7adcd66148 100644 --- a/src/Type/Accessory/HasOffsetValueType.php +++ b/src/Type/Accessory/HasOffsetValueType.php @@ -274,6 +274,15 @@ public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $pre return new MixedType(); } + public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type + { + if ((new ConstantIntegerType(0))->isSuperTypeOf($lengthType)->yes()) { + return $this; + } + + return new MixedType(); + } + public function isIterableAtLeastOnce(): TrinaryLogic { return TrinaryLogic::createYes(); diff --git a/src/Type/Accessory/NonEmptyArrayType.php b/src/Type/Accessory/NonEmptyArrayType.php index d4726fd5c2..a52a9a1761 100644 --- a/src/Type/Accessory/NonEmptyArrayType.php +++ b/src/Type/Accessory/NonEmptyArrayType.php @@ -226,6 +226,18 @@ public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $pre return new MixedType(); } + public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type + { + if ( + (new ConstantIntegerType(0))->isSuperTypeOf($lengthType)->yes() + || $replacementType->toArray()->isIterableAtLeastOnce()->yes() + ) { + return $this; + } + + return new MixedType(); + } + public function isIterable(): TrinaryLogic { return TrinaryLogic::createYes(); diff --git a/src/Type/Accessory/OversizedArrayType.php b/src/Type/Accessory/OversizedArrayType.php index c43c86a903..1f0918a760 100644 --- a/src/Type/Accessory/OversizedArrayType.php +++ b/src/Type/Accessory/OversizedArrayType.php @@ -214,6 +214,11 @@ public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $pre return $this; } + public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type + { + return $this; + } + public function isIterable(): TrinaryLogic { return TrinaryLogic::createYes(); diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php index e68a6a61d3..42027f8a51 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -449,6 +449,23 @@ public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $pre return $this; } + public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type + { + $replacementArrayType = $replacementType->toArray(); + $replacementArrayTypeIsIterableAtLeastOnce = $replacementArrayType->isIterableAtLeastOnce(); + + if ((new ConstantIntegerType(0))->isSuperTypeOf($offsetType)->yes() && $lengthType->isNull()->yes() && $replacementArrayTypeIsIterableAtLeastOnce->no()) { + return new ConstantArrayType([], []); + } + + $arrayType = new self($this->getIterableKeyType(), TypeCombinator::union($this->getIterableValueType(), $replacementArrayType->getIterableValueType())); + if ($replacementArrayTypeIsIterableAtLeastOnce->yes()) { + $arrayType = TypeCombinator::intersect($arrayType, new NonEmptyArrayType()); + } + + return $arrayType; + } + public function isCallable(): TrinaryLogic { return TrinaryLogic::createMaybe()->and($this->itemType->isString()); diff --git a/src/Type/Constant/ConstantArrayType.php b/src/Type/Constant/ConstantArrayType.php index 678510e87e..f90ce344dc 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -945,7 +945,7 @@ public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $pre } if ($keyTypesCount + $offset <= 0) { - // A negative offset cannot reach left outside the array + // A negative offset cannot reach left outside the array twice $offset = 0; } @@ -1006,6 +1006,120 @@ public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $pre return $builder->getArray(); } + public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type + { + $keyTypesCount = count($this->keyTypes); + + $offset = $offsetType instanceof ConstantIntegerType ? $offsetType->getValue() : null; + + if ($lengthType instanceof ConstantIntegerType) { + $length = $lengthType->getValue(); + } elseif ($lengthType->isNull()->yes()) { + $length = $keyTypesCount; + } else { + $length = null; + } + + if ($offset === null || $length === null) { + return $this->degradeToGeneralArray() + ->spliceArray($offsetType, $lengthType, $replacementType); + } + + if ($keyTypesCount + $offset <= 0) { + // A negative offset cannot reach left outside the array twice + $offset = 0; + } + + if ($offset < 0) { + /* + * Transforms the problem with the negative offset in one with a positive offset using array reversion. + * The reason is belows handling of optional keys which works only from left to right. + * + * e.g. + * array{a: 0, b: 1, c: 2, d: 3, e: 4} + * with offset -4, length 2 and replacement array{17, 19} (which would be spliced to array{a: 0, 0: 17, 1: 19, d: 3, e: 4}) + * + * is transformed via reversion to + * + * array{e: 4, d: 3, c: 2, b: 1, a: 0} + * with offset 2 and length 2 (which will be spliced to array{e: 4, d: 3, 1: 19, 0: 17, a: 0} and then reversed again) + */ + $offset *= -1; + $reversedLength = min($length, $offset); + $reversedOffset = $offset - $reversedLength; + return $this->reverseArray(TrinaryLogic::createNo()) + ->spliceArray(new ConstantIntegerType($reversedOffset), new ConstantIntegerType($reversedLength), $replacementType->reverseArray(TrinaryLogic::createYes())) + ->reverseArray(TrinaryLogic::createNo()); + } + + if ($length < 0) { + $length = $keyTypesCount - $offset - $length * -1; + } + + $removeKeysCount = 0; + $optionalKeysIgnored = 0; + $optionalKeysBeforeReplacement = 0; + $builder = ConstantArrayTypeBuilder::createEmpty(); + for ($i = 0;; $i++) { + $isOptional = $this->isOptionalKey($i); + + if ($i < $offset && $isOptional) { + $optionalKeysIgnored++; + $optionalKeysBeforeReplacement++; + } + + if ($i === $offset + $optionalKeysBeforeReplacement) { + // When the offset is reached we have to a) put the replacement array in and b) remove $length elements + $removeKeysCount = $length; + $optionalKeysIgnored += $optionalKeysBeforeReplacement; + + $replacementArrayType = $replacementType->toArray(); + $constantArrays = $replacementArrayType->getConstantArrays(); + if (count($constantArrays) === 1) { + $valuesArray = $constantArrays[0]->getValuesArray(); + for ($j = 0, $jMax = count($valuesArray->keyTypes); $j < $jMax; $j++) { + $builder->setOffsetValueType(null, $valuesArray->valueTypes[$j], $valuesArray->isOptionalKey($j)); + } + } else { + $builder->degradeToGeneralArray(); + $builder->setOffsetValueType(null, $replacementArrayType->getIterableValueType(), true); + } + } + + if (!isset($this->keyTypes[$i])) { + break; + } + + if ($removeKeysCount > 0) { + $removeKeysCount--; + + if ($optionalKeysIgnored === 0) { + if ($isOptional) { + $optionalKeysIgnored++; + } + + continue; + } else { + $optionalKeysIgnored++; + $isOptional = true; + } + } + + if (!$isOptional && $optionalKeysIgnored > 0) { + $optionalKeysIgnored--; + $isOptional = true; + } + + $builder->setOffsetValueType( + $this->keyTypes[$i]->isInteger()->no() ? $this->keyTypes[$i] : null, + $this->valueTypes[$i], + $isOptional, + ); + } + + return $builder->getArray(); + } + public function isIterableAtLeastOnce(): TrinaryLogic { $keysCount = count($this->keyTypes); diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index 149536a573..f0b845ceb2 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -899,6 +899,11 @@ public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $pre return $this->intersectTypes(static fn (Type $type): Type => $type->sliceArray($offsetType, $lengthType, $preserveKeys)); } + public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->spliceArray($offsetType, $lengthType, $replacementType)); + } + public function getEnumCases(): array { $compare = []; diff --git a/src/Type/MixedType.php b/src/Type/MixedType.php index 487a474827..4324ddb748 100644 --- a/src/Type/MixedType.php +++ b/src/Type/MixedType.php @@ -287,6 +287,15 @@ public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $pre return new ArrayType(new MixedType($this->isExplicitMixed), new MixedType($this->isExplicitMixed)); } + public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type + { + if ($this->isArray()->no()) { + return new ErrorType(); + } + + return new ArrayType(new MixedType($this->isExplicitMixed), new MixedType($this->isExplicitMixed)); + } + public function isCallable(): TrinaryLogic { if ($this->subtractedType !== null) { diff --git a/src/Type/NeverType.php b/src/Type/NeverType.php index 518ffa8f4a..46a9369699 100644 --- a/src/Type/NeverType.php +++ b/src/Type/NeverType.php @@ -333,6 +333,11 @@ public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $pre return new NeverType(); } + public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type + { + return new NeverType(); + } + public function isCallable(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/StaticType.php b/src/Type/StaticType.php index 9db20e5b34..cc93c71ee7 100644 --- a/src/Type/StaticType.php +++ b/src/Type/StaticType.php @@ -456,6 +456,11 @@ public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $pre return $this->getStaticObjectType()->sliceArray($offsetType, $lengthType, $preserveKeys); } + public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type + { + return $this->getStaticObjectType()->spliceArray($offsetType, $lengthType, $replacementType); + } + public function isCallable(): TrinaryLogic { return $this->getStaticObjectType()->isCallable(); diff --git a/src/Type/Traits/LateResolvableTypeTrait.php b/src/Type/Traits/LateResolvableTypeTrait.php index 5eb703077f..c8409dffd7 100644 --- a/src/Type/Traits/LateResolvableTypeTrait.php +++ b/src/Type/Traits/LateResolvableTypeTrait.php @@ -308,6 +308,11 @@ public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $pre return $this->resolve()->sliceArray($offsetType, $lengthType, $preserveKeys); } + public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type + { + return $this->resolve()->spliceArray($offsetType, $lengthType, $replacementType); + } + public function isCallable(): TrinaryLogic { return $this->resolve()->isCallable(); diff --git a/src/Type/Traits/MaybeArrayTypeTrait.php b/src/Type/Traits/MaybeArrayTypeTrait.php index afafc91708..f83bce156f 100644 --- a/src/Type/Traits/MaybeArrayTypeTrait.php +++ b/src/Type/Traits/MaybeArrayTypeTrait.php @@ -99,4 +99,9 @@ public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $pre return new ErrorType(); } + public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type + { + return new ErrorType(); + } + } diff --git a/src/Type/Traits/NonArrayTypeTrait.php b/src/Type/Traits/NonArrayTypeTrait.php index 1d1b948242..897ffdb2ef 100644 --- a/src/Type/Traits/NonArrayTypeTrait.php +++ b/src/Type/Traits/NonArrayTypeTrait.php @@ -99,4 +99,9 @@ public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $pre return new ErrorType(); } + public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type + { + return new ErrorType(); + } + } diff --git a/src/Type/Type.php b/src/Type/Type.php index 15886a053c..3882f6b5d3 100644 --- a/src/Type/Type.php +++ b/src/Type/Type.php @@ -158,6 +158,8 @@ public function shuffleArray(): Type; public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type; + public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type; + /** * @return list */ diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index 08d678152a..67a54fe072 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -790,6 +790,11 @@ public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $pre return $this->unionTypes(static fn (Type $type): Type => $type->sliceArray($offsetType, $lengthType, $preserveKeys)); } + public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->spliceArray($offsetType, $lengthType, $replacementType)); + } + public function getEnumCases(): array { return $this->pickFromTypes( diff --git a/tests/PHPStan/Analyser/nsrt/array_splice.php b/tests/PHPStan/Analyser/nsrt/array_splice.php index 7075c0fb8b..4f4893d22b 100644 --- a/tests/PHPStan/Analyser/nsrt/array_splice.php +++ b/tests/PHPStan/Analyser/nsrt/array_splice.php @@ -21,19 +21,19 @@ function insertViaArraySplice(array $arr): void { $brr = $arr; array_splice($brr, 0, 0, 1); - assertType('array', $brr); + assertType('non-empty-array', $brr); $brr = $arr; array_splice($brr, 0, 0, [1]); - assertType('array', $brr); + assertType('non-empty-array', $brr); $brr = $arr; array_splice($brr, 0, 0, ''); - assertType('array', $brr); + assertType('non-empty-array', $brr); $brr = $arr; array_splice($brr, 0, 0, ['']); - assertType('array', $brr); + assertType('non-empty-array', $brr); $brr = $arr; array_splice($brr, 0, 0, null); @@ -41,21 +41,187 @@ function insertViaArraySplice(array $arr): void $brr = $arr; array_splice($brr, 0, 0, [null]); - assertType('array', $brr); + assertType('non-empty-array', $brr); $brr = $arr; array_splice($brr, 0, 0, new Foo()); - assertType('array', $brr); + assertType('non-empty-array', $brr); $brr = $arr; array_splice($brr, 0, 0, [new \stdClass()]); - assertType('array', $brr); + assertType('non-empty-array', $brr); $brr = $arr; array_splice($brr, 0, 0, false); - assertType('array', $brr); + assertType('non-empty-array', $brr); $brr = $arr; array_splice($brr, 0, 0, [false]); - assertType('array', $brr); + assertType('non-empty-array', $brr); + + $brr = $arr; + array_splice($brr, 0); + assertType('array{}', $brr); +} + +function constantArrays(array $arr): void +{ + /** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */ + array_splice($arr, 0, 1, ['hello']); + assertType('array{0: \'hello\', b: \'bar\', 1: \'baz\'}', $arr); + + /** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */ + array_splice($arr, -2, 1, ['hello']); + assertType('array{\'foo\', \'hello\', \'baz\'}', $arr); + + /** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */ + array_splice($arr, -1, 1, ['hello']); + assertType('array{0: \'foo\', b: \'bar\', 1: \'hello\'}', $arr); + + /** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */ + array_splice($arr, 0, null, ['hello']); + assertType('array{\'hello\'}', $arr); + + /** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */ + array_splice($arr, 0); + assertType('array{}', $arr); +} + +function constantArraysWithOptionalKeys(array $arr): void +{ + // array{a?: 0, b: 1, c: 2} + + /** + * @see https://3v4l.org/jrqoZ + * @var array{a?: 0, b: 1, c: 2} $arr + */ + array_splice($arr, 0, 1, ['hello']); + assertType('array{0: \'hello\', b?: 1, c: 2}', $arr); + + /** + * @see https://3v4l.org/lbUJG + * @var array{a?: 0, b: 1, c: 2} $arr + */ + array_splice($arr, 1, 1, ['hello']); + assertType('array{a?: 0, b?: 1, 0: \'hello\', c?: 2}', $arr); + + /** + * @see https://3v4l.org/7uPmV + * @var array{a?: 0, b: 1, c: 2} $arr + */ + array_splice($arr, -1, 0, ['hello']); + assertType('array{a?: 0, b: 1, 0: \'hello\', c: 2}', $arr); + + /** + * @see https://3v4l.org/hB8pG + * @var array{a?: 0, b: 1, c: 2} $arr + */ + array_splice($arr, 0, -1, ['hello']); + assertType('array{0: \'hello\', b?: 1, c?: 2}', $arr); // Could be array{0: 'hello', c: 2} + + // array{a: 0, b?: 1, c: 2} + + /** + * @see https://3v4l.org/TjfHT + * @var array{a: 0, b?: 1, c: 2} $arr + */ + array_splice($arr, 0, 1, ['hello']); + assertType('array{0: \'hello\', b?: 1, c: 2}', $arr); + + /** + * @see https://3v4l.org/D8PSE + * @var array{a: 0, b?: 1, c: 2} $arr + */ + array_splice($arr, 1, 1, ['hello']); + assertType('array{a: 0, 0: \'hello\', c?: 2}', $arr); + + /** + * @see https://3v4l.org/8RfDs + * @var array{a: 0, b?: 1, c: 2} $arr + */ + array_splice($arr, -1, 0, ['hello']); + assertType('array{a: 0, b?: 1, 0: \'hello\', c: 2}', $arr); + + /** + * @see https://3v4l.org/sPfpN + * @var array{a: 0, b?: 1, c: 2} $arr + */ + array_splice($arr, 0, -1, ['hello']); + assertType('array{0: \'hello\', c?: 2}', $arr); // Could be array{0: 'hello', c: 2} + + // array{a: 0, b: 1, c?: 2} + + /** + * @see https://3v4l.org/Ddpku + * @var array{a: 0, b: 1, c?: 2} $arr + */ + array_splice($arr, 0, 1, ['hello']); + assertType('array{0: \'hello\', b: 1, c?: 2}', $arr); + + /** + * @see https://3v4l.org/O4LLi + * @var array{a: 0, b: 1, c?: 2} $arr + */ + array_splice($arr, 1, 1, ['hello']); + assertType('array{a: 0, 0: \'hello\', c?: 2}', $arr); + + /** + * @see https://3v4l.org/QQO84 + * @var array{a: 0, b: 1, c?: 2} $arr + */ + array_splice($arr, -1, 0, ['hello']); + assertType('array{a?: 0, 0: \'hello\', b?: 1, c?: 2}', $arr); // Could be array{a?: 0, 0: 'hello', b: 1, c?: 2} + + /** + * @see https://3v4l.org/K5RDp + * @var array{a: 0, b: 1, c?: 2} $arr + */ + array_splice($arr, 0, -1, ['hello']); + assertType('array{0: \'hello\', c?: 2}', $arr); // Could be array{0: 'hello', b?: 1, c?: 2} +} + +function offsets(array $arr): void +{ + if (array_key_exists(1, $arr)) { + array_splice($arr, 0, 1, 'hello'); + assertType('non-empty-array', $arr); + } + + if (array_key_exists(1, $arr)) { + array_splice($arr, 0, 0, 'hello'); + assertType('non-empty-array&hasOffset(1)', $arr); + } + + if (array_key_exists(1, $arr) && $arr[1] === 'foo') { + array_splice($arr, 0, 1, 'hello'); + assertType('non-empty-array', $arr); + } + + if (array_key_exists(1, $arr) && $arr[1] === 'foo') { + array_splice($arr, 0, 0, 'hello'); + assertType('non-empty-array&hasOffsetValue(1, \'foo\')', $arr); + } +} + +function lists(array $arr): void +{ + /** @var list $arr */ + array_splice($arr, 0, 1, 'hello'); + assertType('non-empty-list', $arr); + + /** @var list $arr */ + array_splice($arr, 0, 0, 'hello'); + assertType('non-empty-list', $arr); + + /** @var list $arr */ + array_splice($arr, 0, null, 'hello'); + assertType('non-empty-list', $arr); + + /** @var list $arr */ + array_splice($arr, 0, null); + assertType('array{}', $arr); + + /** @var list $arr */ + array_splice($arr, 0, 1); + assertType('list', $arr); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-11917.php b/tests/PHPStan/Analyser/nsrt/bug-11917.php new file mode 100644 index 0000000000..c09a7b61ab --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11917.php @@ -0,0 +1,23 @@ + + */ +function generateList(string $name): array +{ + $a = ['a', 'b', 'c', $name]; + assertType('array{\'a\', \'b\', \'c\', string}', $a); + $b = ['d', 'e']; + assertType('array{\'d\', \'e\'}', $b); + + array_splice($a, 2, 0, $b); + assertType('array{\'a\', \'b\', \'d\', \'e\', \'c\', string}', $a); + + return $a; +} + +var_dump(generateList('John')); diff --git a/tests/PHPStan/Analyser/nsrt/bug-5017.php b/tests/PHPStan/Analyser/nsrt/bug-5017.php index 918b56e624..83d5065548 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-5017.php +++ b/tests/PHPStan/Analyser/nsrt/bug-5017.php @@ -12,10 +12,10 @@ public function doFoo() $items = [0, 1, 2, 3, 4]; while ($items) { - assertType('non-empty-array<0|1|2|3|4, 0|1|2|3|4>', $items); + assertType('non-empty-list>', $items); $batch = array_splice($items, 0, 2); - assertType('array<0|1|2|3|4, 0|1|2|3|4>', $items); - assertType('non-empty-list<0|1|2|3|4>', $batch); + assertType('list>', $items); + assertType('non-empty-list>', $batch); } } @@ -37,7 +37,7 @@ public function doBar2() $items = [0, 1, 2, 3, 4]; assertType('array{0, 1, 2, 3, 4}', $items); $batch = array_splice($items, 0, 2); - assertType('array<0|1|2|3|4, 0|1|2|3|4>', $items); + assertType('array{2, 3, 4}', $items); assertType('array{0, 1}', $batch); } diff --git a/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php b/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php index b5ca981736..831cd8ad6c 100644 --- a/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php @@ -345,6 +345,13 @@ public function testBug11301(): void ]); } + public function testBug11917(): void + { + $this->checkExplicitMixed = true; + $this->checkNullables = true; + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-11917.php'], []); + } + public function testBug12274(): void { $this->checkExplicitMixed = true;