1
0
Fork 0
pull/182/merge
Daniel Pozzi 2016-11-24 20:35:23 +00:00 committed by GitHub
commit 63bd2daca8
4 changed files with 224 additions and 232 deletions

View File

@ -3,61 +3,15 @@ declare(strict_types = 1);
namespace LanguageServer; namespace LanguageServer;
use LanguageServer\Protocol\{Diagnostic, DiagnosticSeverity, Range, Position, TextEdit}; use LanguageServer\Protocol\{Diagnostic, DiagnosticSeverity, Range, Position};
use LanguageServer\NodeVisitor\{ use LanguageServer\NodeVisitor\{
NodeAtPositionFinder, NodeAtPositionFinder
ReferencesAdder,
DocBlockParser,
DefinitionCollector,
ColumnCalculator,
ReferencesCollector,
VariableReferencesCollector
}; };
use PhpParser\{Error, ErrorHandler, Node, NodeTraverser}; use PhpParser\{ Node, NodeTraverser};
use PhpParser\NodeVisitor\NameResolver;
use phpDocumentor\Reflection\DocBlockFactory;
use Sabre\Event\Promise;
use function Sabre\Event\coroutine;
use Sabre\Uri; use Sabre\Uri;
class PhpDocument 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 * The URI of the document
@ -104,27 +58,13 @@ class PhpDocument
/** /**
* @param string $uri The URI of the document * @param string $uri The URI of the document
* @param string $content The content 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( public function __construct(
string $uri, string $uri,
string $content, string $content
Project $project,
LanguageClient $client,
Parser $parser,
DocBlockFactory $docBlockFactory,
DefinitionResolver $definitionResolver
) { ) {
$this->uri = $uri; $this->uri = $uri;
$this->project = $project; $this->content = $content;
$this->client = $client;
$this->parser = $parser;
$this->docBlockFactory = $docBlockFactory;
$this->definitionResolver = $definitionResolver;
$this->updateContent($content);
} }
/** /**
@ -138,96 +78,6 @@ class PhpDocument
return isset($this->referenceNodes) && isset($this->referenceNodes[$fqn]) ? $this->referenceNodes[$fqn] : null; 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 * Returns true if the document is a dependency
* *
@ -313,8 +163,11 @@ class PhpDocument
* *
* @return Node[] * @return Node[]
*/ */
public function getDefinitionNodes() public function getDefinitionNodes() : array
{ {
if (!isset($this->definitionNodes))
return [];
return $this->definitionNodes; return $this->definitionNodes;
} }
@ -323,8 +176,11 @@ class PhpDocument
* *
* @return Definition[] * @return Definition[]
*/ */
public function getDefinitions() public function getDefinitions() : array
{ {
if (!isset($this->definitions))
return [];
return $this->definitions; return $this->definitions;
} }
@ -340,55 +196,48 @@ class PhpDocument
} }
/** /**
* Returns the reference nodes for any node * Updates the content and the statements.
* The references node MAY be in other documents, check the ownerDocument attribute
* *
* @param Node $node * @param string $content
* @return Promise <Node[]> * @param array $stmts
*/ */
public function getReferenceNodesByNode(Node $node): Promise public function updateContent(string $content, array $stmts = null)
{ {
return coroutine(function () use ($node) { $this->content = $content;
// Variables always stay in the boundary of the file and need to be searched inside their function scope $this->stmts = $stmts;
// 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)) { * Set the definitions.
$n = $n->getAttribute('parentNode'); *
* @param array $definitions
*/
public function setDefinitions(array $definitions)
{
$this->definitions = $definitions;
} }
if (!isset($n)) {
$n = $node->getAttribute('ownerDocument'); /**
} * Returns the reference nodes.
$traverser = new NodeTraverser; *
$refCollector = new VariableReferencesCollector($node->name); * @return Node[]
$traverser->addVisitor($refCollector); */
$traverser->traverse($n->getStmts()); public function getReferenceNodes()
return $refCollector->nodes; {
} if (!isset($this->referenceNodes))
// Definition with a global FQN
$fqn = DefinitionResolver::getDefinedFqn($node);
if ($fqn === null) {
return []; return [];
return $this->referenceNodes;
} }
$refDocuments = yield $this->project->getReferenceDocuments($fqn);
$nodes = []; public function setDefinitionNodes(array $nodes)
foreach ($refDocuments as $document) { {
$refs = $document->getReferenceNodesByFqn($fqn); $this->definitionNodes = $nodes;
if ($refs !== null) {
foreach ($refs as $ref) {
$nodes[] = $ref;
} }
public function setReferenceNodes(array $nodes)
{
$this->referenceNodes = $nodes;
} }
}
return $nodes;
});
}
} }

View File

@ -3,8 +3,25 @@ declare(strict_types = 1);
namespace LanguageServer; 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 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 Sabre\Event\Promise;
use function Sabre\Event\coroutine; use function Sabre\Event\coroutine;
@ -113,7 +130,7 @@ class Project
return coroutine(function () use ($uri) { return coroutine(function () use ($uri) {
$limit = 150000; $limit = 150000;
if ($this->clientCapabilities->xcontentProvider) { 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); $size = strlen($content);
if ($size > $limit) { if ($size > $limit) {
throw new ContentTooLargeException($uri, $size, $limit); throw new ContentTooLargeException($uri, $size, $limit);
@ -128,18 +145,10 @@ class Project
} }
if (isset($this->documents[$uri])) { if (isset($this->documents[$uri])) {
$document = $this->documents[$uri]; $document = $this->documents[$uri];
$document->updateContent($content);
} else { } else {
$document = new PhpDocument( $document = new PhpDocument($uri, $content);
$uri,
$content,
$this,
$this->client,
$this->parser,
$this->docBlockFactory,
$this->definitionResolver
);
} }
$this->updateContent($document, $content);
return $document; return $document;
}); });
} }
@ -155,19 +164,12 @@ class Project
{ {
if (isset($this->documents[$uri])) { if (isset($this->documents[$uri])) {
$document = $this->documents[$uri]; $document = $this->documents[$uri];
$document->updateContent($content);
} else { } else {
$document = new PhpDocument( $document = new PhpDocument($uri, $content);
$uri,
$content,
$this,
$this->client,
$this->parser,
$this->docBlockFactory,
$this->definitionResolver
);
$this->documents[$uri] = $document; $this->documents[$uri] = $document;
} }
$this->updateContent($document, $content);
return $document; return $document;
} }
@ -354,4 +356,144 @@ class Project
{ {
return isset($this->definitions[$fqn]); 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 <Node[]>
*/
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);
}
}
} }

View File

@ -98,7 +98,10 @@ class TextDocument
*/ */
public function didChange(VersionedTextDocumentIdentifier $textDocument, array $contentChanges) 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) { if ($node === null) {
return []; return [];
} }
$refs = yield $document->getReferenceNodesByNode($node); $refs = yield $this->project->getReferenceNodesByNode($node);
$locations = []; $locations = [];
foreach ($refs as $ref) { foreach ($refs as $ref) {
$locations[] = Location::fromNode($ref); $locations[] = Location::fromNode($ref);

View File

@ -5,10 +5,8 @@ namespace LanguageServer\Tests\Server\TextDocument;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use LanguageServer\Tests\MockProtocolStream; use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\{Server, Client, LanguageClient, Project}; use LanguageServer\{Server, LanguageClient, Project};
use LanguageServer\Protocol\{ use LanguageServer\Protocol\{
TextDocumentIdentifier,
TextDocumentItem,
VersionedTextDocumentIdentifier, VersionedTextDocumentIdentifier,
TextDocumentContentChangeEvent, TextDocumentContentChangeEvent,
Range, Range,