diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index bcefb538b1..9a85c42277 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -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' diff --git a/.github/workflows/phar.yml b/.github/workflows/phar.yml index cfbd0dd75a..20f8de10c5 100644 --- a/.github/workflows/phar.yml +++ b/.github/workflows/phar.yml @@ -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" diff --git a/.github/workflows/reflection-golden-test.yml b/.github/workflows/reflection-golden-test.yml index c3cbd75c5d..d4a6f5159a 100644 --- a/.github/workflows/reflection-golden-test.yml +++ b/.github/workflows/reflection-golden-test.yml @@ -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' @@ -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' diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index b0510ced32..56f7c292bf 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -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' diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 80a7aad1a0..39b11bef28 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -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' @@ -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 diff --git a/conf/config.neon b/conf/config.neon index 5ea0d21f48..bda282be70 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -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 diff --git a/src/Analyser/DirectInternalScopeFactory.php b/src/Analyser/DirectInternalScopeFactory.php index 870016f326..a792c0d6b8 100644 --- a/src/Analyser/DirectInternalScopeFactory.php +++ b/src/Analyser/DirectInternalScopeFactory.php @@ -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; @@ -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, @@ -76,6 +78,7 @@ public function create( $this->reflectionProvider, $this->initializerExprTypeResolver, $this->dynamicReturnTypeExtensionRegistryProvider->getRegistry(), + $this->expressionTypeResolverExtensionRegistryProvider->getRegistry(), $this->exprPrinter, $this->typeSpecifier, $this->propertyReflectionFinder, diff --git a/src/Analyser/LazyInternalScopeFactory.php b/src/Analyser/LazyInternalScopeFactory.php index 7966f61fab..3c70556f53 100644 --- a/src/Analyser/LazyInternalScopeFactory.php +++ b/src/Analyser/LazyInternalScopeFactory.php @@ -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; @@ -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), diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index e59930c147..e95ae2dfdf 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -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; @@ -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, @@ -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(); } diff --git a/src/Broker/BrokerFactory.php b/src/Broker/BrokerFactory.php index adff2e4705..d35b68e30d 100644 --- a/src/Broker/BrokerFactory.php +++ b/src/Broker/BrokerFactory.php @@ -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) { diff --git a/src/Command/CommandHelper.php b/src/Command/CommandHelper.php index d78357577c..d1bced5423 100644 --- a/src/Command/CommandHelper.php +++ b/src/Command/CommandHelper.php @@ -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; @@ -472,6 +473,12 @@ public static function begin( $errorOutput->writeLineFormatted(sprintf('Parameter excludes_analyse has been deprecated so use excludePaths only from now on.')); } + if ($container->hasParameter('scopeClass') && $container->getParameter('scopeClass') !== MutatingScope::class) { + $errorOutput->writeLineFormatted('⚠️ You\'re using a deprecated config option 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); diff --git a/src/DependencyInjection/ConditionalTagsExtension.php b/src/DependencyInjection/ConditionalTagsExtension.php index 126acb8b78..358e673e10 100644 --- a/src/DependencyInjection/ConditionalTagsExtension.php +++ b/src/DependencyInjection/ConditionalTagsExtension.php @@ -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, diff --git a/src/DependencyInjection/Type/ExpressionTypeResolverExtensionRegistryProvider.php b/src/DependencyInjection/Type/ExpressionTypeResolverExtensionRegistryProvider.php new file mode 100644 index 0000000000..5eb6180ca0 --- /dev/null +++ b/src/DependencyInjection/Type/ExpressionTypeResolverExtensionRegistryProvider.php @@ -0,0 +1,12 @@ +registry === null) { + $this->registry = new ExpressionTypeResolverExtensionRegistry( + $this->container->getServicesByTag(BrokerFactory::EXPRESSION_TYPE_RESOLVER_EXTENSION_TAG), + ); + } + + return $this->registry; + } + +} diff --git a/src/Testing/PHPStanTestCase.php b/src/Testing/PHPStanTestCase.php index 935b6d94ee..14efcc7480 100644 --- a/src/Testing/PHPStanTestCase.php +++ b/src/Testing/PHPStanTestCase.php @@ -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; @@ -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(), diff --git a/src/Type/ExpressionTypeResolverExtension.php b/src/Type/ExpressionTypeResolverExtension.php new file mode 100644 index 0000000000..1bc7a710e5 --- /dev/null +++ b/src/Type/ExpressionTypeResolverExtension.php @@ -0,0 +1,28 @@ + $extensions + */ + public function __construct( + private array $extensions, + ) + { + } + + /** + * @return array + */ + public function getExtensions(): array + { + return $this->extensions; + } + +} diff --git a/tests/PHPStan/Analyser/ExpressionTypeResolverExtensionTest.php b/tests/PHPStan/Analyser/ExpressionTypeResolverExtensionTest.php new file mode 100644 index 0000000000..b8dda807d4 --- /dev/null +++ b/tests/PHPStan/Analyser/ExpressionTypeResolverExtensionTest.php @@ -0,0 +1,35 @@ +assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/expression-type-resolver-extension.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/data/MethodCallReturnsBoolExpressionTypeResolverExtension.php b/tests/PHPStan/Analyser/data/MethodCallReturnsBoolExpressionTypeResolverExtension.php new file mode 100644 index 0000000000..fa6fb0a43a --- /dev/null +++ b/tests/PHPStan/Analyser/data/MethodCallReturnsBoolExpressionTypeResolverExtension.php @@ -0,0 +1,46 @@ +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(); + } + +} diff --git a/tests/PHPStan/Analyser/data/expression-type-resolver-extension.php b/tests/PHPStan/Analyser/data/expression-type-resolver-extension.php new file mode 100644 index 0000000000..76b6b00389 --- /dev/null +++ b/tests/PHPStan/Analyser/data/expression-type-resolver-extension.php @@ -0,0 +1,21 @@ +methodReturningBoolNoMatterTheCallerUnlessReturnsString()); +assertType('bool', (new WhateverClass2)->methodReturningBoolNoMatterTheCallerUnlessReturnsString()); +assertType('string', (new WhateverClass3)->methodReturningBoolNoMatterTheCallerUnlessReturnsString()); diff --git a/tests/PHPStan/Analyser/expression-type-resolver-extension.neon b/tests/PHPStan/Analyser/expression-type-resolver-extension.neon new file mode 100644 index 0000000000..de0f92640b --- /dev/null +++ b/tests/PHPStan/Analyser/expression-type-resolver-extension.neon @@ -0,0 +1,7 @@ +# config for ExpressionTypeResolverExtensionTest +services: + - + class: ExpressionTypeResolverExtension\MethodCallReturnsBoolExpressionTypeResolverExtension + tags: + - phpstan.broker.expressionTypeResolverExtension +