1
0
Fork 0

Merge pull request #49 from felixfbecker/definition

Go to definition support
pull/53/head
Felix Becker 2016-10-09 16:25:58 +02:00 committed by GitHub
commit 7032f806d4
27 changed files with 1499 additions and 429 deletions

24
fixtures/references.php Normal file
View File

@ -0,0 +1,24 @@
<?php
namespace TestNamespace;
$obj = new TestClass();
$obj->testMethod();
echo $obj->testProperty;
TestClass::staticTestMethod();
echo TestClass::$staticTestProperty;
echo TestClass::TEST_CLASS_CONST;
test_function();
$var = 123;
echo $var;
function whatever(TestClass $param): TestClass {
echo $param;
}
$fn = function() use ($var) {
echo $var;
};
echo TEST_CONST;

View File

@ -2,10 +2,19 @@
namespace TestNamespace;
class TestClass
const TEST_CONST = 123;
class TestClass implements TestInterface
{
const TEST_CLASS_CONST = 123;
public static $staticTestProperty;
public $testProperty;
public static function staticTestMethod()
{
}
public function testMethod($testParameter)
{
$testVariable = 123;
@ -21,3 +30,24 @@ interface TestInterface
{
}
function test_function()
{
}
new class {
const TEST_CLASS_CONST = 123;
public static $staticTestProperty;
public $testProperty;
public static function staticTestMethod()
{
}
public function testMethod($testParameter)
{
$testVariable = 123;
}
};

6
fixtures/use.php Normal file
View File

@ -0,0 +1,6 @@
<?php
namespace SecondTestNamespace;
use TestNamespace\TestClass;
use TestNamespace\{TestTrait, TestInterface};

View File

@ -103,6 +103,9 @@ class LanguageServer extends \AdvancedJsonRpc\Dispatcher
$serverCapabilities->workspaceSymbolProvider = true;
// Support "Format Code"
$serverCapabilities->documentFormattingProvider = true;
// Support "Go to definition"
$serverCapabilities->definitionProvider = true;
return new InitializeResult($serverCapabilities);
}

View File

@ -1,7 +1,7 @@
<?php
declare(strict_types = 1);
namespace LanguageServer;
namespace LanguageServer\NodeVisitor;
use PhpParser\{NodeVisitorAbstract, Node};

View File

@ -0,0 +1,28 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\NodeVisitor;
use PhpParser\{NodeVisitorAbstract, Node};
/**
* Collects definitions of classes, interfaces, traits, methods, properties and constants
* Depends on ReferencesAdder and NameResolver
*/
class DefinitionCollector extends NodeVisitorAbstract
{
/**
* Map from fully qualified name (FQN) to Node
*
* @var Node[]
*/
public $definitions = [];
public function enterNode(Node $node)
{
$fqn = $node->getAttribute('ownerDocument')->getDefinedFqn($node);
if ($fqn !== null) {
$this->definitions[$fqn] = $node;
}
}
}

View File

@ -0,0 +1,47 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\NodeVisitor;
use PhpParser\{NodeVisitorAbstract, Node};
use LanguageServer\Protocol\{Position, Range};
/**
* Finds the Node at a specified position
* Depends on ColumnCalculator
*/
class NodeAtPositionFinder extends NodeVisitorAbstract
{
/**
* The node at the position, if found
*
* @var Node
*/
public $node;
/**
* @var Position
*/
private $position;
/**
* @param Position $position The position where the node is located
*/
public function __construct(Position $position)
{
$this->position = $position;
}
public function leaveNode(Node $node)
{
$range = Range::fromNode($node);
// Workaround for https://github.com/nikic/PHP-Parser/issues/311
$parent = $node->getAttribute('parentNode');
if (isset($parent) && $parent instanceof Node\Stmt\GroupUse && $parent->prefix === $node) {
return;
}
if (!isset($this->node) && $range->includes($this->position)) {
$this->node = $node;
}
}
}

View File

@ -0,0 +1,55 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\NodeVisitor;
use PhpParser\{NodeVisitorAbstract, Node};
use LanguageServer\Protocol\{SymbolInformation, SymbolKind, Range, Position, Location};
/**
* Decorates all nodes with parent and sibling references (similar to DOM nodes)
*/
class ReferencesAdder extends NodeVisitorAbstract
{
/**
* @var Node[]
*/
private $stack = [];
/**
* @var Node
*/
private $previous;
/**
* @var mixed
*/
private $document;
/**
* @param mixed $document The value for the ownerDocument attribute
*/
public function __construct($document = null)
{
$this->document = $document;
}
public function enterNode(Node $node)
{
$node->setAttribute('ownerDocument', $this->document);
if (!empty($this->stack)) {
$node->setAttribute('parentNode', end($this->stack));
}
if (isset($this->previous) && $this->previous->getAttribute('parentNode') === $node->getAttribute('parentNode')) {
$node->setAttribute('previousSibling', $this->previous);
$this->previous->setAttribute('nextSibling', $node);
}
$this->stack[] = $node;
}
public function leaveNode(Node $node)
{
$this->previous = $node;
array_pop($this->stack);
}
}

View File

@ -3,8 +3,8 @@ declare(strict_types = 1);
namespace LanguageServer;
use \LanguageServer\Protocol\{Diagnostic, DiagnosticSeverity, Range, Position, SymbolKind, TextEdit};
use LanguageServer\Protocol\{Diagnostic, DiagnosticSeverity, Range, Position, SymbolInformation, SymbolKind, TextEdit, Location};
use LanguageServer\NodeVisitor\{NodeAtPositionFinder, ReferencesAdder, DefinitionCollector, ColumnCalculator};
use PhpParser\{Error, Comment, Node, ParserFactory, NodeTraverser, Lexer, Parser};
use PhpParser\PrettyPrinter\Standard as PrettyPrinter;
use PhpParser\NodeVisitor\NameResolver;
@ -23,7 +23,9 @@ class PhpDocument
*
* @var Project
*/
private $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
@ -47,9 +49,25 @@ class PhpDocument
private $content;
/**
* @var SymbolInformation[]
* The AST of the document
*
* @var Node[]
*/
private $symbols = [];
private $stmts = [];
/**
* Map from fully qualified name (FQN) to Node
*
* @var Node[]
*/
private $definitions = [];
/**
* Map from fully qualified name (FQN) to array of nodes that reference the symbol
*
* @var Node[][]
*/
private $references;
/**
* @param string $uri The URI of the document
@ -68,22 +86,54 @@ class PhpDocument
/**
* Returns all symbols in this document.
*
* @return SymbolInformation[]
* @return SymbolInformation[]|null
*/
public function getSymbols()
{
return $this->symbols;
if (!isset($this->definitions)) {
return null;
}
$nodeSymbolKindMap = [
Node\Stmt\Class_::class => SymbolKind::CLASS_,
Node\Stmt\Trait_::class => SymbolKind::CLASS_,
Node\Stmt\Interface_::class => SymbolKind::INTERFACE,
Node\Stmt\Namespace_::class => SymbolKind::NAMESPACE,
Node\Stmt\Function_::class => SymbolKind::FUNCTION,
Node\Stmt\ClassMethod::class => SymbolKind::METHOD,
Node\Stmt\PropertyProperty::class => SymbolKind::PROPERTY,
Node\Const_::class => SymbolKind::CONSTANT
];
$symbols = [];
foreach ($this->definitions as $fqn => $node) {
$class = get_class($node);
if (!isset($nodeSymbolKindMap[$class])) {
continue;
}
$symbol = new SymbolInformation();
$symbol->kind = $nodeSymbolKindMap[$class];
$symbol->name = (string)$node->name;
$symbol->location = Location::fromNode($node);
$parts = preg_split('/(::|\\\\)/', $fqn);
array_pop($parts);
$symbol->containerName = implode('\\', $parts);
$symbols[] = $symbol;
}
return $symbols;
}
/**
* Returns symbols in this document filtered by query string.
*
* @param string $query The search query
* @return SymbolInformation[]
* @return SymbolInformation[]|null
*/
public function findSymbols(string $query)
{
return array_filter($this->symbols, function($symbol) use(&$query) {
$symbols = $this->getSymbols();
if ($symbols === null) {
return null;
}
return array_filter($symbols, function($symbol) use ($query) {
return stripos($symbol->name, $query) !== false;
});
}
@ -104,7 +154,7 @@ class PhpDocument
* Re-parses a source file, updates symbols, reports parsing errors
* that may have occured as diagnostics and returns parsed nodes.
*
* @return \PhpParser\Node[]
* @return void
*/
public function parse()
{
@ -138,16 +188,30 @@ class PhpDocument
// $stmts can be null in case of a fatal parsing error
if ($stmts) {
$traverser = new NodeTraverser;
$finder = new SymbolFinder($this->uri);
// Resolve aliased names to FQNs
$traverser->addVisitor(new NameResolver);
// Add parentNode, previousSibling, nextSibling attributes
$traverser->addVisitor(new ReferencesAdder($this));
// Add column attributes to nodes
$traverser->addVisitor(new ColumnCalculator($this->content));
$traverser->addVisitor($finder);
// Collect all definitions
$definitionCollector = new DefinitionCollector;
$traverser->addVisitor($definitionCollector);
$traverser->traverse($stmts);
$this->symbols = $finder->symbols;
}
$this->definitions = $definitionCollector->definitions;
// Register this document on the project for all the symbols defined in it
foreach ($definitionCollector->definitions as $fqn => $node) {
$this->project->addDefinitionDocument($fqn, $this);
}
return $stmts;
$this->stmts = $stmts;
}
}
/**
@ -157,14 +221,13 @@ class PhpDocument
*/
public function getFormattedText()
{
$stmts = $this->parse();
if (empty($stmts)) {
if (empty($this->stmts)) {
return [];
}
$prettyPrinter = new PrettyPrinter();
$edit = new TextEdit();
$edit->range = new Range(new Position(0, 0), new Position(PHP_INT_MAX, PHP_INT_MAX));
$edit->newText = $prettyPrinter->prettyPrintFile($stmts);
$edit->newText = $prettyPrinter->prettyPrintFile($this->stmts);
return [$edit];
}
@ -177,4 +240,294 @@ class PhpDocument
{
return $this->content;
}
/**
* Returns the URI of the document
*
* @return string
*/
public function getUri(): string
{
return $this->uri;
}
/**
* Returns the node at a specified position
*
* @param Position $position
* @return Node|null
*/
public function getNodeAtPosition(Position $position)
{
if ($this->stmts === null) {
return null;
}
$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 getDefinitionByFqn(string $fqn)
{
return $this->definitions[$fqn] ?? null;
}
/**
* 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 fully qualified name (FQN) that is defined by a node
* Examples of FQNs:
* - testFunction()
* - TestNamespace\TestClass
* - TestNamespace\TestClass::TEST_CONSTANT
* - TestNamespace\TestClass::staticTestProperty
* - TestNamespace\TestClass::testProperty
* - TestNamespace\TestClass::staticTestMethod()
* - TestNamespace\TestClass::testMethod()
*
* @param Node $node
* @return string|null
*/
public function getDefinedFqn(Node $node)
{
if ($node instanceof Node\Name) {
$nameNode = $node;
$node = $node->getAttribute('parentNode');
}
// Only the class node should count as the definition, not the name node
// Anonymous classes don't count as a definition
if ($node instanceof Node\Stmt\ClassLike && !isset($nameNode) && isset($node->name)) {
// Class, interface or trait declaration
return (string)$node->namespacedName;
} else if ($node instanceof Node\Stmt\Function_) {
// Function: use functionName() as the name
return (string)$node->namespacedName . '()';
} else if ($node instanceof Node\Stmt\ClassMethod) {
// Class method: use ClassName::methodName() as name
$class = $node->getAttribute('parentNode');
if (!isset($class->name)) {
// Ignore anonymous classes
return null;
}
return (string)$class->namespacedName . '::' . (string)$node->name . '()';
} else if ($node instanceof Node\Stmt\PropertyProperty) {
// Property: use ClassName::propertyName as name
$class = $node->getAttribute('parentNode')->getAttribute('parentNode');
if (!isset($class->name)) {
// Ignore anonymous classes
return null;
}
return (string)$class->namespacedName . '::' . (string)$node->name;
} else if ($node instanceof Node\Const_) {
$parent = $node->getAttribute('parentNode');
if ($parent instanceof Node\Stmt\Const_) {
// Basic constant: use CONSTANT_NAME as name
return (string)$node->namespacedName;
}
if ($parent instanceof Node\Stmt\ClassConst) {
// Class constant: use ClassName::CONSTANT_NAME as name
$class = $parent->getAttribute('parentNode');
if (!isset($class->name) || $class->name instanceof Node\Expr) {
return null;
}
return (string)$class->namespacedName . '::' . $node->name;
}
}
}
/**
* Returns the FQN that is referenced by a node
*
* @param Node $node
* @return string|null
*/
public function getReferencedFqn(Node $node)
{
if ($node instanceof Node\Name) {
$nameNode = $node;
$node = $node->getAttribute('parentNode');
}
if (
($node instanceof Node\Stmt\ClassLike
|| $node instanceof Node\Param
|| $node instanceof Node\Stmt\Function_)
&& isset($nameNode)
) {
// For extends, implements and type hints use the name directly
$name = (string)$nameNode;
// Only the name node should be considered a reference, not the UseUse node itself
} else if ($node instanceof Node\Stmt\UseUse && isset($nameNode)) {
$name = (string)$node->name;
$parent = $node->getAttribute('parentNode');
if ($parent instanceof Node\Stmt\GroupUse) {
$name = $parent->prefix . '\\' . $name;
}
// Only the name node should be considered a reference, not the New_ node itself
} else if ($node instanceof Node\Expr\New_ && isset($nameNode)) {
if (!($node->class instanceof Node\Name)) {
// Cannot get definition of dynamic calls
return null;
}
$name = (string)$node->class;
} else if ($node instanceof Node\Expr\MethodCall || $node instanceof Node\Expr\PropertyFetch) {
if ($node->name instanceof Node\Expr || !($node->var instanceof Node\Expr\Variable)) {
// Cannot get definition of dynamic calls
return null;
}
// Need to resolve variable to a class
$varDef = $this->getVariableDefinition($node->var);
if (!isset($varDef)) {
return null;
}
if ($varDef instanceof Node\Param) {
if (!isset($varDef->type)) {
// Cannot resolve to class without a type hint
// TODO: parse docblock
return null;
}
$name = (string)$varDef->type;
} else if ($varDef instanceof Node\Expr\Assign) {
if ($varDef->expr instanceof Node\Expr\New_) {
if (!($varDef->expr->class instanceof Node\Name)) {
// Cannot get definition of dynamic calls
return null;
}
$name = (string)$varDef->expr->class;
} else {
return null;
}
} else {
return null;
}
$name .= '::' . (string)$node->name;
} else if ($node instanceof Node\Expr\FuncCall) {
if ($node->name instanceof Node\Expr) {
return null;
}
$name = (string)$node->name;
} else if ($node instanceof Node\Expr\ConstFetch) {
$name = (string)$node->name;
} else if (
$node instanceof Node\Expr\ClassConstFetch
|| $node instanceof Node\Expr\StaticPropertyFetch
|| $node instanceof Node\Expr\StaticCall
) {
if ($node->class instanceof Node\Expr || $node->name instanceof Node\Expr) {
// Cannot get definition of dynamic names
return null;
}
$name = (string)$node->class . '::' . $node->name;
}
if (
$node instanceof Node\Expr\MethodCall
|| $node instanceof Node\Expr\FuncCall
|| $node instanceof Node\Expr\StaticCall
) {
$name .= '()';
}
if (!isset($name)) {
return null;
}
// If the node is a function or constant, it could be namespaced, but PHP falls back to global
// The NameResolver therefor does not resolve these to namespaced names
// http://php.net/manual/en/language.namespaces.fallback.php
if ($node instanceof Node\Expr\FuncCall || $node instanceof Node\Expr\ConstFetch) {
// Find and try with namespace
$n = $node;
while (isset($n)) {
$n = $n->getAttribute('parentNode');
if ($n instanceof Node\Stmt\Namespace_) {
$namespacedName = (string)$n->name . '\\' . $name;
// If the namespaced version is defined, return that
// Otherwise fall back to global
if ($this->project->isDefined($namespacedName)) {
return $namespacedName;
}
}
}
}
return $name;
}
/**
* Returns the definition node for any node
* The definition node MAY be in another document, check the ownerDocument attribute
*
* @param Node $node
* @return Node|null
*/
public function getDefinitionByNode(Node $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) {
return $this->getVariableDefinition($node);
}
$fqn = $this->getReferencedFqn($node);
if (!isset($fqn)) {
return null;
}
$document = $this->project->getDefinitionDocument($fqn);
if (!isset($document)) {
return null;
}
return $document->getDefinitionByFqn($fqn);
}
/**
* Returns the assignment or parameter node where a variable was defined
*
* @param Node\Expr\Variable $n The variable access
* @return Node\Expr\Assign|Node\Param|Node\Expr\ClosureUse|null
*/
public function getVariableDefinition(Node\Expr\Variable $var)
{
$n = $var;
// Traverse the AST up
while (isset($n) && $n = $n->getAttribute('parentNode')) {
// If a function is met, check the parameters and use statements
if ($n instanceof Node\FunctionLike) {
foreach ($n->getParams() as $param) {
if ($param->name === $var->name) {
return $param;
}
}
// If it is a closure, also check use statements
if ($n instanceof Node\Expr\Closure) {
foreach ($n->uses as $use) {
if ($use->var === $var->name) {
return $use;
}
}
}
break;
}
// Check each previous sibling node for a variable assignment to that variable
while ($n->getAttribute('previousSibling') && $n = $n->getAttribute('previousSibling')) {
if ($n instanceof Node\Expr\Assign && $n->var->name === $var->name) {
return $n;
}
}
}
// Return null if nothing was found
return null;
}
}

View File

@ -15,7 +15,15 @@ class Project
*
* @var PhpDocument[]
*/
private $documents;
private $documents = [];
/**
* An associative array [string => PhpDocument]
* that maps fully qualified symbol names to loaded PhpDocuments
*
* @var PhpDocument[]
*/
private $definitions = [];
/**
* Instance of the PHP parser
@ -54,6 +62,39 @@ class Project
return $this->documents[$uri];
}
/**
* Adds a document as the container for a specific symbol
*
* @param string $fqn The fully qualified name of the symbol
* @return void
*/
public function addDefinitionDocument(string $fqn, PhpDocument $document)
{
$this->definitions[$fqn] = $document;
}
/**
* Returns the document where a symbol is defined
*
* @param string $fqn The fully qualified name of the symbol
* @return PhpDocument|null
*/
public function getDefinitionDocument(string $fqn)
{
return $this->definitions[$fqn] ?? null;
}
/**
* Returns true if the given FQN is defined in the project
*
* @param string $fqn The fully qualified name of the symbol
* @return bool
*/
public function isDefined(string $fqn): bool
{
return isset($this->definitions[$fqn]);
}
/**
* Finds symbols in all documents, filtered by query parameter.
*
@ -64,7 +105,10 @@ class Project
{
$queryResult = [];
foreach ($this->documents as $uri => $document) {
$queryResult = array_merge($queryResult, $document->findSymbols($query));
$documentQueryResult = $document->findSymbols($query);
if ($documentQueryResult !== null) {
$queryResult = array_merge($queryResult, $documentQueryResult);
}
}
return $queryResult;
}

View File

@ -2,6 +2,8 @@
namespace LanguageServer\Protocol;
use PhpParser\Node;
/**
* Represents a location inside a resource, such as a line inside a text file.
*/
@ -17,6 +19,17 @@ class Location
*/
public $range;
/**
* Returns the location of the node
*
* @param Node $node
* @return self
*/
public static function fromNode(Node $node)
{
return new self($node->getAttribute('ownerDocument')->getUri(), Range::fromNode($node));
}
public function __construct(string $uri = null, Range $range = null)
{
$this->uri = $uri;

View File

@ -26,4 +26,27 @@ class Position
$this->line = $line;
$this->character = $character;
}
/**
* Compares this position to another position
* Returns
* - 0 if the positions match
* - a negative number if $this is before $position
* - a positive number otherwise
*
* @param Position $position
* @return int
*/
public function compare(Position $position): int
{
if ($this->line === $position->line && $this->character === $position->character) {
return 0;
}
if ($this->line !== $position->line) {
return $this->line - $position->line;
}
return $this->character - $position->character;
}
}

View File

@ -2,6 +2,8 @@
namespace LanguageServer\Protocol;
use PhpParser\Node;
/**
* A range in a text document expressed as (zero-based) start and end positions.
*/
@ -21,9 +23,34 @@ class Range
*/
public $end;
/**
* Returns the range the node spans
*
* @param Node $node
* @return self
*/
public static function fromNode(Node $node)
{
return new self(
new Position($node->getAttribute('startLine') - 1, $node->getAttribute('startColumn') - 1),
new Position($node->getAttribute('endLine') - 1, $node->getAttribute('endColumn'))
);
}
public function __construct(Position $start = null, Position $end = null)
{
$this->start = $start;
$this->end = $end;
}
/**
* Checks if a position is within the range
*
* @param Position $position
* @return bool
*/
public function includes(Position $position): bool
{
return $this->start->compare($position) <= 0 && $this->end->compare($position) >= 0;
}
}

View File

@ -2,6 +2,8 @@
namespace LanguageServer\Protocol;
use PhpParser\Node;
/**
* Represents information about programming constructs like variables, classes,
* interfaces etc.

View File

@ -3,7 +3,7 @@ declare(strict_types = 1);
namespace LanguageServer\Server;
use LanguageServer\{LanguageClient, ColumnCalculator, SymbolFinder, Project};
use LanguageServer\{LanguageClient, ColumnCalculator, Project};
use LanguageServer\Protocol\{
TextDocumentItem,
TextDocumentIdentifier,
@ -13,7 +13,8 @@ use LanguageServer\Protocol\{
Range,
Position,
FormattingOptions,
TextEdit
TextEdit,
Location
};
/**
@ -88,4 +89,26 @@ class TextDocument
{
return $this->project->getDocument($textDocument->uri)->getFormattedText();
}
/**
* The goto definition request is sent from the client to the server to resolve the definition location of a symbol
* at a given text document position.
*
* @param TextDocumentIdentifier $textDocument The text document
* @param Position $position The position inside the text document
* @return Location|Location[]|null
*/
public function definition(TextDocumentIdentifier $textDocument, Position $position)
{
$document = $this->project->getDocument($textDocument->uri);
$node = $document->getNodeAtPosition($position);
if ($node === null) {
return null;
}
$def = $document->getDefinitionByNode($node);
if ($def === null) {
return null;
}
return Location::fromNode($def);
}
}

View File

@ -6,7 +6,7 @@ namespace LanguageServer\Server;
use PhpParser\{Error, Comment, Node, ParserFactory, NodeTraverser, Lexer};
use PhpParser\PrettyPrinter\Standard as PrettyPrinter;
use PhpParser\NodeVisitor\NameResolver;
use LanguageServer\{LanguageClient, ColumnCalculator, SymbolFinder, Project};
use LanguageServer\{LanguageClient, ColumnCalculator, Project};
use LanguageServer\Protocol\{
TextDocumentItem,
TextDocumentIdentifier,

View File

@ -1,121 +0,0 @@
<?php
declare(strict_types = 1);
namespace LanguageServer;
use PhpParser\{NodeVisitorAbstract, Node};
use LanguageServer\Protocol\{SymbolInformation, SymbolKind, Range, Position, Location};
class SymbolFinder extends NodeVisitorAbstract
{
const NODE_SYMBOL_KIND_MAP = [
Node\Stmt\Class_::class => SymbolKind::CLASS_,
Node\Stmt\Trait_::class => SymbolKind::CLASS_,
Node\Stmt\Interface_::class => SymbolKind::INTERFACE,
Node\Stmt\Namespace_::class => SymbolKind::NAMESPACE,
Node\Stmt\Function_::class => SymbolKind::FUNCTION,
Node\Stmt\ClassMethod::class => SymbolKind::METHOD,
Node\Stmt\PropertyProperty::class => SymbolKind::PROPERTY,
Node\Const_::class => SymbolKind::CONSTANT,
Node\Expr\Variable::class => SymbolKind::VARIABLE
];
/**
* @var LanguageServer\Protocol\SymbolInformation[]
*/
public $symbols = [];
/**
* @var string
*/
private $uri;
/**
* @var string
*/
private $containerName;
/**
* @var array
*/
private $nameStack = [];
/**
* @var array
*/
private $nodeStack = [];
/**
* @var int
*/
private $functionCount = 0;
public function __construct(string $uri)
{
$this->uri = $uri;
}
public function enterNode(Node $node)
{
$this->nodeStack[] = $node;
$containerName = end($this->nameStack);
// If we enter a named node, push its name onto name stack.
// Else push the current name onto stack.
if (!empty($node->name) && (is_string($node->name) || method_exists($node->name, '__toString')) && !empty((string)$node->name)) {
if (empty($containerName)) {
$this->nameStack[] = (string)$node->name;
} else if ($node instanceof Node\Stmt\ClassMethod) {
$this->nameStack[] = $containerName . '::' . (string)$node->name;
} else {
$this->nameStack[] = $containerName . '\\' . (string)$node->name;
}
} else {
$this->nameStack[] = $containerName;
// We are not interested in unnamed nodes, return
return;
}
$class = get_class($node);
if (!isset(self::NODE_SYMBOL_KIND_MAP[$class])) {
return;
}
// if we enter a method or function, increase the function counter
if ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassMethod) {
$this->functionCount++;
}
$kind = self::NODE_SYMBOL_KIND_MAP[$class];
// exclude non-global variable symbols.
if ($kind === SymbolKind::VARIABLE && $this->functionCount > 0) {
return;
}
$symbol = new SymbolInformation();
$symbol->kind = $kind;
$symbol->name = (string)$node->name;
$symbol->location = new Location(
$this->uri,
new Range(
new Position($node->getAttribute('startLine') - 1, $node->getAttribute('startColumn') - 1),
new Position($node->getAttribute('endLine') - 1, $node->getAttribute('endColumn'))
)
);
$symbol->containerName = $containerName;
$this->symbols[] = $symbol;
}
public function leaveNode(Node $node)
{
array_pop($this->nodeStack);
array_pop($this->nameStack);
// if we leave a method or function, decrease the function counter
if ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassMethod) {
$this->functionCount--;
}
}
}

View File

@ -34,7 +34,7 @@ class LanguageServerTest extends TestCase
'hoverProvider' => null,
'completionProvider' => null,
'signatureHelpProvider' => null,
'definitionProvider' => null,
'definitionProvider' => true,
'referencesProvider' => null,
'documentHighlightProvider' => null,
'workspaceSymbolProvider' => true,

View File

@ -0,0 +1,52 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Tests\Server\TextDocument;
use PHPUnit\Framework\TestCase;
use PhpParser\{ParserFactory, NodeTraverser, Node};
use PhpParser\NodeVisitor\NameResolver;
use LanguageServer\{LanguageClient, Project, PhpDocument};
use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\NodeVisitor\{ReferencesAdder, DefinitionCollector};
class DefinitionCollectorTest extends TestCase
{
public function test()
{
$client = new LanguageClient(new MockProtocolStream());
$project = new Project($client);
$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
$document = new PhpDocument('whatever', $project, $client, $parser);
$traverser = new NodeTraverser;
$traverser->addVisitor(new NameResolver);
$traverser->addVisitor(new ReferencesAdder($document));
$definitionCollector = new DefinitionCollector;
$traverser->addVisitor($definitionCollector);
$stmts = $parser->parse(file_get_contents(__DIR__ . '/../../fixtures/symbols.php'));
$traverser->traverse($stmts);
$defs = $definitionCollector->definitions;
$this->assertEquals([
'TestNamespace\\TEST_CONST',
'TestNamespace\\TestClass',
'TestNamespace\\TestClass::TEST_CLASS_CONST',
'TestNamespace\\TestClass::staticTestProperty',
'TestNamespace\\TestClass::testProperty',
'TestNamespace\\TestClass::staticTestMethod()',
'TestNamespace\\TestClass::testMethod()',
'TestNamespace\\TestTrait',
'TestNamespace\\TestInterface',
'TestNamespace\\test_function()'
], array_keys($defs));
$this->assertInstanceOf(Node\Const_::class, $defs['TestNamespace\\TEST_CONST']);
$this->assertInstanceOf(Node\Stmt\Class_::class, $defs['TestNamespace\\TestClass']);
$this->assertInstanceOf(Node\Const_::class, $defs['TestNamespace\\TestClass::TEST_CLASS_CONST']);
$this->assertInstanceOf(Node\Stmt\PropertyProperty::class, $defs['TestNamespace\\TestClass::staticTestProperty']);
$this->assertInstanceOf(Node\Stmt\PropertyProperty::class, $defs['TestNamespace\\TestClass::testProperty']);
$this->assertInstanceOf(Node\Stmt\ClassMethod::class, $defs['TestNamespace\\TestClass::staticTestMethod()']);
$this->assertInstanceOf(Node\Stmt\ClassMethod::class, $defs['TestNamespace\\TestClass::testMethod()']);
$this->assertInstanceOf(Node\Stmt\Trait_::class, $defs['TestNamespace\\TestTrait']);
$this->assertInstanceOf(Node\Stmt\Interface_::class, $defs['TestNamespace\\TestInterface']);
$this->assertInstanceOf(Node\Stmt\Function_::class, $defs['TestNamespace\\test_function()']);
}
}

View File

@ -6,7 +6,9 @@ namespace LanguageServer\Tests\Server;
use PHPUnit\Framework\TestCase;
use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\{LanguageClient, Project};
use LanguageServer\Protocol\SymbolKind;
use LanguageServer\NodeVisitor\NodeAtPositionFinder;
use LanguageServer\Protocol\{SymbolKind, Position};
use PhpParser\Node;
class PhpDocumentTest extends TestCase
{
@ -28,43 +30,15 @@ class PhpDocumentTest extends TestCase
$symbols = $document->getSymbols();
$this->assertEquals([
[
'name' => 'a',
'kind' => SymbolKind::VARIABLE,
'location' => [
'uri' => 'whatever',
'range' => [
'start' => [
'line' => 1,
'character' => 0
],
'end' => [
'line' => 1,
'character' => 3
]
]
],
'containerName' => null
],
[
'name' => 'bar',
'kind' => SymbolKind::VARIABLE,
'location' => [
'uri' => 'whatever',
'range' => [
'start' => [
'line' => 2,
'character' => 0
],
'end' => [
'line' => 2,
'character' => 4
]
]
],
'containerName' => null
]
], json_decode(json_encode($symbols), true));
$this->assertEquals([], json_decode(json_encode($symbols), true));
}
public function testGetNodeAtPosition()
{
$document = $this->project->getDocument('whatever');
$document->updateContent("<?php\n$\$a = new SomeClass;");
$node = $document->getNodeAtPosition(new Position(1, 13));
$this->assertInstanceOf(Node\Name\FullyQualified::class, $node);
$this->assertEquals('SomeClass', (string)$node);
}
}

View File

@ -41,6 +41,7 @@ class ProjectTest extends TestCase
{
$this->project->getDocument('file:///document1.php')->updateContent("<?php\nfunction foo() {}\nfunction bar() {}\n");
$this->project->getDocument('file:///document2.php')->updateContent("<?php\nfunction baz() {}\nfunction frob() {}\n");
$this->project->getDocument('invalid_file')->updateContent(file_get_contents(__DIR__ . '/../fixtures/invalid_file.php'));
$symbols = $this->project->findSymbols('ba');

View File

@ -0,0 +1,346 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Tests\Server\TextDocument;
use PHPUnit\Framework\TestCase;
use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\{Server, LanguageClient, Project};
use LanguageServer\Protocol\{TextDocumentIdentifier, Position};
class DefinitionTest extends TestCase
{
/**
* @var Server\TextDocument
*/
private $textDocument;
public function setUp()
{
$client = new LanguageClient(new MockProtocolStream());
$project = new Project($client);
$this->textDocument = new Server\TextDocument($project, $client);
$project->getDocument('references')->updateContent(file_get_contents(__DIR__ . '/../../../fixtures/references.php'));
$project->getDocument('symbols')->updateContent(file_get_contents(__DIR__ . '/../../../fixtures/symbols.php'));
$project->getDocument('use')->updateContent(file_get_contents(__DIR__ . '/../../../fixtures/use.php'));
}
public function testDefinitionForClassLike()
{
// $obj = new TestClass();
// Get definition for TestClass
$result = $this->textDocument->definition(new TextDocumentIdentifier('references'), new Position(4, 16));
$this->assertEquals([
'uri' => 'symbols',
'range' => [
'start' => [
'line' => 6,
'character' => 0
],
'end' => [
'line' => 21,
'character' => 1
]
]
], json_decode(json_encode($result), true));
}
public function testDefinitionForClassLikeUseStatement()
{
// use TestNamespace\TestClass;
// Get definition for TestClass
$result = $this->textDocument->definition(new TextDocumentIdentifier('use'), new Position(4, 22));
$this->assertEquals([
'uri' => 'symbols',
'range' => [
'start' => [
'line' => 6,
'character' => 0
],
'end' => [
'line' => 21,
'character' => 1
]
]
], json_decode(json_encode($result), true));
}
public function testDefinitionForClassLikeGroupUseStatement()
{
// use TestNamespace\{TestTrait, TestInterface};
// Get definition for TestInterface
$result = $this->textDocument->definition(new TextDocumentIdentifier('use'), new Position(5, 37));
$this->assertEquals([
'uri' => 'symbols',
'range' => [
'start' => [
'line' => 28,
'character' => 0
],
'end' => [
'line' => 31,
'character' => 1
]
]
], json_decode(json_encode($result), true));
}
public function testDefinitionForImplements()
{
// class TestClass implements TestInterface
// Get definition for TestInterface
$result = $this->textDocument->definition(new TextDocumentIdentifier('symbols'), new Position(6, 33));
$this->assertEquals([
'uri' => 'symbols',
'range' => [
'start' => [
'line' => 28,
'character' => 0
],
'end' => [
'line' => 31,
'character' => 1
]
]
], json_decode(json_encode($result), true));
}
public function testDefinitionForClassConstants()
{
// echo TestClass::TEST_CLASS_CONST;
// Get definition for TEST_CLASS_CONST
$result = $this->textDocument->definition(new TextDocumentIdentifier('references'), new Position(9, 21));
$this->assertEquals([
'uri' => 'symbols',
'range' => [
'start' => [
'line' => 8,
'character' => 10
],
'end' => [
'line' => 8,
'character' => 32
]
]
], json_decode(json_encode($result), true));
}
public function testDefinitionForConstants()
{
// echo TEST_CONST;
// Get definition for TEST_CONST
$result = $this->textDocument->definition(new TextDocumentIdentifier('references'), new Position(23, 9));
$this->assertEquals([
'uri' => 'symbols',
'range' => [
'start' => [
'line' => 4,
'character' => 6
],
'end' => [
'line' => 4,
'character' => 22
]
]
], json_decode(json_encode($result), true));
}
public function testDefinitionForStaticMethods()
{
// TestClass::staticTestMethod();
// Get definition for staticTestMethod
$result = $this->textDocument->definition(new TextDocumentIdentifier('references'), new Position(7, 20));
$this->assertEquals([
'uri' => 'symbols',
'range' => [
'start' => [
'line' => 12,
'character' => 4
],
'end' => [
'line' => 15,
'character' => 5
]
]
], json_decode(json_encode($result), true));
}
public function testDefinitionForStaticProperties()
{
// echo TestClass::$staticTestProperty;
// Get definition for staticTestProperty
$result = $this->textDocument->definition(new TextDocumentIdentifier('references'), new Position(8, 25));
$this->assertEquals([
'uri' => 'symbols',
'range' => [
'start' => [
'line' => 9,
'character' => 18
],
'end' => [
'line' => 9,
'character' => 37
]
]
], json_decode(json_encode($result), true));
}
public function testDefinitionForMethods()
{
// $obj->testMethod();
// Get definition for testMethod
$result = $this->textDocument->definition(new TextDocumentIdentifier('references'), new Position(5, 11));
$this->assertEquals([
'uri' => 'symbols',
'range' => [
'start' => [
'line' => 17,
'character' => 4
],
'end' => [
'line' => 20,
'character' => 5
]
]
], json_decode(json_encode($result), true));
}
public function testDefinitionForProperties()
{
// echo $obj->testProperty;
// Get definition for testProperty
$result = $this->textDocument->definition(new TextDocumentIdentifier('references'), new Position(6, 18));
$this->assertEquals([
'uri' => 'symbols',
'range' => [
'start' => [
'line' => 10,
'character' => 11
],
'end' => [
'line' => 10,
'character' => 24
]
]
], json_decode(json_encode($result), true));
}
public function testDefinitionForVariables()
{
// echo $var;
// Get definition for $var
$result = $this->textDocument->definition(new TextDocumentIdentifier('references'), new Position(13, 7));
$this->assertEquals([
'uri' => 'references',
'range' => [
'start' => [
'line' => 12,
'character' => 0
],
'end' => [
'line' => 12,
'character' => 10
]
]
], json_decode(json_encode($result), true));
}
public function testDefinitionForParamTypeHints()
{
// function whatever(TestClass $param) {
// Get definition for TestClass
$result = $this->textDocument->definition(new TextDocumentIdentifier('references'), new Position(15, 23));
$this->assertEquals([
'uri' => 'symbols',
'range' => [
'start' => [
'line' => 6,
'character' => 0
],
'end' => [
'line' => 21,
'character' => 1
]
]
], json_decode(json_encode($result), true));
}
public function testDefinitionForReturnTypeHints()
{
// function whatever(TestClass $param) {
// Get definition for TestClass
$result = $this->textDocument->definition(new TextDocumentIdentifier('references'), new Position(15, 42));
$this->assertEquals([
'uri' => 'symbols',
'range' => [
'start' => [
'line' => 6,
'character' => 0
],
'end' => [
'line' => 21,
'character' => 1
]
]
], json_decode(json_encode($result), true));
}
public function testDefinitionForParams()
{
// echo $param;
// Get definition for $param
$result = $this->textDocument->definition(new TextDocumentIdentifier('references'), new Position(16, 13));
$this->assertEquals([
'uri' => 'references',
'range' => [
'start' => [
'line' => 15,
'character' => 18
],
'end' => [
'line' => 15,
'character' => 34
]
]
], json_decode(json_encode($result), true));
}
public function testDefinitionForUsedVariables()
{
// echo $var;
// Get definition for $var
$result = $this->textDocument->definition(new TextDocumentIdentifier('references'), new Position(20, 11));
$this->assertEquals([
'uri' => 'references',
'range' => [
'start' => [
'line' => 19,
'character' => 22
],
'end' => [
'line' => 19,
'character' => 26
]
]
], json_decode(json_encode($result), true));
}
public function testDefinitionForFunctions()
{
// test_function();
// Get definition for test_function
$result = $this->textDocument->definition(new TextDocumentIdentifier('references'), new Position(10, 4));
$this->assertEquals([
'uri' => 'symbols',
'range' => [
'start' => [
'line' => 33,
'character' => 0
],
'end' => [
'line' => 36,
'character' => 1
]
]
], json_decode(json_encode($result), true));
}
}

View File

@ -0,0 +1,38 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Tests\Server\TextDocument;
use PHPUnit\Framework\TestCase;
use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\{Server, Client, LanguageClient, Project};
use LanguageServer\Protocol\{
TextDocumentIdentifier,
TextDocumentItem,
VersionedTextDocumentIdentifier,
TextDocumentContentChangeEvent,
Range,
Position
};
class DidChangeTest extends TestCase
{
public function test()
{
$client = new LanguageClient(new MockProtocolStream());
$project = new Project($client);
$textDocument = new Server\TextDocument($project, $client);
$phpDocument = $project->getDocument('whatever');
$phpDocument->updateContent("<?php\necho 'Hello, World'\n");
$identifier = new VersionedTextDocumentIdentifier('whatever');
$changeEvent = new TextDocumentContentChangeEvent();
$changeEvent->range = new Range(new Position(0, 0), new Position(9999, 9999));
$changeEvent->rangeLength = 9999;
$changeEvent->text = "<?php\necho 'Goodbye, World'\n";
$textDocument->didChange($identifier, [$changeEvent]);
$this->assertEquals("<?php\necho 'Goodbye, World'\n", $phpDocument->getContent());
}
}

View File

@ -0,0 +1,213 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Tests\Server\TextDocument;
use PHPUnit\Framework\TestCase;
use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\{Server, LanguageClient, Project};
use LanguageServer\Protocol\{TextDocumentIdentifier, SymbolKind};
class DocumentSymbolTest extends TestCase
{
/**
* @var Server\TextDocument
*/
private $textDocument;
public function setUp()
{
$client = new LanguageClient(new MockProtocolStream());
$project = new Project($client);
$this->textDocument = new Server\TextDocument($project, $client);
$project->getDocument('symbols')->updateContent(file_get_contents(__DIR__ . '/../../../fixtures/symbols.php'));
}
public function test()
{
// Request symbols
$result = $this->textDocument->documentSymbol(new TextDocumentIdentifier('symbols'));
$this->assertEquals([
[
'name' => 'TEST_CONST',
'kind' => SymbolKind::CONSTANT,
'location' => [
'uri' => 'symbols',
'range' => [
'start' => [
'line' => 4,
'character' => 6
],
'end' => [
'line' => 4,
'character' => 22
]
]
],
'containerName' => 'TestNamespace'
],
[
'name' => 'TestClass',
'kind' => SymbolKind::CLASS_,
'location' => [
'uri' => 'symbols',
'range' => [
'start' => [
'line' => 6,
'character' => 0
],
'end' => [
'line' => 21,
'character' => 1
]
]
],
'containerName' => 'TestNamespace'
],
[
'name' => 'TEST_CLASS_CONST',
'kind' => SymbolKind::CONSTANT,
'location' => [
'uri' => 'symbols',
'range' => [
'start' => [
'line' => 8,
'character' => 10
],
'end' => [
'line' => 8,
'character' => 32
]
]
],
'containerName' => 'TestNamespace\\TestClass'
],
[
'name' => 'staticTestProperty',
'kind' => SymbolKind::PROPERTY,
'location' => [
'uri' => 'symbols',
'range' => [
'start' => [
'line' => 9,
'character' => 18
],
'end' => [
'line' => 9,
'character' => 37
]
]
],
'containerName' => 'TestNamespace\\TestClass'
],
[
'name' => 'testProperty',
'kind' => SymbolKind::PROPERTY,
'location' => [
'uri' => 'symbols',
'range' => [
'start' => [
'line' => 10,
'character' => 11
],
'end' => [
'line' => 10,
'character' => 24
]
]
],
'containerName' => 'TestNamespace\\TestClass'
],
[
'name' => 'staticTestMethod',
'kind' => SymbolKind::METHOD,
'location' => [
'uri' => 'symbols',
'range' => [
'start' => [
'line' => 12,
'character' => 4
],
'end' => [
'line' => 15,
'character' => 5
]
]
],
'containerName' => 'TestNamespace\\TestClass'
],
[
'name' => 'testMethod',
'kind' => SymbolKind::METHOD,
'location' => [
'uri' => 'symbols',
'range' => [
'start' => [
'line' => 17,
'character' => 4
],
'end' => [
'line' => 20,
'character' => 5
]
]
],
'containerName' => 'TestNamespace\\TestClass'
],
[
'name' => 'TestTrait',
'kind' => SymbolKind::CLASS_,
'location' => [
'uri' => 'symbols',
'range' => [
'start' => [
'line' => 23,
'character' => 0
],
'end' => [
'line' => 26,
'character' => 1
]
]
],
'containerName' => 'TestNamespace'
],
[
'name' => 'TestInterface',
'kind' => SymbolKind::INTERFACE,
'location' => [
'uri' => 'symbols',
'range' => [
'start' => [
'line' => 28,
'character' => 0
],
'end' => [
'line' => 31,
'character' => 1
]
]
],
'containerName' => 'TestNamespace'
],
[
'name' => 'test_function',
'kind' => SymbolKind::FUNCTION,
'location' => [
'uri' => 'symbols',
'range' => [
'start' => [
'line' => 33,
'character' => 0
],
'end' => [
'line' => 36,
'character' => 1
]
]
],
'containerName' => 'TestNamespace'
]
], json_decode(json_encode($result), true));
}
}

View File

@ -0,0 +1,57 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Tests\Server\TextDocument;
use PHPUnit\Framework\TestCase;
use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\{Server, Client, LanguageClient, Project};
use LanguageServer\Protocol\{TextDocumentIdentifier, TextDocumentItem, FormattingOptions};
class FormattingTest extends TestCase
{
/**
* @var Server\TextDocument
*/
private $textDocument;
public function setUp()
{
$client = new LanguageClient(new MockProtocolStream());
$project = new Project($client);
$this->textDocument = new Server\TextDocument($project, $client);
}
public function test()
{
$client = new LanguageClient(new MockProtocolStream());
$project = new Project($client);
$textDocument = new Server\TextDocument($project, $client);
// Trigger parsing of source
$textDocumentItem = new TextDocumentItem();
$textDocumentItem->uri = 'whatever';
$textDocumentItem->languageId = 'php';
$textDocumentItem->version = 1;
$textDocumentItem->text = file_get_contents(__DIR__ . '/../../../fixtures/format.php');
$textDocument->didOpen($textDocumentItem);
// how code should look after formatting
$expected = file_get_contents(__DIR__ . '/../../../fixtures/format_expected.php');
// Request formatting
$result = $textDocument->formatting(new TextDocumentIdentifier('whatever'), new FormattingOptions());
$this->assertEquals([0 => [
'range' => [
'start' => [
'line' => 0,
'character' => 0
],
'end' => [
'line' => PHP_INT_MAX,
'character' => PHP_INT_MAX
]
],
'newText' => $expected
]], json_decode(json_encode($result), true));
}
}

View File

@ -0,0 +1,73 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Tests\Server\TextDocument;
use PHPUnit\Framework\TestCase;
use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\{Server, Client, LanguageClient, Project};
use LanguageServer\Protocol\{TextDocumentIdentifier, TextDocumentItem, DiagnosticSeverity};
class ParseErrorsTest extends TestCase
{
/**
* @var Server\TextDocument
*/
private $textDocument;
public function setUp()
{
$client = new LanguageClient(new MockProtocolStream());
$project = new Project($client);
$this->textDocument = new Server\TextDocument($project, $client);
}
public function testParseErrorsArePublishedAsDiagnostics()
{
$args = null;
$client = new LanguageClient(new MockProtocolStream());
$client->textDocument = new class($args) extends Client\TextDocument {
private $args;
public function __construct(&$args)
{
parent::__construct(new MockProtocolStream());
$this->args = &$args;
}
public function publishDiagnostics(string $uri, array $diagnostics)
{
$this->args = func_get_args();
}
};
$project = new Project($client);
$textDocument = new Server\TextDocument($project, $client);
// Trigger parsing of source
$textDocumentItem = new TextDocumentItem();
$textDocumentItem->uri = 'whatever';
$textDocumentItem->languageId = 'php';
$textDocumentItem->version = 1;
$textDocumentItem->text = file_get_contents(__DIR__ . '/../../../fixtures/invalid_file.php');
$textDocument->didOpen($textDocumentItem);
$this->assertEquals([
'whatever',
[[
'range' => [
'start' => [
'line' => 2,
'character' => 10
],
'end' => [
'line' => 2,
'character' => 15
]
],
'severity' => DiagnosticSeverity::ERROR,
'code' => null,
'source' => 'php',
'message' => "Syntax error, unexpected T_CLASS, expecting T_STRING"
]]
], json_decode(json_encode($args), true));
}
}

View File

@ -1,241 +0,0 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Tests\Server;
use PHPUnit\Framework\TestCase;
use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\{Server, Client, LanguageClient, Project, PhpDocument};
use LanguageServer\Protocol\{TextDocumentItem, TextDocumentIdentifier, SymbolKind, DiagnosticSeverity, FormattingOptions, VersionedTextDocumentIdentifier, TextDocumentContentChangeEvent, Range, Position};
use AdvancedJsonRpc\{Request as RequestBody, Response as ResponseBody};
class TextDocumentTest extends TestCase
{
public function testDocumentSymbol()
{
$client = new LanguageClient(new MockProtocolStream());
$project = new Project($client);
$textDocument = new Server\TextDocument($project, $client);
// Trigger parsing of source
$textDocumentItem = new TextDocumentItem();
$textDocumentItem->uri = 'whatever';
$textDocumentItem->languageId = 'php';
$textDocumentItem->version = 1;
$textDocumentItem->text = file_get_contents(__DIR__ . '/../../fixtures/symbols.php');
$textDocument->didOpen($textDocumentItem);
// Request symbols
$result = $textDocument->documentSymbol(new TextDocumentIdentifier('whatever'));
$this->assertEquals([
[
'name' => 'TestNamespace',
'kind' => SymbolKind::NAMESPACE,
'location' => [
'uri' => 'whatever',
'range' => [
'start' => [
'line' => 2,
'character' => 0
],
'end' => [
'line' => 2,
'character' => 24
]
]
],
'containerName' => null
],
[
'name' => 'TestClass',
'kind' => SymbolKind::CLASS_,
'location' => [
'uri' => 'whatever',
'range' => [
'start' => [
'line' => 4,
'character' => 0
],
'end' => [
'line' => 12,
'character' => 1
]
]
],
'containerName' => 'TestNamespace'
],
[
'name' => 'testProperty',
'kind' => SymbolKind::PROPERTY,
'location' => [
'uri' => 'whatever',
'range' => [
'start' => [
'line' => 6,
'character' => 11
],
'end' => [
'line' => 6,
'character' => 24
]
]
],
'containerName' => 'TestNamespace\\TestClass'
],
[
'name' => 'testMethod',
'kind' => SymbolKind::METHOD,
'location' => [
'uri' => 'whatever',
'range' => [
'start' => [
'line' => 8,
'character' => 4
],
'end' => [
'line' => 11,
'character' => 5
]
]
],
'containerName' => 'TestNamespace\\TestClass'
],
[
'name' => 'TestTrait',
'kind' => SymbolKind::CLASS_,
'location' => [
'uri' => 'whatever',
'range' => [
'start' => [
'line' => 14,
'character' => 0
],
'end' => [
'line' => 17,
'character' => 1
]
]
],
'containerName' => 'TestNamespace'
],
[
'name' => 'TestInterface',
'kind' => SymbolKind::INTERFACE,
'location' => [
'uri' => 'whatever',
'range' => [
'start' => [
'line' => 19,
'character' => 0
],
'end' => [
'line' => 22,
'character' => 1
]
]
],
'containerName' => 'TestNamespace'
]
], json_decode(json_encode($result), true));
}
public function testParseErrorsArePublishedAsDiagnostics()
{
$args = null;
$client = new LanguageClient(new MockProtocolStream());
$client->textDocument = new class($args) extends Client\TextDocument {
private $args;
public function __construct(&$args)
{
parent::__construct(new MockProtocolStream());
$this->args = &$args;
}
public function publishDiagnostics(string $uri, array $diagnostics)
{
$this->args = func_get_args();
}
};
$project = new Project($client);
$textDocument = new Server\TextDocument($project, $client);
// Trigger parsing of source
$textDocumentItem = new TextDocumentItem();
$textDocumentItem->uri = 'whatever';
$textDocumentItem->languageId = 'php';
$textDocumentItem->version = 1;
$textDocumentItem->text = file_get_contents(__DIR__ . '/../../fixtures/invalid_file.php');
$textDocument->didOpen($textDocumentItem);
$this->assertEquals([
'whatever',
[[
'range' => [
'start' => [
'line' => 2,
'character' => 10
],
'end' => [
'line' => 2,
'character' => 15
]
],
'severity' => DiagnosticSeverity::ERROR,
'code' => null,
'source' => 'php',
'message' => "Syntax error, unexpected T_CLASS, expecting T_STRING"
]]
], json_decode(json_encode($args), true));
}
public function testFormatting()
{
$client = new LanguageClient(new MockProtocolStream());
$project = new Project($client);
$textDocument = new Server\TextDocument($project, $client);
// Trigger parsing of source
$textDocumentItem = new TextDocumentItem();
$textDocumentItem->uri = 'whatever';
$textDocumentItem->languageId = 'php';
$textDocumentItem->version = 1;
$textDocumentItem->text = file_get_contents(__DIR__ . '/../../fixtures/format.php');
$textDocument->didOpen($textDocumentItem);
// how code should look after formatting
$expected = file_get_contents(__DIR__ . '/../../fixtures/format_expected.php');
// Request formatting
$result = $textDocument->formatting(new TextDocumentIdentifier('whatever'), new FormattingOptions());
$this->assertEquals([0 => [
'range' => [
'start' => [
'line' => 0,
'character' => 0
],
'end' => [
'line' => PHP_INT_MAX,
'character' => PHP_INT_MAX
]
],
'newText' => $expected
]], json_decode(json_encode($result), true));
}
public function testDidChange()
{
$client = new LanguageClient(new MockProtocolStream());
$project = new Project($client);
$textDocument = new Server\TextDocument($project, $client);
$phpDocument = $project->getDocument('whatever');
$phpDocument->updateContent("<?php\necho 'Hello, World'\n");
$identifier = new VersionedTextDocumentIdentifier('whatever');
$changeEvent = new TextDocumentContentChangeEvent();
$changeEvent->range = new Range(new Position(0,0), new Position(9999,9999));
$changeEvent->rangeLength = 9999;
$changeEvent->text = "<?php\necho 'Goodbye, World'\n";
$textDocument->didChange($identifier, [$changeEvent]);
$this->assertEquals("<?php\necho 'Goodbye, World'\n", $phpDocument->getContent());
}
}