Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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 @@ -35,6 +36,7 @@
use PHPStan\Type\OperatorTypeSpecifyingExtensionRegistry;
use PHPStan\Type\Type;
use PHPStan\Type\TypeAliasResolver;
use PHPStan\Type\UnaryOperatorTypeSpecifyingExtensionRegistry;
use function array_keys;
use function array_map;
use function count;
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 @@ -63,6 +63,7 @@
use PHPStan\Type\StaticMethodParameterClosureTypeExtension;
use PHPStan\Type\StaticMethodParameterOutTypeExtension;
use PHPStan\Type\StaticMethodTypeSpecifyingExtension;
use PHPStan\Type\UnaryOperatorTypeSpecifyingExtension;
use ReflectionClass;
use function array_flip;
use function array_key_exists;
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