Skip to content

ExpressionTypeResolverExtension #2789

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
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
4 changes: 2 additions & 2 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@ jobs:
uses: actions/cache@v3
with:
path: ./tmp/rectorCache.php
key: "rector-v3-lint-${{ hashFiles('composer.lock', 'build/rector-downgrade.php') }}-${{ matrix.php-version }}-${{ steps.rector-cache-key.outputs.sha }}"
key: "rector-v4-lint-${{ hashFiles('composer.lock', 'build/rector-downgrade.php') }}-${{ matrix.php-version }}-${{ steps.rector-cache-key.outputs.sha }}"
restore-keys: |
rector-v3-lint-${{ hashFiles('composer.lock', 'build/rector-downgrade.php') }}-${{ matrix.php-version }}-
rector-v4-lint-${{ hashFiles('composer.lock', 'build/rector-downgrade.php') }}-${{ matrix.php-version }}-

- name: "Transform source code"
if: matrix.php-version != '8.1' && matrix.php-version != '8.2' && matrix.php-version != '8.3'
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/phar.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,9 @@ jobs:
uses: actions/cache@v3
with:
path: ./tmp/rectorCache.php
key: "rector-v3-phar-${{ hashFiles('composer.lock', 'build/rector-downgrade.php') }}-${{ steps.rector-cache-key.outputs.sha }}"
key: "rector-v4-phar-${{ hashFiles('composer.lock', 'build/rector-downgrade.php') }}-${{ steps.rector-cache-key.outputs.sha }}"
restore-keys: |
rector-v3-phar-${{ hashFiles('composer.lock', 'build/rector-downgrade.php') }}-
rector-v4-phar-${{ hashFiles('composer.lock', 'build/rector-downgrade.php') }}-

- name: "Prepare for PHAR compilation"
working-directory: "compiler"
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/reflection-golden-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,9 @@ jobs:
uses: actions/cache@v3
with:
path: ./tmp/rectorCache.php
key: "rector-v3-tests-${{ matrix.script }}-${{ matrix.operating-system }}-${{ hashFiles('composer.lock', 'build/rector-downgrade.php') }}-${{ matrix.php-version }}-${{ steps.rector-cache-key-base.outputs.sha }}"
key: "rector-v4-reflection-golden-${{ matrix.script }}-${{ matrix.operating-system }}-${{ hashFiles('composer.lock', 'build/rector-downgrade.php') }}-${{ matrix.php-version }}-${{ steps.rector-cache-key-base.outputs.sha }}"
restore-keys: |
rector-v3-tests-${{ matrix.script }}-${{ matrix.operating-system }}-${{ hashFiles('composer.lock', 'build/rector-downgrade.php') }}-${{ matrix.php-version }}-
rector-v4-reflection-golden-${{ matrix.script }}-${{ matrix.operating-system }}-${{ hashFiles('composer.lock', 'build/rector-downgrade.php') }}-${{ matrix.php-version }}-

- name: "Transform source code"
if: matrix.php-version != '8.1' && matrix.php-version != '8.2' && matrix.php-version != '8.3'
Expand Down Expand Up @@ -167,9 +167,9 @@ jobs:
uses: actions/cache@v3
with:
path: ./tmp/rectorCache.php
key: "rector-v3-tests-${{ matrix.script }}-${{ matrix.operating-system }}-${{ hashFiles('composer.lock', 'build/rector-downgrade.php') }}-${{ matrix.php-version }}-${{ steps.rector-cache-key-head.outputs.sha }}"
key: "rector-v4-reflection-golden-${{ matrix.script }}-${{ matrix.operating-system }}-${{ hashFiles('composer.lock', 'build/rector-downgrade.php') }}-${{ matrix.php-version }}-${{ steps.rector-cache-key-head.outputs.sha }}"
restore-keys: |
rector-v3-tests-${{ matrix.script }}-${{ matrix.operating-system }}-${{ hashFiles('composer.lock', 'build/rector-downgrade.php') }}-${{ matrix.php-version }}-
rector-v4-reflection-golden-${{ matrix.script }}-${{ matrix.operating-system }}-${{ hashFiles('composer.lock', 'build/rector-downgrade.php') }}-${{ matrix.php-version }}-

- name: "Transform source code"
if: matrix.php-version != '8.1' && matrix.php-version != '8.2' && matrix.php-version != '8.3'
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/static-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,9 @@ jobs:
uses: actions/cache@v3
with:
path: ./tmp/rectorCache.php
key: "rector-v3-sa-${{ matrix.script }}-${{ matrix.operating-system }}-${{ hashFiles('composer.lock', 'build/rector-downgrade.php') }}-${{ matrix.php-version }}-${{ steps.rector-cache-key.outputs.sha }}"
key: "rector-v4-sa-${{ matrix.script }}-${{ matrix.operating-system }}-${{ hashFiles('composer.lock', 'build/rector-downgrade.php') }}-${{ matrix.php-version }}-${{ steps.rector-cache-key.outputs.sha }}"
restore-keys: |
rector-v3-sa-${{ matrix.script }}-${{ matrix.operating-system }}-${{ hashFiles('composer.lock', 'build/rector-downgrade.php') }}-${{ matrix.php-version }}-
rector-v4-sa-${{ matrix.script }}-${{ matrix.operating-system }}-${{ hashFiles('composer.lock', 'build/rector-downgrade.php') }}-${{ matrix.php-version }}-

- name: "Transform source code"
if: matrix.php-version != '8.1' && matrix.php-version != '8.2' && matrix.php-version != '8.3'
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,9 @@ jobs:
uses: actions/cache@v3
with:
path: ./tmp/rectorCache.php
key: "rector-v3-tests-${{ matrix.script }}-${{ matrix.operating-system }}-${{ hashFiles('composer.lock', 'build/rector-downgrade.php') }}-${{ matrix.php-version }}-${{ steps.rector-cache-key.outputs.sha }}"
key: "rector-v4-tests-${{ matrix.script }}-${{ matrix.operating-system }}-${{ hashFiles('composer.lock', 'build/rector-downgrade.php') }}-${{ matrix.php-version }}-${{ steps.rector-cache-key.outputs.sha }}"
restore-keys: |
rector-v3-tests-${{ matrix.script }}-${{ matrix.operating-system }}-${{ hashFiles('composer.lock', 'build/rector-downgrade.php') }}-${{ matrix.php-version }}-
rector-v4-tests-${{ matrix.script }}-${{ matrix.operating-system }}-${{ hashFiles('composer.lock', 'build/rector-downgrade.php') }}-${{ matrix.php-version }}-

- name: "Transform source code"
if: matrix.php-version != '8.1' && matrix.php-version != '8.2' && matrix.php-version != '8.3'
Expand Down Expand Up @@ -201,9 +201,9 @@ jobs:
uses: actions/cache@v3
with:
path: ./tmp/rectorCache.php
key: "rector-v3-tests-old-${{ matrix.script }}-${{ matrix.operating-system }}-${{ hashFiles('composer.lock', 'build/rector-downgrade.php') }}-${{ matrix.php-version }}-${{ steps.rector-cache-key.outputs.sha }}"
key: "rector-v4-tests-old-${{ matrix.script }}-${{ matrix.operating-system }}-${{ hashFiles('composer.lock', 'build/rector-downgrade.php') }}-${{ matrix.php-version }}-${{ steps.rector-cache-key.outputs.sha }}"
restore-keys: |
rector-v3-tests-old-${{ matrix.script }}-${{ matrix.operating-system }}-${{ hashFiles('composer.lock', 'build/rector-downgrade.php') }}-${{ matrix.php-version }}-
rector-v4-tests-old-${{ matrix.script }}-${{ matrix.operating-system }}-${{ hashFiles('composer.lock', 'build/rector-downgrade.php') }}-${{ matrix.php-version }}-

- name: "Transform source code"
shell: bash
Expand Down
4 changes: 4 additions & 0 deletions conf/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,10 @@ services:
class: PHPStan\DependencyInjection\Type\DynamicReturnTypeExtensionRegistryProvider
factory: PHPStan\DependencyInjection\Type\LazyDynamicReturnTypeExtensionRegistryProvider

-
class: PHPStan\DependencyInjection\Type\ExpressionTypeResolverExtensionRegistryProvider
factory: PHPStan\DependencyInjection\Type\LazyExpressionTypeResolverExtensionRegistryProvider

-
class: PHPStan\DependencyInjection\Type\OperatorTypeSpecifyingExtensionRegistryProvider
factory: PHPStan\DependencyInjection\Type\LazyOperatorTypeSpecifyingExtensionRegistryProvider
Expand Down
3 changes: 3 additions & 0 deletions src/Analyser/DirectInternalScopeFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace PHPStan\Analyser;

use PHPStan\DependencyInjection\Type\DynamicReturnTypeExtensionRegistryProvider;
use PHPStan\DependencyInjection\Type\ExpressionTypeResolverExtensionRegistryProvider;
use PHPStan\Node\Printer\ExprPrinter;
use PHPStan\Parser\Parser;
use PHPStan\Php\PhpVersion;
Expand All @@ -26,6 +27,7 @@ public function __construct(
private ReflectionProvider $reflectionProvider,
private InitializerExprTypeResolver $initializerExprTypeResolver,
private DynamicReturnTypeExtensionRegistryProvider $dynamicReturnTypeExtensionRegistryProvider,
private ExpressionTypeResolverExtensionRegistryProvider $expressionTypeResolverExtensionRegistryProvider,
private ExprPrinter $exprPrinter,
private TypeSpecifier $typeSpecifier,
private PropertyReflectionFinder $propertyReflectionFinder,
Expand Down Expand Up @@ -76,6 +78,7 @@ public function create(
$this->reflectionProvider,
$this->initializerExprTypeResolver,
$this->dynamicReturnTypeExtensionRegistryProvider->getRegistry(),
$this->expressionTypeResolverExtensionRegistryProvider->getRegistry(),
$this->exprPrinter,
$this->typeSpecifier,
$this->propertyReflectionFinder,
Expand Down
2 changes: 2 additions & 0 deletions src/Analyser/LazyInternalScopeFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use PHPStan\DependencyInjection\Container;
use PHPStan\DependencyInjection\Type\DynamicReturnTypeExtensionRegistryProvider;
use PHPStan\DependencyInjection\Type\ExpressionTypeResolverExtensionRegistryProvider;
use PHPStan\Node\Printer\ExprPrinter;
use PHPStan\Php\PhpVersion;
use PHPStan\Reflection\FunctionReflection;
Expand Down Expand Up @@ -72,6 +73,7 @@ public function create(
$this->container->getByType(ReflectionProvider::class),
$this->container->getByType(InitializerExprTypeResolver::class),
$this->container->getByType(DynamicReturnTypeExtensionRegistryProvider::class)->getRegistry(),
$this->container->getByType(ExpressionTypeResolverExtensionRegistryProvider::class)->getRegistry(),
$this->container->getByType(ExprPrinter::class),
$this->container->getByType(TypeSpecifier::class),
$this->container->getByType(PropertyReflectionFinder::class),
Expand Down
9 changes: 9 additions & 0 deletions src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
use PHPStan\Type\ConstantTypeHelper;
use PHPStan\Type\DynamicReturnTypeExtensionRegistry;
use PHPStan\Type\ErrorType;
use PHPStan\Type\ExpressionTypeResolverExtensionRegistry;
use PHPStan\Type\GeneralizePrecision;
use PHPStan\Type\Generic\GenericClassStringType;
use PHPStan\Type\Generic\GenericObjectType;
Expand Down Expand Up @@ -178,6 +179,7 @@ public function __construct(
private ReflectionProvider $reflectionProvider,
private InitializerExprTypeResolver $initializerExprTypeResolver,
private DynamicReturnTypeExtensionRegistry $dynamicReturnTypeExtensionRegistry,
private ExpressionTypeResolverExtensionRegistry $expressionTypeResolverExtensionRegistry,
private ExprPrinter $exprPrinter,
private TypeSpecifier $typeSpecifier,
private PropertyReflectionFinder $propertyReflectionFinder,
Expand Down Expand Up @@ -679,6 +681,13 @@ private function getNodeKey(Expr $node): string

private function resolveType(string $exprString, Expr $node): Type
{
foreach ($this->expressionTypeResolverExtensionRegistry->getExtensions() as $extension) {
$type = $extension->getType($node, $this);
if ($type !== null) {
return $type;
}
}

if ($node instanceof Expr\Exit_ || $node instanceof Expr\Throw_) {
return new NonAcceptingNeverType();
}
Expand Down
1 change: 1 addition & 0 deletions src/Broker/BrokerFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ 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 EXPRESSION_TYPE_RESOLVER_EXTENSION_TAG = 'phpstan.broker.expressionTypeResolverExtension';

public function __construct(private Container $container)
{
Expand Down
7 changes: 7 additions & 0 deletions src/Command/CommandHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Nette\Schema\ValidationException;
use Nette\Utils\AssertionException;
use Nette\Utils\Strings;
use PHPStan\Analyser\MutatingScope;
use PHPStan\Command\Symfony\SymfonyOutput;
use PHPStan\Command\Symfony\SymfonyStyle;
use PHPStan\DependencyInjection\Container;
Expand Down Expand Up @@ -472,6 +473,12 @@ public static function begin(
$errorOutput->writeLineFormatted(sprintf('Parameter <fg=cyan>excludes_analyse</> has been deprecated so use <fg=cyan>excludePaths</> only from now on.'));
}

if ($container->hasParameter('scopeClass') && $container->getParameter('scopeClass') !== MutatingScope::class) {
$errorOutput->writeLineFormatted('⚠️ You\'re using a deprecated config option <fg=cyan>scopeClass</>. ⚠️️');
$errorOutput->writeLineFormatted('');
$errorOutput->writeLineFormatted(sprintf('Please implement PHPStan\Type\ExpressionTypeResolverExtension interface instead and register it as a service.'));
}

$tempResultCachePath = $container->getParameter('tempResultCachePath');
$createDir($tempResultCachePath);

Expand Down
1 change: 1 addition & 0 deletions src/DependencyInjection/ConditionalTagsExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public function getConfigSchema(): Nette\Schema\Schema
BrokerFactory::DYNAMIC_METHOD_RETURN_TYPE_EXTENSION_TAG => $bool,
BrokerFactory::DYNAMIC_STATIC_METHOD_RETURN_TYPE_EXTENSION_TAG => $bool,
BrokerFactory::DYNAMIC_FUNCTION_RETURN_TYPE_EXTENSION_TAG => $bool,
BrokerFactory::EXPRESSION_TYPE_RESOLVER_EXTENSION_TAG => $bool,
BrokerFactory::OPERATOR_TYPE_SPECIFYING_EXTENSION_TAG => $bool,
BrokerFactory::ALLOWED_SUB_TYPES_CLASS_REFLECTION_EXTENSION_TAG => $bool,
LazyRegistry::RULE_TAG => $bool,
Expand Down
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\ExpressionTypeResolverExtensionRegistry;

interface ExpressionTypeResolverExtensionRegistryProvider
{

public function getRegistry(): ExpressionTypeResolverExtensionRegistry;

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

namespace PHPStan\DependencyInjection\Type;

use PHPStan\Broker\BrokerFactory;
use PHPStan\DependencyInjection\Container;
use PHPStan\Type\ExpressionTypeResolverExtensionRegistry;

class LazyExpressionTypeResolverExtensionRegistryProvider implements ExpressionTypeResolverExtensionRegistryProvider
{

private ?ExpressionTypeResolverExtensionRegistry $registry = null;

public function __construct(private Container $container)
{
}

public function getRegistry(): ExpressionTypeResolverExtensionRegistry
{
if ($this->registry === null) {
$this->registry = new ExpressionTypeResolverExtensionRegistry(
$this->container->getServicesByTag(BrokerFactory::EXPRESSION_TYPE_RESOLVER_EXTENSION_TAG),
);
}

return $this->registry;
}

}
2 changes: 2 additions & 0 deletions src/Testing/PHPStanTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use PHPStan\DependencyInjection\ContainerFactory;
use PHPStan\DependencyInjection\Reflection\ClassReflectionExtensionRegistryProvider;
use PHPStan\DependencyInjection\Type\DynamicReturnTypeExtensionRegistryProvider;
use PHPStan\DependencyInjection\Type\ExpressionTypeResolverExtensionRegistryProvider;
use PHPStan\DependencyInjection\Type\OperatorTypeSpecifyingExtensionRegistryProvider;
use PHPStan\File\FileHelper;
use PHPStan\Internal\DirectoryCreator;
Expand Down Expand Up @@ -180,6 +181,7 @@ public static function createScopeFactory(ReflectionProvider $reflectionProvider
$container->getParameter('usePathConstantsAsConstantString'),
),
$container->getByType(DynamicReturnTypeExtensionRegistryProvider::class),
$container->getByType(ExpressionTypeResolverExtensionRegistryProvider::class),
$container->getByType(ExprPrinter::class),
$typeSpecifier,
new PropertyReflectionFinder(),
Expand Down
28 changes: 28 additions & 0 deletions src/Type/ExpressionTypeResolverExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type;

use PhpParser\Node\Expr;
use PHPStan\Analyser\Scope;

/**
* To register it in the configuration file use the `phpstan.broker.expressionTypeResolverExtension` service tag:
*
* ```
* services:
* -
* class: App\PHPStan\MyExtension
* tags:
* - phpstan.broker.expressionTypeResolverExtension
* ```
*
* You should return null in your extension if you don't care about given Expr.
*
* @api
*/
interface ExpressionTypeResolverExtension
{

public function getType(Expr $expr, Scope $scope): ?Type;

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

namespace PHPStan\Type;

class ExpressionTypeResolverExtensionRegistry
{

/**
* @param array<ExpressionTypeResolverExtension> $extensions
*/
public function __construct(
private array $extensions,
)
{
}

/**
* @return array<ExpressionTypeResolverExtension>
*/
public function getExtensions(): array
{
return $this->extensions;
}

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

namespace PHPStan\Analyser;

use PHPStan\Testing\TypeInferenceTestCase;

class ExpressionTypeResolverExtensionTest extends TypeInferenceTestCase
{

public function dataFileAsserts(): iterable
{
yield from self::gatherAssertTypes(__DIR__ . '/data/expression-type-resolver-extension.php');
}

/**
* @dataProvider dataFileAsserts
* @param mixed ...$args
*/
public function testFileAsserts(
string $assertType,
string $file,
...$args,
): void
{
$this->assertFileAsserts($assertType, $file, ...$args);
}

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

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

namespace ExpressionTypeResolverExtension;

use PhpParser\Node\Expr;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Identifier;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Type\BooleanType;
use PHPStan\Type\ExpressionTypeResolverExtension;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;

class MethodCallReturnsBoolExpressionTypeResolverExtension implements ExpressionTypeResolverExtension {

public function getType(Expr $expr, Scope $scope): ?Type
{
if (!$expr instanceof MethodCall) {
return null;
}

if (!$expr->name instanceof Identifier) {
return null;
}

if ($expr->name->name !== 'methodReturningBoolNoMatterTheCallerUnlessReturnsString') {
return null;
}

$methodReflection = $scope->getMethodReflection($scope->getType($expr->var), $expr->name->name);

if ($methodReflection === null) {
return null;
}

$returnType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();

if ($returnType instanceof StringType) {
return null;
}

return new BooleanType();
}

}
Loading