From c4e495a3ac2d1a7f2d0ff87d2b69726477234451 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Fri, 4 Feb 2022 15:09:30 +0100 Subject: [PATCH 01/21] add test case for narrow typed json_decode --- .../Analyser/NodeScopeResolverTest.php | 5 ++++ .../Analyser/data/json-decode/narrow_type.php | 23 +++++++++++++++++++ .../narrow_type_with_force_array.php | 23 +++++++++++++++++++ 3 files changed, 51 insertions(+) create mode 100644 tests/PHPStan/Analyser/data/json-decode/narrow_type.php create mode 100644 tests/PHPStan/Analyser/data/json-decode/narrow_type_with_force_array.php diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 92b3dd7059..77be86c597 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -14,8 +14,13 @@ class NodeScopeResolverTest extends TypeInferenceTestCase public function dataFileAsserts(): iterable { +<<<<<<< HEAD require_once __DIR__ . '/data/implode.php'; yield from $this->gatherAssertTypes(__DIR__ . '/data/implode.php'); +======= + yield from $this->gatherAssertTypes(__DIR__ . '/data/json-decode/narrow_type.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/json-decode/narrow_type_with_force_array.php'); +>>>>>>> add test case for narrow typed json_decode require_once __DIR__ . '/data/bug2574.php'; diff --git a/tests/PHPStan/Analyser/data/json-decode/narrow_type.php b/tests/PHPStan/Analyser/data/json-decode/narrow_type.php new file mode 100644 index 0000000000..15251b69a0 --- /dev/null +++ b/tests/PHPStan/Analyser/data/json-decode/narrow_type.php @@ -0,0 +1,23 @@ + Date: Fri, 4 Feb 2022 15:14:40 +0100 Subject: [PATCH 02/21] Extend JsonThrowOnErrorDynamicReturnTypeExtension to detect knonw type from contssant string value --- ...ThrowOnErrorDynamicReturnTypeExtension.php | 118 ++++++++++++++++-- 1 file changed, 109 insertions(+), 9 deletions(-) diff --git a/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php b/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php index 996e0ddf92..13451367f9 100644 --- a/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php +++ b/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php @@ -2,18 +2,41 @@ namespace PHPStan\Type\Php; +<<<<<<< HEAD +======= +use PhpParser\Node\Arg; +use PhpParser\Node\Expr; +use PhpParser\Node\Expr\BinaryOp\BitwiseOr; +use PhpParser\Node\Expr\ConstFetch; +>>>>>>> Extend JsonThrowOnErrorDynamicReturnTypeExtension to detect knonw type from contssant string value use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Name\FullyQualified; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ReflectionProvider; +<<<<<<< HEAD use PHPStan\Type\BitwiseFlagHelper; use PHPStan\Type\Constant\ConstantBooleanType; +======= +use PHPStan\Type\ArrayType; +use PHPStan\Type\BooleanType; +use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\ConstantTypeHelper; +>>>>>>> Extend JsonThrowOnErrorDynamicReturnTypeExtension to detect knonw type from contssant string value use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\FloatType; +use PHPStan\Type\IntegerType; +use PHPStan\Type\MixedType; +use PHPStan\Type\ObjectType; +use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use function in_array; +use PHPStan\Type\UnionType; +use stdClass; +use function json_decode; class JsonThrowOnErrorDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { @@ -35,14 +58,11 @@ public function isFunctionSupported( FunctionReflection $functionReflection, ): bool { - return $this->reflectionProvider->hasConstant(new FullyQualified('JSON_THROW_ON_ERROR'), null) && in_array( - $functionReflection->getName(), - [ - 'json_encode', - 'json_decode', - ], - true, - ); + if ($functionReflection->getName() === 'json_decode') { + return true; + } + + return $this->reflectionProvider->hasConstant(new FullyQualified('JSON_THROW_ON_ERROR'), null) && $functionReflection->getName() === 'json_encode'; } public function getTypeFromFunctionCall( @@ -51,8 +71,19 @@ public function getTypeFromFunctionCall( Scope $scope, ): Type { + // update type based on JSON_THROW_ON_ERROR $argumentPosition = $this->argumentPositions[$functionReflection->getName()]; $defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + + // narrow type for json_decode() + if ($functionReflection->getName() === 'json_decode') { + $jsonDecodeNarrowedType = $this->narrowTypeForJsonDecode($functionCall, $scope); + // improve type + if (! $jsonDecodeNarrowedType instanceof MixedType) { + $defaultReturnType = $jsonDecodeNarrowedType; + } + } + if (!isset($functionCall->getArgs()[$argumentPosition])) { return $defaultReturnType; } @@ -65,4 +96,73 @@ public function getTypeFromFunctionCall( return $defaultReturnType; } + private function narrowTypeForJsonDecode(FuncCall $funcCall, Scope $scope): Type + { + $args = $funcCall->getArgs(); + $isForceArray = $this->isForceArray($funcCall); + + $firstArgValue = $args[0]->value; + $firstValueType = $scope->getType($firstArgValue); + + if ($firstValueType instanceof ConstantStringType) { + $resolvedType = $this->resolveConstantStringType($firstValueType, $isForceArray); + } else { + $resolvedType = new MixedType(); + } + + // prefer specific type + if (! $resolvedType instanceof MixedType) { + return $resolvedType; + } + + // fallback type + if ($isForceArray) { + return new UnionType([ + new ArrayType(new MixedType(), new MixedType()), + new StringType(), + new FloatType(), + new IntegerType(), + new BooleanType(), + ]); + } + + // scalar types with stdClass + return new UnionType([ + new ObjectType(stdClass::class), + new StringType(), + new FloatType(), + new IntegerType(), + new BooleanType(), + ]); + } + + /** + * Is "json_decode(..., true)"? + * @param Arg[] $args + */ + private function isForceArray(FuncCall $funcCall): bool + { + $args = $funcCall->getArgs(); + + if (!isset($args[1])) { + return false; + } + + $secondArgValue = $args[1]->value; + if ($secondArgValue instanceof ConstFetch) { + if ($secondArgValue->name->toLowerString() === 'true') { + return true; + } + } + + return false; + } + + private function resolveConstantStringType(ConstantStringType $constantStringType, bool $isForceArray): Type + { + $decodedValue = json_decode($constantStringType->getValue(), $isForceArray); + + return ConstantTypeHelper::getTypeFromValue($decodedValue); + } + } From 6d4e3b3111461f935686a794026d0520186861d7 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Fri, 4 Feb 2022 15:26:14 +0100 Subject: [PATCH 03/21] update type in LegacyNodeScopeResolverTest --- tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php index 19aef9b64a..6d61af627b 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -8699,15 +8699,23 @@ public function dataPhp73Functions(): array 'json_encode($mixed, $integer | JSON_THROW_ON_ERROR | JSON_NUMERIC_CHECK)', ], [ - 'mixed', + 'bool|float|int|stdClass|string', 'json_decode($mixed)', ], [ +<<<<<<< HEAD 'mixed', 'json_decode($mixed, false, 512, JSON_THROW_ON_ERROR | JSON_NUMERIC_CHECK)', ], [ 'mixed', +======= + 'float|int|stdClass|string|true', + 'json_decode($mixed, false, 512, JSON_THROW_ON_ERROR | JSON_NUMERIC_CHECK)', + ], + [ + 'float|int|stdClass|string|true', +>>>>>>> update type in LegacyNodeScopeResolverTest 'json_decode($mixed, false, 512, $integer | JSON_THROW_ON_ERROR | JSON_NUMERIC_CHECK)', ], [ From 581f51ad046089ec1973b4a52e9da0a6dd479502 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Fri, 4 Feb 2022 17:17:57 +0100 Subject: [PATCH 04/21] return mixed type --- ...ThrowOnErrorDynamicReturnTypeExtension.php | 50 ++++--------------- .../Analyser/LegacyNodeScopeResolverTest.php | 10 +++- .../Analyser/data/json-decode/narrow_type.php | 6 +++ .../narrow_type_with_force_array.php | 5 ++ 4 files changed, 31 insertions(+), 40 deletions(-) diff --git a/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php b/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php index 13451367f9..3663863374 100644 --- a/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php +++ b/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php @@ -16,25 +16,24 @@ use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ReflectionProvider; <<<<<<< HEAD +<<<<<<< HEAD use PHPStan\Type\BitwiseFlagHelper; use PHPStan\Type\Constant\ConstantBooleanType; ======= use PHPStan\Type\ArrayType; use PHPStan\Type\BooleanType; +======= +>>>>>>> return mixed type use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ConstantTypeHelper; >>>>>>> Extend JsonThrowOnErrorDynamicReturnTypeExtension to detect knonw type from contssant string value use PHPStan\Type\DynamicFunctionReturnTypeExtension; -use PHPStan\Type\FloatType; -use PHPStan\Type\IntegerType; use PHPStan\Type\MixedType; use PHPStan\Type\ObjectType; -use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\UnionType; use stdClass; use function json_decode; @@ -77,11 +76,7 @@ public function getTypeFromFunctionCall( // narrow type for json_decode() if ($functionReflection->getName() === 'json_decode') { - $jsonDecodeNarrowedType = $this->narrowTypeForJsonDecode($functionCall, $scope); - // improve type - if (! $jsonDecodeNarrowedType instanceof MixedType) { - $defaultReturnType = $jsonDecodeNarrowedType; - } + $defaultReturnType = $this->narrowTypeForJsonDecode($functionCall, $scope); } if (!isset($functionCall->getArgs()[$argumentPosition])) { @@ -105,57 +100,34 @@ private function narrowTypeForJsonDecode(FuncCall $funcCall, Scope $scope): Type $firstValueType = $scope->getType($firstArgValue); if ($firstValueType instanceof ConstantStringType) { - $resolvedType = $this->resolveConstantStringType($firstValueType, $isForceArray); - } else { - $resolvedType = new MixedType(); - } - - // prefer specific type - if (! $resolvedType instanceof MixedType) { - return $resolvedType; + return $this->resolveConstantStringType($firstValueType, $isForceArray); } // fallback type if ($isForceArray) { - return new UnionType([ - new ArrayType(new MixedType(), new MixedType()), - new StringType(), - new FloatType(), - new IntegerType(), - new BooleanType(), - ]); + return new MixedType(true, new ObjectType(stdClass::class)); } - // scalar types with stdClass - return new UnionType([ - new ObjectType(stdClass::class), - new StringType(), - new FloatType(), - new IntegerType(), - new BooleanType(), - ]); + return new MixedType(true); } /** * Is "json_decode(..., true)"? - * @param Arg[] $args */ private function isForceArray(FuncCall $funcCall): bool { $args = $funcCall->getArgs(); - if (!isset($args[1])) { + if (! isset($args[1])) { return false; } $secondArgValue = $args[1]->value; - if ($secondArgValue instanceof ConstFetch) { - if ($secondArgValue->name->toLowerString() === 'true') { - return true; - } + if (! $secondArgValue instanceof ConstFetch) { + return false; } - return false; + return $secondArgValue->name->toLowerString() === 'true'; } private function resolveConstantStringType(ConstantStringType $constantStringType, bool $isForceArray): Type diff --git a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php index 6d61af627b..fcbcf1b76c 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -8699,10 +8699,11 @@ public function dataPhp73Functions(): array 'json_encode($mixed, $integer | JSON_THROW_ON_ERROR | JSON_NUMERIC_CHECK)', ], [ - 'bool|float|int|stdClass|string', + 'mixed', 'json_decode($mixed)', ], [ +<<<<<<< HEAD <<<<<<< HEAD 'mixed', 'json_decode($mixed, false, 512, JSON_THROW_ON_ERROR | JSON_NUMERIC_CHECK)', @@ -8716,6 +8717,13 @@ public function dataPhp73Functions(): array [ 'float|int|stdClass|string|true', >>>>>>> update type in LegacyNodeScopeResolverTest +======= + 'mixed~false', + 'json_decode($mixed, false, 512, JSON_THROW_ON_ERROR | JSON_NUMERIC_CHECK)', + ], + [ + 'mixed~false', +>>>>>>> return mixed type 'json_decode($mixed, false, 512, $integer | JSON_THROW_ON_ERROR | JSON_NUMERIC_CHECK)', ], [ diff --git a/tests/PHPStan/Analyser/data/json-decode/narrow_type.php b/tests/PHPStan/Analyser/data/json-decode/narrow_type.php index 15251b69a0..e7c72b4e31 100644 --- a/tests/PHPStan/Analyser/data/json-decode/narrow_type.php +++ b/tests/PHPStan/Analyser/data/json-decode/narrow_type.php @@ -21,3 +21,9 @@ $value = json_decode('[1, 2, 3]'); assertType('array{1, 2, 3}', $value); + + +function ($mixed) { + $value = json_decode($mixed); + assertType('mixed', $value); +}; diff --git a/tests/PHPStan/Analyser/data/json-decode/narrow_type_with_force_array.php b/tests/PHPStan/Analyser/data/json-decode/narrow_type_with_force_array.php index 9aefff0540..479085c814 100644 --- a/tests/PHPStan/Analyser/data/json-decode/narrow_type_with_force_array.php +++ b/tests/PHPStan/Analyser/data/json-decode/narrow_type_with_force_array.php @@ -21,3 +21,8 @@ $value = json_decode('[1, 2, 3]', true); assertType('array{1, 2, 3}', $value); + +function ($mixed) { + $value = json_decode($mixed, true); + assertType('mixed~stdClass', $value); +}; From d665722a52e223864959fb3dbe9e76d772a420dd Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sun, 6 Feb 2022 12:42:55 +0100 Subject: [PATCH 05/21] add json_decode false --- tests/PHPStan/Analyser/data/json-decode/narrow_type.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/PHPStan/Analyser/data/json-decode/narrow_type.php b/tests/PHPStan/Analyser/data/json-decode/narrow_type.php index e7c72b4e31..b00f971b56 100644 --- a/tests/PHPStan/Analyser/data/json-decode/narrow_type.php +++ b/tests/PHPStan/Analyser/data/json-decode/narrow_type.php @@ -27,3 +27,8 @@ function ($mixed) { $value = json_decode($mixed); assertType('mixed', $value); }; + +function ($mixed) { + $value = json_decode($mixed, false); + assertType('mixed', $value); +}; From b2b979b7f9254fdbe016abc83d04896f7c16fb3c Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sun, 6 Feb 2022 13:25:46 +0100 Subject: [PATCH 06/21] check for JSON_OBJECT_AS_ARRAY, in case of null and array --- ...ThrowOnErrorDynamicReturnTypeExtension.php | 55 +++++++++++++++---- .../narrow_type_with_force_array.php | 6 ++ 2 files changed, 49 insertions(+), 12 deletions(-) diff --git a/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php b/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php index 3663863374..6570c8494a 100644 --- a/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php +++ b/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php @@ -2,9 +2,13 @@ namespace PHPStan\Type\Php; +<<<<<<< HEAD <<<<<<< HEAD ======= use PhpParser\Node\Arg; +======= +use PhpParser\ConstExprEvaluator; +>>>>>>> check for JSON_OBJECT_AS_ARRAY, in case of null and array use PhpParser\Node\Expr; use PhpParser\Node\Expr\BinaryOp\BitwiseOr; use PhpParser\Node\Expr\ConstFetch; @@ -35,7 +39,9 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use stdClass; +use function constant; use function json_decode; +use const JSON_OBJECT_AS_ARRAY; class JsonThrowOnErrorDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { @@ -94,17 +100,17 @@ public function getTypeFromFunctionCall( private function narrowTypeForJsonDecode(FuncCall $funcCall, Scope $scope): Type { $args = $funcCall->getArgs(); - $isForceArray = $this->isForceArray($funcCall); + $isArrayWithoutStdClass = $this->isForceArrayWithoutStdClass($funcCall); $firstArgValue = $args[0]->value; $firstValueType = $scope->getType($firstArgValue); if ($firstValueType instanceof ConstantStringType) { - return $this->resolveConstantStringType($firstValueType, $isForceArray); + return $this->resolveConstantStringType($firstValueType, $isArrayWithoutStdClass); } // fallback type - if ($isForceArray) { + if ($isArrayWithoutStdClass) { return new MixedType(true, new ObjectType(stdClass::class)); } @@ -114,20 +120,45 @@ private function narrowTypeForJsonDecode(FuncCall $funcCall, Scope $scope): Type /** * Is "json_decode(..., true)"? */ - private function isForceArray(FuncCall $funcCall): bool + private function isForceArrayWithoutStdClass(FuncCall $funcCall): bool { $args = $funcCall->getArgs(); - if (! isset($args[1])) { - return false; - } - - $secondArgValue = $args[1]->value; - if (! $secondArgValue instanceof ConstFetch) { - return false; + $constExprEvaluator = new ConstExprEvaluator(static function (Expr $expr) { + if ($expr instanceof ConstFetch) { + return constant($expr->name->toString()); + } + + return null; + }); + + if (isset($args[1])) { + $secondArgValue = $args[1]->value; + + $constValue = $constExprEvaluator->evaluateSilently($secondArgValue); + if ($constValue === true) { + return true; + } + + if ($constValue === false) { + return false; + } + + // depends on used constants + if ($constValue === null) { + if (! isset($args[3])) { + return false; + } + + // @see https://www.php.net/manual/en/json.constants.php#constant.json-object-as-array + $thirdArgValue = $constExprEvaluator->evaluateSilently($args[3]->value); + if ($thirdArgValue & JSON_OBJECT_AS_ARRAY) { + return true; + } + } } - return $secondArgValue->name->toLowerString() === 'true'; + return false; } private function resolveConstantStringType(ConstantStringType $constantStringType, bool $isForceArray): Type diff --git a/tests/PHPStan/Analyser/data/json-decode/narrow_type_with_force_array.php b/tests/PHPStan/Analyser/data/json-decode/narrow_type_with_force_array.php index 479085c814..6475c8e5a9 100644 --- a/tests/PHPStan/Analyser/data/json-decode/narrow_type_with_force_array.php +++ b/tests/PHPStan/Analyser/data/json-decode/narrow_type_with_force_array.php @@ -26,3 +26,9 @@ function ($mixed) { $value = json_decode($mixed, true); assertType('mixed~stdClass', $value); }; + +// @see https://3v4l.org/YFlHF +function ($mixed) { + $value = json_decode($mixed, null, 512, JSON_OBJECT_AS_ARRAY); + assertType('mixed~stdClass', $value); +}; From 665e9da06c5190743476f6064cbc59c227ce7dad Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sun, 6 Feb 2022 13:28:12 +0100 Subject: [PATCH 07/21] add test case for invalid json string --- .../PHPStan/Analyser/NodeScopeResolverTest.php | 6 +++--- .../Analyser/data/json-decode/invalid_type.php | 17 +++++++++++++++++ .../data/json-decode/json_object_as_array.php | 16 ++++++++++++++++ .../narrow_type_with_force_array.php | 6 ------ 4 files changed, 36 insertions(+), 9 deletions(-) create mode 100644 tests/PHPStan/Analyser/data/json-decode/invalid_type.php create mode 100644 tests/PHPStan/Analyser/data/json-decode/json_object_as_array.php diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 77be86c597..008456768f 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -14,13 +14,13 @@ class NodeScopeResolverTest extends TypeInferenceTestCase public function dataFileAsserts(): iterable { -<<<<<<< HEAD require_once __DIR__ . '/data/implode.php'; yield from $this->gatherAssertTypes(__DIR__ . '/data/implode.php'); -======= + yield from $this->gatherAssertTypes(__DIR__ . '/data/json-decode/narrow_type.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/json-decode/narrow_type_with_force_array.php'); ->>>>>>> add test case for narrow typed json_decode + yield from $this->gatherAssertTypes(__DIR__ . '/data/json-decode/invalid_type.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/json-decode/json_object_as_array.php'); require_once __DIR__ . '/data/bug2574.php'; diff --git a/tests/PHPStan/Analyser/data/json-decode/invalid_type.php b/tests/PHPStan/Analyser/data/json-decode/invalid_type.php new file mode 100644 index 0000000000..4919a83bf9 --- /dev/null +++ b/tests/PHPStan/Analyser/data/json-decode/invalid_type.php @@ -0,0 +1,17 @@ + Date: Sun, 6 Feb 2022 13:39:57 +0100 Subject: [PATCH 08/21] add test for multiple flags --- .../Analyser/data/json-decode/json_object_as_array.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/PHPStan/Analyser/data/json-decode/json_object_as_array.php b/tests/PHPStan/Analyser/data/json-decode/json_object_as_array.php index 895ff5e5ce..1a1311bb54 100644 --- a/tests/PHPStan/Analyser/data/json-decode/json_object_as_array.php +++ b/tests/PHPStan/Analyser/data/json-decode/json_object_as_array.php @@ -10,7 +10,17 @@ function ($mixed) { assertType('mixed~stdClass', $value); }; +function ($mixed) { + $value = json_decode($mixed, null, 512, JSON_OBJECT_AS_ARRAY | JSON_BIGINT_AS_STRING); + assertType('mixed~stdClass', $value); +}; + function ($mixed) { $value = json_decode($mixed, null); assertType('mixed', $value); }; + +function ($mixed, $unknownFlags) { + $value = json_decode($mixed, null, 512, $unknownFlags); + assertType('mixed', $value); +}; From 1a124ea6531b80df6579cc6baaf2f351712c9bfc Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sun, 6 Feb 2022 16:35:48 +0100 Subject: [PATCH 09/21] decopule type resolution to static --- ...ThrowOnErrorDynamicReturnTypeExtension.php | 72 +++++++++++++------ .../data/json-decode/json_object_as_array.php | 7 ++ 2 files changed, 58 insertions(+), 21 deletions(-) diff --git a/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php b/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php index 6570c8494a..2b4b958203 100644 --- a/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php +++ b/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php @@ -2,11 +2,15 @@ namespace PHPStan\Type\Php; +<<<<<<< HEAD <<<<<<< HEAD <<<<<<< HEAD ======= use PhpParser\Node\Arg; ======= +======= +use PhpParser\ConstExprEvaluationException; +>>>>>>> decopule type resolution to static use PhpParser\ConstExprEvaluator; >>>>>>> check for JSON_OBJECT_AS_ARRAY, in case of null and array use PhpParser\Node\Expr; @@ -40,23 +44,39 @@ use PHPStan\Type\TypeCombinator; use stdClass; use function constant; +use function is_bool; use function json_decode; use const JSON_OBJECT_AS_ARRAY; class JsonThrowOnErrorDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + private const UNABLE_TO_RESOLVE = '__UNABLE_TO_RESOLVE__'; + /** @var array */ private array $argumentPositions = [ 'json_encode' => 1, 'json_decode' => 3, ]; +<<<<<<< HEAD public function __construct( private ReflectionProvider $reflectionProvider, private BitwiseFlagHelper $bitwiseFlagAnalyser, ) +======= + private ConstExprEvaluator $constExprEvaluator; + + public function __construct(private ReflectionProvider $reflectionProvider) +>>>>>>> decopule type resolution to static { + $this->constExprEvaluator = new ConstExprEvaluator(static function (Expr $expr) { + if ($expr instanceof ConstFetch) { + return constant($expr->name->toString()); + } + + return null; + }); } public function isFunctionSupported( @@ -100,7 +120,7 @@ public function getTypeFromFunctionCall( private function narrowTypeForJsonDecode(FuncCall $funcCall, Scope $scope): Type { $args = $funcCall->getArgs(); - $isArrayWithoutStdClass = $this->isForceArrayWithoutStdClass($funcCall); + $isArrayWithoutStdClass = $this->isForceArrayWithoutStdClass($funcCall, $scope); $firstArgValue = $args[0]->value; $firstValueType = $scope->getType($firstArgValue); @@ -110,7 +130,7 @@ private function narrowTypeForJsonDecode(FuncCall $funcCall, Scope $scope): Type } // fallback type - if ($isArrayWithoutStdClass) { + if ($isArrayWithoutStdClass === true) { return new MixedType(true, new ObjectType(stdClass::class)); } @@ -120,39 +140,30 @@ private function narrowTypeForJsonDecode(FuncCall $funcCall, Scope $scope): Type /** * Is "json_decode(..., true)"? */ - private function isForceArrayWithoutStdClass(FuncCall $funcCall): bool + private function isForceArrayWithoutStdClass(FuncCall $funcCall, Scope $scope): bool { $args = $funcCall->getArgs(); - $constExprEvaluator = new ConstExprEvaluator(static function (Expr $expr) { - if ($expr instanceof ConstFetch) { - return constant($expr->name->toString()); - } - - return null; - }); - if (isset($args[1])) { - $secondArgValue = $args[1]->value; - - $constValue = $constExprEvaluator->evaluateSilently($secondArgValue); - if ($constValue === true) { - return true; + $secondArgValue = $this->resolveMaskValue($args[1]->value, $scope); + if ($secondArgValue === self::UNABLE_TO_RESOLVE) { + return false; } - if ($constValue === false) { - return false; + if (is_bool($secondArgValue)) { + return $secondArgValue; } // depends on used constants - if ($constValue === null) { + if ($secondArgValue === null) { if (! isset($args[3])) { return false; } // @see https://www.php.net/manual/en/json.constants.php#constant.json-object-as-array - $thirdArgValue = $constExprEvaluator->evaluateSilently($args[3]->value); - if ($thirdArgValue & JSON_OBJECT_AS_ARRAY) { + $thirdArgValue = $args[3]->value; + $resolvedThirdArgValue = $this->resolveMaskValue($thirdArgValue, $scope); + if (($resolvedThirdArgValue & JSON_OBJECT_AS_ARRAY) !== 0) { return true; } } @@ -168,4 +179,23 @@ private function resolveConstantStringType(ConstantStringType $constantStringTyp return ConstantTypeHelper::getTypeFromValue($decodedValue); } + /** + * @return mixed + */ + private function resolveMaskValue(Expr $expr, Scope $scope) + { + $thirdArgValueType = $scope->getType($expr); + if ($thirdArgValueType instanceof ConstantIntegerType) { + return $thirdArgValueType->getValue(); + } + + // fallback to value resolver + try { + return $this->constExprEvaluator->evaluateSilently($expr); + } catch (ConstExprEvaluationException) { + // unable to resolve + return self::UNABLE_TO_RESOLVE; + } + } + } diff --git a/tests/PHPStan/Analyser/data/json-decode/json_object_as_array.php b/tests/PHPStan/Analyser/data/json-decode/json_object_as_array.php index 1a1311bb54..cf8b093fae 100644 --- a/tests/PHPStan/Analyser/data/json-decode/json_object_as_array.php +++ b/tests/PHPStan/Analyser/data/json-decode/json_object_as_array.php @@ -10,6 +10,13 @@ function ($mixed) { assertType('mixed~stdClass', $value); }; +function ($mixed) { + $flagsAsVariable = JSON_OBJECT_AS_ARRAY; + + $value = json_decode($mixed, null, 512, $flagsAsVariable); + assertType('mixed~stdClass', $value); +}; + function ($mixed) { $value = json_decode($mixed, null, 512, JSON_OBJECT_AS_ARRAY | JSON_BIGINT_AS_STRING); assertType('mixed~stdClass', $value); From bea4f29ce9d731c7161b5ee8d05dd4e1ae447a38 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sun, 6 Feb 2022 22:31:53 +0100 Subject: [PATCH 10/21] check if JSON_THROW_ON_ERROR exists before infer --- ...ThrowOnErrorDynamicReturnTypeExtension.php | 28 ++----------------- 1 file changed, 2 insertions(+), 26 deletions(-) diff --git a/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php b/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php index 2b4b958203..fc57f88149 100644 --- a/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php +++ b/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php @@ -2,41 +2,21 @@ namespace PHPStan\Type\Php; -<<<<<<< HEAD -<<<<<<< HEAD -<<<<<<< HEAD -======= -use PhpParser\Node\Arg; -======= -======= use PhpParser\ConstExprEvaluationException; ->>>>>>> decopule type resolution to static use PhpParser\ConstExprEvaluator; ->>>>>>> check for JSON_OBJECT_AS_ARRAY, in case of null and array use PhpParser\Node\Expr; -use PhpParser\Node\Expr\BinaryOp\BitwiseOr; use PhpParser\Node\Expr\ConstFetch; ->>>>>>> Extend JsonThrowOnErrorDynamicReturnTypeExtension to detect knonw type from contssant string value use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Name\FullyQualified; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ReflectionProvider; -<<<<<<< HEAD -<<<<<<< HEAD use PHPStan\Type\BitwiseFlagHelper; use PHPStan\Type\Constant\ConstantBooleanType; -======= -use PHPStan\Type\ArrayType; -use PHPStan\Type\BooleanType; -======= ->>>>>>> return mixed type -use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ConstantTypeHelper; ->>>>>>> Extend JsonThrowOnErrorDynamicReturnTypeExtension to detect knonw type from contssant string value use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\MixedType; use PHPStan\Type\ObjectType; @@ -53,22 +33,18 @@ class JsonThrowOnErrorDynamicReturnTypeExtension implements DynamicFunctionRetur private const UNABLE_TO_RESOLVE = '__UNABLE_TO_RESOLVE__'; + private ConstExprEvaluator $constExprEvaluator; + /** @var array */ private array $argumentPositions = [ 'json_encode' => 1, 'json_decode' => 3, ]; -<<<<<<< HEAD public function __construct( private ReflectionProvider $reflectionProvider, private BitwiseFlagHelper $bitwiseFlagAnalyser, ) -======= - private ConstExprEvaluator $constExprEvaluator; - - public function __construct(private ReflectionProvider $reflectionProvider) ->>>>>>> decopule type resolution to static { $this->constExprEvaluator = new ConstExprEvaluator(static function (Expr $expr) { if ($expr instanceof ConstFetch) { From 8748e9c830fa58401cc88b6d3b4395cd3f23c2b2 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Thu, 24 Mar 2022 18:38:56 +0100 Subject: [PATCH 11/21] [ci] add json to composer require checker json --- build/composer-require-checker.json | 1 + 1 file changed, 1 insertion(+) diff --git a/build/composer-require-checker.json b/build/composer-require-checker.json index 05a759a45c..81bbb32500 100644 --- a/build/composer-require-checker.json +++ b/build/composer-require-checker.json @@ -12,6 +12,7 @@ "Clue\\React\\Block\\await", "Hoa\\File\\Read" ], "php-core-extensions" : [ + "json", "Core", "date", "pcre", From 43eb54255ac1a5c7ab4e4e1eb9a7b6fb85b00cae Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Mon, 28 Mar 2022 10:51:05 +0200 Subject: [PATCH 12/21] use bitwiseFlagAnalyser --- src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php b/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php index fc57f88149..8803f594fb 100644 --- a/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php +++ b/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php @@ -26,7 +26,6 @@ use function constant; use function is_bool; use function json_decode; -use const JSON_OBJECT_AS_ARRAY; class JsonThrowOnErrorDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { @@ -138,8 +137,7 @@ private function isForceArrayWithoutStdClass(FuncCall $funcCall, Scope $scope): // @see https://www.php.net/manual/en/json.constants.php#constant.json-object-as-array $thirdArgValue = $args[3]->value; - $resolvedThirdArgValue = $this->resolveMaskValue($thirdArgValue, $scope); - if (($resolvedThirdArgValue & JSON_OBJECT_AS_ARRAY) !== 0) { + if ($this->bitwiseFlagAnalyser->bitwiseOrContainsConstant($thirdArgValue, $scope, 'JSON_OBJECT_AS_ARRAY')->yes()) { return true; } } From 99debb9d9c6c967c52fab18462db9c85e5f617e0 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Mon, 25 Apr 2022 11:27:11 +0200 Subject: [PATCH 13/21] fixup! use bitwiseFlagAnalyser --- tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php index fcbcf1b76c..14dc11daea 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -8703,27 +8703,19 @@ public function dataPhp73Functions(): array 'json_decode($mixed)', ], [ -<<<<<<< HEAD -<<<<<<< HEAD 'mixed', 'json_decode($mixed, false, 512, JSON_THROW_ON_ERROR | JSON_NUMERIC_CHECK)', ], [ - 'mixed', -======= 'float|int|stdClass|string|true', 'json_decode($mixed, false, 512, JSON_THROW_ON_ERROR | JSON_NUMERIC_CHECK)', ], [ 'float|int|stdClass|string|true', ->>>>>>> update type in LegacyNodeScopeResolverTest -======= - 'mixed~false', 'json_decode($mixed, false, 512, JSON_THROW_ON_ERROR | JSON_NUMERIC_CHECK)', ], [ 'mixed~false', ->>>>>>> return mixed type 'json_decode($mixed, false, 512, $integer | JSON_THROW_ON_ERROR | JSON_NUMERIC_CHECK)', ], [ From d70703846028af5b93db3dda67aa3e7e80d063fa Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Mon, 25 Apr 2022 11:29:00 +0200 Subject: [PATCH 14/21] Update src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php Co-authored-by: Martin Herndl --- src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php b/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php index 8803f594fb..acb0c160dc 100644 --- a/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php +++ b/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php @@ -75,7 +75,6 @@ public function getTypeFromFunctionCall( $argumentPosition = $this->argumentPositions[$functionReflection->getName()]; $defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); - // narrow type for json_decode() if ($functionReflection->getName() === 'json_decode') { $defaultReturnType = $this->narrowTypeForJsonDecode($functionCall, $scope); } From 02554253117995de59ad1aadfee87fa78ef57a40 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Mon, 25 Apr 2022 11:29:06 +0200 Subject: [PATCH 15/21] Update src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php Co-authored-by: Martin Herndl --- src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php b/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php index acb0c160dc..b9001751dc 100644 --- a/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php +++ b/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php @@ -71,7 +71,6 @@ public function getTypeFromFunctionCall( Scope $scope, ): Type { - // update type based on JSON_THROW_ON_ERROR $argumentPosition = $this->argumentPositions[$functionReflection->getName()]; $defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); From d586c6e590408d507ac9346195ee4ac691f0beb4 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Mon, 25 Apr 2022 11:29:46 +0200 Subject: [PATCH 16/21] fixup! fixup! use bitwiseFlagAnalyser --- tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php index 14dc11daea..bfce75b17f 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -8710,10 +8710,6 @@ public function dataPhp73Functions(): array 'float|int|stdClass|string|true', 'json_decode($mixed, false, 512, JSON_THROW_ON_ERROR | JSON_NUMERIC_CHECK)', ], - [ - 'float|int|stdClass|string|true', - 'json_decode($mixed, false, 512, JSON_THROW_ON_ERROR | JSON_NUMERIC_CHECK)', - ], [ 'mixed~false', 'json_decode($mixed, false, 512, $integer | JSON_THROW_ON_ERROR | JSON_NUMERIC_CHECK)', From 98abc6f41503b0d98d230de8c04c0c9da9fd1ae6 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Mon, 25 Apr 2022 11:44:48 +0200 Subject: [PATCH 17/21] fixup! fixup! fixup! use bitwiseFlagAnalyser --- tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php index bfce75b17f..cc0b8decb5 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -8704,14 +8704,6 @@ public function dataPhp73Functions(): array ], [ 'mixed', - 'json_decode($mixed, false, 512, JSON_THROW_ON_ERROR | JSON_NUMERIC_CHECK)', - ], - [ - 'float|int|stdClass|string|true', - 'json_decode($mixed, false, 512, JSON_THROW_ON_ERROR | JSON_NUMERIC_CHECK)', - ], - [ - 'mixed~false', 'json_decode($mixed, false, 512, $integer | JSON_THROW_ON_ERROR | JSON_NUMERIC_CHECK)', ], [ From 8a841feef9f92b25ac03044fc744bf9aa2689344 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Mon, 25 Apr 2022 11:46:48 +0200 Subject: [PATCH 18/21] fixup! fixup! fixup! fixup! use bitwiseFlagAnalyser --- tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php index cc0b8decb5..19aef9b64a 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -8702,6 +8702,10 @@ public function dataPhp73Functions(): array 'mixed', 'json_decode($mixed)', ], + [ + 'mixed', + 'json_decode($mixed, false, 512, JSON_THROW_ON_ERROR | JSON_NUMERIC_CHECK)', + ], [ 'mixed', 'json_decode($mixed, false, 512, $integer | JSON_THROW_ON_ERROR | JSON_NUMERIC_CHECK)', From 3ad68aaf102f4e3c2c6adea8eccb24704407353e Mon Sep 17 00:00:00 2001 From: Martin Herndl Date: Thu, 21 Apr 2022 16:47:07 +0200 Subject: [PATCH 19/21] Simplify isForceArrayWithoutStdClass --- ...ThrowOnErrorDynamicReturnTypeExtension.php | 74 ++++--------------- 1 file changed, 15 insertions(+), 59 deletions(-) diff --git a/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php b/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php index b9001751dc..c2f50d664e 100644 --- a/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php +++ b/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php @@ -2,10 +2,6 @@ namespace PHPStan\Type\Php; -use PhpParser\ConstExprEvaluationException; -use PhpParser\ConstExprEvaluator; -use PhpParser\Node\Expr; -use PhpParser\Node\Expr\ConstFetch; use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Name\FullyQualified; use PHPStan\Analyser\Scope; @@ -14,8 +10,8 @@ use PHPStan\Reflection\ReflectionProvider; use PHPStan\Type\BitwiseFlagHelper; use PHPStan\Type\Constant\ConstantBooleanType; -use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\ConstantScalarType; use PHPStan\Type\ConstantTypeHelper; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\MixedType; @@ -23,17 +19,12 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use stdClass; -use function constant; use function is_bool; use function json_decode; class JsonThrowOnErrorDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - private const UNABLE_TO_RESOLVE = '__UNABLE_TO_RESOLVE__'; - - private ConstExprEvaluator $constExprEvaluator; - /** @var array */ private array $argumentPositions = [ 'json_encode' => 1, @@ -45,13 +36,6 @@ public function __construct( private BitwiseFlagHelper $bitwiseFlagAnalyser, ) { - $this->constExprEvaluator = new ConstExprEvaluator(static function (Expr $expr) { - if ($expr instanceof ConstFetch) { - return constant($expr->name->toString()); - } - - return null; - }); } public function isFunctionSupported( @@ -116,32 +100,23 @@ private function narrowTypeForJsonDecode(FuncCall $funcCall, Scope $scope): Type private function isForceArrayWithoutStdClass(FuncCall $funcCall, Scope $scope): bool { $args = $funcCall->getArgs(); + if (!isset($args[1])) { + return false; + } - if (isset($args[1])) { - $secondArgValue = $this->resolveMaskValue($args[1]->value, $scope); - if ($secondArgValue === self::UNABLE_TO_RESOLVE) { - return false; - } - - if (is_bool($secondArgValue)) { - return $secondArgValue; - } - - // depends on used constants - if ($secondArgValue === null) { - if (! isset($args[3])) { - return false; - } - - // @see https://www.php.net/manual/en/json.constants.php#constant.json-object-as-array - $thirdArgValue = $args[3]->value; - if ($this->bitwiseFlagAnalyser->bitwiseOrContainsConstant($thirdArgValue, $scope, 'JSON_OBJECT_AS_ARRAY')->yes()) { - return true; - } - } + $secondArgType = $scope->getType($args[1]->value); + $secondArgValue = $secondArgType instanceof ConstantScalarType ? $secondArgType->getValue() : null; + + if (is_bool($secondArgValue)) { + return $secondArgValue; + } + + if ($secondArgValue !== null || !isset($args[3])) { + return false; } - return false; + // 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(); } private function resolveConstantStringType(ConstantStringType $constantStringType, bool $isForceArray): Type @@ -151,23 +126,4 @@ private function resolveConstantStringType(ConstantStringType $constantStringTyp return ConstantTypeHelper::getTypeFromValue($decodedValue); } - /** - * @return mixed - */ - private function resolveMaskValue(Expr $expr, Scope $scope) - { - $thirdArgValueType = $scope->getType($expr); - if ($thirdArgValueType instanceof ConstantIntegerType) { - return $thirdArgValueType->getValue(); - } - - // fallback to value resolver - try { - return $this->constExprEvaluator->evaluateSilently($expr); - } catch (ConstExprEvaluationException) { - // unable to resolve - return self::UNABLE_TO_RESOLVE; - } - } - } From 741abd2f77e178925a7a7a6c0a35cbccfb0e922e Mon Sep 17 00:00:00 2001 From: Martin Herndl Date: Thu, 21 Apr 2022 16:57:30 +0200 Subject: [PATCH 20/21] Simplify fallback type handling --- .../JsonThrowOnErrorDynamicReturnTypeExtension.php | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php b/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php index c2f50d664e..726ca63ae8 100644 --- a/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php +++ b/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php @@ -14,7 +14,6 @@ use PHPStan\Type\ConstantScalarType; use PHPStan\Type\ConstantTypeHelper; use PHPStan\Type\DynamicFunctionReturnTypeExtension; -use PHPStan\Type\MixedType; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; @@ -59,7 +58,7 @@ public function getTypeFromFunctionCall( $defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); if ($functionReflection->getName() === 'json_decode') { - $defaultReturnType = $this->narrowTypeForJsonDecode($functionCall, $scope); + $defaultReturnType = $this->narrowTypeForJsonDecode($functionCall, $scope, $defaultReturnType); } if (!isset($functionCall->getArgs()[$argumentPosition])) { @@ -74,7 +73,7 @@ public function getTypeFromFunctionCall( return $defaultReturnType; } - private function narrowTypeForJsonDecode(FuncCall $funcCall, Scope $scope): Type + private function narrowTypeForJsonDecode(FuncCall $funcCall, Scope $scope, Type $fallbackType): Type { $args = $funcCall->getArgs(); $isArrayWithoutStdClass = $this->isForceArrayWithoutStdClass($funcCall, $scope); @@ -86,12 +85,11 @@ private function narrowTypeForJsonDecode(FuncCall $funcCall, Scope $scope): Type return $this->resolveConstantStringType($firstValueType, $isArrayWithoutStdClass); } - // fallback type - if ($isArrayWithoutStdClass === true) { - return new MixedType(true, new ObjectType(stdClass::class)); + if ($isArrayWithoutStdClass) { + return TypeCombinator::remove($fallbackType, new ObjectType(stdClass::class)); } - return new MixedType(true); + return $fallbackType; } /** From 7d306fbe47563b61ec01d7323f4a114609e81b3b Mon Sep 17 00:00:00 2001 From: Martin Herndl Date: Mon, 25 Apr 2022 15:11:37 +0200 Subject: [PATCH 21/21] Remove unneeded var in narrowTypeForJsonDecode --- src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php b/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php index 726ca63ae8..f4ca8d4058 100644 --- a/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php +++ b/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php @@ -78,9 +78,7 @@ private function narrowTypeForJsonDecode(FuncCall $funcCall, Scope $scope, Type $args = $funcCall->getArgs(); $isArrayWithoutStdClass = $this->isForceArrayWithoutStdClass($funcCall, $scope); - $firstArgValue = $args[0]->value; - $firstValueType = $scope->getType($firstArgValue); - + $firstValueType = $scope->getType($args[0]->value); if ($firstValueType instanceof ConstantStringType) { return $this->resolveConstantStringType($firstValueType, $isArrayWithoutStdClass); }