From 2a3eab40d411d28e6238858dcfa55985991d1a1f Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Sun, 15 Mar 2026 15:06:50 -0700 Subject: [PATCH 1/2] Call operator type specifying extensions for bitwise and arithmetic operators - Add extension calls to getBitwiseAndType, getBitwiseOrType, getBitwiseXorType - Move extension call to top of resolveCommonMath (before integer range optimization) - Remove duplicate extension call later in resolveCommonMath - Add TestBitwiseOperatorTypeSpecifyingExtension for testing bitwise extension calls - Add OperatorTypeSpecifyingExtensionTypeInferenceTest with tests for both bitwise (TestBitwiseOperand) and arithmetic (TestDecimal) operators This ensures operator type specifying extensions are called consistently for all supported operators, allowing custom types to specify operator return types. Co-Authored-By: Claude Opus 4.5 --- .../InitializerExprTypeResolver.php | 30 ++++++++-- ...peSpecifyingExtensionTypeInferenceTest.php | 36 ++++++++++++ .../operator-type-specifying-extension.php | 55 +++++++++++++++++++ .../operator-type-specifying-extension.neon | 9 +++ tests/PHPStan/Fixture/TestBitwiseOperand.php | 11 ++++ ...BitwiseOperatorTypeSpecifyingExtension.php | 28 ++++++++++ 6 files changed, 163 insertions(+), 6 deletions(-) create mode 100644 tests/PHPStan/Analyser/OperatorTypeSpecifyingExtensionTypeInferenceTest.php create mode 100644 tests/PHPStan/Analyser/data/operator-type-specifying-extension.php create mode 100644 tests/PHPStan/Analyser/operator-type-specifying-extension.neon create mode 100644 tests/PHPStan/Fixture/TestBitwiseOperand.php create mode 100644 tests/PHPStan/Type/TestBitwiseOperatorTypeSpecifyingExtension.php diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index 0891f487b7f..108d2c20f15 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -986,6 +986,12 @@ public function getBitwiseAndType(Expr $left, Expr $right, callable $getTypeCall $leftType = $getTypeCallback($left); $rightType = $getTypeCallback($right); + $specifiedTypes = $this->operatorTypeSpecifyingExtensionRegistryProvider->getRegistry() + ->callOperatorTypeSpecifyingExtensions(new BinaryOp\BitwiseAnd($left, $right), $leftType, $rightType); + if ($specifiedTypes !== null) { + return $specifiedTypes; + } + return $this->getBitwiseAndTypeFromTypes($leftType, $rightType); } @@ -1044,6 +1050,12 @@ public function getBitwiseOrType(Expr $left, Expr $right, callable $getTypeCallb $leftType = $getTypeCallback($left); $rightType = $getTypeCallback($right); + $specifiedTypes = $this->operatorTypeSpecifyingExtensionRegistryProvider->getRegistry() + ->callOperatorTypeSpecifyingExtensions(new BinaryOp\BitwiseOr($left, $right), $leftType, $rightType); + if ($specifiedTypes !== null) { + return $specifiedTypes; + } + return $this->getBitwiseOrTypeFromTypes($leftType, $rightType); } @@ -1092,6 +1104,12 @@ public function getBitwiseXorType(Expr $left, Expr $right, callable $getTypeCall $leftType = $getTypeCallback($left); $rightType = $getTypeCallback($right); + $specifiedTypes = $this->operatorTypeSpecifyingExtensionRegistryProvider->getRegistry() + ->callOperatorTypeSpecifyingExtensions(new BinaryOp\BitwiseXor($left, $right), $leftType, $rightType); + if ($specifiedTypes !== null) { + return $specifiedTypes; + } + return $this->getBitwiseXorTypeFromTypes($leftType, $rightType); } @@ -2034,6 +2052,12 @@ private function resolveConstantArrayTypeComparison(ConstantArrayType $leftType, */ private function resolveCommonMath(Expr\BinaryOp $expr, Type $leftType, Type $rightType): Type { + $specifiedTypes = $this->operatorTypeSpecifyingExtensionRegistryProvider->getRegistry() + ->callOperatorTypeSpecifyingExtensions($expr, $leftType, $rightType); + if ($specifiedTypes !== null) { + return $specifiedTypes; + } + $types = TypeCombinator::union($leftType, $rightType); $leftNumberType = $leftType->toNumber(); $rightNumberType = $rightType->toNumber(); @@ -2073,12 +2097,6 @@ private function resolveCommonMath(Expr\BinaryOp $expr, Type $leftType, Type $ri } } - $specifiedTypes = $this->operatorTypeSpecifyingExtensionRegistryProvider->getRegistry() - ->callOperatorTypeSpecifyingExtensions($expr, $leftType, $rightType); - if ($specifiedTypes !== null) { - return $specifiedTypes; - } - if ( $leftType->isArray()->yes() || $rightType->isArray()->yes() diff --git a/tests/PHPStan/Analyser/OperatorTypeSpecifyingExtensionTypeInferenceTest.php b/tests/PHPStan/Analyser/OperatorTypeSpecifyingExtensionTypeInferenceTest.php new file mode 100644 index 00000000000..bc2493201eb --- /dev/null +++ b/tests/PHPStan/Analyser/OperatorTypeSpecifyingExtensionTypeInferenceTest.php @@ -0,0 +1,36 @@ +assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/operator-type-specifying-extension.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/data/operator-type-specifying-extension.php b/tests/PHPStan/Analyser/data/operator-type-specifying-extension.php new file mode 100644 index 00000000000..6d53c8c2436 --- /dev/null +++ b/tests/PHPStan/Analyser/data/operator-type-specifying-extension.php @@ -0,0 +1,55 @@ +isSuperTypeOf($leftSide)->yes() + && $testType->isSuperTypeOf($rightSide)->yes(); + } + + public function specifyType(string $operatorSigil, Type $leftSide, Type $rightSide): Type + { + return new ObjectType(TestBitwiseOperand::class); + } + +} From 027f16dc074a10c3cc56d1ca54231c641bd0495a Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Wed, 18 Mar 2026 09:57:49 -0700 Subject: [PATCH 2/2] Add extension calls for shift left/right operators Co-Authored-By: Claude Opus 4.5 --- src/Reflection/InitializerExprTypeResolver.php | 12 ++++++++++++ .../data/operator-type-specifying-extension.php | 10 ++++++++++ .../TestBitwiseOperatorTypeSpecifyingExtension.php | 2 +- 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index 108d2c20f15..f7a8f872e2e 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -1774,6 +1774,12 @@ public function getShiftLeftType(Expr $left, Expr $right, callable $getTypeCallb $leftType = $getTypeCallback($left); $rightType = $getTypeCallback($right); + $specifiedTypes = $this->operatorTypeSpecifyingExtensionRegistryProvider->getRegistry() + ->callOperatorTypeSpecifyingExtensions(new BinaryOp\ShiftLeft($left, $right), $leftType, $rightType); + if ($specifiedTypes !== null) { + return $specifiedTypes; + } + return $this->getShiftLeftTypeFromTypes($left, $right, $leftType, $rightType); } @@ -1838,6 +1844,12 @@ public function getShiftRightType(Expr $left, Expr $right, callable $getTypeCall $leftType = $getTypeCallback($left); $rightType = $getTypeCallback($right); + $specifiedTypes = $this->operatorTypeSpecifyingExtensionRegistryProvider->getRegistry() + ->callOperatorTypeSpecifyingExtensions(new BinaryOp\ShiftRight($left, $right), $leftType, $rightType); + if ($specifiedTypes !== null) { + return $specifiedTypes; + } + return $this->getShiftRightTypeFromTypes($left, $right, $leftType, $rightType); } diff --git a/tests/PHPStan/Analyser/data/operator-type-specifying-extension.php b/tests/PHPStan/Analyser/data/operator-type-specifying-extension.php index 6d53c8c2436..24a3e15a9c2 100644 --- a/tests/PHPStan/Analyser/data/operator-type-specifying-extension.php +++ b/tests/PHPStan/Analyser/data/operator-type-specifying-extension.php @@ -25,6 +25,16 @@ function testBitwiseXor(TestBitwiseOperand $a, TestBitwiseOperand $b): void assertType('PHPStan\Fixture\TestBitwiseOperand', $a ^ $b); } +function testShiftLeft(TestBitwiseOperand $a, TestBitwiseOperand $b): void +{ + assertType('PHPStan\Fixture\TestBitwiseOperand', $a << $b); +} + +function testShiftRight(TestBitwiseOperand $a, TestBitwiseOperand $b): void +{ + assertType('PHPStan\Fixture\TestBitwiseOperand', $a >> $b); +} + // ============================================================================= // Arithmetic operator extension tests (via TestDecimal) // ============================================================================= diff --git a/tests/PHPStan/Type/TestBitwiseOperatorTypeSpecifyingExtension.php b/tests/PHPStan/Type/TestBitwiseOperatorTypeSpecifyingExtension.php index 44df16a2c8b..b679d73efd3 100644 --- a/tests/PHPStan/Type/TestBitwiseOperatorTypeSpecifyingExtension.php +++ b/tests/PHPStan/Type/TestBitwiseOperatorTypeSpecifyingExtension.php @@ -15,7 +15,7 @@ public function isOperatorSupported(string $operatorSigil, Type $leftSide, Type { $testType = new ObjectType(TestBitwiseOperand::class); - return in_array($operatorSigil, ['&', '|', '^'], true) + return in_array($operatorSigil, ['&', '|', '^', '<<', '>>'], true) && $testType->isSuperTypeOf($leftSide)->yes() && $testType->isSuperTypeOf($rightSide)->yes(); }