Skip to content

Commit 332e405

Browse files
Firehedclaude
andauthored
Introduce UnaryOperatorTypeSpecifyingExtension interface (#5284)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 3255013 commit 332e405

15 files changed

+268
-2
lines changed

src/Analyser/ExprHandler/UnaryPlusHandler.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use PHPStan\Analyser\MutatingScope;
1313
use PHPStan\Analyser\NodeScopeResolver;
1414
use PHPStan\DependencyInjection\AutowiredService;
15+
use PHPStan\Reflection\InitializerExprTypeResolver;
1516
use PHPStan\Type\Type;
1617

1718
/**
@@ -21,6 +22,12 @@
2122
final class UnaryPlusHandler implements ExprHandler
2223
{
2324

25+
public function __construct(
26+
private InitializerExprTypeResolver $initializerExprTypeResolver,
27+
)
28+
{
29+
}
30+
2431
public function supports(Expr $expr): bool
2532
{
2633
return $expr instanceof UnaryPlus;
@@ -41,7 +48,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
4148

4249
public function resolveType(MutatingScope $scope, Expr $expr): Type
4350
{
44-
return $scope->getType($expr->expr)->toNumber();
51+
return $this->initializerExprTypeResolver->getUnaryPlusType($expr->expr, static fn (Expr $expr): Type => $scope->getType($expr));
4552
}
4653

4754
}

src/Broker/BrokerFactory.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ final class BrokerFactory
1212
public const DYNAMIC_STATIC_METHOD_RETURN_TYPE_EXTENSION_TAG = 'phpstan.broker.dynamicStaticMethodReturnTypeExtension';
1313
public const DYNAMIC_FUNCTION_RETURN_TYPE_EXTENSION_TAG = 'phpstan.broker.dynamicFunctionReturnTypeExtension';
1414
public const OPERATOR_TYPE_SPECIFYING_EXTENSION_TAG = 'phpstan.broker.operatorTypeSpecifyingExtension';
15+
public const UNARY_OPERATOR_TYPE_SPECIFYING_EXTENSION_TAG = 'phpstan.broker.unaryOperatorTypeSpecifyingExtension';
1516
public const EXPRESSION_TYPE_RESOLVER_EXTENSION_TAG = 'phpstan.broker.expressionTypeResolverExtension';
1617

1718
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\DependencyInjection\Type;
4+
5+
use PHPStan\Broker\BrokerFactory;
6+
use PHPStan\DependencyInjection\AutowiredService;
7+
use PHPStan\DependencyInjection\Container;
8+
use PHPStan\Type\UnaryOperatorTypeSpecifyingExtensionRegistry;
9+
10+
#[AutowiredService(as: UnaryOperatorTypeSpecifyingExtensionRegistryProvider::class)]
11+
final class LazyUnaryOperatorTypeSpecifyingExtensionRegistryProvider implements UnaryOperatorTypeSpecifyingExtensionRegistryProvider
12+
{
13+
14+
private ?UnaryOperatorTypeSpecifyingExtensionRegistry $registry = null;
15+
16+
public function __construct(private Container $container)
17+
{
18+
}
19+
20+
public function getRegistry(): UnaryOperatorTypeSpecifyingExtensionRegistry
21+
{
22+
return $this->registry ??= new UnaryOperatorTypeSpecifyingExtensionRegistry(
23+
$this->container->getServicesByTag(BrokerFactory::UNARY_OPERATOR_TYPE_SPECIFYING_EXTENSION_TAG),
24+
);
25+
}
26+
27+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\DependencyInjection\Type;
4+
5+
use PHPStan\Type\UnaryOperatorTypeSpecifyingExtensionRegistry;
6+
7+
interface UnaryOperatorTypeSpecifyingExtensionRegistryProvider
8+
{
9+
10+
public function getRegistry(): UnaryOperatorTypeSpecifyingExtensionRegistry;
11+
12+
}

src/DependencyInjection/ValidateIgnoredErrorsExtension.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use PHPStan\Analyser\NameScope;
1313
use PHPStan\Command\IgnoredRegexValidator;
1414
use PHPStan\DependencyInjection\Type\OperatorTypeSpecifyingExtensionRegistryProvider;
15+
use PHPStan\DependencyInjection\Type\UnaryOperatorTypeSpecifyingExtensionRegistryProvider;
1516
use PHPStan\File\FileExcluder;
1617
use PHPStan\Php\ComposerPhpVersionFactory;
1718
use PHPStan\Php\PhpVersion;
@@ -35,6 +36,7 @@
3536
use PHPStan\Type\OperatorTypeSpecifyingExtensionRegistry;
3637
use PHPStan\Type\Type;
3738
use PHPStan\Type\TypeAliasResolver;
39+
use PHPStan\Type\UnaryOperatorTypeSpecifyingExtensionRegistry;
3840
use function array_keys;
3941
use function array_map;
4042
use function count;
@@ -129,6 +131,13 @@ public function getRegistry(): OperatorTypeSpecifyingExtensionRegistry
129131
return new OperatorTypeSpecifyingExtensionRegistry([]);
130132
}
131133

134+
}, new class implements UnaryOperatorTypeSpecifyingExtensionRegistryProvider {
135+
136+
public function getRegistry(): UnaryOperatorTypeSpecifyingExtensionRegistry
137+
{
138+
return new UnaryOperatorTypeSpecifyingExtensionRegistry([]);
139+
}
140+
132141
}, new OversizedArrayBuilder(), true),
133142
),
134143
),

src/DependencyInjection/ValidateServiceTagsExtension.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
use PHPStan\Type\StaticMethodParameterClosureTypeExtension;
6464
use PHPStan\Type\StaticMethodParameterOutTypeExtension;
6565
use PHPStan\Type\StaticMethodTypeSpecifyingExtension;
66+
use PHPStan\Type\UnaryOperatorTypeSpecifyingExtension;
6667
use ReflectionClass;
6768
use function array_flip;
6869
use function array_key_exists;
@@ -80,6 +81,7 @@ final class ValidateServiceTagsExtension extends CompilerExtension
8081
DynamicStaticMethodReturnTypeExtension::class => BrokerFactory::DYNAMIC_STATIC_METHOD_RETURN_TYPE_EXTENSION_TAG,
8182
DynamicFunctionReturnTypeExtension::class => BrokerFactory::DYNAMIC_FUNCTION_RETURN_TYPE_EXTENSION_TAG,
8283
OperatorTypeSpecifyingExtension::class => BrokerFactory::OPERATOR_TYPE_SPECIFYING_EXTENSION_TAG,
84+
UnaryOperatorTypeSpecifyingExtension::class => BrokerFactory::UNARY_OPERATOR_TYPE_SPECIFYING_EXTENSION_TAG,
8385
ExpressionTypeResolverExtension::class => BrokerFactory::EXPRESSION_TYPE_RESOLVER_EXTENSION_TAG,
8486
TypeNodeResolverExtension::class => TypeNodeResolverExtension::EXTENSION_TAG,
8587
Rule::class => LazyRegistry::RULE_TAG,

src/Reflection/InitializerExprTypeResolver.php

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
use PHPStan\DependencyInjection\AutowiredParameter;
3434
use PHPStan\DependencyInjection\AutowiredService;
3535
use PHPStan\DependencyInjection\Type\OperatorTypeSpecifyingExtensionRegistryProvider;
36+
use PHPStan\DependencyInjection\Type\UnaryOperatorTypeSpecifyingExtensionRegistryProvider;
3637
use PHPStan\Node\Expr\TypeExpr;
3738
use PHPStan\Php\PhpVersion;
3839
use PHPStan\PhpDoc\Tag\TemplateTag;
@@ -136,6 +137,7 @@ public function __construct(
136137
private ReflectionProviderProvider $reflectionProviderProvider,
137138
private PhpVersion $phpVersion,
138139
private OperatorTypeSpecifyingExtensionRegistryProvider $operatorTypeSpecifyingExtensionRegistryProvider,
140+
private UnaryOperatorTypeSpecifyingExtensionRegistryProvider $unaryOperatorTypeSpecifyingExtensionRegistryProvider,
139141
private OversizedArrayBuilder $oversizedArrayBuilder,
140142
#[AutowiredParameter]
141143
private bool $usePathConstantsAsConstantString,
@@ -270,7 +272,7 @@ public function getType(Expr $expr, InitializerExprContext $context): Type
270272
return $this->getClassConstFetchType($expr->class, $expr->name->toString(), $context->getClassName(), fn (Expr $expr): Type => $this->getType($expr, $context));
271273
}
272274
if ($expr instanceof Expr\UnaryPlus) {
273-
return $this->getType($expr->expr, $context)->toNumber();
275+
return $this->getUnaryPlusType($expr->expr, fn (Expr $expr): Type => $this->getType($expr, $context));
274276
}
275277
if ($expr instanceof Expr\UnaryMinus) {
276278
return $this->getUnaryMinusType($expr->expr, fn (Expr $expr): Type => $this->getType($expr, $context));
@@ -2604,13 +2606,35 @@ public function getClassConstFetchType(Name|Expr $class, string $constantName, ?
26042606
return $this->getClassConstFetchTypeByReflection($class, $constantName, $classReflection, $getTypeCallback);
26052607
}
26062608

2609+
/**
2610+
* @param callable(Expr): Type $getTypeCallback
2611+
*/
2612+
public function getUnaryPlusType(Expr $expr, callable $getTypeCallback): Type
2613+
{
2614+
$type = $getTypeCallback($expr);
2615+
2616+
$specifiedTypes = $this->unaryOperatorTypeSpecifyingExtensionRegistryProvider->getRegistry()
2617+
->callUnaryOperatorTypeSpecifyingExtensions('+', $type);
2618+
if ($specifiedTypes !== null) {
2619+
return $specifiedTypes;
2620+
}
2621+
2622+
return $type->toNumber();
2623+
}
2624+
26072625
/**
26082626
* @param callable(Expr): Type $getTypeCallback
26092627
*/
26102628
public function getUnaryMinusType(Expr $expr, callable $getTypeCallback): Type
26112629
{
26122630
$type = $getTypeCallback($expr);
26132631

2632+
$specifiedTypes = $this->unaryOperatorTypeSpecifyingExtensionRegistryProvider->getRegistry()
2633+
->callUnaryOperatorTypeSpecifyingExtensions('-', $type);
2634+
if ($specifiedTypes !== null) {
2635+
return $specifiedTypes;
2636+
}
2637+
26142638
$type = $this->getUnaryMinusTypeFromType($expr, $type);
26152639
if ($type instanceof IntegerRangeType) {
26162640
return $getTypeCallback(new Expr\BinaryOp\Mul($expr, new Int_(-1)));
@@ -2652,6 +2676,12 @@ public function getBitwiseNotType(Expr $expr, callable $getTypeCallback): Type
26522676
{
26532677
$exprType = $getTypeCallback($expr);
26542678

2679+
$specifiedTypes = $this->unaryOperatorTypeSpecifyingExtensionRegistryProvider->getRegistry()
2680+
->callUnaryOperatorTypeSpecifyingExtensions('~', $exprType);
2681+
if ($specifiedTypes !== null) {
2682+
return $specifiedTypes;
2683+
}
2684+
26552685
return $this->getBitwiseNotTypeFromType($exprType);
26562686
}
26572687

src/Testing/PHPStanTestCase.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use PHPStan\DependencyInjection\Reflection\ClassReflectionExtensionRegistryProvider;
1212
use PHPStan\DependencyInjection\Type\ExpressionTypeResolverExtensionRegistryProvider;
1313
use PHPStan\DependencyInjection\Type\OperatorTypeSpecifyingExtensionRegistryProvider;
14+
use PHPStan\DependencyInjection\Type\UnaryOperatorTypeSpecifyingExtensionRegistryProvider;
1415
use PHPStan\Node\Printer\ExprPrinter;
1516
use PHPStan\Parser\Parser;
1617
use PHPStan\Php\ComposerPhpVersionFactory;
@@ -82,6 +83,7 @@ public static function createScopeFactory(ReflectionProvider $reflectionProvider
8283
$reflectionProviderProvider,
8384
$container->getByType(PhpVersion::class),
8485
$container->getByType(OperatorTypeSpecifyingExtensionRegistryProvider::class),
86+
$container->getByType(UnaryOperatorTypeSpecifyingExtensionRegistryProvider::class),
8587
new OversizedArrayBuilder(),
8688
$container->getParameter('usePathConstantsAsConstantString'),
8789
);
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type;
4+
5+
/**
6+
* This is the extension interface to implement if you want to describe
7+
* how unary operators like -, +, ~ should infer types
8+
* for PHP extensions that overload the behaviour, like GMP.
9+
*
10+
* To register it in the configuration file use the `phpstan.broker.unaryOperatorTypeSpecifyingExtension` service tag:
11+
*
12+
* ```
13+
* services:
14+
* -
15+
* class: App\PHPStan\MyExtension
16+
* tags:
17+
* - phpstan.broker.unaryOperatorTypeSpecifyingExtension
18+
* ```
19+
*
20+
* @api
21+
*/
22+
interface UnaryOperatorTypeSpecifyingExtension
23+
{
24+
25+
public function isOperatorSupported(string $operatorSigil, Type $operand): bool;
26+
27+
public function specifyType(string $operatorSigil, Type $operand): Type;
28+
29+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type;
4+
5+
use function array_filter;
6+
use function array_values;
7+
use function count;
8+
9+
final class UnaryOperatorTypeSpecifyingExtensionRegistry
10+
{
11+
12+
/**
13+
* @param UnaryOperatorTypeSpecifyingExtension[] $extensions
14+
*/
15+
public function __construct(
16+
private array $extensions,
17+
)
18+
{
19+
}
20+
21+
/**
22+
* @return UnaryOperatorTypeSpecifyingExtension[]
23+
*/
24+
private function getOperatorTypeSpecifyingExtensions(string $operator, Type $operandType): array
25+
{
26+
return array_values(array_filter($this->extensions, static fn (UnaryOperatorTypeSpecifyingExtension $extension): bool => $extension->isOperatorSupported($operator, $operandType)));
27+
}
28+
29+
public function callUnaryOperatorTypeSpecifyingExtensions(string $operatorSigil, Type $operandType): ?Type
30+
{
31+
$operatorTypeSpecifyingExtensions = $this->getOperatorTypeSpecifyingExtensions($operatorSigil, $operandType);
32+
33+
/** @var list<Type> $extensionTypes */
34+
$extensionTypes = [];
35+
36+
foreach ($operatorTypeSpecifyingExtensions as $extension) {
37+
$extensionTypes[] = $extension->specifyType($operatorSigil, $operandType);
38+
}
39+
40+
if (count($extensionTypes) > 0) {
41+
return TypeCombinator::union(...$extensionTypes);
42+
}
43+
44+
return null;
45+
}
46+
47+
}

0 commit comments

Comments
 (0)