diff --git a/src/Definition.php b/src/Definition.php index ae52826..021c66e 100644 --- a/src/Definition.php +++ b/src/Definition.php @@ -45,127 +45,4 @@ class Definition * @var \phpDocumentor\Type|null */ public $type; - - /** - * Returns the type a reference to this symbol will resolve to. - * For properties and constants, this is the type of the property/constant. - * For functions and methods, this is the return type. - * For classes and interfaces, this is the class type (object). - * Variables are not indexed for performance reasons. - * Can also be a compound type. - * If it is unknown, will be Types\Mixed. - * Returns null if the node does not have a type. - * - * @param Node $node - * @return \phpDocumentor\Type|null - */ - public static function getTypeFromNode(Node $node) - { - if ($node instanceof Node\Param) { - // Parameters - $docBlock = $node->getAttribute('docBlock'); - if ($docBlock !== null && count($paramTags = $docBlock->getTagsByName('param')) > 0) { - // Use @param tag - return $paramTags[0]->getType(); - } - if ($node->type !== null) { - // Use PHP7 return type hint - if (is_string($node->type)) { - // Resolve a string like "bool" to a type object - $type = (new TypeResolver)->resolve($node->type); - } - $type = new Types\Object_(new Fqsen('\\' . (string)$node->type)); - if ($node->default !== null) { - if (is_string($node->default)) { - // Resolve a string like "bool" to a type object - $defaultType = (new TypeResolver)->resolve($node->default); - } - $defaultType = new Types\Object_(new Fqsen('\\' . (string)$node->default)); - $type = new Types\Compound([$type, $defaultType]); - } - } - // Unknown parameter type - return new Types\Mixed; - } - if ($node instanceof Node\FunctionLike) { - // Functions/methods - $docBlock = $node->getAttribute('docBlock'); - if ($docBlock !== null && count($returnTags = $docBlock->getTagsByName('return')) > 0) { - // Use @return tag - return $returnTags[0]->getType(); - } - if ($node->returnType !== null) { - // Use PHP7 return type hint - if (is_string($node->returnType)) { - // Resolve a string like "bool" to a type object - return (new TypeResolver)->resolve($node->returnType); - } - return new Types\Object_(new Fqsen('\\' . (string)$node->returnType)); - } - // Unknown return type - return new Types\Mixed; - } - if ($node instanceof Node\Stmt\PropertyProperty || $node instanceof Node\Const_) { - // Property or constant - $docBlock = $node->getAttribute('parentNode')->getAttribute('docBlock'); - if ($docBlock !== null && count($varTags = $docBlock->getTagsByName('var')) > 0) { - // Use @var tag - return $varTags[0]->getType(); - } - // TODO: read @property tags of class - // TODO: Try to infer the type from default value / constant value - // Unknown - return new Types\Mixed; - } - return null; - } - - /** - * Returns the fully qualified name (FQN) that is defined by a node - * Returns null if the node does not declare any symbol that can be referenced by an FQN - * - * @param Node $node - * @return string|null - */ - public static 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/DefinitionResolver.php b/src/DefinitionResolver.php index f191491..80495d7 100644 --- a/src/DefinitionResolver.php +++ b/src/DefinitionResolver.php @@ -12,10 +12,12 @@ use function Sabre\Event\coroutine; class DefinitionResolver { private $project; + private $typeResolver; public function __construct(Project $project) { $this->project = $project; + $this->typeResolver = new TypeResolver; } /** @@ -35,7 +37,7 @@ class DefinitionResolver $def->symbolInformation = SymbolInformation::fromNode($defNode); if ($defNode instanceof Node\Param) { // Get parameter type - $def->type = Definition::getTypeFromNode($defNode); + $def->type = $this->getTypeFromNode($defNode); } else { // Resolve the type of the assignment/closure use node $def->type = $this->resolveExpression($defNode); @@ -244,7 +246,7 @@ class DefinitionResolver return $this->resolveExpression($defNode); } if ($defNode instanceof Node\Param) { - return Definition::getTypeFromNode($defNode); + return $this->getTypeFromNode($defNode); } } if ($expr instanceof Node\Expr\FuncCall) { @@ -434,4 +436,123 @@ class DefinitionResolver } return new Types\Mixed; } + + /** + * Returns the type a reference to this symbol will resolve to. + * For properties and constants, this is the type of the property/constant. + * For functions and methods, this is the return type. + * For classes and interfaces, this is the class type (object). + * Variables are not indexed for performance reasons. + * Can also be a compound type. + * If it is unknown, will be Types\Mixed. + * Returns null if the node does not have a type. + * + * @param Node $node + * @return \phpDocumentor\Type|null + */ + public function getTypeFromNode(Node $node) + { + if ($node instanceof Node\Param) { + // Parameters + $docBlock = $node->getAttribute('docBlock'); + if ($docBlock !== null && count($paramTags = $docBlock->getTagsByName('param')) > 0) { + // Use @param tag + return $paramTags[0]->getType(); + } + if ($node->type !== null) { + // Use PHP7 return type hint + if (is_string($node->type)) { + // Resolve a string like "bool" to a type object + $type = $this->typeResolver->resolve($node->type); + } + $type = new Types\Object_(new Fqsen('\\' . (string)$node->type)); + if ($node->default !== null) { + $defaultType = $this->resolveExpression($node->default); + $type = new Types\Compound([$type, $defaultType]); + } + } + // Unknown parameter type + return new Types\Mixed; + } + if ($node instanceof Node\FunctionLike) { + // Functions/methods + $docBlock = $node->getAttribute('docBlock'); + if ($docBlock !== null && count($returnTags = $docBlock->getTagsByName('return')) > 0) { + // Use @return tag + return $returnTags[0]->getType(); + } + if ($node->returnType !== null) { + // Use PHP7 return type hint + if (is_string($node->returnType)) { + // Resolve a string like "bool" to a type object + return $this->typeResolver->resolve($node->returnType); + } + return new Types\Object_(new Fqsen('\\' . (string)$node->returnType)); + } + // Unknown return type + return new Types\Mixed; + } + if ($node instanceof Node\Stmt\PropertyProperty || $node instanceof Node\Const_) { + // Property or constant + $docBlock = $node->getAttribute('parentNode')->getAttribute('docBlock'); + if ($docBlock !== null && count($varTags = $docBlock->getTagsByName('var')) > 0) { + // Use @var tag + return $varTags[0]->getType(); + } + // TODO: read @property tags of class + // TODO: Try to infer the type from default value / constant value + // Unknown + return new Types\Mixed; + } + return null; + } + + /** + * Returns the fully qualified name (FQN) that is defined by a node + * Returns null if the node does not declare any symbol that can be referenced by an FQN + * + * @param Node $node + * @return string|null + */ + public static 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 e0c3521..0cf5ea9 100644 --- a/src/NodeVisitor/DefinitionCollector.php +++ b/src/NodeVisitor/DefinitionCollector.php @@ -4,7 +4,7 @@ declare(strict_types = 1); namespace LanguageServer\NodeVisitor; use PhpParser\{NodeVisitorAbstract, Node}; -use LanguageServer\Definition; +use LanguageServer\{Definition, DefinitionResolver}; use LanguageServer\Protocol\SymbolInformation; /** @@ -27,9 +27,16 @@ class DefinitionCollector extends NodeVisitorAbstract */ public $nodes = []; + public $definitionResolver; + + public function __construct(DefinitionResolver $definitionResolver) + { + $this->definitionResolver = $definitionResolver; + } + public function enterNode(Node $node) { - $fqn = Definition::getDefinedFqn($node); + $fqn = DefinitionResolver::getDefinedFqn($node); // Only index definitions with an FQN (no variables) if ($fqn === null) { return; @@ -38,7 +45,7 @@ class DefinitionCollector extends NodeVisitorAbstract $def = new Definition; $def->fqn = $fqn; $def->symbolInformation = SymbolInformation::fromNode($node, $fqn); - $def->type = Definition::getTypeFromNode($node); + $def->type = $this->definitionResolver->getTypeFromNode($node); $this->definitions[$fqn] = $def; } } diff --git a/src/PhpDocument.php b/src/PhpDocument.php index 8a3fec0..e24b9ac 100644 --- a/src/PhpDocument.php +++ b/src/PhpDocument.php @@ -186,7 +186,7 @@ class PhpDocument $traverser = new NodeTraverser; // Collect all definitions - $definitionCollector = new DefinitionCollector; + $definitionCollector = new DefinitionCollector($this->definitionResolver); $traverser->addVisitor($definitionCollector); // Collect all references @@ -374,7 +374,7 @@ class PhpDocument return $refCollector->nodes; } // Definition with a global FQN - $fqn = Definition::getDefinedFqn($node); + $fqn = DefinitionResolver::getDefinedFqn($node); if ($fqn === null) { return []; } diff --git a/tests/NodeVisitor/DefinitionCollectorTest.php b/tests/NodeVisitor/DefinitionCollectorTest.php index 4ed7508..ded65d1 100644 --- a/tests/NodeVisitor/DefinitionCollectorTest.php +++ b/tests/NodeVisitor/DefinitionCollectorTest.php @@ -6,7 +6,7 @@ namespace LanguageServer\Tests\Server\TextDocument; use PHPUnit\Framework\TestCase; use PhpParser\{NodeTraverser, Node}; use PhpParser\NodeVisitor\NameResolver; -use LanguageServer\{LanguageClient, Project, PhpDocument, Parser}; +use LanguageServer\{LanguageClient, Project, PhpDocument, Parser, DefinitionResolver}; use LanguageServer\Protocol\ClientCapabilities; use LanguageServer\Tests\MockProtocolStream; use LanguageServer\NodeVisitor\{ReferencesAdder, DefinitionCollector}; @@ -24,7 +24,7 @@ class DefinitionCollectorTest extends TestCase $traverser = new NodeTraverser; $traverser->addVisitor(new NameResolver); $traverser->addVisitor(new ReferencesAdder($document)); - $definitionCollector = new DefinitionCollector; + $definitionCollector = new DefinitionCollector(new DefinitionResolver($project)); $traverser->addVisitor($definitionCollector); $stmts = $parser->parse(file_get_contents($uri)); $traverser->traverse($stmts); @@ -63,7 +63,7 @@ class DefinitionCollectorTest extends TestCase $traverser = new NodeTraverser; $traverser->addVisitor(new NameResolver); $traverser->addVisitor(new ReferencesAdder($document)); - $definitionCollector = new DefinitionCollector; + $definitionCollector = new DefinitionCollector(new DefinitionResolver($project)); $traverser->addVisitor($definitionCollector); $stmts = $parser->parse(file_get_contents($uri)); $traverser->traverse($stmts);