From e3f2479512acc930530593cc523f00cd4b95c79c Mon Sep 17 00:00:00 2001 From: Daniel Pozzi Date: Wed, 23 Nov 2016 17:57:15 +0100 Subject: [PATCH] extracted a document factory --- src/PhpDocument.php | 224 +++---------------- src/PhpDocumentFactory.php | 215 ++++++++++++++++++ src/Project.php | 228 ++++++++++---------- src/Server/TextDocument.php | 4 +- tests/ProjectTest.php | 5 - tests/Server/TextDocument/DidChangeTest.php | 2 - 6 files changed, 356 insertions(+), 322 deletions(-) create mode 100644 src/PhpDocumentFactory.php diff --git a/src/PhpDocument.php b/src/PhpDocument.php index e24b9ac..1606aa2 100644 --- a/src/PhpDocument.php +++ b/src/PhpDocument.php @@ -1,4 +1,5 @@ uri = $uri; - $this->project = $project; - $this->client = $client; - $this->parser = $parser; - $this->docBlockFactory = $docBlockFactory; - $this->definitionResolver = $definitionResolver; - $this->updateContent($content); } /** @@ -140,92 +79,15 @@ class PhpDocument /** * 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 + * @param array $stmts * @return void */ - public function updateContent(string $content) + public function updateContent(string $content, array $stmts) { $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); - } + $this->stmts = $stmts; } /** @@ -339,56 +201,28 @@ class PhpDocument 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 + public function setDefinitions(array $definitions) { - 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->definitions = $definitions; + } + + public function setDefinitionNodes(array $nodes) + { + $this->definitionNodes = $nodes; + } + + public function setReferenceNodes(array $nodes) + { + $this->referenceNodes = $nodes; + } + + public function hasReferenceNodes() : bool + { + return isset($this->referenceNodes); + } + + public function getReferenceNodes() + { + return $this->referenceNodes; } } diff --git a/src/PhpDocumentFactory.php b/src/PhpDocumentFactory.php new file mode 100644 index 0000000..309eaa8 --- /dev/null +++ b/src/PhpDocumentFactory.php @@ -0,0 +1,215 @@ +client = $client; + $this->clientCapabilities = $clientCapabilities ? : new ClientCapabilities(); + $this->parser = new Parser; + $this->docBlockFactory = DocBlockFactory::createInstance(); + $this->definitionResolver = new DefinitionResolver($project); + $this->project = $project; + } + + /** + * Loads the document by doing a textDocument/xcontent request to the client. + * If the client does not support textDocument/xcontent, tries to read the file from the file system. + * The document is NOT added to the list of open documents, but definitions are registered. + * + * @param string $uri + * @return Promise + */ + public function loadDocument(string $uri): Promise + { + return coroutine(function () use ($uri) { + $limit = 150000; + if ($this->clientCapabilities->xcontentProvider) { + $content = (yield $this->client->textDocument->xcontent(new Protocol\TextDocumentIdentifier($uri)))->text; + $size = strlen($content); + if ($size > $limit) { + throw new ContentTooLargeException($uri, $size, $limit); + } + } else { + $path = uriToPath($uri); + $size = filesize($path); + if ($size > $limit) { + throw new ContentTooLargeException($uri, $size, $limit); + } + $content = file_get_contents($path); + } + + return $this->createDocument($uri, $content, false); + }); + } + + /** + * Ensures a document is loaded and added to the list of open documents. + * + * @param string $uri + * @param string $content + * @param boolean $index + * @return void + */ + public function createDocument(string $uri, string $content, bool $index = true) + { + if ($this->project->getDocument($uri)) { + $document = $this->project->getDocument($uri); + } else { + $document = new PhpDocument($uri, $this->project); + if ($index) + $this->project->addDocument($document); + } + + $this->handleContent($document, $content); + return $document; + } + + + /** + * 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 + */ + private function handleContent(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, $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 ($document->getDefinitions() !== null) { + foreach ($document->getDefinitions() as $fqn => $definition) { + $this->project->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->project->setDefinition($fqn, $definition); + } + + // Unregister old references + if ($document->hasReferenceNodes()) { + foreach ($document->getReferenceNodes() as $fqn => $node) { + $this->project->removeReferenceUri($fqn, $document->getUri()); + } + } + // Register this document on the project for references + $document->setReferenceNodes($referencesCollector->nodes); + foreach ($referencesCollector->nodes as $fqn => $nodes) { + $this->project->addReferenceUri($fqn, $document->getUri()); + } + } + + $document->updateContent($content, $stmts); + + if (!$document->isVendored()) { + $this->client->textDocument->publishDiagnostics($document->getUri(), $diagnostics); + } + } +} diff --git a/src/Project.php b/src/Project.php index 3021a1c..68c599b 100644 --- a/src/Project.php +++ b/src/Project.php @@ -3,14 +3,20 @@ declare(strict_types = 1); namespace LanguageServer; -use LanguageServer\Protocol\{SymbolInformation, TextDocumentIdentifier, ClientCapabilities}; -use phpDocumentor\Reflection\DocBlockFactory; +use LanguageServer\NodeVisitor\VariableReferencesCollector; +use LanguageServer\Protocol\ClientCapabilities; +use PhpParser\Node; +use PhpParser\Node\Expr\ClosureUse; +use PhpParser\Node\Expr\Variable; +use PhpParser\Node\FunctionLike; +use PhpParser\Node\Param; +use PhpParser\NodeTraverser; use Sabre\Event\Promise; use function Sabre\Event\coroutine; class Project { - /** + /** * An associative array [string => PhpDocument] * that maps URIs to loaded PhpDocuments * @@ -33,49 +39,15 @@ class Project private $references = []; /** - * Instance of the PHP parser - * - * @var Parser + * @var PhpDocumentFactory */ - 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; - - /** - * Reference to the language server client interface - * - * @var LanguageClient - */ - private $client; - - /** - * The client's capabilities - * - * @var ClientCapabilities - */ - private $clientCapabilities; - + private $documentFactory; + public function __construct(LanguageClient $client, ClientCapabilities $clientCapabilities) { - $this->client = $client; - $this->clientCapabilities = $clientCapabilities; - $this->parser = new Parser; - $this->docBlockFactory = DocBlockFactory::createInstance(); - $this->definitionResolver = new DefinitionResolver($this); + $this->documentFactory = new PhpDocumentFactory($client, $this, $clientCapabilities); } - + /** * Returns the document indicated by uri. * Returns null if the document if not loaded. @@ -89,86 +61,14 @@ class Project } /** - * Returns the document indicated by uri. - * If the document is not open, loads it. + * Ensures a document is added to the list of open documents. * - * @param string $uri - * @return Promise - */ - public function getOrLoadDocument(string $uri) - { - return isset($this->documents[$uri]) ? Promise\resolve($this->documents[$uri]) : $this->loadDocument($uri); - } - - /** - * Loads the document by doing a textDocument/xcontent request to the client. - * If the client does not support textDocument/xcontent, tries to read the file from the file system. - * The document is NOT added to the list of open documents, but definitions are registered. - * - * @param string $uri - * @return Promise - */ - public function loadDocument(string $uri): Promise - { - return coroutine(function () use ($uri) { - $limit = 150000; - if ($this->clientCapabilities->xcontentProvider) { - $content = (yield $this->client->textDocument->xcontent(new TextDocumentIdentifier($uri)))->text; - $size = strlen($content); - if ($size > $limit) { - throw new ContentTooLargeException($uri, $size, $limit); - } - } else { - $path = uriToPath($uri); - $size = filesize($path); - if ($size > $limit) { - throw new ContentTooLargeException($uri, $size, $limit); - } - $content = file_get_contents($path); - } - 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 - ); - } - return $document; - }); - } - - /** - * Ensures a document is loaded and added to the list of open documents. - * - * @param string $uri - * @param string $content + * @param PhpDocument $document * @return void */ - public function openDocument(string $uri, string $content) + public function addDocument(PhpDocument $document) { - 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 - ); - $this->documents[$uri] = $document; - } - return $document; + $this->documents[$document->getUri()] = $document; } /** @@ -354,4 +254,96 @@ class Project { return isset($this->definitions[$fqn]); } + + /** + * Returns the document indicated by uri. + * If the document is not open, loads it. + * + * @param string $uri + * @return Promise + */ + public function getOrLoadDocument(string $uri) + { + $document = $this->getDocument($uri); + return isset($document) ? Promise\resolve($document) : $this->loadDocument($uri); + } + + /** + * Loads the document by doing a textDocument/xcontent request to the client. + * If the client does not support textDocument/xcontent, tries to read the file from the file system. + * The document is NOT added to the list of open documents, but definitions are registered. + * + * @param string $uri + * @return Promise + */ + public function loadDocument(string $uri): Promise + { + return $this->documentFactory->loadDocument($uri); + + } + + /** + * Ensures a document is loaded and added to the list of open documents. + * + * @param string $uri + * @param string $content + * @return void + */ + public function openDocument(string $uri, string $content) + { + return $this->documentFactory->createDocument($uri, $content); + } + + /** + * Returns the reference nodes for any node + * The references node MAY be in other documents, check the ownerDocument attribute + * + * @param Node2 $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 \PhpParserNode\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; + }); + } } diff --git a/src/Server/TextDocument.php b/src/Server/TextDocument.php index 6c67388..b8805d7 100644 --- a/src/Server/TextDocument.php +++ b/src/Server/TextDocument.php @@ -98,7 +98,7 @@ class TextDocument */ public function didChange(VersionedTextDocumentIdentifier $textDocument, array $contentChanges) { - $this->project->getDocument($textDocument->uri)->updateContent($contentChanges[0]->text); + $this->project->openDocument($textDocument->uri, $contentChanges[0]->text); } /** @@ -146,7 +146,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/ProjectTest.php b/tests/ProjectTest.php index 161370c..50ac0b7 100644 --- a/tests/ProjectTest.php +++ b/tests/ProjectTest.php @@ -7,11 +7,6 @@ use PHPUnit\Framework\TestCase; use LanguageServer\Tests\MockProtocolStream; use LanguageServer\{Server, Client, LanguageClient, Project, PhpDocument}; use LanguageServer\Protocol\{ - TextDocumentItem, - TextDocumentIdentifier, - SymbolKind, - DiagnosticSeverity, - FormattingOptions, ClientCapabilities }; use AdvancedJsonRpc\{Request as RequestBody, Response as ResponseBody}; diff --git a/tests/Server/TextDocument/DidChangeTest.php b/tests/Server/TextDocument/DidChangeTest.php index 1df0505..592daef 100644 --- a/tests/Server/TextDocument/DidChangeTest.php +++ b/tests/Server/TextDocument/DidChangeTest.php @@ -7,8 +7,6 @@ use PHPUnit\Framework\TestCase; use LanguageServer\Tests\MockProtocolStream; use LanguageServer\{Server, Client, LanguageClient, Project}; use LanguageServer\Protocol\{ - TextDocumentIdentifier, - TextDocumentItem, VersionedTextDocumentIdentifier, TextDocumentContentChangeEvent, Range,