diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index a07ea29556..b667913d59 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -903,7 +903,7 @@ parameters: - message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantArrayType is error\-prone and deprecated\. Use Type\:\:getConstantArrays\(\) instead\.$#' identifier: phpstanApi.instanceofType - count: 5 + count: 6 path: src/Type/Constant/ConstantArrayType.php - diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 8c070bb040..1c830cbdcf 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -2670,19 +2670,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 2fbea580ca..be9dcd817b 100644 --- a/src/Type/Accessory/NonEmptyArrayType.php +++ b/src/Type/Accessory/NonEmptyArrayType.php @@ -223,6 +223,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 e6c0097db7..e8d812b33f 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -453,6 +453,27 @@ 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( + TypeCombinator::union($this->getIterableKeyType(), $replacementArrayType->getKeysArray()->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 01476d4d01..a3b6ffaa0f 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -919,10 +919,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 @@ -943,10 +940,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); } @@ -1028,6 +1022,108 @@ 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 ($keyTypesCount + $length <= 0) { + // A negative length cannot reach left outside the array twice + $length = 0; + } + + $offsetWasNegative = false; + if ($offset < 0) { + $offsetWasNegative = true; + $offset = $keyTypesCount + $offset; + } + + if ($length < 0) { + $length = $keyTypesCount - $offset + $length; + } + + $extractType = $this->sliceArray($offsetType, $lengthType, TrinaryLogic::createYes()); + + $types = []; + foreach ($replacementType->toArray()->getArrays() as $replacementArrayType) { + $removeKeysCount = 0; + $optionalKeysBeforeReplacement = 0; + + $builder = ConstantArrayTypeBuilder::createEmpty(); + for ($i = 0;; $i++) { + $isOptional = $this->isOptionalKey($i); + + if (!$offsetWasNegative && $i < $offset && $isOptional) { + $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; + + if ($replacementArrayType instanceof self) { + $valuesArray = $replacementArrayType->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($replacementArrayType->getValuesArray()->getIterableKeyType(), $replacementArrayType->getIterableValueType(), true); + } + } + + if (!isset($this->keyTypes[$i])) { + break; + } + + if ($removeKeysCount > 0) { + $extractTypeHasOffsetValueType = $extractType->hasOffsetValueType($this->keyTypes[$i]); + + if ( + (!$isOptional && $extractTypeHasOffsetValueType->yes()) + || ($isOptional && $extractTypeHasOffsetValueType->maybe()) + ) { + $removeKeysCount--; + continue; + } + } + + if (!$isOptional && $extractType->hasOffsetValueType($this->keyTypes[$i])->maybe()) { + $isOptional = true; + } + + $builder->setOffsetValueType( + $this->keyTypes[$i]->isInteger()->no() ? $this->keyTypes[$i] : null, + $this->valueTypes[$i], + $isOptional, + ); + } + + $types[] = $builder->getArray(); + } + + return TypeCombinator::union(...$types); + } + public function isIterableAtLeastOnce(): TrinaryLogic { $keysCount = count($this->keyTypes); @@ -1268,6 +1364,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); diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index a53f7ef4f5..085e0b636f 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -913,6 +913,11 @@ public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $pre return $result; } + 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 10c2b7cccd..6b2c68879e 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..92385d0786 100644 --- a/tests/PHPStan/Analyser/nsrt/array_splice.php +++ b/tests/PHPStan/Analyser/nsrt/array_splice.php @@ -20,42 +20,367 @@ final class Foo function insertViaArraySplice(array $arr): void { $brr = $arr; - array_splice($brr, 0, 0, 1); - assertType('array', $brr); + $extract = array_splice($brr, 0, 0, 1); + assertType('non-empty-array', $brr); + assertType('array{}', $extract); $brr = $arr; - array_splice($brr, 0, 0, [1]); - assertType('array', $brr); + $extract = array_splice($brr, 0, 0, [1]); + assertType('non-empty-array', $brr); + assertType('array{}', $extract); $brr = $arr; - array_splice($brr, 0, 0, ''); - assertType('array', $brr); + $extract = array_splice($brr, 0, 0, ''); + assertType('non-empty-array', $brr); + assertType('array{}', $extract); $brr = $arr; - array_splice($brr, 0, 0, ['']); - assertType('array', $brr); + $extract = array_splice($brr, 0, 0, ['']); + assertType('non-empty-array', $brr); + assertType('array{}', $extract); $brr = $arr; - array_splice($brr, 0, 0, null); + $extract = array_splice($brr, 0, 0, null); assertType('array', $brr); + assertType('array{}', $extract); + + $brr = $arr; + $extract = array_splice($brr, 0, 0, [null]); + assertType('non-empty-array', $brr); + assertType('array{}', $extract); $brr = $arr; - array_splice($brr, 0, 0, [null]); - assertType('array', $brr); + $extract = array_splice($brr, 0, 0, new Foo()); + assertType('non-empty-array', $brr); + assertType('array{}', $extract); $brr = $arr; - array_splice($brr, 0, 0, new Foo()); - assertType('array', $brr); + $extract = array_splice($brr, 0, 0, [new \stdClass()]); + assertType('non-empty-array', $brr); + assertType('array{}', $extract); $brr = $arr; - array_splice($brr, 0, 0, [new \stdClass()]); - assertType('array', $brr); + $extract = array_splice($brr, 0, 0, false); + assertType('non-empty-array', $brr); + assertType('array{}', $extract); $brr = $arr; - array_splice($brr, 0, 0, false); - assertType('array', $brr); + $extract = array_splice($brr, 0, 0, [false]); + assertType('non-empty-array', $brr); + assertType('array{}', $extract); $brr = $arr; - array_splice($brr, 0, 0, [false]); - assertType('array', $brr); + $extract = array_splice($brr, 0); + assertType('array{}', $brr); + assertType('list', $extract); +} + +function constantArrays(array $arr, array $arr2): void +{ + /** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */ + $arr; + $extract = array_splice($arr, 0, 1, ['hello']); + assertType('array{0: \'hello\', b: \'bar\', 1: \'baz\'}', $arr); + assertType('array{\'foo\'}', $extract); + + /** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */ + $arr; + $extract = array_splice($arr, 1, 2, ['hello']); + assertType('array{\'foo\', \'hello\'}', $arr); + assertType('array{b: \'bar\', 0: \'baz\'}', $extract); + + /** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */ + $arr; + $extract = array_splice($arr, 0, -1, ['hello']); + assertType('array{\'hello\', \'baz\'}', $arr); + assertType('array{0: \'foo\', b: \'bar\'}', $extract); + + /** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */ + $arr; + $extract = array_splice($arr, 0, -2, ['hello']); + assertType('array{0: \'hello\', b: \'bar\', 1: \'baz\'}', $arr); + assertType('array{\'foo\'}', $extract); + + /** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */ + $arr; + $extract = array_splice($arr, -1, -1, ['hello']); + assertType('array{0: \'foo\', b: \'bar\', 1: \'hello\', 2: \'baz\'}', $arr); + assertType('array{}', $extract); + + /** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */ + $arr; + $extract = array_splice($arr, -2, -2, ['hello']); + assertType('array{0: \'foo\', 1: \'hello\', b: \'bar\', 2: \'baz\'}', $arr); + assertType('array{}', $extract); + + /** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */ + $arr; + $extract = array_splice($arr, 99, 0, ['hello']); + assertType('array{0: \'foo\', b: \'bar\', 1: \'baz\'}', $arr); + assertType('array{}', $extract); + + /** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */ + $arr; + $extract = array_splice($arr, 1, 99, ['hello']); + assertType('array{\'foo\', \'hello\'}', $arr); + assertType('array{b: \'bar\', 0: \'baz\'}', $extract); + + /** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */ + $arr; + $extract = array_splice($arr, -99, 99, ['hello']); + assertType('array{\'hello\'}', $arr); + assertType('array{0: \'foo\', b: \'bar\', 1: \'baz\'}', $extract); + + /** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */ + $arr; + $extract = array_splice($arr, 0, -99, ['hello']); + assertType('array{0: \'hello\', 1: \'foo\', b: \'bar\', 2: \'baz\'}', $arr); + assertType('array{}', $extract); + + /** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */ + $arr; + $extract = array_splice($arr, -2, 1, ['hello']); + assertType('array{\'foo\', \'hello\', \'baz\'}', $arr); + assertType('array{b: \'bar\'}', $extract); + + /** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */ + $arr; + $extract = array_splice($arr, -1, 1, ['hello']); + assertType('array{0: \'foo\', b: \'bar\', 1: \'hello\'}', $arr); + assertType('array{\'baz\'}', $extract); + + /** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */ + $arr; + $extract = array_splice($arr, 0, null, ['hello']); + assertType('array{\'hello\'}', $arr); + assertType('array{0: \'foo\', b: \'bar\', 1: \'baz\'}', $extract); + + /** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */ + $arr; + $extract = array_splice($arr, 0); + assertType('array{}', $arr); + assertType('array{0: \'foo\', b: \'bar\', 1: \'baz\'}', $extract); + + /** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */ + /** @var array<\stdClass> $arr2 */ + $arr; + $extract = array_splice($arr, 1, 1, $arr2); + assertType('non-empty-array, \'baz\'|\'foo\'|stdClass>', $arr); + assertType('array{b: \'bar\'}', $extract); + + /** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */ + /** @var array<\stdClass> $arr2 */ + $arr; + $extract = array_splice($arr, 0, 1, $arr2); + assertType('non-empty-array<\'b\'|int<0, max>, \'bar\'|\'baz\'|stdClass>', $arr); + assertType('array{\'foo\'}', $extract); + + /** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */ + /** @var array{x: 'x', y?: 'y', 3: 66}|array{z: 'z', 5?: 77, 4: int} $arr2 */ + $arr; + $extract = array_splice($arr, 0, 1, $arr2); + assertType('array{0: \'x\'|\'z\', 1: \'y\'|int, 2: \'baz\'|int, b: \'bar\', 3?: \'baz\'}', $arr); + assertType('array{\'foo\'}', $extract); + + /** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */ + /** @var array{x: 'x', y?: 'y', 3: 66}|array{z: 'z', 5?: 77, 4: int}|array $arr2 */ + $arr; + $extract = array_splice($arr, 0, 1, $arr2); + assertType('non-empty-array<\'b\'|int<0, max>, \'bar\'|\'baz\'|\'x\'|\'y\'|\'z\'|int|object|null>', $arr); + assertType('array{\'foo\'}', $extract); +} + +function constantArraysWithOptionalKeys(array $arr): void +{ + /** + * @see https://3v4l.org/2UJ3u + * @var array{a?: 0, b: 1, c: 2} $arr + */ + $arr; + $extract = array_splice($arr, 0, 1, ['hello']); + assertType('array{0: \'hello\', b?: 1, c: 2}', $arr); + assertType('array{a?: 0, b?: 1}', $extract); + + /** + * @see https://3v4l.org/Aq4l6 + * @var array{a?: 0, b: 1, c: 2} $arr + */ + $arr; + $extract = array_splice($arr, 1, 1, ['hello']); + assertType('array{a?: 0, b?: 1, 0: \'hello\', c?: 2}', $arr); + assertType('array{b?: 1, c?: 2}', $extract); + + /** + * @see https://3v4l.org/GBMps + * @var array{a?: 0, b: 1, c: 2} $arr + */ + $arr; + $extract = array_splice($arr, -1, 0, ['hello']); + assertType('array{a?: 0, b: 1, 0: \'hello\', c: 2}', $arr); + assertType('array{}', $extract); + + /** + * @see https://3v4l.org/dQVgY + * @var array{a?: 0, b: 1, c: 2} $arr + */ + $arr; + $extract = array_splice($arr, 0, -1, ['hello']); + assertType('array{0: \'hello\', c: 2}', $arr); + assertType('array{a?: 0, b: 1}', $extract); + + /** + * @see https://3v4l.org/5XWRC + * @var array{a: 0, b?: 1, c: 2} $arr + */ + $arr; + $extract = array_splice($arr, 0, 1, ['hello']); + assertType('array{0: \'hello\', b?: 1, c: 2}', $arr); + assertType('array{a: 0}', $extract); + + /** + * @see https://3v4l.org/QXZre + * @var array{a: 0, b?: 1, c: 2} $arr + */ + $arr; + $extract = array_splice($arr, 1, 1, ['hello']); + assertType('array{a: 0, 0: \'hello\', c?: 2}', $arr); + assertType('array{b?: 1, c?: 2}', $extract); + + /** + * @see https://3v4l.org/4JvMu + * @var array{a: 0, b?: 1, c: 2} $arr + */ + $arr; + $extract = array_splice($arr, -1, 0, ['hello']); + assertType('array{a: 0, b?: 1, 0: \'hello\', c: 2}', $arr); + assertType('array{}', $extract); + + /** + * @see https://3v4l.org/srHon + * @var array{a: 0, b?: 1, c: 2} $arr + */ + $arr; + $extract = array_splice($arr, 0, -1, ['hello']); + assertType('array{0: \'hello\', c: 2}', $arr); + assertType('array{a: 0, b?: 1}', $extract); + + /** + * @see https://3v4l.org/d0b0c + * @var array{a: 0, b: 1, c?: 2} $arr + */ + $arr; + $extract = array_splice($arr, 0, 1, ['hello']); + assertType('array{0: \'hello\', b: 1, c?: 2}', $arr); + assertType('array{a: 0}', $extract); + + /** + * @see https://3v4l.org/OPfIf + * @var array{a: 0, b: 1, c?: 2} $arr + */ + $arr; + $extract = array_splice($arr, 1, 1, ['hello']); + assertType('array{a: 0, 0: \'hello\', c?: 2}', $arr); + assertType('array{b: 1}', $extract); + + /** + * @see https://3v4l.org/b9R9E + * @var array{a: 0, b: 1, c?: 2} $arr + */ + $arr; + $extract = array_splice($arr, -1, 0, ['hello']); + assertType('array{a: 0, b: 1, 0: \'hello\', c?: 2}', $arr); + assertType('array{}', $extract); + + /** + * @see https://3v4l.org/0lFX6 + * @var array{a: 0, b: 1, c?: 2} $arr + */ + $arr; + $extract = array_splice($arr, 0, -1, ['hello']); + assertType('array{0: \'hello\', b?: 1, c?: 2}', $arr); + assertType('array{a: 0, b?: 1}', $extract); + + /** + * @see https://3v4l.org/PLHYv + * @var array{a: 0, b?: 1, c?: 2, d: 3} $arr + */ + $arr; + $extract = array_splice($arr, 1, 2, ['hello']); + assertType('array{a: 0, 0: \'hello\', d?: 3}', $arr); + assertType('array{b?: 1, c?: 2, d?: 3}', $extract); + + /** + * @see https://3v4l.org/Li5bj + * @var array{a: 0, b?: 1, c?: 2, d: 3} $arr + */ + $arr; + $extract = array_splice($arr, -2, 2, ['hello']); + assertType('array{a?: 0, b?: 1, 0: \'hello\'}', $arr); + assertType('array{a?: 0, b?: 1, c?: 2, d: 3}', $extract); +} + +function offsets(array $arr): void +{ + if (array_key_exists(1, $arr)) { + $extract = array_splice($arr, 0, 1, 'hello'); + assertType('non-empty-array', $arr); + assertType('array', $extract); + } + + if (array_key_exists(1, $arr)) { + $extract = array_splice($arr, 0, 0, 'hello'); + assertType('non-empty-array&hasOffset(1)', $arr); + assertType('array{}', $extract); + } + + if (array_key_exists(1, $arr) && $arr[1] === 'foo') { + $extract = array_splice($arr, 0, 1, 'hello'); + assertType('non-empty-array', $arr); + assertType('array', $extract); + } + + if (array_key_exists(1, $arr) && $arr[1] === 'foo') { + $extract = array_splice($arr, 0, 0, 'hello'); + assertType('non-empty-array&hasOffsetValue(1, \'foo\')', $arr); + assertType('array{}', $extract); + } +} + +function lists(array $arr): void +{ + /** @var list $arr */ + $arr; + $extract = array_splice($arr, 0, 1, 'hello'); + assertType('non-empty-list', $arr); + assertType('list', $extract); + + /** @var non-empty-list $arr */ + $arr; + $extract = array_splice($arr, 0, 1); + assertType('list', $arr); + assertType('non-empty-list', $extract); + + /** @var list $arr */ + $arr; + $extract = array_splice($arr, 0, 0, 'hello'); + assertType('non-empty-list', $arr); + assertType('array{}', $extract); + + /** @var list $arr */ + $arr; + $extract = array_splice($arr, 0, null, 'hello'); + assertType('non-empty-list', $arr); + assertType('list', $extract); + + /** @var list $arr */ + $arr; + $extract = array_splice($arr, 0, null); + assertType('array{}', $arr); + assertType('list', $extract); + + /** @var list $arr */ + $arr; + $extract = array_splice($arr, 0, 1); + assertType('list', $arr); + assertType('list', $extract); } 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 f90994ee89..c4e7cfebaa 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('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;