diff --git a/fixtures/references.php b/fixtures/references.php new file mode 100644 index 0000000..6cb9190 --- /dev/null +++ b/fixtures/references.php @@ -0,0 +1,24 @@ +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; diff --git a/fixtures/symbols.php b/fixtures/symbols.php index 03281c5..26379e4 100644 --- a/fixtures/symbols.php +++ b/fixtures/symbols.php @@ -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; + } +}; diff --git a/fixtures/use.php b/fixtures/use.php new file mode 100644 index 0000000..28e3aec --- /dev/null +++ b/fixtures/use.php @@ -0,0 +1,6 @@ +workspaceSymbolProvider = true; // Support "Format Code" $serverCapabilities->documentFormattingProvider = true; + // Support "Go to definition" + $serverCapabilities->definitionProvider = true; + return new InitializeResult($serverCapabilities); } diff --git a/src/ColumnCalculator.php b/src/NodeVisitor/ColumnCalculator.php similarity index 96% rename from src/ColumnCalculator.php rename to src/NodeVisitor/ColumnCalculator.php index 9050264..6f6b3bf 100644 --- a/src/ColumnCalculator.php +++ b/src/NodeVisitor/ColumnCalculator.php @@ -1,7 +1,7 @@ getAttribute('ownerDocument')->getDefinedFqn($node); + if ($fqn !== null) { + $this->definitions[$fqn] = $node; + } + } +} diff --git a/src/NodeVisitor/NodeAtPositionFinder.php b/src/NodeVisitor/NodeAtPositionFinder.php new file mode 100644 index 0000000..b6b9cc1 --- /dev/null +++ b/src/NodeVisitor/NodeAtPositionFinder.php @@ -0,0 +1,47 @@ +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; + } + } +} diff --git a/src/NodeVisitor/ReferencesAdder.php b/src/NodeVisitor/ReferencesAdder.php new file mode 100644 index 0000000..f3f912f --- /dev/null +++ b/src/NodeVisitor/ReferencesAdder.php @@ -0,0 +1,55 @@ +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); + } +} diff --git a/src/PhpDocument.php b/src/PhpDocument.php index febff0e..9979d6a 100644 --- a/src/PhpDocument.php +++ b/src/PhpDocument.php @@ -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; + } } diff --git a/src/Project.php b/src/Project.php index adf37f6..b889c8f 100644 --- a/src/Project.php +++ b/src/Project.php @@ -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; } diff --git a/src/Protocol/Location.php b/src/Protocol/Location.php index d6bdc14..a1d9861 100644 --- a/src/Protocol/Location.php +++ b/src/Protocol/Location.php @@ -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; diff --git a/src/Protocol/Position.php b/src/Protocol/Position.php index 7831268..01cff0b 100644 --- a/src/Protocol/Position.php +++ b/src/Protocol/Position.php @@ -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; + } } diff --git a/src/Protocol/Range.php b/src/Protocol/Range.php index c4a59ff..17f31da 100644 --- a/src/Protocol/Range.php +++ b/src/Protocol/Range.php @@ -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; + } } diff --git a/src/Protocol/SymbolInformation.php b/src/Protocol/SymbolInformation.php index 12e9ccc..e02fd9b 100644 --- a/src/Protocol/SymbolInformation.php +++ b/src/Protocol/SymbolInformation.php @@ -2,6 +2,8 @@ namespace LanguageServer\Protocol; +use PhpParser\Node; + /** * Represents information about programming constructs like variables, classes, * interfaces etc. diff --git a/src/Server/TextDocument.php b/src/Server/TextDocument.php index a6541c1..39a9699 100644 --- a/src/Server/TextDocument.php +++ b/src/Server/TextDocument.php @@ -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); + } } diff --git a/src/Server/Workspace.php b/src/Server/Workspace.php index cbd0da4..c6d963a 100644 --- a/src/Server/Workspace.php +++ b/src/Server/Workspace.php @@ -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, diff --git a/src/SymbolFinder.php b/src/SymbolFinder.php deleted file mode 100644 index f49677c..0000000 --- a/src/SymbolFinder.php +++ /dev/null @@ -1,121 +0,0 @@ - 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--; - } - } -} diff --git a/tests/LanguageServerTest.php b/tests/LanguageServerTest.php index 7d82c23..521d602 100644 --- a/tests/LanguageServerTest.php +++ b/tests/LanguageServerTest.php @@ -34,7 +34,7 @@ class LanguageServerTest extends TestCase 'hoverProvider' => null, 'completionProvider' => null, 'signatureHelpProvider' => null, - 'definitionProvider' => null, + 'definitionProvider' => true, 'referencesProvider' => null, 'documentHighlightProvider' => null, 'workspaceSymbolProvider' => true, diff --git a/tests/NodeVisitor/DefinitionCollectorTest.php b/tests/NodeVisitor/DefinitionCollectorTest.php new file mode 100644 index 0000000..cbd82f2 --- /dev/null +++ b/tests/NodeVisitor/DefinitionCollectorTest.php @@ -0,0 +1,52 @@ +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()']); + } +} diff --git a/tests/PhpDocumentTest.php b/tests/PhpDocumentTest.php index 465748a..93075fc 100644 --- a/tests/PhpDocumentTest.php +++ b/tests/PhpDocumentTest.php @@ -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 { @@ -23,48 +25,20 @@ class PhpDocumentTest extends TestCase public function testParsesVariableVariables() { $document = $this->project->getDocument('whatever'); - + $document->updateContent("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("getNodeAtPosition(new Position(1, 13)); + $this->assertInstanceOf(Node\Name\FullyQualified::class, $node); + $this->assertEquals('SomeClass', (string)$node); } } diff --git a/tests/ProjectTest.php b/tests/ProjectTest.php index eb37fa7..5fb9afb 100644 --- a/tests/ProjectTest.php +++ b/tests/ProjectTest.php @@ -41,8 +41,9 @@ class ProjectTest extends TestCase { $this->project->getDocument('file:///document1.php')->updateContent("project->getDocument('file:///document2.php')->updateContent("project->getDocument('invalid_file')->updateContent(file_get_contents(__DIR__ . '/../fixtures/invalid_file.php')); - $symbols = $this->project->findSymbols('ba'); + $symbols = $this->project->findSymbols('ba'); $this->assertEquals([ [ diff --git a/tests/Server/TextDocument/DefinitionTest.php b/tests/Server/TextDocument/DefinitionTest.php new file mode 100644 index 0000000..948fb35 --- /dev/null +++ b/tests/Server/TextDocument/DefinitionTest.php @@ -0,0 +1,346 @@ +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)); + } +} diff --git a/tests/Server/TextDocument/DidChangeTest.php b/tests/Server/TextDocument/DidChangeTest.php new file mode 100644 index 0000000..081978f --- /dev/null +++ b/tests/Server/TextDocument/DidChangeTest.php @@ -0,0 +1,38 @@ +getDocument('whatever'); + $phpDocument->updateContent("range = new Range(new Position(0, 0), new Position(9999, 9999)); + $changeEvent->rangeLength = 9999; + $changeEvent->text = "didChange($identifier, [$changeEvent]); + + $this->assertEquals("getContent()); + } +} diff --git a/tests/Server/TextDocument/DocumentSymbolTest.php b/tests/Server/TextDocument/DocumentSymbolTest.php new file mode 100644 index 0000000..e5edfe7 --- /dev/null +++ b/tests/Server/TextDocument/DocumentSymbolTest.php @@ -0,0 +1,213 @@ +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)); + } +} diff --git a/tests/Server/TextDocument/FormattingTest.php b/tests/Server/TextDocument/FormattingTest.php new file mode 100644 index 0000000..2dcc46e --- /dev/null +++ b/tests/Server/TextDocument/FormattingTest.php @@ -0,0 +1,57 @@ +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)); + } +} diff --git a/tests/Server/TextDocument/ParseErrorsTest.php b/tests/Server/TextDocument/ParseErrorsTest.php new file mode 100644 index 0000000..24d155c --- /dev/null +++ b/tests/Server/TextDocument/ParseErrorsTest.php @@ -0,0 +1,73 @@ +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)); + } +} diff --git a/tests/Server/TextDocumentTest.php b/tests/Server/TextDocumentTest.php deleted file mode 100644 index 746ded6..0000000 --- a/tests/Server/TextDocumentTest.php +++ /dev/null @@ -1,241 +0,0 @@ -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("range = new Range(new Position(0,0), new Position(9999,9999)); - $changeEvent->rangeLength = 9999; - $changeEvent->text = "didChange($identifier, [$changeEvent]); - - $this->assertEquals("getContent()); - } -}