diff --git a/src/Analyser/DirectInternalScopeFactory.php b/src/Analyser/DirectInternalScopeFactory.php index f65a599113..13018f2b94 100644 --- a/src/Analyser/DirectInternalScopeFactory.php +++ b/src/Analyser/DirectInternalScopeFactory.php @@ -2,6 +2,7 @@ namespace PHPStan\Analyser; +use PhpParser\Node; use PHPStan\DependencyInjection\Type\DynamicReturnTypeExtensionRegistryProvider; use PHPStan\DependencyInjection\Type\ExpressionTypeResolverExtensionRegistryProvider; use PHPStan\Node\Printer\ExprPrinter; @@ -19,6 +20,7 @@ final class DirectInternalScopeFactory implements InternalScopeFactory /** * @param int|array{min: int, max: int}|null $configPhpVersion + * @param callable(Node $node, Scope $scope): void|null $nodeCallback */ public function __construct( private ReflectionProvider $reflectionProvider, @@ -34,6 +36,7 @@ public function __construct( private PhpVersion $phpVersion, private AttributeReflectionFactory $attributeReflectionFactory, private int|array|null $configPhpVersion, + private $nodeCallback, private ConstantResolver $constantResolver, ) { @@ -75,6 +78,7 @@ public function create( $this->phpVersion, $this->attributeReflectionFactory, $this->configPhpVersion, + $this->nodeCallback, $declareStrictTypes, $function, $namespace, diff --git a/src/Analyser/DirectInternalScopeFactoryFactory.php b/src/Analyser/DirectInternalScopeFactoryFactory.php new file mode 100644 index 0000000000..1abc52d990 --- /dev/null +++ b/src/Analyser/DirectInternalScopeFactoryFactory.php @@ -0,0 +1,65 @@ +reflectionProvider, + $this->initializerExprTypeResolver, + $this->dynamicReturnTypeExtensionRegistryProvider, + $this->expressionTypeResolverExtensionRegistryProvider, + $this->exprPrinter, + $this->typeSpecifier, + $this->propertyReflectionFinder, + $this->parser, + $this->nodeScopeResolver, + $this->richerScopeGetTypeHelper, + $this->phpVersion, + $this->attributeReflectionFactory, + $this->configPhpVersion, + $nodeCallback, + $this->constantResolver, + ); + } + +} diff --git a/src/Analyser/FileAnalyser.php b/src/Analyser/FileAnalyser.php index fbdd71d83f..845915cf52 100644 --- a/src/Analyser/FileAnalyser.php +++ b/src/Analyser/FileAnalyser.php @@ -98,7 +98,8 @@ public function analyseFile( $parserNodes = $this->parser->parseFile($file); $linesToIgnore = $unmatchedLineIgnores = [$file => $this->getLinesToIgnoreFromTokens($parserNodes)]; $temporaryFileErrors = []; - $nodeCallback = function (Node $node, Scope $scope) use (&$fileErrors, &$fileCollectedData, &$fileDependencies, &$usedTraitFileDependencies, &$exportedNodes, $file, $ruleRegistry, $collectorRegistry, $outerNodeCallback, $analysedFiles, &$linesToIgnore, &$unmatchedLineIgnores, &$temporaryFileErrors, $parserNodes): void { + $nodeCallback = function (Node $node, $scope) use (&$fileErrors, &$fileCollectedData, &$fileDependencies, &$usedTraitFileDependencies, &$exportedNodes, $file, $ruleRegistry, $collectorRegistry, $outerNodeCallback, $analysedFiles, &$linesToIgnore, &$unmatchedLineIgnores, &$temporaryFileErrors, $parserNodes): void { + /** @var Scope&NodeCallbackInvoker $scope */ if ($node instanceof Node\Stmt\Trait_) { foreach (array_keys($linesToIgnore[$file] ?? []) as $lineToIgnore) { if ($lineToIgnore < $node->getStartLine() || $lineToIgnore > $node->getEndLine()) { @@ -242,7 +243,7 @@ public function analyseFile( } }; - $scope = $this->scopeFactory->create(ScopeContext::create($file)); + $scope = $this->scopeFactory->create(ScopeContext::create($file), $nodeCallback); $nodeCallback(new FileNode($parserNodes), $scope); $this->nodeScopeResolver->processNodes( $parserNodes, diff --git a/src/Analyser/InternalScopeFactoryFactory.php b/src/Analyser/InternalScopeFactoryFactory.php new file mode 100644 index 0000000000..2587b31c05 --- /dev/null +++ b/src/Analyser/InternalScopeFactoryFactory.php @@ -0,0 +1,17 @@ +phpVersion = $this->container->getParameter('phpVersion'); @@ -65,6 +70,7 @@ public function create( $this->container->getByType(PhpVersion::class), $this->container->getByType(AttributeReflectionFactory::class), $this->phpVersion, + $this->nodeCallback, $declareStrictTypes, $function, $namespace, diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 21fd76cb6b..1e323aff2c 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -173,7 +173,7 @@ use const PHP_INT_MAX; use const PHP_INT_MIN; -final class MutatingScope implements Scope +final class MutatingScope implements Scope, NodeCallbackInvoker { private const BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH = 4; @@ -200,6 +200,7 @@ final class MutatingScope implements Scope /** * @param int|array{min: int, max: int}|null $configPhpVersion + * @param callable(Node $node, Scope $scope): void|null $nodeCallback * @param array $expressionTypes * @param array $conditionalExpressions * @param list $inClosureBindScopeClasses @@ -225,6 +226,7 @@ public function __construct( private PhpVersion $phpVersion, private AttributeReflectionFactory $attributeReflectionFactory, private int|array|null $configPhpVersion, + private $nodeCallback = null, private bool $declareStrictTypes = false, private PhpFunctionFromParserNodeReflection|null $function = null, ?string $namespace = null, @@ -6456,4 +6458,14 @@ public function getPhpVersion(): PhpVersions return new PhpVersions(new ConstantIntegerType($this->phpVersion->getVersionId())); } + public function invokeNodeCallback(Node $node): void + { + $nodeCallback = $this->nodeCallback; + if ($nodeCallback === null) { + throw new ShouldNotHappenException('Node callback is not present in this scope'); + } + + $nodeCallback($node, $this); + } + } diff --git a/src/Analyser/NodeCallbackInvoker.php b/src/Analyser/NodeCallbackInvoker.php new file mode 100644 index 0000000000..ef73c40c85 --- /dev/null +++ b/src/Analyser/NodeCallbackInvoker.php @@ -0,0 +1,37 @@ +invokeNodeCallback(new New_(new Name($className), $args))` + * + * And PHPStan will call all the registered rules for New_, checking as if the instantiation + * is actually in the code. + * + * @api + */ +interface NodeCallbackInvoker +{ + + public function invokeNodeCallback(Node $node): void; + +} diff --git a/src/Analyser/ScopeFactory.php b/src/Analyser/ScopeFactory.php index be9ca1982d..48d70b7c4c 100644 --- a/src/Analyser/ScopeFactory.php +++ b/src/Analyser/ScopeFactory.php @@ -2,6 +2,7 @@ namespace PHPStan\Analyser; +use PhpParser\Node; use PHPStan\DependencyInjection\AutowiredService; /** @@ -11,13 +12,16 @@ final class ScopeFactory { - public function __construct(private InternalScopeFactory $internalScopeFactory) + public function __construct(private InternalScopeFactoryFactory $internalScopeFactoryFactory) { } - public function create(ScopeContext $context): MutatingScope + /** + * @param callable(Node $node, Scope $scope): void $nodeCallback + */ + public function create(ScopeContext $context, ?callable $nodeCallback = null): MutatingScope { - return $this->internalScopeFactory->create($context); + return $this->internalScopeFactoryFactory->create($nodeCallback)->create($context); } } diff --git a/src/DependencyInjection/AutowiredAttributeServicesExtension.php b/src/DependencyInjection/AutowiredAttributeServicesExtension.php index c8dd87fa6c..2310e4b9ae 100644 --- a/src/DependencyInjection/AutowiredAttributeServicesExtension.php +++ b/src/DependencyInjection/AutowiredAttributeServicesExtension.php @@ -84,6 +84,10 @@ public function loadConfiguration(): void $definition = $builder->addFactoryDefinition(null) ->setImplement($attribute->interface); + if ($attribute->resultType !== null) { + $definition->getResultDefinition()->setType($attribute->resultType); + } + $resultDefinition = $definition->getResultDefinition(); $this->processParameters($class->name, $resultDefinition, $autowiredParameters); } diff --git a/src/DependencyInjection/GenerateFactory.php b/src/DependencyInjection/GenerateFactory.php index 41a70d10b8..c4b6a59ce0 100644 --- a/src/DependencyInjection/GenerateFactory.php +++ b/src/DependencyInjection/GenerateFactory.php @@ -22,8 +22,9 @@ final class GenerateFactory /** * @param class-string $interface + * @param class-string $resultType */ - public function __construct(public string $interface) + public function __construct(public string $interface, public ?string $resultType = null) { } diff --git a/src/Rules/Methods/OverridingMethodRule.php b/src/Rules/Methods/OverridingMethodRule.php index f1d3044b75..3112bb3c3c 100644 --- a/src/Rules/Methods/OverridingMethodRule.php +++ b/src/Rules/Methods/OverridingMethodRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PhpParser\Node\Attribute; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\RegisteredRule; @@ -48,7 +49,7 @@ public function getNodeType(): string return InClassMethodNode::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker $scope): array { $method = $node->getMethodReflection(); $prototypeData = $this->methodPrototypeFinder->findPrototype($node->getClassReflection(), $method->getName()); @@ -323,7 +324,7 @@ private function filterOverrideAttribute(array $attrGroups): array private function addErrors( array $errors, InClassMethodNode $classMethod, - Scope $scope, + Scope&NodeCallbackInvoker $scope, ): array { if (count($errors) > 0) { diff --git a/src/Rules/Playground/PromoteParameterRule.php b/src/Rules/Playground/PromoteParameterRule.php index 9381a36922..8dc1d32916 100644 --- a/src/Rules/Playground/PromoteParameterRule.php +++ b/src/Rules/Playground/PromoteParameterRule.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Playground; use PhpParser\Node; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\Container; use PHPStan\DependencyInjection\MissingServiceException; @@ -87,7 +88,7 @@ private function getOriginalRule(): ?Rule return $this->originalRule = $originalRule; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker $scope): array { if ($this->parameterValue) { return []; diff --git a/src/Rules/Rule.php b/src/Rules/Rule.php index e42e6ccc52..03a5a047b0 100644 --- a/src/Rules/Rule.php +++ b/src/Rules/Rule.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules; use PhpParser\Node; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; /** @@ -34,6 +35,6 @@ public function getNodeType(): string; * @param TNodeType $node * @return list */ - public function processNode(Node $node, Scope $scope): array; + public function processNode(Node $node, Scope&NodeCallbackInvoker $scope): array; } diff --git a/src/Testing/DelayedRule.php b/src/Testing/DelayedRule.php index a3ae370111..27b35cb2f4 100644 --- a/src/Testing/DelayedRule.php +++ b/src/Testing/DelayedRule.php @@ -3,6 +3,7 @@ namespace PHPStan\Testing; use PhpParser\Node; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\Rules\DirectRegistry; use PHPStan\Rules\IdentifierRuleError; @@ -42,7 +43,7 @@ public function getDelayedErrors(): array return $this->errors; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker $scope): array { $nodeType = get_class($node); foreach ($this->registry->getRules($nodeType) as $rule) { diff --git a/src/Testing/PHPStanTestCase.php b/src/Testing/PHPStanTestCase.php index d294f81d24..5749b34e44 100644 --- a/src/Testing/PHPStanTestCase.php +++ b/src/Testing/PHPStanTestCase.php @@ -3,7 +3,7 @@ namespace PHPStan\Testing; use PHPStan\Analyser\ConstantResolver; -use PHPStan\Analyser\DirectInternalScopeFactory; +use PHPStan\Analyser\DirectInternalScopeFactoryFactory; use PHPStan\Analyser\Error; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\RicherScopeGetTypeHelper; @@ -152,7 +152,7 @@ public static function createScopeFactory(ReflectionProvider $reflectionProvider ); return new ScopeFactory( - new DirectInternalScopeFactory( + new DirectInternalScopeFactoryFactory( $reflectionProvider, $initializerExprTypeResolver, $container->getByType(DynamicReturnTypeExtensionRegistryProvider::class), diff --git a/tests/PHPStan/Rules/NodeCallbackInvokerRule.php b/tests/PHPStan/Rules/NodeCallbackInvokerRule.php new file mode 100644 index 0000000000..e481a9c011 --- /dev/null +++ b/tests/PHPStan/Rules/NodeCallbackInvokerRule.php @@ -0,0 +1,43 @@ + + */ +class NodeCallbackInvokerRule implements Rule +{ + + public function getNodeType(): string + { + return Node\Stmt\Echo_::class; + } + + public function processNode(Node $node, NodeCallbackInvoker&Scope $scope): array + { + if ((bool) $node->getAttribute('virtual', false)) { + // prevent infinite recursion + return [ + RuleErrorBuilder::message('found virtual echo') + ->identifier('tests.nodeCallbackInvoker') + ->build(), + ]; + } + + $scope->invokeNodeCallback(new Node\Stmt\Echo_( + [new Node\Scalar\String_('virtual')], + ['startLine' => $node->getStartLine() + 1, 'virtual' => true], + )); + + return [ + RuleErrorBuilder::message('found echo') + ->identifier('tests.nodeCallbackInvoker') + ->build(), + ]; + } + +} diff --git a/tests/PHPStan/Rules/NodeCallbackInvokerRuleTest.php b/tests/PHPStan/Rules/NodeCallbackInvokerRuleTest.php new file mode 100644 index 0000000000..1c197af208 --- /dev/null +++ b/tests/PHPStan/Rules/NodeCallbackInvokerRuleTest.php @@ -0,0 +1,32 @@ + + */ +class NodeCallbackInvokerRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new NodeCallbackInvokerRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/node-callback-invoker.php'], [ + [ + 'found virtual echo', + 6, + ], + [ + 'found echo', + 5, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/data/node-callback-invoker.php b/tests/PHPStan/Rules/data/node-callback-invoker.php new file mode 100644 index 0000000000..1b0e2c3f14 --- /dev/null +++ b/tests/PHPStan/Rules/data/node-callback-invoker.php @@ -0,0 +1,7 @@ +