From 34385de65a80ca2991ca555802ffec30b194151c Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Thu, 9 Oct 2025 00:41:48 +0200 Subject: [PATCH 1/3] Fix bitwise on mixed --- .../InitializerExprTypeResolver.php | 21 +++++++-- tests/PHPStan/Analyser/nsrt/bitwise.php | 45 +++++++++++++++++++ .../CallToFunctionParametersRuleTest.php | 5 +++ .../PHPStan/Rules/Functions/data/bug-8094.php | 10 +++++ 4 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bitwise.php create mode 100644 tests/PHPStan/Rules/Functions/data/bug-8094.php diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index 384b945d17..068b423d21 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -1008,9 +1008,14 @@ public function getBitwiseAndTypeFromTypes(Type $leftType, Type $rightType): Typ $rightType = $this->optimizeScalarType($rightType); } - if ($leftType->isString()->yes() && $rightType->isString()->yes()) { + $leftIsString = $leftType->isString(); + $rightIsString = $rightType->isString(); + if ($leftIsString->yes() && $rightIsString->yes()) { return new StringType(); } + if ($leftIsString->maybe() && $rightIsString->maybe()) { + return new ErrorType(); + } $leftNumberType = $leftType->toNumber(); $rightNumberType = $rightType->toNumber(); @@ -1082,9 +1087,14 @@ public function getBitwiseOrTypeFromTypes(Type $leftType, Type $rightType): Type $rightType = $this->optimizeScalarType($rightType); } - if ($leftType->isString()->yes() && $rightType->isString()->yes()) { + $leftIsString = $leftType->isString(); + $rightIsString = $rightType->isString(); + if ($leftIsString->yes() && $rightIsString->yes()) { return new StringType(); } + if ($leftIsString->maybe() && $rightIsString->maybe()) { + return new ErrorType(); + } if (TypeCombinator::union($leftType->toNumber(), $rightType->toNumber()) instanceof ErrorType) { return new ErrorType(); @@ -1146,9 +1156,14 @@ public function getBitwiseXorTypeFromTypes(Type $leftType, Type $rightType): Typ $rightType = $this->optimizeScalarType($rightType); } - if ($leftType->isString()->yes() && $rightType->isString()->yes()) { + $leftIsString = $leftType->isString(); + $rightIsString = $rightType->isString(); + if ($leftIsString->yes() && $rightIsString->yes()) { return new StringType(); } + if ($leftIsString->maybe() && $rightIsString->maybe()) { + return new ErrorType(); + } if (TypeCombinator::union($leftType->toNumber(), $rightType->toNumber()) instanceof ErrorType) { return new ErrorType(); diff --git a/tests/PHPStan/Analyser/nsrt/bitwise.php b/tests/PHPStan/Analyser/nsrt/bitwise.php new file mode 100644 index 0000000000..fb5a06b622 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bitwise.php @@ -0,0 +1,45 @@ +analyse([__DIR__ . '/data/bug-13784.php'], []); } + public function testBug8094(): void + { + $this->analyse([__DIR__ . '/data/bug-8094.php'], []); + } + public function testBug13556(): void { $this->checkExplicitMixed = true; diff --git a/tests/PHPStan/Rules/Functions/data/bug-8094.php b/tests/PHPStan/Rules/Functions/data/bug-8094.php new file mode 100644 index 0000000000..620ffd0597 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-8094.php @@ -0,0 +1,10 @@ + Date: Fri, 5 Dec 2025 11:47:46 +0100 Subject: [PATCH 2/3] Update --- tests/PHPStan/Analyser/Generator/data/gnsr.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/PHPStan/Analyser/Generator/data/gnsr.php b/tests/PHPStan/Analyser/Generator/data/gnsr.php index c6bddf33ff..c1708bd0d6 100644 --- a/tests/PHPStan/Analyser/Generator/data/gnsr.php +++ b/tests/PHPStan/Analyser/Generator/data/gnsr.php @@ -110,7 +110,7 @@ public function doBitwiseNot($a, int $b): void public function doBitwiseAnd($a, $b, int $c, int $d): void { assertType('int', $a & $b); - assertNativeType('int', $a & $b); + assertNativeType('*ERROR*', $a & $b); assertType('1', 1 & 1); assertNativeType('1', 1 & 1); assertType('int', $c & $d); @@ -125,7 +125,7 @@ public function doBitwiseAnd($a, $b, int $c, int $d): void public function doBitwiseOr($a, $b, int $c, int $d): void { assertType('int', $a | $b); - assertNativeType('int', $a | $b); + assertNativeType('*ERROR*', $a | $b); assertType('1', 1 | 1); assertNativeType('1', 1 | 1); assertType('int', $c | $d); @@ -140,7 +140,7 @@ public function doBitwiseOr($a, $b, int $c, int $d): void public function doBitwiseXor($a, $b, int $c, int $d): void { assertType('int', $a ^ $b); - assertNativeType('int', $a ^ $b); + assertNativeType('*ERROR*', $a ^ $b); assertType('0', 1 ^ 1); assertNativeType('0', 1 ^ 1); assertType('int', $c ^ $d); From 03cd5a40a1252e6c8fd46652f188eb1549865023 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 5 Dec 2025 12:21:38 +0100 Subject: [PATCH 3/3] Try --- .../InitializerExprTypeResolver.php | 27 +++++++++++++++-- .../PHPStan/Analyser/Generator/data/gnsr.php | 6 ++-- tests/PHPStan/Analyser/nsrt/bitwise.php | 30 +++++++++++++++---- 3 files changed, 51 insertions(+), 12 deletions(-) diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index 068b423d21..c76661d6d2 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -1008,9 +1008,16 @@ public function getBitwiseAndTypeFromTypes(Type $leftType, Type $rightType): Typ $rightType = $this->optimizeScalarType($rightType); } + if ($leftType instanceof MixedType && $rightType instanceof MixedType) { + return new BenevolentUnionType([new IntegerType(), new StringType()]); + } + $leftIsString = $leftType->isString(); $rightIsString = $rightType->isString(); - if ($leftIsString->yes() && $rightIsString->yes()) { + if ( + ($leftIsString->yes() || $leftType instanceof MixedType) + && ($rightIsString->yes() || $rightType instanceof MixedType) + ) { return new StringType(); } if ($leftIsString->maybe() && $rightIsString->maybe()) { @@ -1087,9 +1094,16 @@ public function getBitwiseOrTypeFromTypes(Type $leftType, Type $rightType): Type $rightType = $this->optimizeScalarType($rightType); } + if ($leftType instanceof MixedType && $rightType instanceof MixedType) { + return new BenevolentUnionType([new IntegerType(), new StringType()]); + } + $leftIsString = $leftType->isString(); $rightIsString = $rightType->isString(); - if ($leftIsString->yes() && $rightIsString->yes()) { + if ( + ($leftIsString->yes() || $leftType instanceof MixedType) + && ($rightIsString->yes() || $rightType instanceof MixedType) + ) { return new StringType(); } if ($leftIsString->maybe() && $rightIsString->maybe()) { @@ -1156,9 +1170,16 @@ public function getBitwiseXorTypeFromTypes(Type $leftType, Type $rightType): Typ $rightType = $this->optimizeScalarType($rightType); } + if ($leftType instanceof MixedType && $rightType instanceof MixedType) { + return new BenevolentUnionType([new IntegerType(), new StringType()]); + } + $leftIsString = $leftType->isString(); $rightIsString = $rightType->isString(); - if ($leftIsString->yes() && $rightIsString->yes()) { + if ( + ($leftIsString->yes() || $leftType instanceof MixedType) + && ($rightIsString->yes() || $rightType instanceof MixedType) + ) { return new StringType(); } if ($leftIsString->maybe() && $rightIsString->maybe()) { diff --git a/tests/PHPStan/Analyser/Generator/data/gnsr.php b/tests/PHPStan/Analyser/Generator/data/gnsr.php index c1708bd0d6..a5324af90c 100644 --- a/tests/PHPStan/Analyser/Generator/data/gnsr.php +++ b/tests/PHPStan/Analyser/Generator/data/gnsr.php @@ -110,7 +110,7 @@ public function doBitwiseNot($a, int $b): void public function doBitwiseAnd($a, $b, int $c, int $d): void { assertType('int', $a & $b); - assertNativeType('*ERROR*', $a & $b); + assertNativeType('(int|string)', $a & $b); assertType('1', 1 & 1); assertNativeType('1', 1 & 1); assertType('int', $c & $d); @@ -125,7 +125,7 @@ public function doBitwiseAnd($a, $b, int $c, int $d): void public function doBitwiseOr($a, $b, int $c, int $d): void { assertType('int', $a | $b); - assertNativeType('*ERROR*', $a | $b); + assertNativeType('(int|string)', $a | $b); assertType('1', 1 | 1); assertNativeType('1', 1 | 1); assertType('int', $c | $d); @@ -140,7 +140,7 @@ public function doBitwiseOr($a, $b, int $c, int $d): void public function doBitwiseXor($a, $b, int $c, int $d): void { assertType('int', $a ^ $b); - assertNativeType('*ERROR*', $a ^ $b); + assertNativeType('(int|string)', $a ^ $b); assertType('0', 1 ^ 1); assertNativeType('0', 1 ^ 1); assertType('int', $c ^ $d); diff --git a/tests/PHPStan/Analyser/nsrt/bitwise.php b/tests/PHPStan/Analyser/nsrt/bitwise.php index fb5a06b622..6acac4237c 100644 --- a/tests/PHPStan/Analyser/nsrt/bitwise.php +++ b/tests/PHPStan/Analyser/nsrt/bitwise.php @@ -14,32 +14,50 @@ function test(int $int, string $string, $stringOrInt, $mixed) : void assertType('*ERROR*', $int & $string); assertType('*ERROR*', $int & $stringOrInt); assertType('int', $int & $mixed); + assertType('*ERROR*', $string & $int); assertType('string', $string & $string); assertType('*ERROR*', $string & $stringOrInt); - assertType('*ERROR*', $string & $mixed); + assertType('string', $string & $mixed); + assertType('*ERROR*', $stringOrInt & $int); + assertType('*ERROR*', $stringOrInt & $string); assertType('*ERROR*', $stringOrInt & $stringOrInt); assertType('*ERROR*', $stringOrInt & $mixed); - assertType('*ERROR*', $mixed & $mixed); + assertType('int', $mixed & $int); + assertType('string', $mixed & $string); + assertType('*ERROR*', $mixed & $stringOrInt); + assertType('(int|string)', $mixed & $mixed); assertType('int', $int | $int); assertType('*ERROR*', $int | $string); assertType('*ERROR*', $int | $stringOrInt); assertType('int', $int | $mixed); + assertType('*ERROR*', $string | $int); assertType('string', $string | $string); assertType('*ERROR*', $string | $stringOrInt); - assertType('*ERROR*', $string | $mixed); + assertType('string', $string | $mixed); + assertType('*ERROR*', $stringOrInt | $int); + assertType('*ERROR*', $stringOrInt | $string); assertType('*ERROR*', $stringOrInt | $stringOrInt); assertType('*ERROR*', $stringOrInt | $mixed); - assertType('*ERROR*', $mixed | $mixed); + assertType('int', $mixed | $int); + assertType('string', $mixed | $string); + assertType('*ERROR*', $mixed | $stringOrInt); + assertType('(int|string)', $mixed | $mixed); assertType('int', $int ^ $int); assertType('*ERROR*', $int ^ $string); assertType('*ERROR*', $int ^ $stringOrInt); assertType('int', $int ^ $mixed); + assertType('*ERROR*', $string ^ $int); assertType('string', $string ^ $string); assertType('*ERROR*', $string ^ $stringOrInt); - assertType('*ERROR*', $string ^ $mixed); + assertType('string', $string ^ $mixed); + assertType('*ERROR*', $stringOrInt ^ $int); + assertType('*ERROR*', $stringOrInt ^ $string); assertType('*ERROR*', $stringOrInt ^ $stringOrInt); assertType('*ERROR*', $stringOrInt ^ $mixed); - assertType('*ERROR*', $mixed ^ $mixed); + assertType('int', $mixed ^ $int); + assertType('string', $mixed ^ $string); + assertType('*ERROR*', $mixed ^ $stringOrInt); + assertType('(int|string)', $mixed ^ $mixed); }