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 0891f487b7..108d2c20f1 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 0000000000..bc2493201e --- /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 0000000000..6d53c8c243 --- /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 108d2c20f1..f7a8f872e2 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 6d53c8c243..24a3e15a9c 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 44df16a2c8..b679d73efd 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(); }