Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ReturnTypeExtension] add test case for narrow typed json_decode #993

Merged
merged 21 commits into from May 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
c4e495a
add test case for narrow typed json_decode
TomasVotruba Feb 4, 2022
38ca032
Extend JsonThrowOnErrorDynamicReturnTypeExtension to detect knonw typ…
TomasVotruba Feb 4, 2022
6d4e3b3
update type in LegacyNodeScopeResolverTest
TomasVotruba Feb 4, 2022
581f51a
return mixed type
TomasVotruba Feb 4, 2022
d665722
add json_decode false
TomasVotruba Feb 6, 2022
b2b979b
check for JSON_OBJECT_AS_ARRAY, in case of null and array
TomasVotruba Feb 6, 2022
665e9da
add test case for invalid json string
TomasVotruba Feb 6, 2022
66e3aa0
add test for multiple flags
TomasVotruba Feb 6, 2022
1a124ea
decopule type resolution to static
TomasVotruba Feb 6, 2022
bea4f29
check if JSON_THROW_ON_ERROR exists before infer
TomasVotruba Feb 6, 2022
8748e9c
[ci] add json to composer require checker json
TomasVotruba Mar 24, 2022
43eb542
use bitwiseFlagAnalyser
TomasVotruba Mar 28, 2022
99debb9
fixup! use bitwiseFlagAnalyser
TomasVotruba Apr 25, 2022
d707038
Update src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php
TomasVotruba Apr 25, 2022
0255425
Update src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php
TomasVotruba Apr 25, 2022
d586c6e
fixup! fixup! use bitwiseFlagAnalyser
TomasVotruba Apr 25, 2022
98abc6f
fixup! fixup! fixup! use bitwiseFlagAnalyser
TomasVotruba Apr 25, 2022
8a841fe
fixup! fixup! fixup! fixup! use bitwiseFlagAnalyser
TomasVotruba Apr 25, 2022
3ad68aa
Simplify isForceArrayWithoutStdClass
herndlm Apr 21, 2022
741abd2
Simplify fallback type handling
herndlm Apr 21, 2022
7d306fb
Remove unneeded var in narrowTypeForJsonDecode
herndlm Apr 25, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions build/composer-require-checker.json
Expand Up @@ -12,6 +12,7 @@
"Clue\\React\\Block\\await", "Hoa\\File\\Read"
],
"php-core-extensions" : [
"json",
"Core",
"date",
"pcre",
Expand Down
75 changes: 66 additions & 9 deletions src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php
Expand Up @@ -10,10 +10,16 @@
use PHPStan\Reflection\ReflectionProvider;
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\ObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function in_array;
use stdClass;
use function is_bool;
use function json_decode;

class JsonThrowOnErrorDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension
{
Expand All @@ -35,14 +41,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(
Expand All @@ -53,6 +56,11 @@ public function getTypeFromFunctionCall(
{
$argumentPosition = $this->argumentPositions[$functionReflection->getName()];
$defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType();

if ($functionReflection->getName() === 'json_decode') {
$defaultReturnType = $this->narrowTypeForJsonDecode($functionCall, $scope, $defaultReturnType);
}

if (!isset($functionCall->getArgs()[$argumentPosition])) {
return $defaultReturnType;
}
Expand All @@ -65,4 +73,53 @@ public function getTypeFromFunctionCall(
return $defaultReturnType;
}

private function narrowTypeForJsonDecode(FuncCall $funcCall, Scope $scope, Type $fallbackType): Type
{
$args = $funcCall->getArgs();
$isArrayWithoutStdClass = $this->isForceArrayWithoutStdClass($funcCall, $scope);

$firstValueType = $scope->getType($args[0]->value);
if ($firstValueType instanceof ConstantStringType) {
return $this->resolveConstantStringType($firstValueType, $isArrayWithoutStdClass);
}

if ($isArrayWithoutStdClass) {
return TypeCombinator::remove($fallbackType, new ObjectType(stdClass::class));
}

return $fallbackType;
}

/**
* Is "json_decode(..., true)"?
*/
private function isForceArrayWithoutStdClass(FuncCall $funcCall, Scope $scope): bool
{
$args = $funcCall->getArgs();
if (!isset($args[1])) {
return false;
}

$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;
}

// 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
{
$decodedValue = json_decode($constantStringType->getValue(), $isForceArray);

return ConstantTypeHelper::getTypeFromValue($decodedValue);
}

}
5 changes: 5 additions & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Expand Up @@ -17,6 +17,11 @@ public function dataFileAsserts(): iterable
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');
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';

yield from $this->gatherAssertTypes(__DIR__ . '/data/bug2574.php');
Expand Down
17 changes: 17 additions & 0 deletions tests/PHPStan/Analyser/data/json-decode/invalid_type.php
@@ -0,0 +1,17 @@
<?php

namespace Analyser\JsonDecode;

use function PHPStan\Testing\assertType;

$value = json_decode('{"key"}');
assertType('null', $value);

$value = json_decode('{"key"}', true);
assertType('null', $value);

$value = json_decode('{"key"}', null);
assertType('null', $value);

$value = json_decode('{"key"}', false);
assertType('null', $value);
33 changes: 33 additions & 0 deletions tests/PHPStan/Analyser/data/json-decode/json_object_as_array.php
@@ -0,0 +1,33 @@
<?php

namespace Analyser\JsonDecode;

use function PHPStan\Testing\assertType;

// @see https://3v4l.org/YFlHF
function ($mixed) {
$value = json_decode($mixed, null, 512, JSON_OBJECT_AS_ARRAY);
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);
};

function ($mixed) {
$value = json_decode($mixed, null);
assertType('mixed', $value);
};

function ($mixed, $unknownFlags) {
$value = json_decode($mixed, null, 512, $unknownFlags);
assertType('mixed', $value);
};
34 changes: 34 additions & 0 deletions tests/PHPStan/Analyser/data/json-decode/narrow_type.php
@@ -0,0 +1,34 @@
<?php

namespace Analyser\JsonDecode;

use function PHPStan\Testing\assertType;

$value = json_decode('true');
assertType('true', $value);

$value = json_decode('1');
assertType('1', $value);

$value = json_decode('1.5');
assertType('1.5', $value);

$value = json_decode('false');
assertType('false', $value);

$value = json_decode('{}');
assertType('stdClass', $value);

$value = json_decode('[1, 2, 3]');
assertType('array{1, 2, 3}', $value);


function ($mixed) {
$value = json_decode($mixed);
assertType('mixed', $value);
};

function ($mixed) {
$value = json_decode($mixed, false);
assertType('mixed', $value);
};
@@ -0,0 +1,28 @@
<?php

namespace Analyser\JsonDecode;

use function PHPStan\Testing\assertType;

$value = json_decode('true', true);
assertType('true', $value);

$value = json_decode('1', true);
assertType('1', $value);

$value = json_decode('1.5', true);
assertType('1.5', $value);

$value = json_decode('false', true);
TomasVotruba marked this conversation as resolved.
Show resolved Hide resolved
assertType('false', $value);

$value = json_decode('{}', true);
assertType('array{}', $value);

$value = json_decode('[1, 2, 3]', true);
assertType('array{1, 2, 3}', $value);
TomasVotruba marked this conversation as resolved.
Show resolved Hide resolved

function ($mixed) {
$value = json_decode($mixed, true);
assertType('mixed~stdClass', $value);
};