diff --git a/src/PhpDocument.php b/src/PhpDocument.php index e24b9ac..ef70098 100644 --- a/src/PhpDocument.php +++ b/src/PhpDocument.php @@ -3,61 +3,15 @@ declare(strict_types = 1); namespace LanguageServer; -use LanguageServer\Protocol\{Diagnostic, DiagnosticSeverity, Range, Position, TextEdit}; +use LanguageServer\Protocol\{Diagnostic, DiagnosticSeverity, Range, Position}; use LanguageServer\NodeVisitor\{ - NodeAtPositionFinder, - ReferencesAdder, - DocBlockParser, - DefinitionCollector, - ColumnCalculator, - ReferencesCollector, - VariableReferencesCollector + NodeAtPositionFinder }; -use PhpParser\{Error, ErrorHandler, Node, NodeTraverser}; -use PhpParser\NodeVisitor\NameResolver; -use phpDocumentor\Reflection\DocBlockFactory; -use Sabre\Event\Promise; -use function Sabre\Event\coroutine; +use PhpParser\{ Node, NodeTraverser}; use Sabre\Uri; class PhpDocument { - /** - * The LanguageClient instance (to report errors etc) - * - * @var LanguageClient - */ - private $client; - - /** - * The Project this document belongs to (to register definitions etc) - * - * @var Project - */ - public $project; - // for whatever reason I get "cannot access private property" error if $project is not public - // https://github.com/felixfbecker/php-language-server/pull/49#issuecomment-252427359 - - /** - * The PHPParser instance - * - * @var Parser - */ - private $parser; - - /** - * The DocBlockFactory instance to parse docblocks - * - * @var DocBlockFactory - */ - private $docBlockFactory; - - /** - * The DefinitionResolver instance to resolve reference nodes to definitions - * - * @var DefinitionResolver - */ - private $definitionResolver; /** * The URI of the document @@ -104,27 +58,13 @@ class PhpDocument /** * @param string $uri The URI of the document * @param string $content The content of the document - * @param Project $project The Project this document belongs to (to register definitions etc) - * @param LanguageClient $client The LanguageClient instance (to report errors etc) - * @param Parser $parser The PHPParser instance - * @param DocBlockFactory $docBlockFactory The DocBlockFactory instance to parse docblocks */ public function __construct( string $uri, - string $content, - Project $project, - LanguageClient $client, - Parser $parser, - DocBlockFactory $docBlockFactory, - DefinitionResolver $definitionResolver + string $content ) { $this->uri = $uri; - $this->project = $project; - $this->client = $client; - $this->parser = $parser; - $this->docBlockFactory = $docBlockFactory; - $this->definitionResolver = $definitionResolver; - $this->updateContent($content); + $this->content = $content; } /** @@ -138,96 +78,6 @@ class PhpDocument 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($this->definitionResolver); - $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 * @@ -313,8 +163,11 @@ class PhpDocument * * @return Node[] */ - public function getDefinitionNodes() + public function getDefinitionNodes() : array { + if (!isset($this->definitionNodes)) + return []; + return $this->definitionNodes; } @@ -323,8 +176,11 @@ class PhpDocument * * @return Definition[] */ - public function getDefinitions() + public function getDefinitions() : array { + if (!isset($this->definitions)) + return []; + return $this->definitions; } @@ -340,55 +196,48 @@ class PhpDocument } /** - * Returns the reference nodes for any node - * The references node MAY be in other documents, check the ownerDocument attribute - * - * @param Node $node - * @return Promise + * Updates the content and the statements. + * + * @param string $content + * @param array $stmts */ - public function getReferenceNodesByNode(Node $node): Promise + public function updateContent(string $content, array $stmts = null) { - 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 = DefinitionResolver::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; - }); + $this->content = $content; + $this->stmts = $stmts; } + + /** + * Set the definitions. + * + * @param array $definitions + */ + public function setDefinitions(array $definitions) + { + $this->definitions = $definitions; + } + + /** + * Returns the reference nodes. + * + * @return Node[] + */ + public function getReferenceNodes() + { + if (!isset($this->referenceNodes)) + return []; + + return $this->referenceNodes; + } + + public function setDefinitionNodes(array $nodes) + { + $this->definitionNodes = $nodes; + } + + public function setReferenceNodes(array $nodes) + { + $this->referenceNodes = $nodes; + } + } diff --git a/src/Project.php b/src/Project.php index 3021a1c..f1657ea 100644 --- a/src/Project.php +++ b/src/Project.php @@ -3,8 +3,25 @@ declare(strict_types = 1); namespace LanguageServer; -use LanguageServer\Protocol\{SymbolInformation, TextDocumentIdentifier, ClientCapabilities}; +use LanguageServer\NodeVisitor\ColumnCalculator; +use LanguageServer\NodeVisitor\DefinitionCollector; +use LanguageServer\NodeVisitor\DocBlockParser; +use LanguageServer\NodeVisitor\ReferencesAdder; +use LanguageServer\NodeVisitor\ReferencesCollector; +use LanguageServer\NodeVisitor\VariableReferencesCollector; +use LanguageServer\Protocol\ClientCapabilities; +use LanguageServer\Protocol\Diagnostic; +use LanguageServer\Protocol\DiagnosticSeverity; use phpDocumentor\Reflection\DocBlockFactory; +use PhpParser\ErrorHandler\Collecting; +use PhpParser\Node; +use PhpParser\Node\Expr; +use PhpParser\Node\Expr\ClosureUse; +use PhpParser\Node\Expr\Variable; +use PhpParser\Node\FunctionLike; +use PhpParser\Node\Param; +use PhpParser\NodeTraverser; +use PhpParser\NodeVisitor\NameResolver; use Sabre\Event\Promise; use function Sabre\Event\coroutine; @@ -113,7 +130,7 @@ class Project return coroutine(function () use ($uri) { $limit = 150000; if ($this->clientCapabilities->xcontentProvider) { - $content = (yield $this->client->textDocument->xcontent(new TextDocumentIdentifier($uri)))->text; + $content = (yield $this->client->textDocument->xcontent(new Protocol\TextDocumentIdentifier($uri)))->text; $size = strlen($content); if ($size > $limit) { throw new ContentTooLargeException($uri, $size, $limit); @@ -128,18 +145,10 @@ class Project } if (isset($this->documents[$uri])) { $document = $this->documents[$uri]; - $document->updateContent($content); } else { - $document = new PhpDocument( - $uri, - $content, - $this, - $this->client, - $this->parser, - $this->docBlockFactory, - $this->definitionResolver - ); + $document = new PhpDocument($uri, $content); } + $this->updateContent($document, $content); return $document; }); } @@ -155,19 +164,12 @@ class Project { if (isset($this->documents[$uri])) { $document = $this->documents[$uri]; - $document->updateContent($content); } else { - $document = new PhpDocument( - $uri, - $content, - $this, - $this->client, - $this->parser, - $this->docBlockFactory, - $this->definitionResolver - ); + $document = new PhpDocument($uri, $content); $this->documents[$uri] = $document; } + $this->updateContent($document, $content); + return $document; } @@ -354,4 +356,144 @@ class Project { 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 Variable + || $node instanceof Param + || $node instanceof ClosureUse + ) { + if ($node->name instanceof Expr) { + return null; + } + // Find function/method/closure scope + $n = $node; + while (isset($n) && !($n instanceof 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 = DefinitionResolver::getDefinedFqn($node); + if ($fqn === null) { + return []; + } + $refDocuments = yield $this->getReferenceDocuments($fqn); + $nodes = []; + foreach ($refDocuments as $document) { + $refs = $document->getReferenceNodesByFqn($fqn); + if ($refs !== null) { + foreach ($refs as $ref) { + $nodes[] = $ref; + } + } + } + return $nodes; + }); + } + + /** + * Updates the content on this document. + * Re-parses a source file, updates symbols and reports parsing errors + * that may have occured as diagnostics. + * + * @param PhpDocument $document + * @param string $content + * @return void + */ + public function updateContent(PhpDocument $document, string $content) + { + $errorHandler = new Collecting; + $stmts = $this->parser->parse($content, $errorHandler); + + $diagnostics = []; + foreach ($errorHandler->getErrors() as $error) { + $diagnostics[] = Diagnostic::fromError($error, $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($document)); + + // 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, $document->content, DiagnosticSeverity::WARNING, 'php'); + } + + $traverser = new NodeTraverser; + + // Collect all definitions + $definitionCollector = new DefinitionCollector($this->definitionResolver); + $traverser->addVisitor($definitionCollector); + + // Collect all references + $referencesCollector = new ReferencesCollector($this->definitionResolver); + $traverser->addVisitor($referencesCollector); + + $traverser->traverse($stmts); + + // Unregister old definitions + foreach ($document->getDefinitions() as $fqn => $definition) { + $this->removeDefinition($fqn); + } + + // Register this document on the project for all the symbols defined in it + $document->setDefinitions($definitionCollector->definitions); + $document->setDefinitionNodes($definitionCollector->nodes); + foreach ($definitionCollector->definitions as $fqn => $definition) { + $this->setDefinition($fqn, $definition); + } + + // Unregister old references + foreach ($document->getReferenceNodes() as $fqn => $node) { + $this->removeReferenceUri($fqn, $document->uri); + } + + // Register this document on the project for references + $document->setReferenceNodes($referencesCollector->nodes); + foreach ($referencesCollector->nodes as $fqn => $nodes) { + $this->addReferenceUri($fqn, $document->getUri()); + } + + } + + $document->updateContent($content, $stmts); + + if (!$document->isVendored()) { + $this->client->textDocument->publishDiagnostics($document->getUri(), $diagnostics); + } + } } diff --git a/src/Server/TextDocument.php b/src/Server/TextDocument.php index 6c67388..70fceef 100644 --- a/src/Server/TextDocument.php +++ b/src/Server/TextDocument.php @@ -98,7 +98,10 @@ class TextDocument */ public function didChange(VersionedTextDocumentIdentifier $textDocument, array $contentChanges) { - $this->project->getDocument($textDocument->uri)->updateContent($contentChanges[0]->text); + $this->project->updateContent( + $this->project->getDocument($textDocument->uri), + $contentChanges[0]->text + ); } /** @@ -146,7 +149,7 @@ class TextDocument if ($node === null) { return []; } - $refs = yield $document->getReferenceNodesByNode($node); + $refs = yield $this->project->getReferenceNodesByNode($node); $locations = []; foreach ($refs as $ref) { $locations[] = Location::fromNode($ref); diff --git a/tests/Server/TextDocument/DidChangeTest.php b/tests/Server/TextDocument/DidChangeTest.php index 1df0505..111f1fa 100644 --- a/tests/Server/TextDocument/DidChangeTest.php +++ b/tests/Server/TextDocument/DidChangeTest.php @@ -5,10 +5,8 @@ namespace LanguageServer\Tests\Server\TextDocument; use PHPUnit\Framework\TestCase; use LanguageServer\Tests\MockProtocolStream; -use LanguageServer\{Server, Client, LanguageClient, Project}; +use LanguageServer\{Server, LanguageClient, Project}; use LanguageServer\Protocol\{ - TextDocumentIdentifier, - TextDocumentItem, VersionedTextDocumentIdentifier, TextDocumentContentChangeEvent, Range,