2016-09-30 09:30:08 +00:00
|
|
|
<?php
|
2016-09-30 09:54:49 +00:00
|
|
|
declare(strict_types = 1);
|
2016-09-30 09:30:08 +00:00
|
|
|
|
|
|
|
namespace LanguageServer;
|
|
|
|
|
2016-10-11 13:28:53 +00:00
|
|
|
use LanguageServer\Protocol\{Diagnostic, DiagnosticSeverity, Range, Position, TextEdit};
|
2016-10-11 23:45:15 +00:00
|
|
|
use LanguageServer\NodeVisitor\{
|
|
|
|
NodeAtPositionFinder,
|
|
|
|
ReferencesAdder,
|
2016-10-19 10:31:32 +00:00
|
|
|
DocBlockParser,
|
2016-10-11 23:45:15 +00:00
|
|
|
DefinitionCollector,
|
|
|
|
ColumnCalculator,
|
|
|
|
ReferencesCollector,
|
|
|
|
VariableReferencesCollector
|
|
|
|
};
|
2016-10-26 20:25:24 +00:00
|
|
|
use PhpParser\{Error, ErrorHandler, Node, NodeTraverser};
|
2016-09-30 09:30:08 +00:00
|
|
|
use PhpParser\NodeVisitor\NameResolver;
|
2016-10-19 10:31:32 +00:00
|
|
|
use phpDocumentor\Reflection\DocBlockFactory;
|
2016-11-14 09:25:44 +00:00
|
|
|
use Sabre\Event\Promise;
|
|
|
|
use function Sabre\Event\coroutine;
|
2016-11-14 19:00:10 +00:00
|
|
|
use Sabre\Uri;
|
2016-09-30 09:30:08 +00:00
|
|
|
|
|
|
|
class PhpDocument
|
|
|
|
{
|
2016-10-08 10:51:55 +00:00
|
|
|
/**
|
|
|
|
* The LanguageClient instance (to report errors etc)
|
|
|
|
*
|
|
|
|
* @var LanguageClient
|
|
|
|
*/
|
2016-09-30 09:30:08 +00:00
|
|
|
private $client;
|
2016-10-08 10:51:55 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* The Project this document belongs to (to register definitions etc)
|
|
|
|
*
|
|
|
|
* @var Project
|
|
|
|
*/
|
2016-10-08 12:59:08 +00:00
|
|
|
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
|
2016-10-08 10:51:55 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* The PHPParser instance
|
|
|
|
*
|
|
|
|
* @var Parser
|
|
|
|
*/
|
2016-09-30 09:30:08 +00:00
|
|
|
private $parser;
|
|
|
|
|
2016-10-19 10:31:32 +00:00
|
|
|
/**
|
|
|
|
* The DocBlockFactory instance to parse docblocks
|
|
|
|
*
|
|
|
|
* @var DocBlockFactory
|
|
|
|
*/
|
|
|
|
private $docBlockFactory;
|
|
|
|
|
2016-11-18 14:22:24 +00:00
|
|
|
/**
|
|
|
|
* The DefinitionResolver instance to resolve reference nodes to definitions
|
|
|
|
*
|
|
|
|
* @var DefinitionResolver
|
|
|
|
*/
|
|
|
|
private $definitionResolver;
|
|
|
|
|
2016-10-08 10:51:55 +00:00
|
|
|
/**
|
|
|
|
* The URI of the document
|
|
|
|
*
|
|
|
|
* @var string
|
|
|
|
*/
|
2016-09-30 09:30:08 +00:00
|
|
|
private $uri;
|
2016-10-08 10:51:55 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* The content of the document
|
|
|
|
*
|
|
|
|
* @var string
|
|
|
|
*/
|
2016-09-30 09:30:08 +00:00
|
|
|
private $content;
|
2016-10-08 10:51:55 +00:00
|
|
|
|
2016-10-08 12:59:08 +00:00
|
|
|
/**
|
|
|
|
* The AST of the document
|
|
|
|
*
|
|
|
|
* @var Node[]
|
|
|
|
*/
|
2016-10-11 23:45:15 +00:00
|
|
|
private $stmts;
|
2016-10-08 12:59:08 +00:00
|
|
|
|
|
|
|
/**
|
2016-11-18 14:22:24 +00:00
|
|
|
* Map from fully qualified name (FQN) to Definition
|
2016-10-08 12:59:08 +00:00
|
|
|
*
|
2016-11-18 14:22:24 +00:00
|
|
|
* @var Definition[]
|
2016-10-08 12:59:08 +00:00
|
|
|
*/
|
2016-10-11 12:42:56 +00:00
|
|
|
private $definitions;
|
2016-10-08 12:59:08 +00:00
|
|
|
|
2016-10-09 08:09:09 +00:00
|
|
|
/**
|
2016-11-18 14:22:24 +00:00
|
|
|
* Map from fully qualified name (FQN) to Node
|
2016-10-09 08:09:09 +00:00
|
|
|
*
|
2016-11-18 14:22:24 +00:00
|
|
|
* @var Node[]
|
2016-10-09 08:09:09 +00:00
|
|
|
*/
|
2016-11-18 14:22:24 +00:00
|
|
|
private $definitionNodes;
|
2016-10-09 08:09:09 +00:00
|
|
|
|
2016-10-20 00:08:23 +00:00
|
|
|
/**
|
2016-11-18 14:22:24 +00:00
|
|
|
* Map from fully qualified name (FQN) to array of nodes that reference the symbol
|
2016-10-20 00:08:23 +00:00
|
|
|
*
|
2016-11-18 14:22:24 +00:00
|
|
|
* @var Node[][]
|
2016-10-20 00:08:23 +00:00
|
|
|
*/
|
2016-11-18 14:22:24 +00:00
|
|
|
private $referenceNodes;
|
2016-10-20 00:08:23 +00:00
|
|
|
|
2016-10-08 10:51:55 +00:00
|
|
|
/**
|
2016-10-19 10:31:32 +00:00
|
|
|
* @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
|
2016-10-08 10:51:55 +00:00
|
|
|
*/
|
2016-11-18 14:22:24 +00:00
|
|
|
public function __construct(
|
|
|
|
string $uri,
|
|
|
|
string $content,
|
|
|
|
Project $project,
|
|
|
|
LanguageClient $client,
|
|
|
|
Parser $parser,
|
|
|
|
DocBlockFactory $docBlockFactory,
|
|
|
|
DefinitionResolver $definitionResolver
|
|
|
|
) {
|
2016-09-30 09:30:08 +00:00
|
|
|
$this->uri = $uri;
|
|
|
|
$this->project = $project;
|
|
|
|
$this->client = $client;
|
|
|
|
$this->parser = $parser;
|
2016-10-19 10:31:32 +00:00
|
|
|
$this->docBlockFactory = $docBlockFactory;
|
2016-11-18 14:22:24 +00:00
|
|
|
$this->definitionResolver = $definitionResolver;
|
2016-10-11 12:42:56 +00:00
|
|
|
$this->updateContent($content);
|
2016-09-30 09:30:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2016-10-11 23:45:15 +00:00
|
|
|
* Get all references of a fully qualified name
|
|
|
|
*
|
|
|
|
* @param string $fqn The fully qualified name of the symbol
|
|
|
|
* @return Node[]
|
|
|
|
*/
|
2016-11-18 14:22:24 +00:00
|
|
|
public function getReferenceNodesByFqn(string $fqn)
|
2016-10-11 23:45:15 +00:00
|
|
|
{
|
2016-11-18 14:22:24 +00:00
|
|
|
return isset($this->referenceNodes) && isset($this->referenceNodes[$fqn]) ? $this->referenceNodes[$fqn] : null;
|
2016-10-11 23:45:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Updates the content on this document.
|
2016-10-11 12:42:56 +00:00
|
|
|
* Re-parses a source file, updates symbols and reports parsing errors
|
|
|
|
* that may have occured as diagnostics.
|
2016-09-30 09:30:08 +00:00
|
|
|
*
|
|
|
|
* @param string $content
|
2016-10-08 10:51:55 +00:00
|
|
|
* @return void
|
2016-09-30 09:30:08 +00:00
|
|
|
*/
|
|
|
|
public function updateContent(string $content)
|
|
|
|
{
|
|
|
|
$this->content = $content;
|
|
|
|
$stmts = null;
|
|
|
|
|
2016-10-26 20:25:24 +00:00
|
|
|
$errorHandler = new ErrorHandler\Collecting;
|
|
|
|
$stmts = $this->parser->parse($content, $errorHandler);
|
2016-09-30 09:30:08 +00:00
|
|
|
|
|
|
|
$diagnostics = [];
|
2016-10-26 20:25:24 +00:00
|
|
|
foreach ($errorHandler->getErrors() as $error) {
|
2016-10-19 10:31:32 +00:00
|
|
|
$diagnostics[] = Diagnostic::fromError($error, $this->content, DiagnosticSeverity::ERROR, 'php');
|
2016-09-30 09:30:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// $stmts can be null in case of a fatal parsing error
|
|
|
|
if ($stmts) {
|
|
|
|
$traverser = new NodeTraverser;
|
2016-10-08 11:22:34 +00:00
|
|
|
|
|
|
|
// Resolve aliased names to FQNs
|
2016-10-26 20:25:24 +00:00
|
|
|
$traverser->addVisitor(new NameResolver($errorHandler));
|
2016-10-08 11:22:34 +00:00
|
|
|
|
|
|
|
// Add parentNode, previousSibling, nextSibling attributes
|
2016-10-08 12:53:13 +00:00
|
|
|
$traverser->addVisitor(new ReferencesAdder($this));
|
2016-10-08 11:22:34 +00:00
|
|
|
|
|
|
|
// Add column attributes to nodes
|
2016-10-11 12:42:56 +00:00
|
|
|
$traverser->addVisitor(new ColumnCalculator($content));
|
2016-10-08 11:22:34 +00:00
|
|
|
|
2016-10-19 10:31:32 +00:00
|
|
|
// Parse docblocks and add docBlock attributes to nodes
|
|
|
|
$docBlockParser = new DocBlockParser($this->docBlockFactory);
|
|
|
|
$traverser->addVisitor($docBlockParser);
|
|
|
|
|
2016-10-11 23:45:15 +00:00
|
|
|
$traverser->traverse($stmts);
|
2016-10-19 10:31:32 +00:00
|
|
|
|
|
|
|
// Report errors from parsing docblocks
|
|
|
|
foreach ($docBlockParser->errors as $error) {
|
|
|
|
$diagnostics[] = Diagnostic::fromError($error, $this->content, DiagnosticSeverity::WARNING, 'php');
|
|
|
|
}
|
|
|
|
|
2016-10-11 23:45:15 +00:00
|
|
|
$traverser = new NodeTraverser;
|
|
|
|
|
2016-10-08 12:59:08 +00:00
|
|
|
// Collect all definitions
|
2016-11-18 14:22:24 +00:00
|
|
|
$definitionCollector = new DefinitionCollector($this->definitionResolver);
|
2016-10-08 12:59:08 +00:00
|
|
|
$traverser->addVisitor($definitionCollector);
|
|
|
|
|
2016-10-11 23:45:15 +00:00
|
|
|
// Collect all references
|
2016-11-18 14:22:24 +00:00
|
|
|
$referencesCollector = new ReferencesCollector($this->definitionResolver);
|
2016-10-11 23:45:15 +00:00
|
|
|
$traverser->addVisitor($referencesCollector);
|
|
|
|
|
2016-09-30 09:30:08 +00:00
|
|
|
$traverser->traverse($stmts);
|
|
|
|
|
2016-10-19 11:33:43 +00:00
|
|
|
// Unregister old definitions
|
|
|
|
if (isset($this->definitions)) {
|
2016-11-18 14:22:24 +00:00
|
|
|
foreach ($this->definitions as $fqn => $definition) {
|
|
|
|
$this->project->removeDefinition($fqn);
|
2016-10-19 11:33:43 +00:00
|
|
|
}
|
|
|
|
}
|
2016-10-08 12:59:08 +00:00
|
|
|
// Register this document on the project for all the symbols defined in it
|
2016-10-11 23:45:15 +00:00
|
|
|
$this->definitions = $definitionCollector->definitions;
|
2016-11-18 14:22:24 +00:00
|
|
|
$this->definitionNodes = $definitionCollector->nodes;
|
|
|
|
foreach ($definitionCollector->definitions as $fqn => $definition) {
|
|
|
|
$this->project->setDefinition($fqn, $definition);
|
2016-10-08 12:59:08 +00:00
|
|
|
}
|
|
|
|
|
2016-10-19 11:33:43 +00:00
|
|
|
// Unregister old references
|
2016-11-18 14:22:24 +00:00
|
|
|
if (isset($this->referenceNodes)) {
|
|
|
|
foreach ($this->referenceNodes as $fqn => $node) {
|
2016-10-19 11:33:43 +00:00
|
|
|
$this->project->removeReferenceUri($fqn, $this->uri);
|
|
|
|
}
|
|
|
|
}
|
2016-10-11 23:45:15 +00:00
|
|
|
// Register this document on the project for references
|
2016-11-18 14:22:24 +00:00
|
|
|
$this->referenceNodes = $referencesCollector->nodes;
|
|
|
|
foreach ($referencesCollector->nodes as $fqn => $nodes) {
|
2016-10-11 23:45:15 +00:00
|
|
|
$this->project->addReferenceUri($fqn, $this->uri);
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->stmts = $stmts;
|
2016-10-08 11:34:49 +00:00
|
|
|
}
|
2016-10-19 10:31:32 +00:00
|
|
|
|
2016-11-14 19:00:10 +00:00
|
|
|
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;
|
2016-09-30 09:30:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2016-10-10 13:06:02 +00:00
|
|
|
* Returns array of TextEdit changes to format this document.
|
2016-09-30 09:30:08 +00:00
|
|
|
*
|
2016-10-10 13:06:02 +00:00
|
|
|
* @return \LanguageServer\Protocol\TextEdit[]
|
2016-09-30 09:30:08 +00:00
|
|
|
*/
|
|
|
|
public function getFormattedText()
|
|
|
|
{
|
2016-10-11 12:42:56 +00:00
|
|
|
if (empty($this->content)) {
|
2016-09-30 09:30:08 +00:00
|
|
|
return [];
|
|
|
|
}
|
2016-10-10 13:06:02 +00:00
|
|
|
return Formatter::format($this->content, $this->uri);
|
2016-09-30 09:30:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns this document's text content.
|
|
|
|
*
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public function getContent()
|
|
|
|
{
|
|
|
|
return $this->content;
|
|
|
|
}
|
2016-10-08 11:34:49 +00:00
|
|
|
|
2016-10-08 12:59:08 +00:00
|
|
|
/**
|
|
|
|
* Returns the URI of the document
|
|
|
|
*
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public function getUri(): string
|
|
|
|
{
|
|
|
|
return $this->uri;
|
|
|
|
}
|
|
|
|
|
2016-10-11 23:45:15 +00:00
|
|
|
/**
|
|
|
|
* Returns the AST of the document
|
|
|
|
*
|
|
|
|
* @return Node[]
|
|
|
|
*/
|
|
|
|
public function getStmts(): array
|
|
|
|
{
|
|
|
|
return $this->stmts;
|
|
|
|
}
|
|
|
|
|
2016-10-08 11:34:49 +00:00
|
|
|
/**
|
|
|
|
* Returns the node at a specified position
|
|
|
|
*
|
|
|
|
* @param Position $position
|
|
|
|
* @return Node|null
|
|
|
|
*/
|
|
|
|
public function getNodeAtPosition(Position $position)
|
|
|
|
{
|
2016-11-30 21:23:51 +00:00
|
|
|
if ($this->stmts === null) {
|
|
|
|
return null;
|
|
|
|
}
|
2016-10-08 11:34:49 +00:00
|
|
|
$traverser = new NodeTraverser;
|
|
|
|
$finder = new NodeAtPositionFinder($position);
|
|
|
|
$traverser->addVisitor($finder);
|
2016-10-11 23:45:15 +00:00
|
|
|
$traverser->traverse($this->stmts);
|
2016-10-08 11:34:49 +00:00
|
|
|
return $finder->node;
|
|
|
|
}
|
2016-10-08 12:59:08 +00:00
|
|
|
|
2016-11-30 21:23:51 +00:00
|
|
|
/**
|
|
|
|
* Returns a range of the content
|
|
|
|
*
|
|
|
|
* @param Range $range
|
|
|
|
* @return string|null
|
|
|
|
*/
|
|
|
|
public function getRange(Range $range)
|
|
|
|
{
|
|
|
|
if ($this->content === null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
$start = $range->start->toOffset($this->content);
|
|
|
|
$length = $range->end->toOffset($this->content) - $start;
|
|
|
|
return substr($this->content, $start, $length);
|
|
|
|
}
|
|
|
|
|
2016-10-08 12:59:08 +00:00
|
|
|
/**
|
|
|
|
* Returns the definition node for a fully qualified name
|
|
|
|
*
|
|
|
|
* @param string $fqn
|
|
|
|
* @return Node|null
|
|
|
|
*/
|
2016-11-18 14:22:24 +00:00
|
|
|
public function getDefinitionNodeByFqn(string $fqn)
|
2016-10-08 12:59:08 +00:00
|
|
|
{
|
2016-11-18 14:22:24 +00:00
|
|
|
return $this->definitionNodes[$fqn] ?? null;
|
2016-10-08 12:59:08 +00:00
|
|
|
}
|
|
|
|
|
2016-10-11 12:42:56 +00:00
|
|
|
/**
|
|
|
|
* Returns a map from fully qualified name (FQN) to Nodes defined in this document
|
|
|
|
*
|
|
|
|
* @return Node[]
|
|
|
|
*/
|
2016-11-18 14:22:24 +00:00
|
|
|
public function getDefinitionNodes()
|
2016-10-11 12:42:56 +00:00
|
|
|
{
|
2016-11-18 14:22:24 +00:00
|
|
|
return $this->definitionNodes;
|
2016-10-11 12:42:56 +00:00
|
|
|
}
|
|
|
|
|
2016-10-20 00:08:23 +00:00
|
|
|
/**
|
2016-11-18 14:22:24 +00:00
|
|
|
* Returns a map from fully qualified name (FQN) to Definition defined in this document
|
2016-10-20 00:08:23 +00:00
|
|
|
*
|
2016-11-18 14:22:24 +00:00
|
|
|
* @return Definition[]
|
2016-10-20 00:08:23 +00:00
|
|
|
*/
|
2016-11-18 14:22:24 +00:00
|
|
|
public function getDefinitions()
|
2016-10-20 00:08:23 +00:00
|
|
|
{
|
2016-11-29 12:10:02 +00:00
|
|
|
return $this->definitions ?? [];
|
2016-10-20 00:08:23 +00:00
|
|
|
}
|
|
|
|
|
2016-10-08 12:59:08 +00:00
|
|
|
/**
|
2016-10-09 08:09:09 +00:00
|
|
|
* 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]);
|
|
|
|
}
|
|
|
|
|
2016-10-11 23:45:15 +00:00
|
|
|
/**
|
|
|
|
* Returns the reference nodes for any node
|
|
|
|
* The references node MAY be in other documents, check the ownerDocument attribute
|
|
|
|
*
|
|
|
|
* @param Node $node
|
2016-11-14 09:25:44 +00:00
|
|
|
* @return Promise <Node[]>
|
2016-10-11 23:45:15 +00:00
|
|
|
*/
|
2016-11-18 14:22:24 +00:00
|
|
|
public function getReferenceNodesByNode(Node $node): Promise
|
2016-10-11 23:45:15 +00:00
|
|
|
{
|
2016-11-14 09:25:44 +00:00
|
|
|
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
|
2016-11-18 14:22:24 +00:00
|
|
|
if (
|
|
|
|
$node instanceof Node\Expr\Variable
|
|
|
|
|| $node instanceof Node\Param
|
|
|
|
|| $node instanceof Node\Expr\ClosureUse
|
|
|
|
) {
|
2016-11-14 09:25:44 +00:00
|
|
|
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());
|
2016-11-18 14:22:24 +00:00
|
|
|
return $refCollector->nodes;
|
2016-10-11 23:45:15 +00:00
|
|
|
}
|
2016-11-14 09:25:44 +00:00
|
|
|
// Definition with a global FQN
|
2016-11-18 14:22:24 +00:00
|
|
|
$fqn = DefinitionResolver::getDefinedFqn($node);
|
2016-11-14 09:25:44 +00:00
|
|
|
if ($fqn === null) {
|
|
|
|
return [];
|
2016-10-11 23:45:15 +00:00
|
|
|
}
|
2016-11-14 09:25:44 +00:00
|
|
|
$refDocuments = yield $this->project->getReferenceDocuments($fqn);
|
|
|
|
$nodes = [];
|
|
|
|
foreach ($refDocuments as $document) {
|
2016-11-18 14:22:24 +00:00
|
|
|
$refs = $document->getReferenceNodesByFqn($fqn);
|
2016-11-14 09:25:44 +00:00
|
|
|
if ($refs !== null) {
|
|
|
|
foreach ($refs as $ref) {
|
|
|
|
$nodes[] = $ref;
|
|
|
|
}
|
2016-10-11 23:45:15 +00:00
|
|
|
}
|
|
|
|
}
|
2016-11-14 09:25:44 +00:00
|
|
|
return $nodes;
|
|
|
|
});
|
2016-10-11 23:45:15 +00:00
|
|
|
}
|
2016-09-30 09:30:08 +00:00
|
|
|
}
|