1
0
Fork 0

Symbol support for Microsoft/tolerant-php-parser

pull/357/head
Sara Itani 2017-03-04 20:56:01 -08:00
parent 722898f74d
commit 15eed55158
9 changed files with 241 additions and 116 deletions

View File

@ -6,7 +6,7 @@ use Microsoft\PhpParser as Tolerant;
use LanguageServer\Index\ReadableIndex;
class ParserResourceFactory {
const PARSER_KIND = ParserKind::PHP_PARSER;
const PARSER_KIND = ParserKind::TOLERANT_PHP_PARSER;
public function getParser() {
if (self::PARSER_KIND === ParserKind::PHP_PARSER) {

View File

@ -3,6 +3,7 @@
namespace LanguageServer\Protocol;
use PhpParser\Node;
use Microsoft\PhpParser as Tolerant;
/**
* Represents a location inside a resource, such as a line inside a text file.
@ -22,12 +23,20 @@ class Location
/**
* Returns the location of the node
*
* @param Node $node
* @param Node | Tolerant\Node $node
* @return self
*/
public static function fromNode(Node $node)
public static function fromNode($node)
{
return new self($node->getAttribute('ownerDocument')->getUri(), Range::fromNode($node));
if ($node instanceof Node) {
return new self($node->getAttribute('ownerDocument')->getUri(), Range::fromNode($node));
} else {
$range = Tolerant\PositionUtilities::getRangeFromPosition($node->getStart(), $node->getWidth(), $node->getFileContents());
return new self($node->getUri(), new Range(
new Position($range->start->line, $range->start->character),
new Position($range->end->line, $range->end->character)
));
}
}
public function __construct(string $uri = null, Range $range = null)

View File

@ -10,58 +10,66 @@ use Exception;
* Represents information about programming constructs like variables, classes,
* interfaces etc.
*/
class TolerantSymbolInformation extends SymbolInformation
class TolerantSymbolInformation
{
/**
* Converts a Node to a SymbolInformation
*
* @param Tolerant\Node $node
* @param string $fqn If given, $containerName will be extracted from it
* @return self|null
* @return SymbolInformation|null
*/
public static function fromNode($node, string $fqn = null)
{
$parent = $node->getAttribute('parentNode');
$symbol = new self;
if ($node instanceof Node\Stmt\Class_) {
$symbol = new SymbolInformation();
if ($node instanceof Tolerant\Node\Statement\ClassDeclaration) {
$symbol->kind = SymbolKind::CLASS_;
} else if ($node instanceof Node\Stmt\Trait_) {
} else if ($node instanceof Tolerant\Node\Statement\TraitDeclaration) {
$symbol->kind = SymbolKind::CLASS_;
} else if ($node instanceof Node\Stmt\Interface_) {
} else if ($node instanceof Tolerant\Node\Statement\InterfaceDeclaration) {
$symbol->kind = SymbolKind::INTERFACE;
} else if ($node instanceof Node\Name && $parent instanceof Node\Stmt\Namespace_) {
} else if ($node instanceof Tolerant\Node\Statement\NamespaceDefinition) {
$symbol->kind = SymbolKind::NAMESPACE;
} else if ($node instanceof Node\Stmt\Function_) {
} else if ($node instanceof Tolerant\Node\Statement\FunctionDeclaration) {
$symbol->kind = SymbolKind::FUNCTION;
} else if ($node instanceof Node\Stmt\ClassMethod) {
} else if ($node instanceof Tolerant\Node\MethodDeclaration) {
$symbol->kind = SymbolKind::METHOD;
} else if ($node instanceof Node\Stmt\PropertyProperty) {
} else if ($node instanceof Tolerant\Node\Expression\Variable && $node->getFirstAncestor(Tolerant\Node\PropertyDeclaration::class) !== null) {
$symbol->kind = SymbolKind::PROPERTY;
} else if ($node instanceof Node\Const_) {
} else if ($node instanceof Tolerant\Node\ConstElement) {
$symbol->kind = SymbolKind::CONSTANT;
} else if (
}
else if (
(
($node instanceof Node\Expr\Assign || $node instanceof Node\Expr\AssignOp)
&& $node->var instanceof Node\Expr\Variable
($node instanceof Tolerant\Node\Expression\AssignmentExpression)
&& $node->leftOperand instanceof Tolerant\Node\Expression\Variable
)
|| $node instanceof Node\Expr\ClosureUse
|| $node instanceof Node\Param
|| $node instanceof Tolerant\Node\UseVariableName
|| $node instanceof Tolerant\Node\Parameter
) {
$symbol->kind = SymbolKind::VARIABLE;
} else {
return null;
}
if ($node instanceof Node\Name) {
$symbol->name = (string)$node;
} else if ($node instanceof Node\Expr\Assign || $node instanceof Node\Expr\AssignOp) {
$symbol->name = $node->var->name;
} else if ($node instanceof Node\Expr\ClosureUse) {
$symbol->name = $node->var;
} else if ($node instanceof Tolerant\Node\Expression\AssignmentExpression) {
if ($node->leftOperand instanceof Tolerant\Node\Expression\Variable) {
$symbol->name = $node->leftOperand->getName();
} elseif ($node->leftOperand instanceof Tolerant\Token) {
$symbol->name = trim($node->leftOperand->getText($node->getFileContents()), "$");
}
} else if ($node instanceof Tolerant\Node\UseVariableName) {
$symbol->name = $node->getName();
} else if (isset($node->name)) {
$symbol->name = (string)$node->name;
$symbol->name = trim((string)$node->name->getText($node->getFileContents()), "$");
} else {
return null;
}
$symbol->location = Location::fromNode($node);
if ($fqn !== null) {
$parts = preg_split('/(::|->|\\\\)/', $fqn);

View File

@ -6,9 +6,12 @@ namespace LanguageServer;
use LanguageServer\Protocol\TolerantSymbolInformation;
use PhpParser\Node;
use PhpParser\PrettyPrinter\Standard as PrettyPrinter;
use phpDocumentor\Reflection\{Types, Type, Fqsen, TypeResolver};
use phpDocumentor\Reflection\{
DocBlockFactory, Types, Type, Fqsen, TypeResolver
};
use LanguageServer\Protocol\SymbolInformation;
use LanguageServer\Index\ReadableIndex;
use Microsoft\PhpParser as Tolerant;
class TolerantDefinitionResolver implements DefinitionResolverInterface
{
@ -38,99 +41,157 @@ class TolerantDefinitionResolver implements DefinitionResolverInterface
}
/**
* Builds the declaration line for a given node
* Builds the declaration line for a given node.
*
* @param Node $node
*
* @param Tolerant\Node $node
* @return string
*/
public function getDeclarationLineFromNode($node): string
{
if ($node instanceof Node\Stmt\PropertyProperty || $node instanceof Node\Const_) {
// Properties and constants can have multiple declarations
// Use the parent node (that includes the modifiers), but only render the requested declaration
$child = $node;
/** @var Node */
$node = $node->getAttribute('parentNode');
$defLine = clone $node;
$defLine->props = [$child];
} else {
$defLine = clone $node;
// TODO Tolerant\Node\Statement\FunctionStaticDeclaration::class
// we should have a better way of determining whether something is a property or constant
// If part of a declaration list -> get the parent declaration
if (
// PropertyDeclaration // public $a, $b, $c;
$node instanceof Tolerant\Node\Expression\Variable &&
($propertyDeclaration = $node->getFirstAncestor(Tolerant\Node\PropertyDeclaration::class)) !== null
) {
$defLine = $propertyDeclaration->getText();
$defLineStart = $propertyDeclaration->getStart();
$defLine = \substr_replace(
$defLine,
$node->getFullText(),
$propertyDeclaration->propertyElements->getFullStart() - $defLineStart,
$propertyDeclaration->propertyElements->getWidth()
);
} elseif (
// ClassConstDeclaration or ConstDeclaration // const A = 1, B = 2;
$node instanceof Tolerant\Node\ConstElement &&
($constDeclaration = $node->getFirstAncestor(Tolerant\Node\Statement\ConstDeclaration::class, Tolerant\Node\ClassConstDeclaration::class))
) {
$defLine = $constDeclaration->getText();
$defLineStart = $constDeclaration->getStart();
$defLine = \substr_replace(
$defLine,
$node->getFullText(),
$constDeclaration->constElements->getFullStart() - $defLineStart,
$constDeclaration->constElements->getWidth()
);
}
// Don't include the docblock in the declaration string
$defLine->setAttribute('comments', []);
if (isset($defLine->stmts)) {
$defLine->stmts = [];
// Get the current node
else {
$defLine = $node->getText();
}
$defText = $this->prettyPrinter->prettyPrint([$defLine]);
return strstr($defText, "\n", true) ?: $defText;
$defLine = \strtok($defLine, "\n");
return $defLine;
}
/**
* Gets the documentation string for a node, if it has one
*
* @param Node $node
* @param Tolerant\Node $node
* @return string|null
*/
public function getDocumentationFromNode($node)
{
if ($node instanceof Node\Stmt\PropertyProperty || $node instanceof Node\Const_) {
$node = $node->getAttribute('parentNode');
// For properties and constants, set the node to the declaration node, rather than the individual property.
// This is because they get defined as part of a list.
$constOrPropertyDeclaration = $node->getFirstAncestor(
Tolerant\Node\PropertyDeclaration::class,
Tolerant\Node\Statement\ConstDeclaration::class,
Tolerant\Node\ClassConstDeclaration::class
);
if ($constOrPropertyDeclaration !== null) {
$node = $constOrPropertyDeclaration;
}
if ($node instanceof Node\Param) {
$fn = $node->getAttribute('parentNode');
$docBlock = $fn->getAttribute('docBlock');
// For parameters, parse the documentation to get the parameter tag.
if ($node instanceof Tolerant\Node\Parameter) {
$functionLikeDeclaration = $this->getFunctionLikeDeclarationFromParameter($node);
$variableName = $node->variableName->getText($node->getFileContents());
$docBlock = $this->getDocBlock($functionLikeDeclaration);
if ($docBlock !== null) {
$tags = $docBlock->getTagsByName('param');
foreach ($tags as $tag) {
if ($tag->getVariableName() === $node->name) {
return $tag->getDescription()->render();
}
}
$parameterDocBlockTag = $this->getDocBlockTagForParameter($docBlock, $variableName);
return $parameterDocBlockTag !== null ? $parameterDocBlockTag->getDescription()->render() : null;
}
} else {
$docBlock = $node->getAttribute('docBlock');
}
// for everything else, get the doc block summary corresponding to the current node.
else {
$docBlock = $this->getDocBlock($node);
if ($docBlock !== null) {
return $docBlock->getSummary();
}
}
}
function getDocBlock(Tolerant\Node $node) {
// TODO context information
static $docBlockFactory;
$docBlockFactory = $docBlockFactory ?? DocBlockFactory::createInstance();
$docCommentText = $node->getDocCommentText();
return $docCommentText !== null ? $docBlockFactory->create($docCommentText) : null;
}
/**
* Create a Definition for a definition node
*
* @param Node $node
* @param Tolerant\Node $node
* @param string $fqn
* @return Definition
*/
public function createDefinitionFromNode($node, string $fqn = null): Definition
{
$parent = $node->getAttribute('parentNode');
$def = new Definition;
$def->canBeInstantiated = $node instanceof Node\Stmt\Class_;
// this determines whether the suggestion will show after "new"
$def->isClass = $node instanceof Tolerant\Node\Statement\ClassDeclaration;
$def->isGlobal = (
$node instanceof Node\Stmt\ClassLike
|| ($node instanceof Node\Name && $parent instanceof Node\Stmt\Namespace_)
|| $node instanceof Node\Stmt\Function_
|| $parent instanceof Node\Stmt\Const_
$node instanceof Tolerant\Node\Statement\InterfaceDeclaration
|| $node instanceof Tolerant\Node\Statement\ClassDeclaration
|| $node instanceof Tolerant\Node\Statement\TraitDeclaration
|| $node instanceof Tolerant\Node\Statement\NamespaceDefinition && $node->name !== null
|| $node instanceof Tolerant\Node\Statement\FunctionDeclaration
|| $node instanceof Tolerant\Node\Statement\ConstDeclaration
|| $node instanceof Tolerant\Node\ClassConstDeclaration
);
$def->isStatic = (
($node instanceof Node\Stmt\ClassMethod && $node->isStatic())
|| ($node instanceof Node\Stmt\PropertyProperty && $parent->isStatic())
($node instanceof Tolerant\Node\MethodDeclaration && $node->isStatic())
|| ($node instanceof Tolerant\Node\Expression\Variable &&
($propertyDeclaration = $node->getFirstAncestor(Tolerant\Node\PropertyDeclaration::class)) !== null &&
$propertyDeclaration->isStatic())
);
$def->fqn = $fqn;
if ($node instanceof Node\Stmt\Class_) {
if ($node instanceof Tolerant\Node\Statement\ClassDeclaration) {
$def->extends = [];
if ($node->extends) {
$def->extends[] = (string)$node->extends;
if ($node->classBaseClause !== null && $node->classBaseClause->baseClass !== null) {
$def->extends[] = (string)$node->classBaseClause->baseClass;
}
} else if ($node instanceof Node\Stmt\Interface_) {
// TODO what about class interfaces
} else if ($node instanceof Tolerant\Node\Statement\InterfaceDeclaration) {
$def->extends = [];
foreach ($node->extends as $n) {
$def->extends[] = (string)$n;
if ($node->interfaceBaseClause !== null && $node->interfaceBaseClause->interfaceNameList !== null) {
foreach ($node->interfaceBaseClause->interfaceNameList->getChildNodes() as $n) {
$def->extends[] = (string)$n;
}
}
}
$def->symbolInformation = TolerantSymbolInformation::fromNode($node, $fqn);
$def->type = $this->getTypeFromNode($node);
// $def->type = $this->getTypeFromNode($node); //TODO
$def->type = new Types\Mixed;
$def->declarationLine = $this->getDeclarationLineFromNode($node);
$def->documentation = $this->getDocumentationFromNode($node);
return $def;
@ -796,60 +857,97 @@ class TolerantDefinitionResolver implements DefinitionResolverInterface
* 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
* @param Tolerant\Node $node
* @return string|null
*/
public static function getDefinedFqn($node)
{
$parent = $node->getAttribute('parentNode');
$parent = $node->getParent();
// 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\Name && $parent instanceof Node\Stmt\Namespace_) {
return (string)$node;
} else if ($node instanceof Node\Stmt\Function_) {
// INPUT OUTPUT:
// namespace A\B;
// class C { } A\B\C
// interface C { } A\B\C
// trait C { } A\B\C
if (
$node instanceof Tolerant\Node\Statement\ClassDeclaration ||
$node instanceof Tolerant\Node\Statement\InterfaceDeclaration ||
$node instanceof Tolerant\Node\Statement\TraitDeclaration
) {
return (string) $node->getNamespacedName();
}
// INPUT OUTPUT:
// namespace A\B; A\B
else if ($node instanceof Tolerant\Node\Statement\NamespaceDefinition && $node->name instanceof Tolerant\Node\QualifiedName) {
return (string) $node->name;
}
// INPUT OUTPUT:
// namespace A\B;
// function a(); A\B\a();
else if ($node instanceof Tolerant\Node\Statement\FunctionDeclaration) {
// Function: use functionName() as the name
return (string)$node->namespacedName . '()';
} else if ($node instanceof Node\Stmt\ClassMethod) {
return (string)$node->getNamespacedName() . '()';
}
// INPUT OUTPUT
// namespace A\B;
// class C {
// function a () {} A\B\C::a()
// static function b() {} A\B\C->b()
// }
else if ($node instanceof Tolerant\Node\MethodDeclaration) {
// Class method: use ClassName->methodName() as name
$class = $node->getAttribute('parentNode');
$class = $node->getFirstAncestor(Tolerant\Node\Statement\ClassDeclaration::class);
if (!isset($class->name)) {
// Ignore anonymous classes
return null;
}
if ($node->isStatic()) {
return (string)$class->namespacedName . '::' . (string)$node->name . '()';
return (string)$class->getNamespacedName() . '::' . $node->getName() . '()';
} else {
return (string)$class->namespacedName . '->' . (string)$node->name . '()';
return (string)$class->getNamespacedName() . '->' . $node->getName() . '()';
}
} else if ($node instanceof Node\Stmt\PropertyProperty) {
$property = $node->getAttribute('parentNode');
$class = $property->getAttribute('parentNode');
if (!isset($class->name)) {
// Ignore anonymous classes
return null;
}
if ($property->isStatic()) {
}
// INPUT OUTPUT
// namespace A\B;
// class C {
// static $a = 4, $b = 4 A\B\C::$a, A\B\C::$b
// $a = 4, $b = 4 A\B\C->$a, A\B\C->$b
// }
else if (
$node instanceof Tolerant\Node\Expression\Variable &&
($propertyDeclaration = $node->getFirstAncestor(Tolerant\Node\PropertyDeclaration::class)) !== null &&
($classDeclaration = $node->getFirstAncestor(Tolerant\Node\Statement\ClassDeclaration::class)) !== null)
{
if ($propertyDeclaration->isStatic()) {
// Static Property: use ClassName::$propertyName as name
return (string)$class->namespacedName . '::$' . (string)$node->name;
} else {
return (string)$classDeclaration->getNamespacedName() . '::$' . (string)$node->getName();
} elseif (($name = $node->getName()) !== null) {
// Instance Property: use ClassName->propertyName as name
return (string)$class->namespacedName . '->' . (string)$node->name;
return (string)$classDeclaration->getNamespacedName() . '->' . $name;
}
} else if ($node instanceof Node\Const_) {
$parent = $node->getAttribute('parentNode');
if ($parent instanceof Node\Stmt\Const_) {
}
// INPUT OUTPUT
// namespace A\B;
// const FOO = 5; A\B\FOO
// class C {
// const $a, $b = 4 A\B\C::$a(), A\B\C::$b
// }
else if ($node instanceof Tolerant\Node\ConstElement) {
$constDeclaration = $node->getFirstAncestor(Tolerant\Node\Statement\ConstDeclaration::class, Tolerant\Node\ClassConstDeclaration::class);
if ($constDeclaration instanceof Tolerant\Node\Statement\ConstDeclaration) {
// Basic constant: use CONSTANT_NAME as name
return (string)$node->namespacedName;
return (string)$node->getNamespacedName();
}
if ($parent instanceof Node\Stmt\ClassConst) {
if ($constDeclaration instanceof Tolerant\Node\ClassConstDeclaration) {
// Class constant: use ClassName::CONSTANT_NAME as name
$class = $parent->getAttribute('parentNode');
if (!isset($class->name) || $class->name instanceof Node\Expr) {
$classDeclaration = $constDeclaration->getFirstAncestor(Tolerant\Node\Statement\ClassDeclaration::class);
if (!isset($classDeclaration->name)) {
return null;
}
return (string)$class->namespacedName . '::' . $node->name;
return (string)$classDeclaration->getNamespacedName() . '::' . $node->getName();
}
}
}

View File

@ -22,20 +22,29 @@ use Microsoft\PhpParser as Tolerant;
class TolerantTreeAnalyzer implements TreeAnalyzerInterface {
private $parser;
/** @var Tolerant\Node */
private $stmts;
public function __construct(Parser $parser, $content, $docBlockFactory, $definitionResolver, $uri) {
/**
* TolerantTreeAnalyzer constructor.
* @param Tolerant\Parser $parser
* @param $content
* @param $docBlockFactory
* @param TolerantDefinitionResolver $definitionResolver
* @param $uri
*/
public function __construct($parser, $content, $docBlockFactory, $definitionResolver, $uri) {
$this->uri = $uri;
$this->parser = $parser;
$this->docBlockFactory = $docBlockFactory;
$this->definitionResolver = $definitionResolver;
$this->content = $content;
$this->$stmts = $this->parser->parse($content);
$this->stmts = $this->parser->parseSourceFile($content, $uri);
// TODO - docblock errors
foreach ($this->stmts->getDescendantNodes() as $node) {
$fqn = DefinitionResolver::getDefinedFqn($node);
$fqn = $definitionResolver::getDefinedFqn($node);
// Only index definitions with an FQN (no variables)
if ($fqn === null) {
continue;
@ -47,7 +56,8 @@ class TolerantTreeAnalyzer implements TreeAnalyzerInterface {
public function getDiagnostics() {
$diagnostics = [];
foreach (Tolerant\DiagnosticsProvider::getDiagnostics($tolerantStmts) as $_error) {
$content = $this->stmts->getFileContents();
foreach (Tolerant\DiagnosticsProvider::getDiagnostics($this->stmts) as $_error) {
$range = Tolerant\PositionUtilities::getRangeFromPosition($_error->start, $_error->length, $content);
$diagnostics[] = new Diagnostic(

View File

@ -28,7 +28,7 @@ class TreeAnalyzer implements TreeAnalyzerInterface {
private $diagnostics;
public function __construct(Parser $parser, $content, $docBlockFactory, $definitionResolver, $uri) {
public function __construct($parser, $content, $docBlockFactory, $definitionResolver, $uri) {
$this->uri = $uri;
$this->parser = $parser;
$this->docBlockFactory = $docBlockFactory;

View File

@ -20,7 +20,7 @@ use Sabre\Uri;
use Microsoft\PhpParser as Tolerant;
interface TreeAnalyzerInterface {
public function __construct(Parser $parser, $content, $docBlockFactory, $definitionResolver, $uri);
public function __construct($parser, $content, $docBlockFactory, $definitionResolver, $uri);
public function getDiagnostics();

View File

@ -88,8 +88,8 @@ abstract class ServerTestCase extends TestCase
'whatever()' => new Location($globalReferencesUri, new Range(new Position(21, 0), new Position(23, 1))),
// Namespaced
'TestNamespace' => new Location($symbolsUri, new Range(new Position( 2, 10), new Position( 2, 23))),
'SecondTestNamespace' => new Location($useUri, new Range(new Position( 2, 10), new Position( 2, 29))),
'TestNamespace' => new Location($symbolsUri, new Range(new Position( 2, 0), new Position( 2, 24))),
'SecondTestNamespace' => new Location($useUri, new Range(new Position( 2, 0), new Position( 2, 30))),
'TestNamespace\\TEST_CONST' => new Location($symbolsUri, new Range(new Position( 9, 6), new Position( 9, 22))),
'TestNamespace\\TestClass' => new Location($symbolsUri, new Range(new Position(20, 0), new Position(61, 1))),
'TestNamespace\\ChildClass' => new Location($symbolsUri, new Range(new Position(99, 0), new Position(99, 37))),

View File

@ -29,7 +29,7 @@ class SymbolTest extends ServerTestCase
$referencesUri = pathToUri(realpath(__DIR__ . '/../../../fixtures/references.php'));
// @codingStandardsIgnoreStart
$this->assertEquals([
new SymbolInformation('TestNamespace', SymbolKind::NAMESPACE, new Location($referencesUri, new Range(new Position(2, 10), new Position(2, 23))), ''),
new SymbolInformation('TestNamespace', SymbolKind::NAMESPACE, new Location($referencesUri, new Range(new Position(2, 0), new Position(2, 24))), ''),
// Namespaced
new SymbolInformation('TEST_CONST', SymbolKind::CONSTANT, $this->getDefinitionLocation('TestNamespace\\TEST_CONST'), 'TestNamespace'),
new SymbolInformation('TestClass', SymbolKind::CLASS_, $this->getDefinitionLocation('TestNamespace\\TestClass'), 'TestNamespace'),