diff --git a/src/Analyser/ExprHandler/UnaryPlusHandler.php b/src/Analyser/ExprHandler/UnaryPlusHandler.php index 501fc6e095..b03b24d527 100644 --- a/src/Analyser/ExprHandler/UnaryPlusHandler.php +++ b/src/Analyser/ExprHandler/UnaryPlusHandler.php @@ -12,6 +12,7 @@ use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Type\Type; /** @@ -21,6 +22,12 @@ final class UnaryPlusHandler implements ExprHandler { + public function __construct( + private InitializerExprTypeResolver $initializerExprTypeResolver, + ) + { + } + public function supports(Expr $expr): bool { return $expr instanceof UnaryPlus; @@ -41,7 +48,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex public function resolveType(MutatingScope $scope, Expr $expr): Type { - return $scope->getType($expr->expr)->toNumber(); + return $this->initializerExprTypeResolver->getUnaryPlusType($expr->expr, static fn (Expr $expr): Type => $scope->getType($expr)); } } diff --git a/src/Broker/BrokerFactory.php b/src/Broker/BrokerFactory.php index bbd8d97a3d..3637fcd48b 100644 --- a/src/Broker/BrokerFactory.php +++ b/src/Broker/BrokerFactory.php @@ -12,6 +12,7 @@ final class BrokerFactory public const DYNAMIC_STATIC_METHOD_RETURN_TYPE_EXTENSION_TAG = 'phpstan.broker.dynamicStaticMethodReturnTypeExtension'; public const DYNAMIC_FUNCTION_RETURN_TYPE_EXTENSION_TAG = 'phpstan.broker.dynamicFunctionReturnTypeExtension'; public const OPERATOR_TYPE_SPECIFYING_EXTENSION_TAG = 'phpstan.broker.operatorTypeSpecifyingExtension'; + public const UNARY_OPERATOR_TYPE_SPECIFYING_EXTENSION_TAG = 'phpstan.broker.unaryOperatorTypeSpecifyingExtension'; public const EXPRESSION_TYPE_RESOLVER_EXTENSION_TAG = 'phpstan.broker.expressionTypeResolverExtension'; } diff --git a/src/DependencyInjection/Type/LazyUnaryOperatorTypeSpecifyingExtensionRegistryProvider.php b/src/DependencyInjection/Type/LazyUnaryOperatorTypeSpecifyingExtensionRegistryProvider.php new file mode 100644 index 0000000000..631ad4f3b5 --- /dev/null +++ b/src/DependencyInjection/Type/LazyUnaryOperatorTypeSpecifyingExtensionRegistryProvider.php @@ -0,0 +1,27 @@ +registry ??= new UnaryOperatorTypeSpecifyingExtensionRegistry( + $this->container->getServicesByTag(BrokerFactory::UNARY_OPERATOR_TYPE_SPECIFYING_EXTENSION_TAG), + ); + } + +} diff --git a/src/DependencyInjection/Type/UnaryOperatorTypeSpecifyingExtensionRegistryProvider.php b/src/DependencyInjection/Type/UnaryOperatorTypeSpecifyingExtensionRegistryProvider.php new file mode 100644 index 0000000000..6fb8b39530 --- /dev/null +++ b/src/DependencyInjection/Type/UnaryOperatorTypeSpecifyingExtensionRegistryProvider.php @@ -0,0 +1,12 @@ + BrokerFactory::DYNAMIC_STATIC_METHOD_RETURN_TYPE_EXTENSION_TAG, DynamicFunctionReturnTypeExtension::class => BrokerFactory::DYNAMIC_FUNCTION_RETURN_TYPE_EXTENSION_TAG, OperatorTypeSpecifyingExtension::class => BrokerFactory::OPERATOR_TYPE_SPECIFYING_EXTENSION_TAG, + UnaryOperatorTypeSpecifyingExtension::class => BrokerFactory::UNARY_OPERATOR_TYPE_SPECIFYING_EXTENSION_TAG, ExpressionTypeResolverExtension::class => BrokerFactory::EXPRESSION_TYPE_RESOLVER_EXTENSION_TAG, TypeNodeResolverExtension::class => TypeNodeResolverExtension::EXTENSION_TAG, Rule::class => LazyRegistry::RULE_TAG, diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index f7a8f872e2..b4578930ac 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -33,6 +33,7 @@ use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\DependencyInjection\Type\OperatorTypeSpecifyingExtensionRegistryProvider; +use PHPStan\DependencyInjection\Type\UnaryOperatorTypeSpecifyingExtensionRegistryProvider; use PHPStan\Node\Expr\TypeExpr; use PHPStan\Php\PhpVersion; use PHPStan\PhpDoc\Tag\TemplateTag; @@ -136,6 +137,7 @@ public function __construct( private ReflectionProviderProvider $reflectionProviderProvider, private PhpVersion $phpVersion, private OperatorTypeSpecifyingExtensionRegistryProvider $operatorTypeSpecifyingExtensionRegistryProvider, + private UnaryOperatorTypeSpecifyingExtensionRegistryProvider $unaryOperatorTypeSpecifyingExtensionRegistryProvider, private OversizedArrayBuilder $oversizedArrayBuilder, #[AutowiredParameter] private bool $usePathConstantsAsConstantString, @@ -270,7 +272,7 @@ public function getType(Expr $expr, InitializerExprContext $context): Type return $this->getClassConstFetchType($expr->class, $expr->name->toString(), $context->getClassName(), fn (Expr $expr): Type => $this->getType($expr, $context)); } if ($expr instanceof Expr\UnaryPlus) { - return $this->getType($expr->expr, $context)->toNumber(); + return $this->getUnaryPlusType($expr->expr, fn (Expr $expr): Type => $this->getType($expr, $context)); } if ($expr instanceof Expr\UnaryMinus) { return $this->getUnaryMinusType($expr->expr, fn (Expr $expr): Type => $this->getType($expr, $context)); @@ -2604,6 +2606,22 @@ public function getClassConstFetchType(Name|Expr $class, string $constantName, ? return $this->getClassConstFetchTypeByReflection($class, $constantName, $classReflection, $getTypeCallback); } + /** + * @param callable(Expr): Type $getTypeCallback + */ + public function getUnaryPlusType(Expr $expr, callable $getTypeCallback): Type + { + $type = $getTypeCallback($expr); + + $specifiedTypes = $this->unaryOperatorTypeSpecifyingExtensionRegistryProvider->getRegistry() + ->callUnaryOperatorTypeSpecifyingExtensions('+', $type); + if ($specifiedTypes !== null) { + return $specifiedTypes; + } + + return $type->toNumber(); + } + /** * @param callable(Expr): Type $getTypeCallback */ @@ -2611,6 +2629,12 @@ public function getUnaryMinusType(Expr $expr, callable $getTypeCallback): Type { $type = $getTypeCallback($expr); + $specifiedTypes = $this->unaryOperatorTypeSpecifyingExtensionRegistryProvider->getRegistry() + ->callUnaryOperatorTypeSpecifyingExtensions('-', $type); + if ($specifiedTypes !== null) { + return $specifiedTypes; + } + $type = $this->getUnaryMinusTypeFromType($expr, $type); if ($type instanceof IntegerRangeType) { return $getTypeCallback(new Expr\BinaryOp\Mul($expr, new Int_(-1))); @@ -2652,6 +2676,12 @@ public function getBitwiseNotType(Expr $expr, callable $getTypeCallback): Type { $exprType = $getTypeCallback($expr); + $specifiedTypes = $this->unaryOperatorTypeSpecifyingExtensionRegistryProvider->getRegistry() + ->callUnaryOperatorTypeSpecifyingExtensions('~', $exprType); + if ($specifiedTypes !== null) { + return $specifiedTypes; + } + return $this->getBitwiseNotTypeFromType($exprType); } diff --git a/src/Testing/PHPStanTestCase.php b/src/Testing/PHPStanTestCase.php index d85c4f3a5f..ac028fdcf3 100644 --- a/src/Testing/PHPStanTestCase.php +++ b/src/Testing/PHPStanTestCase.php @@ -11,6 +11,7 @@ use PHPStan\DependencyInjection\Reflection\ClassReflectionExtensionRegistryProvider; use PHPStan\DependencyInjection\Type\ExpressionTypeResolverExtensionRegistryProvider; use PHPStan\DependencyInjection\Type\OperatorTypeSpecifyingExtensionRegistryProvider; +use PHPStan\DependencyInjection\Type\UnaryOperatorTypeSpecifyingExtensionRegistryProvider; use PHPStan\Node\Printer\ExprPrinter; use PHPStan\Parser\Parser; use PHPStan\Php\ComposerPhpVersionFactory; @@ -82,6 +83,7 @@ public static function createScopeFactory(ReflectionProvider $reflectionProvider $reflectionProviderProvider, $container->getByType(PhpVersion::class), $container->getByType(OperatorTypeSpecifyingExtensionRegistryProvider::class), + $container->getByType(UnaryOperatorTypeSpecifyingExtensionRegistryProvider::class), new OversizedArrayBuilder(), $container->getParameter('usePathConstantsAsConstantString'), ); diff --git a/src/Type/UnaryOperatorTypeSpecifyingExtension.php b/src/Type/UnaryOperatorTypeSpecifyingExtension.php new file mode 100644 index 0000000000..cfa65d5abf --- /dev/null +++ b/src/Type/UnaryOperatorTypeSpecifyingExtension.php @@ -0,0 +1,29 @@ +extensions, static fn (UnaryOperatorTypeSpecifyingExtension $extension): bool => $extension->isOperatorSupported($operator, $operandType))); + } + + public function callUnaryOperatorTypeSpecifyingExtensions(string $operatorSigil, Type $operandType): ?Type + { + $operatorTypeSpecifyingExtensions = $this->getOperatorTypeSpecifyingExtensions($operatorSigil, $operandType); + + /** @var list $extensionTypes */ + $extensionTypes = []; + + foreach ($operatorTypeSpecifyingExtensions as $extension) { + $extensionTypes[] = $extension->specifyType($operatorSigil, $operandType); + } + + if (count($extensionTypes) > 0) { + return TypeCombinator::union(...$extensionTypes); + } + + return null; + } + +} diff --git a/tests/PHPStan/Analyser/UnaryOperatorTypeSpecifyingExtensionTypeInferenceTest.php b/tests/PHPStan/Analyser/UnaryOperatorTypeSpecifyingExtensionTypeInferenceTest.php new file mode 100644 index 0000000000..9661496dcb --- /dev/null +++ b/tests/PHPStan/Analyser/UnaryOperatorTypeSpecifyingExtensionTypeInferenceTest.php @@ -0,0 +1,36 @@ +assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/unary-operator-type-specifying-extension.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/data/unary-operator-type-specifying-extension.php b/tests/PHPStan/Analyser/data/unary-operator-type-specifying-extension.php new file mode 100644 index 0000000000..29715bd00c --- /dev/null +++ b/tests/PHPStan/Analyser/data/unary-operator-type-specifying-extension.php @@ -0,0 +1,21 @@ +isSuperTypeOf($operand)->yes(); + } + + public function specifyType(string $operatorSigil, Type $operand): Type + { + return new ObjectType(TestUnaryOperand::class); + } + +}