From 3f248d85367939a36cc5928efd98828191a8ee7e Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sun, 30 Jan 2022 10:13:17 +0100 Subject: [PATCH 1/9] [composer] autoload tests with PSR-4 --- composer.json | 6 +++++- .../FormContainerValuesDynamicReturnTypeExtensionTest.php | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index dd310ac..9d2571e 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,8 @@ "phpstan/phpstan-php-parser": "^1.1", "phpstan/phpstan-phpunit": "^1.0", "phpstan/phpstan-strict-rules": "^1.0", - "phpunit/phpunit": "^9.5" + "phpunit/phpunit": "^9.5", + "tracy/tracy": "^2.9" }, "config": { "platform": { @@ -50,6 +51,9 @@ } }, "autoload-dev": { + "psr-4": { + "PHPStan\\Tests\\": "tests/" + }, "classmap": [ "tests/" ] diff --git a/tests/Type/Nette/FormContainerValuesDynamicReturnTypeExtensionTest.php b/tests/Type/Nette/FormContainerValuesDynamicReturnTypeExtensionTest.php index c1f69e3..19c1c9c 100644 --- a/tests/Type/Nette/FormContainerValuesDynamicReturnTypeExtensionTest.php +++ b/tests/Type/Nette/FormContainerValuesDynamicReturnTypeExtensionTest.php @@ -1,6 +1,6 @@ Date: Sun, 30 Jan 2022 10:40:38 +0100 Subject: [PATCH 2/9] add JsonDecodeDynamicReturnTypeExtension --- composer.json | 2 +- .../JsonDecodeDynamicReturnTypeExtension.php | 142 ++++++++++++++++++ ...erValuesDynamicReturnTypeExtensionTest.php | 3 +- ...onDecodeDynamicReturnTypeExtensionTest.php | 38 +++++ .../Nette/config/json_decode_extension.neon | 5 + tests/Type/Nette/data/json_decode.php | 23 +++ .../Nette/data/json_decode_force_array.php | 25 +++ 7 files changed, 235 insertions(+), 3 deletions(-) create mode 100644 src/Type/Nette/JsonDecodeDynamicReturnTypeExtension.php create mode 100644 tests/Type/Nette/JsonDecodeDynamicReturnTypeExtensionTest.php create mode 100644 tests/Type/Nette/config/json_decode_extension.neon create mode 100644 tests/Type/Nette/data/json_decode.php create mode 100644 tests/Type/Nette/data/json_decode_force_array.php diff --git a/composer.json b/composer.json index 9d2571e..2dd00e9 100644 --- a/composer.json +++ b/composer.json @@ -52,7 +52,7 @@ }, "autoload-dev": { "psr-4": { - "PHPStan\\Tests\\": "tests/" + "PHPStan\\": "tests/" }, "classmap": [ "tests/" diff --git a/src/Type/Nette/JsonDecodeDynamicReturnTypeExtension.php b/src/Type/Nette/JsonDecodeDynamicReturnTypeExtension.php new file mode 100644 index 0000000..6db77a1 --- /dev/null +++ b/src/Type/Nette/JsonDecodeDynamicReturnTypeExtension.php @@ -0,0 +1,142 @@ +getName() === 'decode'; + } + + public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): Type + { + $args = $methodCall->getArgs(); + + $isForceArray = $this->isForceArray($args); + + $firstArgValue = $args[0]->value; + $firstValueType = $scope->getType($firstArgValue); + + if ($firstValueType instanceof ConstantStringType) { + $resolvedType = $this->resolveConstantStringType($firstValueType, $isForceArray); + } else { + $resolvedType = new MixedType(); + } + + 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(), + ]); + } + + /** + * @param Arg[] $args + */ + private function isForceArray(array $args): bool + { + if (!isset($args[1])) { + return false; + } + + $secondArg = $args[1]; + + // is second arg force array? + if ($secondArg->value instanceof ClassConstFetch) { + $classConstFetch = $secondArg->value; + + if ($classConstFetch->class instanceof Name) { + if (! $classConstFetch->name instanceof \PhpParser\Node\Identifier) { + return false; + } + + if ($classConstFetch->class->toString() !== 'Nette\Utils\Json') { + return false; + } + + if ($classConstFetch->name->toString() === 'FORCE_ARRAY') { + return true; + } + } + } + + return false; + } + + private function resolveConstantStringType(ConstantStringType $constantStringType, bool $isForceArray): Type + { + if ($isForceArray) { + $decodedValue = Json::decode($constantStringType->getValue(), Json::FORCE_ARRAY); + } else { + $decodedValue = Json::decode($constantStringType->getValue()); + } + + if (is_bool($decodedValue)) { + return new BooleanType(); + } + + if (is_array($decodedValue)) { + return new ArrayType(new MixedType(), new MixedType()); + } + + if (is_object($decodedValue) && get_class($decodedValue) === stdClass::class) { + return new ObjectType(stdClass::class); + } + + if (is_int($decodedValue)) { + return new IntegerType(); + } + + if (is_float($decodedValue)) { + return new FloatType(); + } + + return new MixedType(); + } + +} diff --git a/tests/Type/Nette/FormContainerValuesDynamicReturnTypeExtensionTest.php b/tests/Type/Nette/FormContainerValuesDynamicReturnTypeExtensionTest.php index 19c1c9c..c1f69e3 100644 --- a/tests/Type/Nette/FormContainerValuesDynamicReturnTypeExtensionTest.php +++ b/tests/Type/Nette/FormContainerValuesDynamicReturnTypeExtensionTest.php @@ -1,6 +1,6 @@ + */ + public function dataAsserts(): iterable + { + yield from $this->gatherAssertTypes(__DIR__ . '/data/json_decode.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/json_decode_force_array.php'); + } + + /** + * @dataProvider dataAsserts() + * @param string $assertType + * @param string $file + * @param mixed ...$args + */ + public function testAsserts(string $assertType, string $file, ...$args): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + /** + * @return string[] + */ + public static function getAdditionalConfigFiles(): array + { + return [__DIR__ . '/config/json_decode_extension.neon']; + } + +} diff --git a/tests/Type/Nette/config/json_decode_extension.neon b/tests/Type/Nette/config/json_decode_extension.neon new file mode 100644 index 0000000..e6b0a1d --- /dev/null +++ b/tests/Type/Nette/config/json_decode_extension.neon @@ -0,0 +1,5 @@ +services: + - + class: PHPStan\Type\Nette\JsonDecodeDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicStaticMethodReturnTypeExtension diff --git a/tests/Type/Nette/data/json_decode.php b/tests/Type/Nette/data/json_decode.php new file mode 100644 index 0000000..4946a2b --- /dev/null +++ b/tests/Type/Nette/data/json_decode.php @@ -0,0 +1,23 @@ + Date: Sun, 30 Jan 2022 11:34:03 +0100 Subject: [PATCH 3/9] [ci] add downgrade of nette/utils to use json_decode() for old PHP --- .github/workflows/build.yml | 5 +++++ src/Type/Nette/JsonDecodeDynamicReturnTypeExtension.php | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 85d9184..ecf19cc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -113,6 +113,11 @@ jobs: if: matrix.php-version == '7.1' || matrix.php-version == '7.2' || matrix.php-version == '7.3' run: "composer require --dev phpunit/phpunit:^7.5.20 --update-with-dependencies" + # in PHP 7.1, the json_decode() 2nd parameter requires bool, while PHP 7.2 it's null|int + - name: "Downgrade nette/utils" + if: matrix.php-version == '7.1' + run: "composer require --dev nette/utils:^2.3 nette/forms:^2.4 --update-with-dependencies" + - name: "Tests" run: "make tests" diff --git a/src/Type/Nette/JsonDecodeDynamicReturnTypeExtension.php b/src/Type/Nette/JsonDecodeDynamicReturnTypeExtension.php index 6db77a1..382f74c 100644 --- a/src/Type/Nette/JsonDecodeDynamicReturnTypeExtension.php +++ b/src/Type/Nette/JsonDecodeDynamicReturnTypeExtension.php @@ -6,6 +6,7 @@ use PhpParser\Node\Arg; use PhpParser\Node\Expr\ClassConstFetch; use PhpParser\Node\Expr\StaticCall; +use PhpParser\Node\Identifier; use PhpParser\Node\Name; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; @@ -91,7 +92,7 @@ private function isForceArray(array $args): bool $classConstFetch = $secondArg->value; if ($classConstFetch->class instanceof Name) { - if (! $classConstFetch->name instanceof \PhpParser\Node\Identifier) { + if (! $classConstFetch->name instanceof Identifier) { return false; } From 1041390dc7de28c53fcc41a31cb59d2ff3dcb437 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sun, 30 Jan 2022 22:02:05 +0100 Subject: [PATCH 4/9] make use of ConstantTypeHelper --- .../JsonDecodeDynamicReturnTypeExtension.php | 45 ++++++++++--------- tests/Type/Nette/data/json_decode.php | 8 ++-- .../Nette/data/json_decode_force_array.php | 10 ++--- 3 files changed, 33 insertions(+), 30 deletions(-) diff --git a/src/Type/Nette/JsonDecodeDynamicReturnTypeExtension.php b/src/Type/Nette/JsonDecodeDynamicReturnTypeExtension.php index 382f74c..525cd44 100644 --- a/src/Type/Nette/JsonDecodeDynamicReturnTypeExtension.php +++ b/src/Type/Nette/JsonDecodeDynamicReturnTypeExtension.php @@ -13,6 +13,7 @@ use PHPStan\Type\ArrayType; use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\ConstantTypeHelper; use PHPStan\Type\DynamicStaticMethodReturnTypeExtension; use PHPStan\Type\FloatType; use PHPStan\Type\IntegerType; @@ -117,27 +118,29 @@ private function resolveConstantStringType(ConstantStringType $constantStringTyp $decodedValue = Json::decode($constantStringType->getValue()); } - if (is_bool($decodedValue)) { - return new BooleanType(); - } - - if (is_array($decodedValue)) { - return new ArrayType(new MixedType(), new MixedType()); - } - - if (is_object($decodedValue) && get_class($decodedValue) === stdClass::class) { - return new ObjectType(stdClass::class); - } - - if (is_int($decodedValue)) { - return new IntegerType(); - } - - if (is_float($decodedValue)) { - return new FloatType(); - } - - return new MixedType(); + return ConstantTypeHelper::getTypeFromValue($decodedValue); +// +// if (is_bool($decodedValue)) { +// return new BooleanType(); +// } +// +// if (is_array($decodedValue)) { +// return new ArrayType(new MixedType(), new MixedType()); +// } +// +// if (is_object($decodedValue) && get_class($decodedValue) === stdClass::class) { +// return new ObjectType(stdClass::class); +// } +// +// if (is_int($decodedValue)) { +// return new IntegerType(); +// } +// +// if (is_float($decodedValue)) { +// return new FloatType(); +// } +// +// return new MixedType(); } } diff --git a/tests/Type/Nette/data/json_decode.php b/tests/Type/Nette/data/json_decode.php index 4946a2b..843b203 100644 --- a/tests/Type/Nette/data/json_decode.php +++ b/tests/Type/Nette/data/json_decode.php @@ -4,16 +4,16 @@ use function PHPStan\Testing\assertType; $value = Json::decode('true'); -assertType('bool', $value); +assertType('true', $value); $value = Json::decode('1'); -assertType('int', $value); +assertType('1', $value); $value = Json::decode('1.5'); -assertType('float', $value); +assertType('1.5', $value); $value = Json::decode('false'); -assertType('bool', $value); +assertType('false', $value); function unknownType($mixed) { $value = Json::decode($mixed); diff --git a/tests/Type/Nette/data/json_decode_force_array.php b/tests/Type/Nette/data/json_decode_force_array.php index c41d589..0a4e9c5 100644 --- a/tests/Type/Nette/data/json_decode_force_array.php +++ b/tests/Type/Nette/data/json_decode_force_array.php @@ -4,19 +4,19 @@ use function PHPStan\Testing\assertType; $value = Json::decode('true', Json::FORCE_ARRAY); -assertType('bool', $value); +assertType('true', $value); $value = Json::decode('1', Json::FORCE_ARRAY); -assertType('int', $value); +assertType('1', $value); $value = Json::decode('1.5', Json::FORCE_ARRAY); -assertType('float', $value); +assertType('1.5', $value); $value = Json::decode('false', Json::FORCE_ARRAY); -assertType('bool', $value); +assertType('false', $value); $value = Json::decode('{}', Json::FORCE_ARRAY); -assertType('array', $value); +assertType('array{}', $value); function unknownType($mixed) { From 024bd78d6088724608acedae78ed0523d9ff2376 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sun, 30 Jan 2022 22:06:22 +0100 Subject: [PATCH 5/9] cleanup --- .../JsonDecodeDynamicReturnTypeExtension.php | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/src/Type/Nette/JsonDecodeDynamicReturnTypeExtension.php b/src/Type/Nette/JsonDecodeDynamicReturnTypeExtension.php index 525cd44..67287e7 100644 --- a/src/Type/Nette/JsonDecodeDynamicReturnTypeExtension.php +++ b/src/Type/Nette/JsonDecodeDynamicReturnTypeExtension.php @@ -119,28 +119,6 @@ private function resolveConstantStringType(ConstantStringType $constantStringTyp } return ConstantTypeHelper::getTypeFromValue($decodedValue); -// -// if (is_bool($decodedValue)) { -// return new BooleanType(); -// } -// -// if (is_array($decodedValue)) { -// return new ArrayType(new MixedType(), new MixedType()); -// } -// -// if (is_object($decodedValue) && get_class($decodedValue) === stdClass::class) { -// return new ObjectType(stdClass::class); -// } -// -// if (is_int($decodedValue)) { -// return new IntegerType(); -// } -// -// if (is_float($decodedValue)) { -// return new FloatType(); -// } -// -// return new MixedType(); } } From 43b9215ae96f862772b6e89b70fa956e30a65362 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sun, 30 Jan 2022 22:08:13 +0100 Subject: [PATCH 6/9] add array types --- tests/Type/Nette/data/json_decode.php | 7 +++++++ tests/Type/Nette/data/json_decode_force_array.php | 3 +++ 2 files changed, 10 insertions(+) diff --git a/tests/Type/Nette/data/json_decode.php b/tests/Type/Nette/data/json_decode.php index 843b203..0afd963 100644 --- a/tests/Type/Nette/data/json_decode.php +++ b/tests/Type/Nette/data/json_decode.php @@ -15,6 +15,13 @@ $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 unknownType($mixed) { $value = Json::decode($mixed); assertType('bool|float|int|stdClass|string', $value); diff --git a/tests/Type/Nette/data/json_decode_force_array.php b/tests/Type/Nette/data/json_decode_force_array.php index 0a4e9c5..754490c 100644 --- a/tests/Type/Nette/data/json_decode_force_array.php +++ b/tests/Type/Nette/data/json_decode_force_array.php @@ -18,6 +18,9 @@ $value = Json::decode('{}', Json::FORCE_ARRAY); assertType('array{}', $value); +$value = Json::decode('[1, 2, 3]', Json::FORCE_ARRAY); +assertType('array{1, 2, 3}', $value); + function unknownType($mixed) { $value = Json::decode($mixed, Json::FORCE_ARRAY); From 62a2ec9164db57afda9c87799e9aadb9d8cbae03 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sun, 30 Jan 2022 22:11:03 +0100 Subject: [PATCH 7/9] split unknown types test --- .../Nette/JsonDecodeDynamicReturnTypeExtensionTest.php | 3 +++ tests/Type/Nette/data/json_decode.php | 8 -------- tests/Type/Nette/data/json_decode_force_array.php | 6 ------ .../Nette/data/json_decode_force_array_unknown_type.php | 9 +++++++++ tests/Type/Nette/data/json_decode_unknown_type.php | 9 +++++++++ 5 files changed, 21 insertions(+), 14 deletions(-) create mode 100644 tests/Type/Nette/data/json_decode_force_array_unknown_type.php create mode 100644 tests/Type/Nette/data/json_decode_unknown_type.php diff --git a/tests/Type/Nette/JsonDecodeDynamicReturnTypeExtensionTest.php b/tests/Type/Nette/JsonDecodeDynamicReturnTypeExtensionTest.php index 41e0d5e..7bbe202 100644 --- a/tests/Type/Nette/JsonDecodeDynamicReturnTypeExtensionTest.php +++ b/tests/Type/Nette/JsonDecodeDynamicReturnTypeExtensionTest.php @@ -14,6 +14,9 @@ public function dataAsserts(): iterable { yield from $this->gatherAssertTypes(__DIR__ . '/data/json_decode.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/json_decode_force_array.php'); + + yield from $this->gatherAssertTypes(__DIR__ . '/data/json_decode_unknown_type.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/json_decode_force_array_unknown_type.php'); } /** diff --git a/tests/Type/Nette/data/json_decode.php b/tests/Type/Nette/data/json_decode.php index 0afd963..328a9e1 100644 --- a/tests/Type/Nette/data/json_decode.php +++ b/tests/Type/Nette/data/json_decode.php @@ -20,11 +20,3 @@ $value = Json::decode('[1, 2, 3]'); assertType('array{1, 2, 3}', $value); - - -function unknownType($mixed) { - $value = Json::decode($mixed); - assertType('bool|float|int|stdClass|string', $value); -} - - diff --git a/tests/Type/Nette/data/json_decode_force_array.php b/tests/Type/Nette/data/json_decode_force_array.php index 754490c..79fbc53 100644 --- a/tests/Type/Nette/data/json_decode_force_array.php +++ b/tests/Type/Nette/data/json_decode_force_array.php @@ -20,9 +20,3 @@ $value = Json::decode('[1, 2, 3]', Json::FORCE_ARRAY); assertType('array{1, 2, 3}', $value); - - -function unknownType($mixed) { - $value = Json::decode($mixed, Json::FORCE_ARRAY); - assertType('array|bool|float|int|string', $value); -} diff --git a/tests/Type/Nette/data/json_decode_force_array_unknown_type.php b/tests/Type/Nette/data/json_decode_force_array_unknown_type.php new file mode 100644 index 0000000..dcf8fed --- /dev/null +++ b/tests/Type/Nette/data/json_decode_force_array_unknown_type.php @@ -0,0 +1,9 @@ + Date: Sun, 30 Jan 2022 22:18:02 +0100 Subject: [PATCH 8/9] rename JsonDecode... to NetteUtilsJsonDecode.. --- ...n.php => NetteUtilsJsonDecodeDynamicReturnTypeExtension.php} | 2 +- ...p => NetteUtilsJsonDecodeDynamicReturnTypeExtensionTest.php} | 2 +- tests/Type/Nette/config/json_decode_extension.neon | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename src/Type/Nette/{JsonDecodeDynamicReturnTypeExtension.php => NetteUtilsJsonDecodeDynamicReturnTypeExtension.php} (96%) rename tests/Type/Nette/{JsonDecodeDynamicReturnTypeExtensionTest.php => NetteUtilsJsonDecodeDynamicReturnTypeExtensionTest.php} (91%) diff --git a/src/Type/Nette/JsonDecodeDynamicReturnTypeExtension.php b/src/Type/Nette/NetteUtilsJsonDecodeDynamicReturnTypeExtension.php similarity index 96% rename from src/Type/Nette/JsonDecodeDynamicReturnTypeExtension.php rename to src/Type/Nette/NetteUtilsJsonDecodeDynamicReturnTypeExtension.php index 67287e7..8dbcd43 100644 --- a/src/Type/Nette/JsonDecodeDynamicReturnTypeExtension.php +++ b/src/Type/Nette/NetteUtilsJsonDecodeDynamicReturnTypeExtension.php @@ -24,7 +24,7 @@ use PHPStan\Type\UnionType; use stdClass; -final class JsonDecodeDynamicReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension +final class NetteUtilsJsonDecodeDynamicReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension { public function getClass(): string diff --git a/tests/Type/Nette/JsonDecodeDynamicReturnTypeExtensionTest.php b/tests/Type/Nette/NetteUtilsJsonDecodeDynamicReturnTypeExtensionTest.php similarity index 91% rename from tests/Type/Nette/JsonDecodeDynamicReturnTypeExtensionTest.php rename to tests/Type/Nette/NetteUtilsJsonDecodeDynamicReturnTypeExtensionTest.php index 7bbe202..d6fb906 100644 --- a/tests/Type/Nette/JsonDecodeDynamicReturnTypeExtensionTest.php +++ b/tests/Type/Nette/NetteUtilsJsonDecodeDynamicReturnTypeExtensionTest.php @@ -4,7 +4,7 @@ use PHPStan\Testing\TypeInferenceTestCase; -final class JsonDecodeDynamicReturnTypeExtensionTest extends TypeInferenceTestCase +final class NetteUtilsJsonDecodeDynamicReturnTypeExtensionTest extends TypeInferenceTestCase { /** diff --git a/tests/Type/Nette/config/json_decode_extension.neon b/tests/Type/Nette/config/json_decode_extension.neon index e6b0a1d..283fe30 100644 --- a/tests/Type/Nette/config/json_decode_extension.neon +++ b/tests/Type/Nette/config/json_decode_extension.neon @@ -1,5 +1,5 @@ services: - - class: PHPStan\Type\Nette\JsonDecodeDynamicReturnTypeExtension + class: PHPStan\Type\Nette\NetteUtilsJsonDecodeDynamicReturnTypeExtension tags: - phpstan.broker.dynamicStaticMethodReturnTypeExtension From 172e2fbd8667acb83f4694d07c9e0d379877e2f7 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Mon, 31 Jan 2022 11:11:46 +0100 Subject: [PATCH 9/9] propose JsonDecodeDynamicReturnTypeExtension --- .../JsonDecodeDynamicReturnTypeExtension.php | 86 +++++++++++++++++++ ...onDecodeDynamicReturnTypeExtensionTest.php | 2 +- ....neon => nette_json_decode_extension.neon} | 0 ...onDecodeDynamicReturnTypeExtensionTest.php | 42 +++++++++ .../Php/config/json_decode_extension.neon | 5 ++ tests/Type/Php/data/json_decode.php | 21 +++++ .../Type/Php/data/json_decode_force_array.php | 21 +++++ 7 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 src/Type/Php/JsonDecodeDynamicReturnTypeExtension.php rename tests/Type/Nette/config/{json_decode_extension.neon => nette_json_decode_extension.neon} (100%) create mode 100644 tests/Type/Php/JsonDecodeDynamicReturnTypeExtensionTest.php create mode 100644 tests/Type/Php/config/json_decode_extension.neon create mode 100644 tests/Type/Php/data/json_decode.php create mode 100644 tests/Type/Php/data/json_decode_force_array.php diff --git a/src/Type/Php/JsonDecodeDynamicReturnTypeExtension.php b/src/Type/Php/JsonDecodeDynamicReturnTypeExtension.php new file mode 100644 index 0000000..673a01d --- /dev/null +++ b/src/Type/Php/JsonDecodeDynamicReturnTypeExtension.php @@ -0,0 +1,86 @@ +getName()); + + return $functionReflection->getName() === 'json_decode'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $funcCall, Scope $scope): Type + { + $args = $funcCall->getArgs(); + + dump('___'); + die; + + $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(); + } + + 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(), + ]); + } + + private function resolveConstantStringType(ConstantStringType $constantStringType, bool $isForceArray): Type + { + if ($isForceArray) { + $decodedValue = Json::decode($constantStringType->getValue(), Json::FORCE_ARRAY); + } else { + $decodedValue = Json::decode($constantStringType->getValue()); + } + + return ConstantTypeHelper::getTypeFromValue($decodedValue); + } +} diff --git a/tests/Type/Nette/NetteUtilsJsonDecodeDynamicReturnTypeExtensionTest.php b/tests/Type/Nette/NetteUtilsJsonDecodeDynamicReturnTypeExtensionTest.php index d6fb906..b2bbf75 100644 --- a/tests/Type/Nette/NetteUtilsJsonDecodeDynamicReturnTypeExtensionTest.php +++ b/tests/Type/Nette/NetteUtilsJsonDecodeDynamicReturnTypeExtensionTest.php @@ -35,7 +35,7 @@ public function testAsserts(string $assertType, string $file, ...$args): void */ public static function getAdditionalConfigFiles(): array { - return [__DIR__ . '/config/json_decode_extension.neon']; + return [__DIR__ . '/config/nette_json_decode_extension.neon']; } } diff --git a/tests/Type/Nette/config/json_decode_extension.neon b/tests/Type/Nette/config/nette_json_decode_extension.neon similarity index 100% rename from tests/Type/Nette/config/json_decode_extension.neon rename to tests/Type/Nette/config/nette_json_decode_extension.neon diff --git a/tests/Type/Php/JsonDecodeDynamicReturnTypeExtensionTest.php b/tests/Type/Php/JsonDecodeDynamicReturnTypeExtensionTest.php new file mode 100644 index 0000000..dbb798c --- /dev/null +++ b/tests/Type/Php/JsonDecodeDynamicReturnTypeExtensionTest.php @@ -0,0 +1,42 @@ + + */ + public function dataAsserts(): iterable + { + yield from $this->gatherAssertTypes(__DIR__ . '/data/json_decode.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/json_decode_force_array.php'); + } + + /** + * @dataProvider dataAsserts() + * @param string $assertType + * @param string $file + * @param mixed ...$args + */ + public function testAsserts(string $assertType, string $file, ...$args): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + /** + * @return string[] + */ + public static function getAdditionalConfigFiles(): array + { + return [__DIR__ . '/config/json_decode_extension.neon']; + } + + + +} diff --git a/tests/Type/Php/config/json_decode_extension.neon b/tests/Type/Php/config/json_decode_extension.neon new file mode 100644 index 0000000..5f18ab6 --- /dev/null +++ b/tests/Type/Php/config/json_decode_extension.neon @@ -0,0 +1,5 @@ +services: + - + class: PHPStan\Type\Php\JsonDecodeDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension diff --git a/tests/Type/Php/data/json_decode.php b/tests/Type/Php/data/json_decode.php new file mode 100644 index 0000000..f02b790 --- /dev/null +++ b/tests/Type/Php/data/json_decode.php @@ -0,0 +1,21 @@ +