From 6bd1b10e4d9d9d8762dac3ba600658af738aad15 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Thu, 20 Oct 2016 00:10:47 +0200 Subject: [PATCH 01/11] Resolve $this (#98) --- fixtures/global_symbols.php | 2 +- fixtures/symbols.php | 2 +- src/PhpDocument.php | 56 ++++++++++++------- tests/Server/ServerTestCase.php | 6 +- .../TextDocument/Definition/GlobalTest.php | 9 +++ 5 files changed, 52 insertions(+), 23 deletions(-) diff --git a/fixtures/global_symbols.php b/fixtures/global_symbols.php index 915dcfe..9b08afc 100644 --- a/fixtures/global_symbols.php +++ b/fixtures/global_symbols.php @@ -57,7 +57,7 @@ class TestClass implements TestInterface */ public function testMethod($testParameter) { - $testVariable = 123; + $this->testProperty = $testParameter; } } diff --git a/fixtures/symbols.php b/fixtures/symbols.php index 4904e07..9a14c0e 100644 --- a/fixtures/symbols.php +++ b/fixtures/symbols.php @@ -57,7 +57,7 @@ class TestClass implements TestInterface */ public function testMethod($testParameter) { - $testVariable = 123; + $this->testProperty = $testParameter; } } diff --git a/src/PhpDocument.php b/src/PhpDocument.php index c208ae4..00face9 100644 --- a/src/PhpDocument.php +++ b/src/PhpDocument.php @@ -395,29 +395,47 @@ class PhpDocument 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; + if ($node->var->name === 'this') { + // $this resolved to the class it is contained in + $n = $node; + while ($n = $n->getAttribute('parentNode')) { + if ($n instanceof Node\Stmt\Class_) { + if ($n->isAnonymous()) { + return null; + } + $name = (string)$n->namespacedName; + break; } - $name = (string)$varDef->expr->class; - } else { + } + if (!isset($name)) { return null; } } else { - return null; + // Other variables resolve to their definition + $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 ($parent instanceof Node\Expr\FuncCall) { diff --git a/tests/Server/ServerTestCase.php b/tests/Server/ServerTestCase.php index 47981b9..00d5471 100644 --- a/tests/Server/ServerTestCase.php +++ b/tests/Server/ServerTestCase.php @@ -110,7 +110,8 @@ abstract class ServerTestCase extends TestCase 0 => new Location($referencesUri, new Range(new Position( 9, 5), new Position( 9, 32))) ], 'TestNamespace\\TestClass::testProperty' => [ - 0 => new Location($referencesUri, new Range(new Position( 6, 5), new Position( 6, 23))) + 0 => new Location($symbolsUri, new Range(new Position(59, 8), new Position(59, 27))), // $this->testProperty = $testParameter; + 1 => new Location($referencesUri, new Range(new Position( 6, 5), new Position( 6, 23))) ], 'TestNamespace\\TestClass::staticTestProperty' => [ 0 => new Location($referencesUri, new Range(new Position( 8, 5), new Position( 8, 35))) @@ -145,7 +146,8 @@ abstract class ServerTestCase extends TestCase 0 => new Location($globalReferencesUri, new Range(new Position( 9, 5), new Position( 9, 32))) ], 'TestClass::testProperty' => [ - 0 => new Location($globalReferencesUri, new Range(new Position( 6, 5), new Position( 6, 23))) + 0 => new Location($globalSymbolsUri, new Range(new Position(59, 8), new Position(59, 27))), // $this->testProperty = $testParameter; + 1 => new Location($globalReferencesUri, new Range(new Position( 6, 5), new Position( 6, 23))) ], 'TestClass::staticTestProperty' => [ 0 => new Location($globalReferencesUri, new Range(new Position( 8, 5), new Position( 8, 35))) diff --git a/tests/Server/TextDocument/Definition/GlobalTest.php b/tests/Server/TextDocument/Definition/GlobalTest.php index 5c854e2..e89c0eb 100644 --- a/tests/Server/TextDocument/Definition/GlobalTest.php +++ b/tests/Server/TextDocument/Definition/GlobalTest.php @@ -115,6 +115,15 @@ class GlobalTest extends ServerTestCase { // echo $obj->testProperty; // Get definition for testProperty + $reference = $this->getReferenceLocations('TestClass::testProperty')[1]; + $result = $this->textDocument->definition(new TextDocumentIdentifier($reference->uri), $reference->range->end); + $this->assertEquals($this->getDefinitionLocation('TestClass::testProperty'), $result); + } + + public function testDefinitionForPropertiesOnThis() + { + // $this->testProperty = $testParameter; + // Get definition for testProperty $reference = $this->getReferenceLocations('TestClass::testProperty')[0]; $result = $this->textDocument->definition(new TextDocumentIdentifier($reference->uri), $reference->range->end); $this->assertEquals($this->getDefinitionLocation('TestClass::testProperty'), $result); From e19670c1414f4b391a30befc1b90f185d4ed4d22 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Thu, 20 Oct 2016 00:18:36 +0200 Subject: [PATCH 02/11] Resolve self, static, parent (#99) --- fixtures/global_symbols.php | 2 +- fixtures/symbols.php | 2 +- src/PhpDocument.php | 24 ++++++++++++++++++- tests/Server/ServerTestCase.php | 6 +++-- .../TextDocument/Definition/GlobalTest.php | 9 +++++++ 5 files changed, 38 insertions(+), 5 deletions(-) diff --git a/fixtures/global_symbols.php b/fixtures/global_symbols.php index 9b08afc..e39bb0c 100644 --- a/fixtures/global_symbols.php +++ b/fixtures/global_symbols.php @@ -46,7 +46,7 @@ class TestClass implements TestInterface */ public static function staticTestMethod() { - + echo self::TEST_CLASS_CONST; } /** diff --git a/fixtures/symbols.php b/fixtures/symbols.php index 9a14c0e..7fee2d2 100644 --- a/fixtures/symbols.php +++ b/fixtures/symbols.php @@ -46,7 +46,7 @@ class TestClass implements TestInterface */ public static function staticTestMethod() { - + echo self::TEST_CLASS_CONST; } /** diff --git a/src/PhpDocument.php b/src/PhpDocument.php index 00face9..9733b66 100644 --- a/src/PhpDocument.php +++ b/src/PhpDocument.php @@ -454,7 +454,29 @@ class PhpDocument // Cannot get definition of dynamic names return null; } - $name = (string)$node->class . '::' . $node->name; + $className = (string)$node->class; + if ($className === 'self' || $className === 'static' || $className === 'parent') { + // self and static are resolved to the containing class + $n = $node; + while ($n = $n->getAttribute('parentNode')) { + if ($n instanceof Node\Stmt\Class_) { + if ($n->isAnonymous()) { + return null; + } + if ($className === 'parent') { + // parent is resolved to the parent class + if (!isset($n->extends)) { + return null; + } + $className = (string)$n->extends; + } else { + $className = (string)$n->namespacedName; + } + break; + } + } + } + $name = (string)$className . '::' . $node->name; } else { return null; } diff --git a/tests/Server/ServerTestCase.php b/tests/Server/ServerTestCase.php index 00d5471..bb83534 100644 --- a/tests/Server/ServerTestCase.php +++ b/tests/Server/ServerTestCase.php @@ -107,7 +107,8 @@ abstract class ServerTestCase extends TestCase 0 => new Location($symbolsUri, new Range(new Position(20, 27), new Position(20, 40))) // class TestClass implements TestInterface ], 'TestNamespace\\TestClass::TEST_CLASS_CONST' => [ - 0 => new Location($referencesUri, new Range(new Position( 9, 5), new Position( 9, 32))) + 0 => new Location($symbolsUri, new Range(new Position(48, 13), new Position(48, 35))), // echo self::TEST_CLASS_CONSTANT + 1 => new Location($referencesUri, new Range(new Position( 9, 5), new Position( 9, 32))) ], 'TestNamespace\\TestClass::testProperty' => [ 0 => new Location($symbolsUri, new Range(new Position(59, 8), new Position(59, 27))), // $this->testProperty = $testParameter; @@ -143,7 +144,8 @@ abstract class ServerTestCase extends TestCase 0 => new Location($globalSymbolsUri, new Range(new Position(20, 27), new Position(20, 40))) // class TestClass implements TestInterface ], 'TestClass::TEST_CLASS_CONST' => [ - 0 => new Location($globalReferencesUri, new Range(new Position( 9, 5), new Position( 9, 32))) + 0 => new Location($globalSymbolsUri, new Range(new Position(48, 13), new Position(48, 35))), // echo self::TEST_CLASS_CONSTANT + 1 => new Location($globalReferencesUri, new Range(new Position( 9, 5), new Position( 9, 32))) ], 'TestClass::testProperty' => [ 0 => new Location($globalSymbolsUri, new Range(new Position(59, 8), new Position(59, 27))), // $this->testProperty = $testParameter; diff --git a/tests/Server/TextDocument/Definition/GlobalTest.php b/tests/Server/TextDocument/Definition/GlobalTest.php index e89c0eb..f58c35a 100644 --- a/tests/Server/TextDocument/Definition/GlobalTest.php +++ b/tests/Server/TextDocument/Definition/GlobalTest.php @@ -70,6 +70,15 @@ class GlobalTest extends ServerTestCase { // echo TestClass::TEST_CLASS_CONST; // Get definition for TEST_CLASS_CONST + $reference = $this->getReferenceLocations('TestClass::TEST_CLASS_CONST')[1]; + $result = $this->textDocument->definition(new TextDocumentIdentifier($reference->uri), $reference->range->end); + $this->assertEquals($this->getDefinitionLocation('TestClass::TEST_CLASS_CONST'), $result); + } + + public function testDefinitionForClassConstantsOnSelf() + { + // echo self::TEST_CLASS_CONST; + // Get definition for TEST_CLASS_CONST $reference = $this->getReferenceLocations('TestClass::TEST_CLASS_CONST')[0]; $result = $this->textDocument->definition(new TextDocumentIdentifier($reference->uri), $reference->range->end); $this->assertEquals($this->getDefinitionLocation('TestClass::TEST_CLASS_CONST'), $result); From e993b9994a2769d9f90ba51864e39cfcdf8c6b31 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Thu, 20 Oct 2016 00:20:30 +0200 Subject: [PATCH 03/11] Remove unneeded argument to ReferencesCollector --- src/PhpDocument.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpDocument.php b/src/PhpDocument.php index 9733b66..2246a40 100644 --- a/src/PhpDocument.php +++ b/src/PhpDocument.php @@ -172,7 +172,7 @@ class PhpDocument $traverser->addVisitor($definitionCollector); // Collect all references - $referencesCollector = new ReferencesCollector($this->definitions); + $referencesCollector = new ReferencesCollector; $traverser->addVisitor($referencesCollector); $traverser->traverse($stmts); From 96694996f783820ba85ad5434709a4f6ac6da1b9 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Thu, 20 Oct 2016 01:00:20 +0200 Subject: [PATCH 04/11] Refactor FQN functions to own namespace (#100) --- composer.json | 5 +- src/Fqn.php | 245 ++++++++++++++++++++++++ src/NodeVisitor/DefinitionCollector.php | 3 +- src/NodeVisitor/ReferencesCollector.php | 3 +- src/PhpDocument.php | 242 +---------------------- 5 files changed, 257 insertions(+), 241 deletions(-) create mode 100644 src/Fqn.php diff --git a/composer.json b/composer.json index 023b021..468c292 100644 --- a/composer.json +++ b/composer.json @@ -37,7 +37,10 @@ "psr-4": { "LanguageServer\\": "src/" }, - "files" : ["src/utils.php"] + "files" : [ + "src/utils.php", + "src/Fqn.php" + ] }, "autoload-dev": { "psr-4": { diff --git a/src/Fqn.php b/src/Fqn.php new file mode 100644 index 0000000..25e59ae --- /dev/null +++ b/src/Fqn.php @@ -0,0 +1,245 @@ +getAttribute('parentNode'); + + if ( + $node instanceof Node\Name && ( + $parent instanceof Node\Stmt\ClassLike + || $parent instanceof Node\Param + || $parent instanceof Node\Stmt\Function_ + || $parent instanceof Node\Expr\StaticCall + || $parent instanceof Node\Expr\ClassConstFetch + || $parent instanceof Node\Expr\StaticPropertyFetch + ) + ) { + // For extends, implements, type hints and classes of classes of static calls use the name directly + $name = (string)$node; + // Only the name node should be considered a reference, not the UseUse node itself + } else if ($parent instanceof Node\Stmt\UseUse) { + $name = (string)$parent->name; + $grandParent = $parent->getAttribute('parentNode'); + if ($grandParent instanceof Node\Stmt\GroupUse) { + $name = $grandParent->prefix . '\\' . $name; + } + // Only the name node should be considered a reference, not the New_ node itself + } else if ($parent instanceof Node\Expr\New_) { + if (!($parent->class instanceof Node\Name)) { + // Cannot get definition of dynamic calls + return null; + } + $name = (string)$parent->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 + if ($node->var->name === 'this') { + // $this resolved to the class it is contained in + $n = $node; + while ($n = $n->getAttribute('parentNode')) { + if ($n instanceof Node\Stmt\Class_) { + if ($n->isAnonymous()) { + return null; + } + $name = (string)$n->namespacedName; + break; + } + } + if (!isset($name)) { + return null; + } + } else { + // Other variables resolve to their definition + $varDef = 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 ($parent instanceof Node\Expr\FuncCall) { + if ($parent->name instanceof Node\Expr) { + return null; + } + $name = (string)($node->getAttribute('namespacedName') ?? $parent->name); + } else if ($parent instanceof Node\Expr\ConstFetch) { + $name = (string)($node->getAttribute('namespacedName') ?? $parent->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; + } + $className = (string)$node->class; + if ($className === 'self' || $className === 'static' || $className === 'parent') { + // self and static are resolved to the containing class + $n = $node; + while ($n = $n->getAttribute('parentNode')) { + if ($n instanceof Node\Stmt\Class_) { + if ($n->isAnonymous()) { + return null; + } + if ($className === 'parent') { + // parent is resolved to the parent class + if (!isset($n->extends)) { + return null; + } + $className = (string)$n->extends; + } else { + $className = (string)$n->namespacedName; + } + break; + } + } + } + $name = (string)$className . '::' . $node->name; + } else { + return null; + } + if ( + $node instanceof Node\Expr\MethodCall + || $node instanceof Node\Expr\StaticCall + || $parent instanceof Node\Expr\FuncCall + ) { + $name .= '()'; + } + if (!isset($name)) { + return null; + } + return $name; +} + +/** + * 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 + */ +function getVariableDefinition(Node\Expr\Variable $var) +{ + $n = $var; + // Traverse the AST up + do { + // 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 instanceof Node\Expr\Variable && $n->var->name === $var->name) { + return $n; + } + } + } while (isset($n) && $n = $n->getAttribute('parentNode')); + // Return null if nothing was found + return null; +} + +/** + * Returns the fully qualified name (FQN) that is defined by a node + * + * @param Node $node + * @return string|null + */ +function getDefinedFqn(Node $node) +{ + // Anonymous classes don't count as a definition + if ($node instanceof Node\Stmt\ClassLike && 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; + } + } +} diff --git a/src/NodeVisitor/DefinitionCollector.php b/src/NodeVisitor/DefinitionCollector.php index 9032155..f902fbf 100644 --- a/src/NodeVisitor/DefinitionCollector.php +++ b/src/NodeVisitor/DefinitionCollector.php @@ -4,6 +4,7 @@ declare(strict_types = 1); namespace LanguageServer\NodeVisitor; use PhpParser\{NodeVisitorAbstract, Node}; +use function LanguageServer\Fqn\getDefinedFqn; /** * Collects definitions of classes, interfaces, traits, methods, properties and constants @@ -20,7 +21,7 @@ class DefinitionCollector extends NodeVisitorAbstract public function enterNode(Node $node) { - $fqn = $node->getAttribute('ownerDocument')->getDefinedFqn($node); + $fqn = getDefinedFqn($node); if ($fqn !== null) { $this->definitions[$fqn] = $node; } diff --git a/src/NodeVisitor/ReferencesCollector.php b/src/NodeVisitor/ReferencesCollector.php index 749f635..08b660d 100644 --- a/src/NodeVisitor/ReferencesCollector.php +++ b/src/NodeVisitor/ReferencesCollector.php @@ -3,6 +3,7 @@ declare(strict_types = 1); namespace LanguageServer\NodeVisitor; +use function LanguageServer\Fqn\getReferencedFqn; use PhpParser\{NodeVisitorAbstract, Node}; /** @@ -21,7 +22,7 @@ class ReferencesCollector extends NodeVisitorAbstract public function enterNode(Node $node) { // Check if the node references any global symbol - $fqn = $node->getAttribute('ownerDocument')->getReferencedFqn($node); + $fqn = getReferencedFqn($node); if ($fqn) { $this->addReference($fqn, $node); // Namespaced constant access and function calls also need to register a reference diff --git a/src/PhpDocument.php b/src/PhpDocument.php index 2246a40..0a97ca2 100644 --- a/src/PhpDocument.php +++ b/src/PhpDocument.php @@ -16,6 +16,7 @@ use LanguageServer\NodeVisitor\{ use PhpParser\{Error, Node, NodeTraverser, Parser}; use PhpParser\NodeVisitor\NameResolver; use phpDocumentor\Reflection\DocBlockFactory; +use function LanguageServer\Fqn\{getDefinedFqn, getVariableDefinition, getReferencedFqn}; class PhpDocument { @@ -297,202 +298,6 @@ class PhpDocument 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) - { - // Anonymous classes don't count as a definition - if ($node instanceof Node\Stmt\ClassLike && 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) - { - $parent = $node->getAttribute('parentNode'); - - if ( - $node instanceof Node\Name && ( - $parent instanceof Node\Stmt\ClassLike - || $parent instanceof Node\Param - || $parent instanceof Node\Stmt\Function_ - || $parent instanceof Node\Expr\StaticCall - || $parent instanceof Node\Expr\ClassConstFetch - || $parent instanceof Node\Expr\StaticPropertyFetch - ) - ) { - // For extends, implements, type hints and classes of classes of static calls use the name directly - $name = (string)$node; - // Only the name node should be considered a reference, not the UseUse node itself - } else if ($parent instanceof Node\Stmt\UseUse) { - $name = (string)$parent->name; - $grandParent = $parent->getAttribute('parentNode'); - if ($grandParent instanceof Node\Stmt\GroupUse) { - $name = $grandParent->prefix . '\\' . $name; - } - // Only the name node should be considered a reference, not the New_ node itself - } else if ($parent instanceof Node\Expr\New_) { - if (!($parent->class instanceof Node\Name)) { - // Cannot get definition of dynamic calls - return null; - } - $name = (string)$parent->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 - if ($node->var->name === 'this') { - // $this resolved to the class it is contained in - $n = $node; - while ($n = $n->getAttribute('parentNode')) { - if ($n instanceof Node\Stmt\Class_) { - if ($n->isAnonymous()) { - return null; - } - $name = (string)$n->namespacedName; - break; - } - } - if (!isset($name)) { - return null; - } - } else { - // Other variables resolve to their definition - $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 ($parent instanceof Node\Expr\FuncCall) { - if ($parent->name instanceof Node\Expr) { - return null; - } - $name = (string)($node->getAttribute('namespacedName') ?? $parent->name); - } else if ($parent instanceof Node\Expr\ConstFetch) { - $name = (string)($node->getAttribute('namespacedName') ?? $parent->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; - } - $className = (string)$node->class; - if ($className === 'self' || $className === 'static' || $className === 'parent') { - // self and static are resolved to the containing class - $n = $node; - while ($n = $n->getAttribute('parentNode')) { - if ($n instanceof Node\Stmt\Class_) { - if ($n->isAnonymous()) { - return null; - } - if ($className === 'parent') { - // parent is resolved to the parent class - if (!isset($n->extends)) { - return null; - } - $className = (string)$n->extends; - } else { - $className = (string)$n->namespacedName; - } - break; - } - } - } - $name = (string)$className . '::' . $node->name; - } else { - return null; - } - if ( - $node instanceof Node\Expr\MethodCall - || $node instanceof Node\Expr\StaticCall - || $parent instanceof Node\Expr\FuncCall - ) { - $name .= '()'; - } - if (!isset($name)) { - return null; - } - return $name; - } - /** * Returns the definition node for any node * The definition node MAY be in another document, check the ownerDocument attribute @@ -505,9 +310,9 @@ class PhpDocument // 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); + return getVariableDefinition($node); } - $fqn = $this->getReferencedFqn($node); + $fqn = getReferencedFqn($node); if (!isset($fqn)) { return null; } @@ -558,7 +363,7 @@ class PhpDocument return $refCollector->references; } // Definition with a global FQN - $fqn = $this->getDefinedFqn($node); + $fqn = getDefinedFqn($node); if ($fqn === null) { return []; } @@ -574,43 +379,4 @@ class PhpDocument } return $nodes; } - - /** - * 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 - do { - // 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 instanceof Node\Expr\Variable && $n->var->name === $var->name) { - return $n; - } - } - } while (isset($n) && $n = $n->getAttribute('parentNode')); - // Return null if nothing was found - return null; - } } From 9cbca1cd7f5beebaacbfb51f7519d17e25669fce Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Thu, 20 Oct 2016 01:53:07 +0200 Subject: [PATCH 05/11] Revert "Use SymbolKind::FIELD for class fields (#78)" This reverts commit 2980941fd11e63407b2965580e09c5c2af2b47c0. --- src/Protocol/SymbolInformation.php | 2 +- tests/Server/TextDocument/DocumentSymbolTest.php | 4 ++-- tests/Server/Workspace/SymbolTest.php | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Protocol/SymbolInformation.php b/src/Protocol/SymbolInformation.php index ee88176..c2fb693 100644 --- a/src/Protocol/SymbolInformation.php +++ b/src/Protocol/SymbolInformation.php @@ -55,7 +55,7 @@ class SymbolInformation Node\Stmt\Namespace_::class => SymbolKind::NAMESPACE, Node\Stmt\Function_::class => SymbolKind::FUNCTION, Node\Stmt\ClassMethod::class => SymbolKind::METHOD, - Node\Stmt\PropertyProperty::class => SymbolKind::FIELD, + Node\Stmt\PropertyProperty::class => SymbolKind::PROPERTY, Node\Const_::class => SymbolKind::CONSTANT ]; $class = get_class($node); diff --git a/tests/Server/TextDocument/DocumentSymbolTest.php b/tests/Server/TextDocument/DocumentSymbolTest.php index 4874d4f..803dcae 100644 --- a/tests/Server/TextDocument/DocumentSymbolTest.php +++ b/tests/Server/TextDocument/DocumentSymbolTest.php @@ -20,8 +20,8 @@ class DocumentSymbolTest extends ServerTestCase new SymbolInformation('TEST_CONST', SymbolKind::CONSTANT, $this->getDefinitionLocation('TestNamespace\\TEST_CONST'), 'TestNamespace'), new SymbolInformation('TestClass', SymbolKind::CLASS_, $this->getDefinitionLocation('TestNamespace\\TestClass'), 'TestNamespace'), new SymbolInformation('TEST_CLASS_CONST', SymbolKind::CONSTANT, $this->getDefinitionLocation('TestNamespace\\TestClass::TEST_CLASS_CONST'), 'TestNamespace\\TestClass'), - new SymbolInformation('staticTestProperty', SymbolKind::FIELD, $this->getDefinitionLocation('TestNamespace\\TestClass::staticTestProperty'), 'TestNamespace\\TestClass'), - new SymbolInformation('testProperty', SymbolKind::FIELD, $this->getDefinitionLocation('TestNamespace\\TestClass::testProperty'), 'TestNamespace\\TestClass'), + new SymbolInformation('staticTestProperty', SymbolKind::PROPERTY, $this->getDefinitionLocation('TestNamespace\\TestClass::staticTestProperty'), 'TestNamespace\\TestClass'), + new SymbolInformation('testProperty', SymbolKind::PROPERTY, $this->getDefinitionLocation('TestNamespace\\TestClass::testProperty'), 'TestNamespace\\TestClass'), new SymbolInformation('staticTestMethod', SymbolKind::METHOD, $this->getDefinitionLocation('TestNamespace\\TestClass::staticTestMethod()'), 'TestNamespace\\TestClass'), new SymbolInformation('testMethod', SymbolKind::METHOD, $this->getDefinitionLocation('TestNamespace\\TestClass::testMethod()'), 'TestNamespace\\TestClass'), new SymbolInformation('TestTrait', SymbolKind::CLASS_, $this->getDefinitionLocation('TestNamespace\\TestTrait'), 'TestNamespace'), diff --git a/tests/Server/Workspace/SymbolTest.php b/tests/Server/Workspace/SymbolTest.php index 90b6090..b029e77 100644 --- a/tests/Server/Workspace/SymbolTest.php +++ b/tests/Server/Workspace/SymbolTest.php @@ -21,8 +21,8 @@ class SymbolTest extends ServerTestCase new SymbolInformation('TEST_CONST', SymbolKind::CONSTANT, $this->getDefinitionLocation('TestNamespace\\TEST_CONST'), 'TestNamespace'), new SymbolInformation('TestClass', SymbolKind::CLASS_, $this->getDefinitionLocation('TestNamespace\\TestClass'), 'TestNamespace'), new SymbolInformation('TEST_CLASS_CONST', SymbolKind::CONSTANT, $this->getDefinitionLocation('TestNamespace\\TestClass::TEST_CLASS_CONST'), 'TestNamespace\\TestClass'), - new SymbolInformation('staticTestProperty', SymbolKind::FIELD, $this->getDefinitionLocation('TestNamespace\\TestClass::staticTestProperty'), 'TestNamespace\\TestClass'), - new SymbolInformation('testProperty', SymbolKind::FIELD, $this->getDefinitionLocation('TestNamespace\\TestClass::testProperty'), 'TestNamespace\\TestClass'), + new SymbolInformation('staticTestProperty', SymbolKind::PROPERTY, $this->getDefinitionLocation('TestNamespace\\TestClass::staticTestProperty'), 'TestNamespace\\TestClass'), + new SymbolInformation('testProperty', SymbolKind::PROPERTY, $this->getDefinitionLocation('TestNamespace\\TestClass::testProperty'), 'TestNamespace\\TestClass'), new SymbolInformation('staticTestMethod', SymbolKind::METHOD, $this->getDefinitionLocation('TestNamespace\\TestClass::staticTestMethod()'), 'TestNamespace\\TestClass'), new SymbolInformation('testMethod', SymbolKind::METHOD, $this->getDefinitionLocation('TestNamespace\\TestClass::testMethod()'), 'TestNamespace\\TestClass'), new SymbolInformation('TestTrait', SymbolKind::CLASS_, $this->getDefinitionLocation('TestNamespace\\TestTrait'), 'TestNamespace'), @@ -33,8 +33,8 @@ class SymbolTest extends ServerTestCase new SymbolInformation('TEST_CONST', SymbolKind::CONSTANT, $this->getDefinitionLocation('TEST_CONST'), ''), new SymbolInformation('TestClass', SymbolKind::CLASS_, $this->getDefinitionLocation('TestClass'), ''), new SymbolInformation('TEST_CLASS_CONST', SymbolKind::CONSTANT, $this->getDefinitionLocation('TestClass::TEST_CLASS_CONST'), 'TestClass'), - new SymbolInformation('staticTestProperty', SymbolKind::FIELD, $this->getDefinitionLocation('TestClass::staticTestProperty'), 'TestClass'), - new SymbolInformation('testProperty', SymbolKind::FIELD, $this->getDefinitionLocation('TestClass::testProperty'), 'TestClass'), + new SymbolInformation('staticTestProperty', SymbolKind::PROPERTY, $this->getDefinitionLocation('TestClass::staticTestProperty'), 'TestClass'), + new SymbolInformation('testProperty', SymbolKind::PROPERTY, $this->getDefinitionLocation('TestClass::testProperty'), 'TestClass'), new SymbolInformation('staticTestMethod', SymbolKind::METHOD, $this->getDefinitionLocation('TestClass::staticTestMethod()'), 'TestClass'), new SymbolInformation('testMethod', SymbolKind::METHOD, $this->getDefinitionLocation('TestClass::testMethod()'), 'TestClass'), new SymbolInformation('TestTrait', SymbolKind::CLASS_, $this->getDefinitionLocation('TestTrait'), ''), From 1e00275e023206cc0bd29f65cb4f6c6113521f8b Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Thu, 20 Oct 2016 02:08:23 +0200 Subject: [PATCH 06/11] Hold SymbolInformation table in memory (#101) --- src/NodeVisitor/DefinitionCollector.php | 15 ++++++++++++-- src/PhpDocument.php | 24 +++++++++++++++++++--- src/Project.php | 27 +++++++++++++------------ src/Server/TextDocument.php | 6 +----- src/Server/Workspace.php | 9 ++++++--- 5 files changed, 55 insertions(+), 26 deletions(-) diff --git a/src/NodeVisitor/DefinitionCollector.php b/src/NodeVisitor/DefinitionCollector.php index f902fbf..a723bdc 100644 --- a/src/NodeVisitor/DefinitionCollector.php +++ b/src/NodeVisitor/DefinitionCollector.php @@ -4,6 +4,7 @@ declare(strict_types = 1); namespace LanguageServer\NodeVisitor; use PhpParser\{NodeVisitorAbstract, Node}; +use LanguageServer\Protocol\SymbolInformation; use function LanguageServer\Fqn\getDefinedFqn; /** @@ -19,11 +20,21 @@ class DefinitionCollector extends NodeVisitorAbstract */ public $definitions = []; + /** + * Map from FQN to SymbolInformation + * + * @var SymbolInformation + */ + public $symbols = []; + public function enterNode(Node $node) { $fqn = getDefinedFqn($node); - if ($fqn !== null) { - $this->definitions[$fqn] = $node; + if ($fqn === null) { + return; } + $this->definitions[$fqn] = $node; + $symbol = SymbolInformation::fromNode($node, $fqn); + $this->symbols[$fqn] = $symbol; } } diff --git a/src/PhpDocument.php b/src/PhpDocument.php index 0a97ca2..02bb854 100644 --- a/src/PhpDocument.php +++ b/src/PhpDocument.php @@ -85,6 +85,13 @@ class PhpDocument */ private $references; + /** + * Map from fully qualified name (FQN) to SymbolInformation + * + * @var SymbolInformation[] + */ + private $symbols; + /** * @param string $uri The URI of the document * @param string $content The content of the document @@ -181,13 +188,14 @@ class PhpDocument // Unregister old definitions if (isset($this->definitions)) { foreach ($this->definitions as $fqn => $node) { - $this->project->removeDefinition($fqn); + $this->project->removeSymbol($fqn); } } // Register this document on the project for all the symbols defined in it $this->definitions = $definitionCollector->definitions; - foreach ($definitionCollector->definitions as $fqn => $node) { - $this->project->setDefinitionUri($fqn, $this->uri); + $this->symbols = $definitionCollector->symbols; + foreach ($definitionCollector->symbols as $fqn => $symbol) { + $this->project->setSymbol($fqn, $symbol); } // Unregister old references @@ -287,6 +295,16 @@ class PhpDocument return $this->definitions; } + /** + * Returns a map from fully qualified name (FQN) to SymbolInformation + * + * @return SymbolInformation[] + */ + public function getSymbols() + { + return $this->symbols; + } + /** * Returns true if the given FQN is defined in this document * diff --git a/src/Project.php b/src/Project.php index 2cb0162..769a4d2 100644 --- a/src/Project.php +++ b/src/Project.php @@ -3,6 +3,7 @@ declare(strict_types = 1); namespace LanguageServer; +use LanguageServer\Protocol\SymbolInformation; use phpDocumentor\Reflection\DocBlockFactory; use PhpParser\{ParserFactory, Lexer}; @@ -17,11 +18,11 @@ class Project private $documents = []; /** - * An associative array that maps fully qualified symbol names to document URIs that define the symbol + * An associative array that maps fully qualified symbol names to SymbolInformations * - * @var string[] + * @var SymbolInformation[] */ - private $definitions = []; + private $symbols = []; /** * An associative array that maps fully qualified symbol names to arrays of document URIs that reference the symbol @@ -140,34 +141,34 @@ class Project * Returns an associative array [string => string] that maps fully qualified symbol names * to URIs of the document where the symbol is defined * - * @return PhpDocument[] + * @return SymbolInformation[] */ - public function getDefinitionUris() + public function getSymbols() { - return $this->definitions; + return $this->symbols; } /** - * Adds a document URI as the container for a specific symbol + * Adds a SymbolInformation for a specific symbol * * @param string $fqn The fully qualified name of the symbol * @param string $uri The URI * @return void */ - public function setDefinitionUri(string $fqn, string $uri) + public function setSymbol(string $fqn, SymbolInformation $symbol) { - $this->definitions[$fqn] = $uri; + $this->symbols[$fqn] = $symbol; } /** - * Unsets a document URI as the container for a specific symbol + * Unsets the SymbolInformation for a specific symbol * and removes all references pointing to that symbol * * @param string $fqn The fully qualified name of the symbol * @return void */ - public function removeDefinition(string $fqn) { - unset($this->definitions[$fqn]); + public function removeSymbol(string $fqn) { + unset($this->symbols[$fqn]); unset($this->references[$fqn]); } @@ -228,7 +229,7 @@ class Project */ public function getDefinitionDocument(string $fqn) { - return isset($this->definitions[$fqn]) ? $this->getDocument($this->definitions[$fqn]) : null; + return isset($this->symbols[$fqn]) ? $this->getDocument($this->symbols[$fqn]->location->uri) : null; } /** diff --git a/src/Server/TextDocument.php b/src/Server/TextDocument.php index e515e43..188e629 100644 --- a/src/Server/TextDocument.php +++ b/src/Server/TextDocument.php @@ -59,11 +59,7 @@ class TextDocument */ public function documentSymbol(TextDocumentIdentifier $textDocument): array { - $symbols = []; - foreach ($this->project->getDocument($textDocument->uri)->getDefinitions() as $fqn => $node) { - $symbols[] = SymbolInformation::fromNode($node, $fqn); - } - return $symbols; + return array_values($this->project->getDocument($textDocument->uri)->getSymbols()); } /** diff --git a/src/Server/Workspace.php b/src/Server/Workspace.php index 44ee50e..e894c49 100644 --- a/src/Server/Workspace.php +++ b/src/Server/Workspace.php @@ -39,10 +39,13 @@ class Workspace */ public function symbol(string $query): array { + if ($query === '') { + return array_values($this->project->getSymbols()); + } $symbols = []; - foreach ($this->project->getDefinitionUris() as $fqn => $uri) { - if ($query === '' || stripos($fqn, $query) !== false) { - $symbols[] = SymbolInformation::fromNode($this->project->getDocument($uri)->getDefinitionByFqn($fqn), $fqn); + foreach ($this->project->getSymbols() as $fqn => $symbol) { + if (stripos($fqn, $query) !== false) { + $symbols[] = $symbol; } } return $symbols; From f8733c741c89cb3ef2e00c0a556313cde79f0ba8 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Thu, 20 Oct 2016 02:13:15 +0200 Subject: [PATCH 07/11] Remove PhpParser workaround (#102) --- src/NodeVisitor/NodeAtPositionFinder.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/NodeVisitor/NodeAtPositionFinder.php b/src/NodeVisitor/NodeAtPositionFinder.php index b6b9cc1..521d940 100644 --- a/src/NodeVisitor/NodeAtPositionFinder.php +++ b/src/NodeVisitor/NodeAtPositionFinder.php @@ -35,11 +35,6 @@ class NodeAtPositionFinder extends NodeVisitorAbstract 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; } From 953a8023b777736c22060e39501a1b5067f59020 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Thu, 20 Oct 2016 03:31:12 +0200 Subject: [PATCH 08/11] Update AdvancedJsonRpc (#103) --- composer.json | 2 +- src/LanguageServer.php | 26 +++++++++++--------------- tests/LanguageServerTest.php | 7 +++---- 3 files changed, 15 insertions(+), 20 deletions(-) diff --git a/composer.json b/composer.json index 468c292..5f52bca 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,7 @@ "nikic/php-parser": "dev-master#90834bff8eaf7b7f893253f312e73d8f532341ca", "phpdocumentor/reflection-docblock": "^3.0", "sabre/event": "^4.0", - "felixfbecker/advanced-json-rpc": "^1.2", + "felixfbecker/advanced-json-rpc": "^2.0", "squizlabs/php_codesniffer" : "^2.7", "symfony/debug": "^3.1" }, diff --git a/src/LanguageServer.php b/src/LanguageServer.php index 389b99c..a58ada9 100644 --- a/src/LanguageServer.php +++ b/src/LanguageServer.php @@ -12,11 +12,12 @@ use LanguageServer\Protocol\{ MessageType, InitializeResult }; -use AdvancedJsonRpc\{Dispatcher, ResponseError, Response as ResponseBody, Request as RequestBody}; +use AdvancedJsonRpc; use Sabre\Event\Loop; use Exception; +use Throwable; -class LanguageServer extends \AdvancedJsonRpc\Dispatcher +class LanguageServer extends AdvancedJsonRpc\Dispatcher { /** * Handles textDocument/* method calls @@ -48,27 +49,22 @@ class LanguageServer extends \AdvancedJsonRpc\Dispatcher parent::__construct($this, '/'); $this->protocolReader = $reader; $this->protocolReader->onMessage(function (Message $msg) { - $err = null; - $result = null; try { // Invoke the method handler to get a result $result = $this->dispatch($msg->body); - } catch (ResponseError $e) { + $responseBody = new AdvancedJsonRpc\SuccessResponse($msg->body->id, $result); + } catch (AdvancedJsonRpc\Error $error) { // If a ResponseError is thrown, send it back in the Response (result will be null) - $err = $e; + $responseBody = new AdvancedJsonRpc\ErrorResponse($msg->body->id, $error); } catch (Throwable $e) { - // If an unexpected error occured, send back an INTERNAL_ERROR error response (result will be null) - $err = new ResponseError( - $e->getMessage(), - $e->getCode() === 0 ? ErrorCode::INTERNAL_ERROR : $e->getCode(), - null, - $e - ); + // If an unexpected error occured, send back an INTERNAL_ERROR error response + $error = new AdvancedJsonRpc\Error($e->getMessage(), AdvancedJsonRpc\ErrorCode::INTERNAL_ERROR, null, $e); + $responseBody = new AdvancedJsonRpc\ErrorResponse($msg->body->id, $error); } // Only send a Response for a Request // Notifications do not send Responses - if (RequestBody::isRequest($msg->body)) { - $this->protocolWriter->write(new Message(new ResponseBody($msg->body->id, $result, $err))); + if (AdvancedJsonRpc\Request::isRequest($msg->body)) { + $this->protocolWriter->write(new Message($responseBody)); } }); $this->protocolWriter = $writer; diff --git a/tests/LanguageServerTest.php b/tests/LanguageServerTest.php index d08ed56..18b580e 100644 --- a/tests/LanguageServerTest.php +++ b/tests/LanguageServerTest.php @@ -6,7 +6,7 @@ namespace LanguageServer\Tests; use PHPUnit\Framework\TestCase; use LanguageServer\LanguageServer; use LanguageServer\Protocol\{Message, ClientCapabilities, TextDocumentSyncKind}; -use AdvancedJsonRpc\{Request as RequestBody, Response as ResponseBody}; +use AdvancedJsonRpc; class LanguageServerTest extends TestCase { @@ -19,14 +19,13 @@ class LanguageServerTest extends TestCase $writer->onMessage(function (Message $message) use (&$msg) { $msg = $message; }); - $reader->write(new Message(new RequestBody(1, 'initialize', [ + $reader->write(new Message(new AdvancedJsonRpc\Request(1, 'initialize', [ 'rootPath' => __DIR__, 'processId' => getmypid(), 'capabilities' => new ClientCapabilities() ]))); $this->assertNotNull($msg, 'onMessage callback should be called'); - $this->assertInstanceOf(ResponseBody::class, $msg->body); - $this->assertNull($msg->body->error); + $this->assertInstanceOf(AdvancedJsonRpc\SuccessResponse::class, $msg->body); $this->assertEquals((object)[ 'capabilities' => (object)[ 'textDocumentSync' => TextDocumentSyncKind::FULL, From 8e36e59e9ad0abbb17b39b46adc3833c8b6b1184 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Thu, 20 Oct 2016 03:36:03 +0200 Subject: [PATCH 09/11] Fix crash --- src/LanguageServer.php | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/LanguageServer.php b/src/LanguageServer.php index a58ada9..ae4b1b9 100644 --- a/src/LanguageServer.php +++ b/src/LanguageServer.php @@ -49,21 +49,26 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher parent::__construct($this, '/'); $this->protocolReader = $reader; $this->protocolReader->onMessage(function (Message $msg) { + $result = null; + $error = null; try { // Invoke the method handler to get a result $result = $this->dispatch($msg->body); - $responseBody = new AdvancedJsonRpc\SuccessResponse($msg->body->id, $result); - } catch (AdvancedJsonRpc\Error $error) { - // If a ResponseError is thrown, send it back in the Response (result will be null) - $responseBody = new AdvancedJsonRpc\ErrorResponse($msg->body->id, $error); + } catch (AdvancedJsonRpc\Error $e) { + // If a ResponseError is thrown, send it back in the Response + $error = $e; } catch (Throwable $e) { // If an unexpected error occured, send back an INTERNAL_ERROR error response $error = new AdvancedJsonRpc\Error($e->getMessage(), AdvancedJsonRpc\ErrorCode::INTERNAL_ERROR, null, $e); - $responseBody = new AdvancedJsonRpc\ErrorResponse($msg->body->id, $error); } // Only send a Response for a Request // Notifications do not send Responses if (AdvancedJsonRpc\Request::isRequest($msg->body)) { + if ($error !== null) { + $responseBody = new AdvancedJsonRpc\ErrorResponse($msg->body->id, $error); + } else { + $responseBody = new AdvancedJsonRpc\SuccessResponse($msg->body->id, $result); + } $this->protocolWriter->write(new Message($responseBody)); } }); From 1e7260a2eae071c14fe6f1e49125d9eeecd3b778 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Thu, 20 Oct 2016 03:48:30 +0200 Subject: [PATCH 10/11] Cache index on disk (#82) --- .gitignore | 1 + src/LanguageServer.php | 88 +++++++++++++++++++++++++++--- src/Project.php | 33 +++++++++++ src/Protocol/SymbolInformation.php | 2 +- 4 files changed, 114 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index 2729a4c..c018fa6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ .vscode .idea vendor/ +.phpls/ composer.lock diff --git a/src/LanguageServer.php b/src/LanguageServer.php index ae4b1b9..15741d1 100644 --- a/src/LanguageServer.php +++ b/src/LanguageServer.php @@ -10,10 +10,12 @@ use LanguageServer\Protocol\{ TextDocumentSyncKind, Message, MessageType, - InitializeResult + InitializeResult, + SymbolInformation }; use AdvancedJsonRpc; use Sabre\Event\Loop; +use JsonMapper; use Exception; use Throwable; @@ -42,6 +44,12 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher private $protocolWriter; private $client; + /** + * The root project path that was passed to initialize() + * + * @var string + */ + private $rootPath; private $project; public function __construct(ProtocolReader $reader, ProtocolWriter $writer) @@ -91,9 +99,12 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher */ public function initialize(int $processId, ClientCapabilities $capabilities, string $rootPath = null): InitializeResult { + $this->rootPath = $rootPath; + // start building project index - if ($rootPath) { - $this->indexProject($rootPath); + if ($rootPath !== null) { + $this->restoreCache(); + $this->indexProject(); } $serverCapabilities = new ServerCapabilities(); @@ -124,7 +135,9 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher */ public function shutdown() { - + if ($this->rootPath !== null) { + $this->saveCache(); + } } /** @@ -140,23 +153,23 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher /** * Parses workspace files, one at a time. * - * @param string $rootPath The rootPath of the workspace. * @return void */ - private function indexProject(string $rootPath) + private function indexProject() { - $fileList = findFilesRecursive($rootPath, '/^.+\.php$/i'); + $fileList = findFilesRecursive($this->rootPath, '/^.+\.php$/i'); $numTotalFiles = count($fileList); $startTime = microtime(true); $fileNum = 0; - $processFile = function() use (&$fileList, &$fileNum, &$processFile, $rootPath, $numTotalFiles, $startTime) { + $processFile = function() use (&$fileList, &$fileNum, &$processFile, $numTotalFiles, $startTime) { if ($fileNum < $numTotalFiles) { $file = $fileList[$fileNum]; $uri = pathToUri($file); $fileNum++; - $shortName = substr($file, strlen($rootPath) + 1); + $shortName = substr($file, strlen($this->rootPath) + 1); + $this->client->window->logMessage(MessageType::INFO, "Parsing file $fileNum/$numTotalFiles: $shortName."); if (filesize($file) > 500000) { $this->client->window->logMessage(MessageType::INFO, "Not parsing $shortName because it exceeds size limit of 0.5MB"); @@ -169,14 +182,71 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher } } + if ($fileNum % 1000 === 0) { + $this->saveCache(); + } + Loop\setTimeout($processFile, 0); } else { $duration = (int)(microtime(true) - $startTime); $mem = (int)(memory_get_usage(true) / (1024 * 1024)); $this->client->window->logMessage(MessageType::INFO, "All PHP files parsed in $duration seconds. $mem MiB allocated."); + $this->saveCache(); } }; Loop\setTimeout($processFile, 0); } + + /** + * Restores the definition and reference index from the .phpls cache directory, if available + * + * @return void + */ + public function restoreCache() + { + $cacheDir = $this->rootPath . '/.phpls'; + if (is_dir($cacheDir)) { + if (file_exists($cacheDir . '/symbols.json')) { + $json = json_decode(file_get_contents($cacheDir . '/symbols.json')); + $mapper = new JsonMapper; + $symbols = $mapper->mapArray($json, [], SymbolInformation::class); + $count = count($symbols); + $this->project->setSymbols($symbols); + $this->client->window->logMessage(MessageType::INFO, "Restoring $count symbols"); + } + if (file_exists($cacheDir . '/references.json')) { + $references = json_decode(file_get_contents($cacheDir . '/references.json'), true); + $count = array_sum(array_map('count', $references)); + $this->project->setReferenceUris($references); + $this->client->window->logMessage(MessageType::INFO, "Restoring $count references"); + } + } else { + $this->client->window->logMessage(MessageType::INFO, 'No cache found'); + } + } + + /** + * Saves the definition and reference index to the .phpls cache directory + * + * @return void + */ + public function saveCache() + { + // Cache definitions, references + $cacheDir = $this->rootPath . '/.phpls'; + if (!is_dir($cacheDir)) { + mkdir($cacheDir); + } + + $symbols = $this->project->getSymbols(); + $count = count($symbols); + $this->client->window->logMessage(MessageType::INFO, "Saving $count symbols to cache"); + file_put_contents($cacheDir . "/symbols.json", json_encode($symbols, JSON_UNESCAPED_SLASHES)); + + $references = $this->project->getReferenceUris(); + $count = array_sum(array_map('count', $references)); + $this->client->window->logMessage(MessageType::INFO, "Saving $count references to cache"); + file_put_contents($cacheDir . "/references.json", json_encode($references, JSON_UNESCAPED_SLASHES)); + } } diff --git a/src/Project.php b/src/Project.php index 769a4d2..5e8a5f9 100644 --- a/src/Project.php +++ b/src/Project.php @@ -160,6 +160,17 @@ class Project $this->symbols[$fqn] = $symbol; } + /** + * Sets the SymbolInformation index + * + * @param SymbolInformation[] $symbols + * @return void + */ + public function setSymbols(array $symbols) + { + $this->symbols = $symbols; + } + /** * Unsets the SymbolInformation for a specific symbol * and removes all references pointing to that symbol @@ -221,6 +232,28 @@ class Project return array_map([$this, 'getDocument'], $this->references[$fqn]); } + /** + * Returns an associative array [string => string[]] that maps fully qualified symbol names + * to URIs of the document where the symbol is referenced + * + * @return string[][] + */ + public function getReferenceUris() + { + return $this->references; + } + + /** + * Sets the reference index + * + * @param string[][] $references an associative array [string => string[]] from FQN to URIs + * @return void + */ + public function setReferenceUris(array $references) + { + $this->references = $references; + } + /** * Returns the document where a symbol is defined * diff --git a/src/Protocol/SymbolInformation.php b/src/Protocol/SymbolInformation.php index c2fb693..19ca6a6 100644 --- a/src/Protocol/SymbolInformation.php +++ b/src/Protocol/SymbolInformation.php @@ -21,7 +21,7 @@ class SymbolInformation /** * The kind of this symbol. * - * @var number + * @var int */ public $kind; From b9222b0fd1a623e560f3ffad51558f61427b544b Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Thu, 20 Oct 2016 04:31:07 +0200 Subject: [PATCH 11/11] Switch to serialize() instead of JSON (#104) --- src/LanguageServer.php | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/LanguageServer.php b/src/LanguageServer.php index 15741d1..af7c9bb 100644 --- a/src/LanguageServer.php +++ b/src/LanguageServer.php @@ -207,16 +207,14 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher { $cacheDir = $this->rootPath . '/.phpls'; if (is_dir($cacheDir)) { - if (file_exists($cacheDir . '/symbols.json')) { - $json = json_decode(file_get_contents($cacheDir . '/symbols.json')); - $mapper = new JsonMapper; - $symbols = $mapper->mapArray($json, [], SymbolInformation::class); + if (file_exists($cacheDir . '/symbols')) { + $symbols = unserialize(file_get_contents($cacheDir . '/symbols')); $count = count($symbols); $this->project->setSymbols($symbols); $this->client->window->logMessage(MessageType::INFO, "Restoring $count symbols"); } - if (file_exists($cacheDir . '/references.json')) { - $references = json_decode(file_get_contents($cacheDir . '/references.json'), true); + if (file_exists($cacheDir . '/references')) { + $references = unserialize(file_get_contents($cacheDir . '/references')); $count = array_sum(array_map('count', $references)); $this->project->setReferenceUris($references); $this->client->window->logMessage(MessageType::INFO, "Restoring $count references"); @@ -242,11 +240,11 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher $symbols = $this->project->getSymbols(); $count = count($symbols); $this->client->window->logMessage(MessageType::INFO, "Saving $count symbols to cache"); - file_put_contents($cacheDir . "/symbols.json", json_encode($symbols, JSON_UNESCAPED_SLASHES)); + file_put_contents($cacheDir . "/symbols", serialize($symbols)); $references = $this->project->getReferenceUris(); $count = array_sum(array_map('count', $references)); $this->client->window->logMessage(MessageType::INFO, "Saving $count references to cache"); - file_put_contents($cacheDir . "/references.json", json_encode($references, JSON_UNESCAPED_SLASHES)); + file_put_contents($cacheDir . "/references", serialize($references)); } }