diff --git a/src/Dependency/DependencyResolver.php b/src/Dependency/DependencyResolver.php index cee316199de..b6eb82dd0e4 100644 --- a/src/Dependency/DependencyResolver.php +++ b/src/Dependency/DependencyResolver.php @@ -36,6 +36,7 @@ use PHPStan\Type\Type; use function array_key_exists; use function count; +use function get_class; use function in_array; use function is_file; use function spl_object_id; @@ -44,9 +45,74 @@ final class DependencyResolver { + private const PROFILE_VAR_TAGS = 1; + + private const PROFILE_CHAIN = 2; + + private const PROFILE_EXPORT = 4; + + private const PROFILE_NAME_SCOPE = 8; + + /** Node classes the branch chain in collectNodeDependencies() reacts to */ + private const CHAIN_NODE_TYPES = [ + Node\Stmt\Class_::class, + Node\Stmt\Interface_::class, + Node\Stmt\Enum_::class, + InClassMethodNode::class, + InPropertyHookNode::class, + ClassPropertyNode::class, + InFunctionNode::class, + Closure::class, + Node\Expr\ArrowFunction::class, + Node\Expr\FuncCall::class, + Node\Expr\MethodCall::class, + Node\Expr\PropertyFetch::class, + Node\Expr\StaticCall::class, + Node\Expr\ClassConstFetch::class, + Node\Expr\ConstFetch::class, + Node\Expr\StaticPropertyFetch::class, + Node\Expr\New_::class, + Node\Stmt\Trait_::class, + Node\Stmt\TraitUse::class, + Node\Expr\Instanceof_::class, + Node\Expr\Include_::class, + Node\Stmt\Catch_::class, + ArrayDimFetch::class, + Foreach_::class, + Array_::class, + StaticMethodCallableNode::class, + MethodCallableNode::class, + FunctionCallableNode::class, + InstantiationCallableNode::class, + ]; + + /** + * Node classes ExportedNodeResolver::resolve() reacts to. A class member is not among them: it is + * exported as part of the class declaring it, through exportClassStatement(), never on its own. + */ + private const EXPORT_NODE_TYPES = [ + Node\Stmt\Class_::class, + Node\Stmt\Interface_::class, + Node\Stmt\Enum_::class, + Node\Stmt\Trait_::class, + Node\Stmt\Function_::class, + Node\Stmt\Const_::class, + Node\Expr\FuncCall::class, + ]; + + /** Node classes ExportedNameScopeTracker::enterNode() reacts to */ + private const NAME_SCOPE_NODE_TYPES = [ + Node\Stmt\Namespace_::class, + Node\Stmt\Use_::class, + Node\Stmt\GroupUse::class, + ]; + /** @var array> reflections keyed by spl_object_id() */ private array $classDependencies = []; + /** @var array */ + private array $nodeProfiles = []; + private ExportedNameScopeTracker $nameScopeTracker; private ?string $nameScopeFile = null; @@ -72,7 +138,12 @@ public function resolveDependencies(Node $node, Scope $scope): NodeDependencies $this->nameScopeFile = $file; $this->nameScopeTracker->reset(); } - $this->nameScopeTracker->enterNode($node); + $nodeClass = get_class($node); + $nodeProfile = $this->nodeProfiles[$nodeClass] ??= $this->resolveNodeProfile($node); + + if (($nodeProfile & self::PROFILE_NAME_SCOPE) !== 0) { + $this->nameScopeTracker->enterNode($node); + } // Keyed by spl_object_id(), so that a reflection collected again - every level of a class hierarchy // repeats the interfaces it inherits, and the classes a node references share most of their @@ -80,19 +151,29 @@ public function resolveDependencies(Node $node, Scope $scope): NodeDependencies $dependenciesReflections = []; $dependenciesFilePaths = []; - if ( - $node instanceof Node\Stmt - && !$node instanceof VirtualNode - && !$node instanceof Node\Stmt\ClassLike - && !$node instanceof Node\Stmt\ClassMethod - && !$node instanceof Node\Stmt\Function_ - && !$node instanceof Node\Stmt\Property - && !$node instanceof Node\Stmt\ClassConst - && !$node instanceof Node\Stmt\Const_ - ) { + if (($nodeProfile & self::PROFILE_VAR_TAGS) !== 0 && $node instanceof Node\Stmt) { $this->extractStmtVarTags($node, $scope, $dependenciesReflections); } + if (($nodeProfile & self::PROFILE_CHAIN) !== 0) { + $this->collectNodeDependencies($node, $scope, $dependenciesReflections, $dependenciesFilePaths); + } + + $exportedNode = ($nodeProfile & self::PROFILE_EXPORT) !== 0 + ? $this->exportedNodeResolver->resolve($node, $this->nameScopeTracker->getNameScope()) + : null; + + return new NodeDependencies($this->fileHelper, $dependenciesReflections, $exportedNode, $dependenciesFilePaths); + } + + /** + * The node-kind branches. Only entered when resolveNodeProfile() says a branch can match. + * + * @param array $dependenciesReflections + * @param list $dependenciesFilePaths + */ + private function collectNodeDependencies(Node $node, Scope $scope, array &$dependenciesReflections, array &$dependenciesFilePaths): void + { if ($node instanceof Node\Stmt\Class_) { if (isset($node->namespacedName)) { $this->addClassToDependencies($node->namespacedName->toString(), $dependenciesReflections); @@ -562,8 +643,6 @@ public function resolveDependencies(Node $node, Scope $scope): NodeDependencies } elseif ($node instanceof InstantiationCallableNode) { $dependenciesReflections += $this->resolveDependencies(new Node\Expr\New_($node->getClass()), $scope)->getReflections(); } - - return new NodeDependencies($this->fileHelper, $dependenciesReflections, $this->exportedNodeResolver->resolve($node, $this->nameScopeTracker->getNameScope()), $dependenciesFilePaths); } public function resolveUsedTraitDependencies(InClassNode $inClassNode): NodeDependencies @@ -606,6 +685,45 @@ private function getClassNamesFromClassString(Type $type): array return $classNames; } + /** + * Which parts of resolveDependencies() a node of this class can reach. Depends only on the class, + * so it is computed once per class and reused for every node of it. + */ + private function resolveNodeProfile(Node $node): int + { + $profile = 0; + if ( + $node instanceof Node\Stmt + && !$node instanceof VirtualNode + && !$node instanceof Node\Stmt\ClassLike + && !$node instanceof Node\Stmt\ClassMethod + && !$node instanceof Node\Stmt\Function_ + && !$node instanceof Node\Stmt\Property + && !$node instanceof Node\Stmt\ClassConst + && !$node instanceof Node\Stmt\Const_ + ) { + $profile |= self::PROFILE_VAR_TAGS; + } + + $lists = [ + self::PROFILE_CHAIN => self::CHAIN_NODE_TYPES, + self::PROFILE_EXPORT => self::EXPORT_NODE_TYPES, + self::PROFILE_NAME_SCOPE => self::NAME_SCOPE_NODE_TYPES, + ]; + foreach ($lists as $bit => $nodeTypes) { + foreach ($nodeTypes as $nodeType) { + if (!$node instanceof $nodeType) { + continue; + } + + $profile |= $bit; + break; + } + } + + return $profile; + } + /** * Extracts the classes referenced from a variable-level var-tag PHPDoc attached to a statement. * diff --git a/tests/PHPStan/Dependency/NodeProfileMirrorsBranchesTest.php b/tests/PHPStan/Dependency/NodeProfileMirrorsBranchesTest.php new file mode 100644 index 00000000000..f953d064edc --- /dev/null +++ b/tests/PHPStan/Dependency/NodeProfileMirrorsBranchesTest.php @@ -0,0 +1,115 @@ + + */ + public static function dataLists(): iterable + { + yield 'the branch chain' => [ + 'CHAIN_NODE_TYPES', + __DIR__ . '/../../../src/Dependency/DependencyResolver.php', + 'collectNodeDependencies', + ]; + yield 'the exported nodes' => [ + 'EXPORT_NODE_TYPES', + __DIR__ . '/../../../src/Dependency/ExportedNodeResolver.php', + 'resolve', + ]; + yield 'the PHPDoc name scope' => [ + 'NAME_SCOPE_NODE_TYPES', + __DIR__ . '/../../../src/Dependency/ExportedNameScopeTracker.php', + 'enterNode', + ]; + } + + #[DataProvider('dataLists')] + public function testListMirrorsTheBranches(string $constantName, string $file, string $methodName): void + { + $reflection = new ReflectionClass(DependencyResolver::class); + /** @var list $listed */ + $listed = $reflection->getConstant($constantName); + sort($listed); + + $matched = $this->nodeClassesMatchedIn($file, $methodName); + + $this->assertSame( + $listed, + $matched, + $constantName . ' does not match the node classes ' . $methodName . '() reacts to.', + ); + } + + /** + * Every class the method's `$node instanceof X` checks name, in source order, deduplicated. + * + * @return list + */ + private function nodeClassesMatchedIn(string $file, string $methodName): array + { + $contents = file_get_contents($file); + $this->assertNotFalse($contents); + + $parser = (new ParserFactory())->createForHostVersion(); + $stmts = $parser->parse($contents); + $this->assertNotNull($stmts); + + $traverser = new NodeTraverser(); + $traverser->addVisitor(new NameResolver()); + $stmts = $traverser->traverse($stmts); + + $method = null; + foreach ((new NodeFinder())->findInstanceOf($stmts, Node\Stmt\ClassMethod::class) as $classMethod) { + if ($classMethod->name->toString() !== $methodName) { + continue; + } + + $method = $classMethod; + break; + } + + $this->assertNotNull($method, $methodName . '() not found in ' . $file); + + $classNames = []; + foreach ((new NodeFinder())->findInstanceOf([$method], Node\Expr\Instanceof_::class) as $instanceof) { + if (!$instanceof->expr instanceof Node\Expr\Variable || $instanceof->expr->name !== 'node') { + continue; + } + if (!$instanceof->class instanceof Node\Name) { + continue; + } + + $className = $instanceof->class->toString(); + $classNames[$className] = $className; + } + + $classNames = array_values($classNames); + sort($classNames); + + return $classNames; + } + +}