uri = $uri; $this->project = $project; $this->client = $client; $this->parser = $parser; $this->docBlockFactory = $docBlockFactory; $this->definitionResolver = $definitionResolver; $this->updateContent($content); } /** * Get all references of a fully qualified name * * @param string $fqn The fully qualified name of the symbol * @return Node[] */ public function getReferenceNodesByFqn(string $fqn) { return isset($this->referenceNodes) && isset($this->referenceNodes[$fqn]) ? $this->referenceNodes[$fqn] : null; } /** * Updates the content on this document. * Re-parses a source file, updates symbols and reports parsing errors * that may have occured as diagnostics. * * @param string $content * @return void */ public function updateContent(string $content) { $this->content = $content; $stmts = null; $errorHandler = new ErrorHandler\Collecting; $stmts = $this->parser->parse($content, $errorHandler); $diagnostics = []; foreach ($errorHandler->getErrors() as $error) { $diagnostics[] = Diagnostic::fromError($error, $this->content, DiagnosticSeverity::ERROR, 'php'); } // $stmts can be null in case of a fatal parsing error if ($stmts) { $traverser = new NodeTraverser; // Resolve aliased names to FQNs $traverser->addVisitor(new NameResolver($errorHandler)); // Add parentNode, previousSibling, nextSibling attributes $traverser->addVisitor(new ReferencesAdder($this)); // Add column attributes to nodes $traverser->addVisitor(new ColumnCalculator($content)); // Parse docblocks and add docBlock attributes to nodes $docBlockParser = new DocBlockParser($this->docBlockFactory); $traverser->addVisitor($docBlockParser); $traverser->traverse($stmts); // Report errors from parsing docblocks foreach ($docBlockParser->errors as $error) { $diagnostics[] = Diagnostic::fromError($error, $this->content, DiagnosticSeverity::WARNING, 'php'); } $traverser = new NodeTraverser; // Collect all definitions $definitionCollector = new DefinitionCollector; $traverser->addVisitor($definitionCollector); // Collect all references $referencesCollector = new ReferencesCollector($this->definitionResolver); $traverser->addVisitor($referencesCollector); $traverser->traverse($stmts); // Unregister old definitions if (isset($this->definitions)) { foreach ($this->definitions as $fqn => $definition) { $this->project->removeDefinition($fqn); } } // Register this document on the project for all the symbols defined in it $this->definitions = $definitionCollector->definitions; $this->definitionNodes = $definitionCollector->nodes; foreach ($definitionCollector->definitions as $fqn => $definition) { $this->project->setDefinition($fqn, $definition); } // Unregister old references if (isset($this->referenceNodes)) { foreach ($this->referenceNodes as $fqn => $node) { $this->project->removeReferenceUri($fqn, $this->uri); } } // Register this document on the project for references $this->referenceNodes = $referencesCollector->nodes; foreach ($referencesCollector->nodes as $fqn => $nodes) { $this->project->addReferenceUri($fqn, $this->uri); } $this->stmts = $stmts; } if (!$this->isVendored()) { $this->client->textDocument->publishDiagnostics($this->uri, $diagnostics); } } /** * Returns true if the document is a dependency * * @return bool */ public function isVendored(): bool { $path = Uri\parse($this->uri)['path']; return strpos($path, '/vendor/') !== false; } /** * Returns array of TextEdit changes to format this document. * * @return \LanguageServer\Protocol\TextEdit[] */ public function getFormattedText() { if (empty($this->content)) { return []; } return Formatter::format($this->content, $this->uri); } /** * Returns this document's text content. * * @return string */ public function getContent() { return $this->content; } /** * Returns the URI of the document * * @return string */ public function getUri(): string { return $this->uri; } /** * Returns the AST of the document * * @return Node[] */ public function getStmts(): array { return $this->stmts; } /** * Returns the node at a specified position * * @param Position $position * @return Node|null */ public function getNodeAtPosition(Position $position) { $traverser = new NodeTraverser; $finder = new NodeAtPositionFinder($position); $traverser->addVisitor($finder); $traverser->traverse($this->stmts); return $finder->node; } /** * Returns the definition node for a fully qualified name * * @param string $fqn * @return Node|null */ public function getDefinitionNodeByFqn(string $fqn) { return $this->definitionNodes[$fqn] ?? null; } /** * Returns a map from fully qualified name (FQN) to Nodes defined in this document * * @return Node[] */ public function getDefinitionNodes() { return $this->definitionNodes; } /** * Returns a map from fully qualified name (FQN) to Definition defined in this document * * @return Definition[] */ public function getDefinitions() { return $this->definitions; } /** * Returns true if the given FQN is defined in this document * * @param string $fqn The fully qualified name of the symbol * @return bool */ public function isDefined(string $fqn): bool { return isset($this->definitions[$fqn]); } /** * Returns the reference nodes for any node * The references node MAY be in other documents, check the ownerDocument attribute * * @param Node $node * @return Promise */ public function getReferenceNodesByNode(Node $node): Promise { return coroutine(function () use ($node) { // Variables always stay in the boundary of the file and need to be searched inside their function scope // by traversing the AST if ( $node instanceof Node\Expr\Variable || $node instanceof Node\Param || $node instanceof Node\Expr\ClosureUse ) { if ($node->name instanceof Node\Expr) { return null; } // Find function/method/closure scope $n = $node; while (isset($n) && !($n instanceof Node\FunctionLike)) { $n = $n->getAttribute('parentNode'); } if (!isset($n)) { $n = $node->getAttribute('ownerDocument'); } $traverser = new NodeTraverser; $refCollector = new VariableReferencesCollector($node->name); $traverser->addVisitor($refCollector); $traverser->traverse($n->getStmts()); return $refCollector->nodes; } // Definition with a global FQN $fqn = Definition::getDefinedFqn($node); if ($fqn === null) { return []; } $refDocuments = yield $this->project->getReferenceDocuments($fqn); $nodes = []; foreach ($refDocuments as $document) { $refs = $document->getReferenceNodesByFqn($fqn); if ($refs !== null) { foreach ($refs as $ref) { $nodes[] = $ref; } } } return $nodes; }); } }