diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 192c3f31e8..1ec2cd321b 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1605,18 +1605,6 @@ parameters: count: 2 path: src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php - - - rawMessage: 'Doing instanceof PHPStan\Type\ConstantScalarType is error-prone and deprecated. Use Type::isConstantScalarValue() or Type::getConstantScalarTypes() or Type::getConstantScalarValues() instead.' - identifier: phpstanApi.instanceofType - count: 1 - path: src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php - - - - rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantStringType is error-prone and deprecated. Use Type::getConstantStrings() instead.' - identifier: phpstanApi.instanceofType - count: 1 - path: src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php - - rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantStringType is error-prone and deprecated. Use Type::getConstantStrings() instead.' identifier: phpstanApi.instanceofType diff --git a/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php b/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php index f9e551cc8a..3d320abd89 100644 --- a/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php +++ b/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php @@ -9,10 +9,10 @@ use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\TrinaryLogic; use PHPStan\Type\BitwiseFlagHelper; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantStringType; -use PHPStan\Type\ConstantScalarType; use PHPStan\Type\ConstantTypeHelper; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\ObjectWithoutClassType; @@ -87,11 +87,17 @@ private function narrowTypeForJsonDecode(FuncCall $funcCall, Scope $scope, Type } $firstValueType = $scope->getType($args[0]->value); - if ($firstValueType instanceof ConstantStringType) { - return $this->resolveConstantStringType($firstValueType, $isForceArray); + if ($firstValueType->getConstantStrings() !== []) { + $types = []; + + foreach ($firstValueType->getConstantStrings() as $constantString) { + $types[] = $this->resolveConstantStringType($constantString, $isForceArray); + } + + return TypeCombinator::union(...$types); } - if ($isForceArray) { + if ($isForceArray->yes()) { return TypeCombinator::remove($fallbackType, new ObjectWithoutClassType()); } @@ -101,33 +107,55 @@ private function narrowTypeForJsonDecode(FuncCall $funcCall, Scope $scope, Type /** * Is "json_decode(..., true)"? */ - private function isForceArray(FuncCall $funcCall, Scope $scope): bool + private function isForceArray(FuncCall $funcCall, Scope $scope): TrinaryLogic { $args = $funcCall->getArgs(); + $flagValue = $this->getFlagValue($funcCall, $scope); if (!isset($args[1])) { - return false; + return TrinaryLogic::createNo(); } $secondArgType = $scope->getType($args[1]->value); - $secondArgValue = $secondArgType instanceof ConstantScalarType ? $secondArgType->getValue() : null; + $secondArgValues = []; + foreach ($secondArgType->getConstantScalarValues() as $value) { + if ($value === null) { + $secondArgValues[] = $flagValue; + continue; + } + if (!is_bool($value)) { + return TrinaryLogic::createNo(); + } + $secondArgValues[] = TrinaryLogic::createFromBoolean($value); + } - if (is_bool($secondArgValue)) { - return $secondArgValue; + if ($secondArgValues === []) { + return TrinaryLogic::createNo(); } - if ($secondArgValue !== null || !isset($args[3])) { - return false; + return TrinaryLogic::extremeIdentity(...$secondArgValues); + } + + private function resolveConstantStringType(ConstantStringType $constantStringType, TrinaryLogic $isForceArray): Type + { + $types = []; + /** @var bool $asArray */ + foreach ($isForceArray->toBooleanType()->getConstantScalarValues() as $asArray) { + $decodedValue = json_decode($constantStringType->getValue(), $asArray); + $types[] = ConstantTypeHelper::getTypeFromValue($decodedValue); } - // depends on used constants, @see https://www.php.net/manual/en/json.constants.php#constant.json-object-as-array - return $this->bitwiseFlagAnalyser->bitwiseOrContainsConstant($args[3]->value, $scope, 'JSON_OBJECT_AS_ARRAY')->yes(); + return TypeCombinator::union(...$types); } - private function resolveConstantStringType(ConstantStringType $constantStringType, bool $isForceArray): Type + private function getFlagValue(FuncCall $funcCall, Scope $scope): TrinaryLogic { - $decodedValue = json_decode($constantStringType->getValue(), $isForceArray); + $args = $funcCall->getArgs(); + if (!isset($args[3])) { + return TrinaryLogic::createNo(); + } - return ConstantTypeHelper::getTypeFromValue($decodedValue); + // depends on used constants, @see https://www.php.net/manual/en/json.constants.php#constant.json-object-as-array + return $this->bitwiseFlagAnalyser->bitwiseOrContainsConstant($args[3]->value, $scope, 'JSON_OBJECT_AS_ARRAY'); } } diff --git a/tests/PHPStan/Analyser/nsrt/json-decode/json_object_as_array.php b/tests/PHPStan/Analyser/nsrt/json-decode/json_object_as_array.php index 517c0bcbf0..e84633b174 100644 --- a/tests/PHPStan/Analyser/nsrt/json-decode/json_object_as_array.php +++ b/tests/PHPStan/Analyser/nsrt/json-decode/json_object_as_array.php @@ -31,3 +31,10 @@ function ($mixed, $unknownFlags) { $value = json_decode($mixed, null, 512, $unknownFlags); assertType('mixed', $value); }; + +function(string $json, ?bool $asArray): void { + /** @var '{}'|'null' $json */ + + $value = json_decode($json, null, 512, JSON_OBJECT_AS_ARRAY); + assertType('array{}|null', $value); +}; diff --git a/tests/PHPStan/Analyser/nsrt/json-decode/narrow_type.php b/tests/PHPStan/Analyser/nsrt/json-decode/narrow_type.php index b00f971b56..c61ea7d3a6 100644 --- a/tests/PHPStan/Analyser/nsrt/json-decode/narrow_type.php +++ b/tests/PHPStan/Analyser/nsrt/json-decode/narrow_type.php @@ -32,3 +32,23 @@ function ($mixed) { $value = json_decode($mixed, false); assertType('mixed', $value); }; + +function ($mixed, $asArray) { + $value = json_decode($mixed, $asArray); + assertType('mixed', $value); +}; + +function(string $json, ?bool $asArray): void { + /** @var '{}'|'null' $json */ + $value = json_decode($json); + assertType('stdClass|null', $value); + + $value = json_decode($json, true); + assertType('array{}|null', $value); + + $value = json_decode($json, $asArray); + assertType('array{}|stdClass|null', $value); + + $value = json_decode($json, 'foo'); + assertType('stdClass|null', $value); +};