Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/Analyser/ExprHandler/UnaryPlusHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use PHPStan\Analyser\MutatingScope;
use PHPStan\Analyser\NodeScopeResolver;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Reflection\InitializerExprTypeResolver;
use PHPStan\Type\Type;

/**
Expand All @@ -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;
Expand All @@ -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));
}

}
1 change: 1 addition & 0 deletions src/Broker/BrokerFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php declare(strict_types = 1);

namespace PHPStan\DependencyInjection\Type;

use PHPStan\Broker\BrokerFactory;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\DependencyInjection\Container;
use PHPStan\Type\UnaryOperatorTypeSpecifyingExtensionRegistry;

#[AutowiredService(as: UnaryOperatorTypeSpecifyingExtensionRegistryProvider::class)]
final class LazyUnaryOperatorTypeSpecifyingExtensionRegistryProvider implements UnaryOperatorTypeSpecifyingExtensionRegistryProvider
{

private ?UnaryOperatorTypeSpecifyingExtensionRegistry $registry = null;

public function __construct(private Container $container)
{
}

public function getRegistry(): UnaryOperatorTypeSpecifyingExtensionRegistry
{
return $this->registry ??= new UnaryOperatorTypeSpecifyingExtensionRegistry(
$this->container->getServicesByTag(BrokerFactory::UNARY_OPERATOR_TYPE_SPECIFYING_EXTENSION_TAG),
);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php declare(strict_types = 1);

namespace PHPStan\DependencyInjection\Type;

use PHPStan\Type\UnaryOperatorTypeSpecifyingExtensionRegistry;

interface UnaryOperatorTypeSpecifyingExtensionRegistryProvider
{

public function getRegistry(): UnaryOperatorTypeSpecifyingExtensionRegistry;

}
9 changes: 9 additions & 0 deletions src/DependencyInjection/ValidateIgnoredErrorsExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use PHPStan\Analyser\NameScope;
use PHPStan\Command\IgnoredRegexValidator;
use PHPStan\DependencyInjection\Type\OperatorTypeSpecifyingExtensionRegistryProvider;
use PHPStan\DependencyInjection\Type\UnaryOperatorTypeSpecifyingExtensionRegistryProvider;
use PHPStan\File\FileExcluder;
use PHPStan\Php\ComposerPhpVersionFactory;
use PHPStan\Php\PhpVersion;
Expand All @@ -33,6 +34,7 @@
use PHPStan\Type\DirectTypeAliasResolverProvider;
use PHPStan\Type\ObjectType;
use PHPStan\Type\OperatorTypeSpecifyingExtensionRegistry;
use PHPStan\Type\UnaryOperatorTypeSpecifyingExtensionRegistry;
use PHPStan\Type\Type;
use PHPStan\Type\TypeAliasResolver;
use function array_keys;
Expand Down Expand Up @@ -129,6 +131,13 @@ public function getRegistry(): OperatorTypeSpecifyingExtensionRegistry
return new OperatorTypeSpecifyingExtensionRegistry([]);
}

}, new class implements UnaryOperatorTypeSpecifyingExtensionRegistryProvider {

public function getRegistry(): UnaryOperatorTypeSpecifyingExtensionRegistry
{
return new UnaryOperatorTypeSpecifyingExtensionRegistry([]);
}

}, new OversizedArrayBuilder(), true),
),
),
Expand Down
2 changes: 2 additions & 0 deletions src/DependencyInjection/ValidateServiceTagsExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
use PHPStan\Type\MethodParameterOutTypeExtension;
use PHPStan\Type\MethodTypeSpecifyingExtension;
use PHPStan\Type\OperatorTypeSpecifyingExtension;
use PHPStan\Type\UnaryOperatorTypeSpecifyingExtension;
use PHPStan\Type\StaticMethodParameterClosureThisExtension;
use PHPStan\Type\StaticMethodParameterClosureTypeExtension;
use PHPStan\Type\StaticMethodParameterOutTypeExtension;
Expand All @@ -80,6 +81,7 @@ final class ValidateServiceTagsExtension extends CompilerExtension
DynamicStaticMethodReturnTypeExtension::class => 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,
Expand Down
32 changes: 31 additions & 1 deletion src/Reflection/InitializerExprTypeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -2604,13 +2606,35 @@ 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
*/
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)));
Expand Down Expand Up @@ -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);
}

Expand Down
2 changes: 2 additions & 0 deletions src/Testing/PHPStanTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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'),
);
Expand Down
29 changes: 29 additions & 0 deletions src/Type/UnaryOperatorTypeSpecifyingExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type;

/**
* This is the extension interface to implement if you want to describe
* how unary operators like -, +, ~ should infer types
* for PHP extensions that overload the behaviour, like GMP.
*
* To register it in the configuration file use the `phpstan.broker.unaryOperatorTypeSpecifyingExtension` service tag:
*
* ```
* services:
* -
* class: App\PHPStan\MyExtension
* tags:
* - phpstan.broker.unaryOperatorTypeSpecifyingExtension
* ```
*
* @api
*/
interface UnaryOperatorTypeSpecifyingExtension
{

public function isOperatorSupported(string $operatorSigil, Type $operand): bool;

public function specifyType(string $operatorSigil, Type $operand): Type;

}
47 changes: 47 additions & 0 deletions src/Type/UnaryOperatorTypeSpecifyingExtensionRegistry.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type;

use function array_filter;
use function array_values;
use function count;

final class UnaryOperatorTypeSpecifyingExtensionRegistry
{

/**
* @param UnaryOperatorTypeSpecifyingExtension[] $extensions
*/
public function __construct(
private array $extensions,
)
{
}

/**
* @return UnaryOperatorTypeSpecifyingExtension[]
*/
private function getOperatorTypeSpecifyingExtensions(string $operator, Type $operandType): array
{
return array_values(array_filter($this->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<Type> $extensionTypes */
$extensionTypes = [];

foreach ($operatorTypeSpecifyingExtensions as $extension) {
$extensionTypes[] = $extension->specifyType($operatorSigil, $operandType);
}

if (count($extensionTypes) > 0) {
return TypeCombinator::union(...$extensionTypes);
}

return null;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php declare(strict_types = 1);

namespace PHPStan\Analyser;

use PHPStan\Testing\TypeInferenceTestCase;
use PHPUnit\Framework\Attributes\DataProvider;

class UnaryOperatorTypeSpecifyingExtensionTypeInferenceTest extends TypeInferenceTestCase
{

public static function dataAsserts(): iterable
{
yield from self::gatherAssertTypes(__DIR__ . '/data/unary-operator-type-specifying-extension.php');
}

/**
* @param mixed ...$args
*/
#[DataProvider('dataAsserts')]
public function testAsserts(
string $assertType,
string $file,
...$args,
): void
{
$this->assertFileAsserts($assertType, $file, ...$args);
}

public static function getAdditionalConfigFiles(): array
{
return [
__DIR__ . '/unary-operator-type-specifying-extension.neon',
];
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace UnaryOperatorTypeSpecifyingExtensionTest;

use PHPStan\Fixture\TestUnaryOperand;
use function PHPStan\Testing\assertType;

function testUnaryMinus(TestUnaryOperand $a): void
{
assertType('PHPStan\Fixture\TestUnaryOperand', -$a);
}

function testUnaryPlus(TestUnaryOperand $a): void
{
assertType('PHPStan\Fixture\TestUnaryOperand', +$a);
}

function testBitwiseNot(TestUnaryOperand $a): void
{
assertType('PHPStan\Fixture\TestUnaryOperand', ~$a);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
services:
-
class: PHPStan\Type\TestUnaryOperatorTypeSpecifyingExtension
tags:
- phpstan.broker.unaryOperatorTypeSpecifyingExtension
11 changes: 11 additions & 0 deletions tests/PHPStan/Fixture/TestUnaryOperand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php declare(strict_types = 1);

namespace PHPStan\Fixture;

/**
* Test fixture class for verifying unary operator type specifying extensions.
*/
final class TestUnaryOperand
{

}
27 changes: 27 additions & 0 deletions tests/PHPStan/Type/TestUnaryOperatorTypeSpecifyingExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type;

use PHPStan\Fixture\TestUnaryOperand;
use function in_array;

/**
* Test extension for verifying that unary operators call type specifying extensions.
*/
final class TestUnaryOperatorTypeSpecifyingExtension implements UnaryOperatorTypeSpecifyingExtension
{

public function isOperatorSupported(string $operatorSigil, Type $operand): bool
{
$testType = new ObjectType(TestUnaryOperand::class);

return in_array($operatorSigil, ['-', '+', '~'], true)
&& $testType->isSuperTypeOf($operand)->yes();
}

public function specifyType(string $operatorSigil, Type $operand): Type
{
return new ObjectType(TestUnaryOperand::class);
}

}
Loading