Skip to content

Commit cf27b2b

Browse files
authored
fix handling of JSON_THROW_ON_ERROR with optional bitwise flags
1 parent d5284ce commit cf27b2b

File tree

7 files changed

+286
-80
lines changed

7 files changed

+286
-80
lines changed

conf/config.neon

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -897,6 +897,9 @@ services:
897897
class: PHPStan\Type\TypeAliasResolverProvider
898898
factory: PHPStan\Type\LazyTypeAliasResolverProvider
899899

900+
-
901+
class: PHPStan\Type\BitwiseFlagHelper
902+
900903
-
901904
class: PHPStan\Type\Php\ArgumentBasedFunctionReturnTypeExtension
902905
tags:

src/Type/BitwiseFlagHelper.php

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type;
4+
5+
use PhpParser\Node\Expr;
6+
use PhpParser\Node\Expr\BinaryOp\BitwiseOr;
7+
use PhpParser\Node\Expr\ConstFetch;
8+
use PhpParser\Node\Name\FullyQualified;
9+
use PHPStan\Analyser\Scope;
10+
use PHPStan\Reflection\ReflectionProvider;
11+
use PHPStan\TrinaryLogic;
12+
use PHPStan\Type\Constant\ConstantIntegerType;
13+
14+
final class BitwiseFlagHelper
15+
{
16+
17+
public function __construct(private ReflectionProvider $reflectionProvider)
18+
{
19+
}
20+
21+
public function bitwiseOrContainsConstant(Expr $expr, Scope $scope, string $constName): TrinaryLogic
22+
{
23+
if ($expr instanceof ConstFetch) {
24+
if (((string) $expr->name) === $constName) {
25+
return TrinaryLogic::createYes();
26+
}
27+
28+
$resolveConstantName = $this->reflectionProvider->resolveConstantName($expr->name, $scope);
29+
if ($resolveConstantName !== null) {
30+
if ($resolveConstantName === $constName) {
31+
return TrinaryLogic::createYes();
32+
}
33+
return TrinaryLogic::createNo();
34+
}
35+
}
36+
37+
if ($expr instanceof BitwiseOr) {
38+
return TrinaryLogic::createFromBoolean($this->bitwiseOrContainsConstant($expr->left, $scope, $constName)->yes() ||
39+
$this->bitwiseOrContainsConstant($expr->right, $scope, $constName)->yes());
40+
}
41+
42+
$fqcn = new FullyQualified($constName);
43+
if ($this->reflectionProvider->hasConstant($fqcn, $scope)) {
44+
$constant = $this->reflectionProvider->getConstant($fqcn, $scope);
45+
46+
$valueType = $constant->getValueType();
47+
48+
if ($valueType instanceof ConstantIntegerType) {
49+
return $this->exprContainsIntFlag($expr, $scope, $valueType->getValue());
50+
}
51+
}
52+
53+
return TrinaryLogic::createNo();
54+
}
55+
56+
private function exprContainsIntFlag(Expr $expr, Scope $scope, int $flag): TrinaryLogic
57+
{
58+
$exprType = $scope->getType($expr);
59+
60+
if ($exprType instanceof UnionType) {
61+
$allTypesContainFlag = true;
62+
$someTypesContainFlag = false;
63+
foreach ($exprType->getTypes() as $type) {
64+
$containsFlag = $this->typeContainsIntFlag($type, $flag);
65+
if (!$containsFlag->yes()) {
66+
$allTypesContainFlag = false;
67+
}
68+
69+
if (!$containsFlag->yes() && !$containsFlag->maybe()) {
70+
continue;
71+
}
72+
73+
$someTypesContainFlag = true;
74+
}
75+
76+
if ($allTypesContainFlag) {
77+
return TrinaryLogic::createYes();
78+
}
79+
if ($someTypesContainFlag) {
80+
return TrinaryLogic::createMaybe();
81+
}
82+
return TrinaryLogic::createNo();
83+
}
84+
85+
return $this->typeContainsIntFlag($exprType, $flag);
86+
}
87+
88+
private function typeContainsIntFlag(Type $type, int $flag): TrinaryLogic
89+
{
90+
if ($type instanceof ConstantIntegerType) {
91+
if (($type->getValue() & $flag) === $flag) {
92+
return TrinaryLogic::createYes();
93+
}
94+
return TrinaryLogic::createNo();
95+
}
96+
97+
$integerType = new IntegerType();
98+
if ($integerType->isSuperTypeOf($type)->yes() || $type instanceof MixedType) {
99+
return TrinaryLogic::createMaybe();
100+
}
101+
102+
return TrinaryLogic::createNo();
103+
}
104+
105+
}

src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php

Lines changed: 7 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,14 @@
22

33
namespace PHPStan\Type\Php;
44

5-
use PhpParser\Node\Expr;
6-
use PhpParser\Node\Expr\BinaryOp\BitwiseOr;
7-
use PhpParser\Node\Expr\ConstFetch;
85
use PhpParser\Node\Expr\FuncCall;
96
use PhpParser\Node\Name\FullyQualified;
107
use PHPStan\Analyser\Scope;
118
use PHPStan\Reflection\FunctionReflection;
129
use PHPStan\Reflection\ParametersAcceptorSelector;
1310
use PHPStan\Reflection\ReflectionProvider;
11+
use PHPStan\Type\BitwiseFlagHelper;
1412
use PHPStan\Type\Constant\ConstantBooleanType;
15-
use PHPStan\Type\Constant\ConstantIntegerType;
1613
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
1714
use PHPStan\Type\Type;
1815
use PHPStan\Type\TypeCombinator;
@@ -27,7 +24,10 @@ class JsonThrowOnErrorDynamicReturnTypeExtension implements DynamicFunctionRetur
2724
'json_decode' => 3,
2825
];
2926

30-
public function __construct(private ReflectionProvider $reflectionProvider)
27+
public function __construct(
28+
private ReflectionProvider $reflectionProvider,
29+
private BitwiseFlagHelper $bitwiseFlagAnalyser,
30+
)
3131
{
3232
}
3333

@@ -59,44 +59,11 @@ public function getTypeFromFunctionCall(
5959

6060
$optionsExpr = $functionCall->getArgs()[$argumentPosition]->value;
6161
$constrictedReturnType = TypeCombinator::remove($defaultReturnType, new ConstantBooleanType(false));
62-
if ($this->isBitwiseOrWithJsonThrowOnError($optionsExpr, $scope)) {
62+
if ($this->bitwiseFlagAnalyser->bitwiseOrContainsConstant($optionsExpr, $scope, 'JSON_THROW_ON_ERROR')->yes()) {
6363
return $constrictedReturnType;
6464
}
6565

66-
$valueType = $scope->getType($optionsExpr);
67-
if (!$valueType instanceof ConstantIntegerType) {
68-
return $defaultReturnType;
69-
}
70-
71-
$value = $valueType->getValue();
72-
$throwOnErrorType = $this->reflectionProvider->getConstant(new FullyQualified('JSON_THROW_ON_ERROR'), null)->getValueType();
73-
if (!$throwOnErrorType instanceof ConstantIntegerType) {
74-
return $defaultReturnType;
75-
}
76-
77-
$throwOnErrorValue = $throwOnErrorType->getValue();
78-
if (($value & $throwOnErrorValue) !== $throwOnErrorValue) {
79-
return $defaultReturnType;
80-
}
81-
82-
return $constrictedReturnType;
83-
}
84-
85-
private function isBitwiseOrWithJsonThrowOnError(Expr $expr, Scope $scope): bool
86-
{
87-
if ($expr instanceof ConstFetch) {
88-
$constant = $this->reflectionProvider->resolveConstantName($expr->name, $scope);
89-
if ($constant === 'JSON_THROW_ON_ERROR') {
90-
return true;
91-
}
92-
}
93-
94-
if (!$expr instanceof BitwiseOr) {
95-
return false;
96-
}
97-
98-
return $this->isBitwiseOrWithJsonThrowOnError($expr->left, $scope) ||
99-
$this->isBitwiseOrWithJsonThrowOnError($expr->right, $scope);
66+
return $defaultReturnType;
10067
}
10168

10269
}

src/Type/Php/JsonThrowTypeExtension.php

Lines changed: 7 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,12 @@
22

33
namespace PHPStan\Type\Php;
44

5-
use PhpParser\Node\Expr;
6-
use PhpParser\Node\Expr\BinaryOp\BitwiseOr;
7-
use PhpParser\Node\Expr\ConstFetch;
85
use PhpParser\Node\Expr\FuncCall;
96
use PhpParser\Node\Name;
107
use PHPStan\Analyser\Scope;
118
use PHPStan\Reflection\FunctionReflection;
129
use PHPStan\Reflection\ReflectionProvider;
13-
use PHPStan\Type\Constant\ConstantIntegerType;
10+
use PHPStan\Type\BitwiseFlagHelper;
1411
use PHPStan\Type\DynamicFunctionThrowTypeExtension;
1512
use PHPStan\Type\ObjectType;
1613
use PHPStan\Type\Type;
@@ -25,7 +22,10 @@ class JsonThrowTypeExtension implements DynamicFunctionThrowTypeExtension
2522
'json_decode' => 3,
2623
];
2724

28-
public function __construct(private ReflectionProvider $reflectionProvider)
25+
public function __construct(
26+
private ReflectionProvider $reflectionProvider,
27+
private BitwiseFlagHelper $bitwiseFlagAnalyser,
28+
)
2929
{
3030
}
3131

@@ -55,44 +55,11 @@ public function getThrowTypeFromFunctionCall(
5555
}
5656

5757
$optionsExpr = $functionCall->getArgs()[$argumentPosition]->value;
58-
if ($this->isBitwiseOrWithJsonThrowOnError($optionsExpr, $scope)) {
58+
if ($this->bitwiseFlagAnalyser->bitwiseOrContainsConstant($optionsExpr, $scope, 'JSON_THROW_ON_ERROR')->yes()) {
5959
return new ObjectType('JsonException');
6060
}
6161

62-
$valueType = $scope->getType($optionsExpr);
63-
if (!$valueType instanceof ConstantIntegerType) {
64-
return null;
65-
}
66-
67-
$value = $valueType->getValue();
68-
$throwOnErrorType = $this->reflectionProvider->getConstant(new Name\FullyQualified('JSON_THROW_ON_ERROR'), null)->getValueType();
69-
if (!$throwOnErrorType instanceof ConstantIntegerType) {
70-
return null;
71-
}
72-
73-
$throwOnErrorValue = $throwOnErrorType->getValue();
74-
if (($value & $throwOnErrorValue) !== $throwOnErrorValue) {
75-
return null;
76-
}
77-
78-
return new ObjectType('JsonException');
79-
}
80-
81-
private function isBitwiseOrWithJsonThrowOnError(Expr $expr, Scope $scope): bool
82-
{
83-
if ($expr instanceof ConstFetch) {
84-
$constant = $this->reflectionProvider->resolveConstantName($expr->name, $scope);
85-
if ($constant === 'JSON_THROW_ON_ERROR') {
86-
return true;
87-
}
88-
}
89-
90-
if (!$expr instanceof BitwiseOr) {
91-
return false;
92-
}
93-
94-
return $this->isBitwiseOrWithJsonThrowOnError($expr->left, $scope) ||
95-
$this->isBitwiseOrWithJsonThrowOnError($expr->right, $scope);
62+
return null;
9663
}
9764

9865
}

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -738,6 +738,10 @@ public function dataFileAsserts(): iterable
738738
yield from $this->gatherAssertTypes(__DIR__ . '/data/preg_match_php8.php');
739739
}
740740

741+
if (PHP_VERSION_ID >= 70300) {
742+
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6654.php');
743+
}
744+
741745
require_once __DIR__ . '/data/countable.php';
742746
yield from $this->gatherAssertTypes(__DIR__ . '/data/countable.php');
743747

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php // lint >= 7.3
2+
3+
namespace Bug6654;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class Foo {
8+
function doFoo() {
9+
$data = '';
10+
$flags = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR;
11+
assertType('string',json_encode($data, $flags));
12+
13+
if (rand(0, 1)) {
14+
$flags |= JSON_FORCE_OBJECT;
15+
}
16+
17+
assertType('string', json_encode($data, $flags));
18+
}
19+
}

0 commit comments

Comments
 (0)