diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 1011e39a00..9977a1992d 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1312,11 +1312,6 @@ parameters: count: 1 path: src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php - - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantArrayType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantArrays\\(\\) instead\\.$#" - count: 4 - path: src/Type/Php/ArrayMergeFunctionDynamicReturnTypeExtension.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ConstantScalarType is error\\-prone and deprecated\\. Use Type\\:\\:isConstantScalarValue\\(\\) or Type\\:\\:getConstantScalarTypes\\(\\) or Type\\:\\:getConstantScalarValues\\(\\) instead\\.$#" count: 16 diff --git a/src/Type/Php/ArrayMergeFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ArrayMergeFunctionDynamicReturnTypeExtension.php index 5b177a9f68..01d2143b25 100644 --- a/src/Type/Php/ArrayMergeFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayMergeFunctionDynamicReturnTypeExtension.php @@ -5,13 +5,14 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\ShouldNotHappenException; +use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntegerType; use PHPStan\Type\NeverType; @@ -39,58 +40,53 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $argTypes = []; $optionalArgTypes = []; - $allConstant = true; foreach ($args as $arg) { $argType = $scope->getType($arg->value); if ($arg->unpack) { - if ($argType instanceof ConstantArrayType) { - $argTypesFound = $argType->getValueTypes(); - } else { - $argTypesFound = [$argType->getIterableValueType()]; - } - - foreach ($argTypesFound as $argTypeFound) { - $argTypes[] = $argTypeFound; - if ($argTypeFound instanceof ConstantArrayType) { - continue; + if ($argType->isConstantArray()->yes()) { + foreach ($argType->getConstantArrays() as $constantArray) { + foreach ($constantArray->getValueTypes() as $valueType) { + $argTypes[] = $valueType; + } } - $allConstant = false; + } else { + $argTypes[] = $argType->getIterableValueType(); } if (!$argType->isIterableAtLeastOnce()->yes()) { // unpacked params can be empty, making them optional $optionalArgTypesOffset = count($argTypes) - 1; - foreach (array_keys($argTypesFound) as $key) { + foreach (array_keys($argTypes) as $key) { $optionalArgTypes[] = $optionalArgTypesOffset + $key; } } } else { $argTypes[] = $argType; - if (!$argType instanceof ConstantArrayType) { - $allConstant = false; - } } } - if ($allConstant) { + $allConstant = TrinaryLogic::createYes()->lazyAnd( + $argTypes, + static fn (Type $argType) => $argType->isConstantArray(), + ); + + if ($allConstant->yes()) { $newArrayBuilder = ConstantArrayTypeBuilder::createEmpty(); foreach ($argTypes as $argType) { - if (!$argType instanceof ConstantArrayType) { - throw new ShouldNotHappenException(); + /** @var array $keyTypes */ + $keyTypes = []; + foreach ($argType->getConstantArrays() as $constantArray) { + foreach ($constantArray->getKeyTypes() as $keyType) { + $keyTypes[$keyType->getValue()] = $keyType; + } } - $keyTypes = $argType->getKeyTypes(); - $valueTypes = $argType->getValueTypes(); - $optionalKeys = $argType->getOptionalKeys(); - - foreach ($keyTypes as $k => $keyType) { - $isOptional = in_array($k, $optionalKeys, true); - + foreach ($keyTypes as $keyType) { $newArrayBuilder->setOffsetValueType( $keyType instanceof ConstantIntegerType ? null : $keyType, - $valueTypes[$k], - $isOptional, + $argType->getOffsetValueType($keyType), + !$argType->hasOffsetValueType($keyType)->yes(), ); } } diff --git a/tests/PHPStan/Analyser/nsrt/array-merge2.php b/tests/PHPStan/Analyser/nsrt/array-merge2.php index a52f640ef2..f0d86e61b6 100644 --- a/tests/PHPStan/Analyser/nsrt/array-merge2.php +++ b/tests/PHPStan/Analyser/nsrt/array-merge2.php @@ -21,6 +21,10 @@ public function arrayMergeArrayShapes($array1, $array2): void assertType("array{foo: '1', bar: '2', lall2: '3', 0: '4', 1: '6', lall: '3', 2: '2', 3: '3'}", array_merge($array2, $array1)); assertType("array{foo: 3, bar: '2', lall2: '3', 0: '4', 1: '6', lall: '3', 2: '2', 3: '3'}", array_merge($array2, $array1, ['foo' => 3])); assertType("array{foo: 3, bar: '2', lall2: '3', 0: '4', 1: '6', lall: '3', 2: '2', 3: '3'}", array_merge($array2, $array1, ...[['foo' => 3]])); + assertType("array{foo: '1', bar: '2'|'4', lall?: '3', 0: '2'|'4', 1: '3'|'6', lall2?: '3'}", array_merge(rand(0, 1) ? $array1 : $array2, [])); + assertType("array{foo?: 3, bar?: 3}", array_merge([], ...[rand(0, 1) ? ['foo' => 3] : ['bar' => 3]])); + assertType("array{foo: '1', bar: '2'|'4', lall?: '3', 0: '2'|'4', 1: '3'|'6', lall2?: '3'}", array_merge([], ...[rand(0, 1) ? $array1 : $array2])); + assertType("array{foo: 1, bar: 2, 0: 2, 1: 3}", array_merge(['foo' => 4, 'bar' => 5], ...[['foo' => 1, 'bar' => 2], [2, 3]])); } /** diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index a89705028b..a3ffa071b7 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -1598,6 +1598,15 @@ public function testBug9399(): void $this->analyse([__DIR__ . '/data/bug-9399.php'], []); } + public function testBug9559(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + + $this->analyse([__DIR__ . '/data/bug-9559.php'], []); + } + public function testBug9923(): void { if (PHP_VERSION_ID < 80000) { diff --git a/tests/PHPStan/Rules/Functions/data/bug-9559.php b/tests/PHPStan/Rules/Functions/data/bug-9559.php new file mode 100644 index 0000000000..8f452e90f0 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-9559.php @@ -0,0 +1,12 @@ + "3" ])); +} diff --git a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php index 13082d79b0..98e54bce0e 100644 --- a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php @@ -846,6 +846,16 @@ public function testBug8573(): void $this->analyse([__DIR__ . '/data/bug-8573.php'], []); } + public function testBug8632(): void + { + $this->analyse([__DIR__ . '/data/bug-8632.php'], []); + } + + public function testBug7857(): void + { + $this->analyse([__DIR__ . '/data/bug-7857.php'], []); + } + public function testBug8879(): void { $this->analyse([__DIR__ . '/data/bug-8879.php'], []); diff --git a/tests/PHPStan/Rules/Methods/data/bug-7857.php b/tests/PHPStan/Rules/Methods/data/bug-7857.php new file mode 100644 index 0000000000..269bbd5a87 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-7857.php @@ -0,0 +1,17 @@ + $page], + $perPage !== null ? ['perPage' => $perPage] : [] + ); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-8632.php b/tests/PHPStan/Rules/Methods/data/bug-8632.php new file mode 100644 index 0000000000..17c2aa50f6 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-8632.php @@ -0,0 +1,26 @@ + 1, + 'categories' => ['news'], + ]; + } else { + $arr = []; + } + + return array_merge($arr, []); + } +}