1
0
Fork 0

perf(parsing): cache docblocks

pull/608/head
Declspeck 2018-02-24 17:31:40 +02:00
parent a8f60c9cf6
commit 9cbd00cb7b
No known key found for this signature in database
GPG Key ID: F0417663122A2189
4 changed files with 92 additions and 47 deletions

View File

@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace LanguageServer;
use Microsoft\PhpParser\Node;
use phpDocumentor\Reflection\DocBlock;
use phpDocumentor\Reflection\DocBlockFactory;
use phpDocumentor\Reflection\Types;
/**
* Caches DocBlocks by node start position and file URI.
*/
class CachingDocBlockFactory
{
/**
* Maps file + node start positions to DocBlocks.
*/
private $cache = [];
/**
* @var DocBlockFactory
*/
private $docBlockFactory;
public function __construct() {
$this->docBlockFactory = DocBlockFactory::createInstance();
}
/**
* @return DocBlock|null
*/
public function getDocBlock(Node $node)
{
$cacheKey = $node->getStart() . ':' . $node->getUri();
if (array_key_exists($cacheKey, $this->cache)) {
return $this->cache[$cacheKey];
}
$text = $node->getDocCommentText();
return $this->cache[$cacheKey] = $text === null ? null : $this->createDocBlockFromNodeAndText($node, $text);
}
public function clearCache() {
$this->cache = [];
}
/**
* @return DocBlock|null
*/
private function createDocBlockFromNodeAndText(Node $node, string $text)
{
list($namespaceImportTable,,) = $node->getImportTablesForCurrentScope();
$namespaceImportTable = array_map('strval', $namespaceImportTable);
$namespaceDefinition = $node->getNamespaceDefinition();
if ($namespaceDefinition !== null && $namespaceDefinition->name !== null) {
$namespaceName = (string)$namespaceDefinition->name->getNamespacedName();
} else {
$namespaceName = 'global';
}
$context = new Types\Context($namespaceName, $namespaceImportTable);
try {
// create() throws when it thinks the doc comment has invalid fields.
// For example, a @see tag that is followed by something that doesn't look like a valid fqsen will throw.
return $this->docBlockFactory->create($text, $context);
} catch (\InvalidArgumentException $e) {
return null;
}
}
}

View File

@ -8,9 +8,7 @@ use LanguageServer\Protocol\SymbolInformation;
use Microsoft\PhpParser; use Microsoft\PhpParser;
use Microsoft\PhpParser\Node; use Microsoft\PhpParser\Node;
use Microsoft\PhpParser\FunctionLike; use Microsoft\PhpParser\FunctionLike;
use phpDocumentor\Reflection\{ use phpDocumentor\Reflection\{DocBlock, Fqsen, Type, TypeResolver, Types};
DocBlock, DocBlockFactory, Fqsen, Type, TypeResolver, Types
};
class DefinitionResolver class DefinitionResolver
{ {
@ -29,11 +27,11 @@ class DefinitionResolver
private $typeResolver; private $typeResolver;
/** /**
* Parses Doc Block comments given the DocBlock text and import tables at a position. * Parses and caches Doc Block comments given Node.
* *
* @var DocBlockFactory * @var CachingDocBlockFactory
*/ */
private $docBlockFactory; private $cachingDocBlockFactory;
/** /**
* Creates SignatureInformation * Creates SignatureInformation
@ -49,7 +47,7 @@ class DefinitionResolver
{ {
$this->index = $index; $this->index = $index;
$this->typeResolver = new TypeResolver; $this->typeResolver = new TypeResolver;
$this->docBlockFactory = DocBlockFactory::createInstance(); $this->cachingDocBlockFactory = new CachingDocBlockFactory;
$this->signatureInformationFactory = new SignatureInformationFactory($this); $this->signatureInformationFactory = new SignatureInformationFactory($this);
} }
@ -114,14 +112,14 @@ class DefinitionResolver
$variableName = $node->getName(); $variableName = $node->getName();
$functionLikeDeclaration = ParserHelpers\getFunctionLikeDeclarationFromParameter($node); $functionLikeDeclaration = ParserHelpers\getFunctionLikeDeclarationFromParameter($node);
$docBlock = $this->getDocBlock($functionLikeDeclaration); $docBlock = $this->cachingDocBlockFactory->getDocBlock($functionLikeDeclaration);
$parameterDocBlockTag = $this->tryGetDocBlockTagForParameter($docBlock, $variableName); $parameterDocBlockTag = $this->tryGetDocBlockTagForParameter($docBlock, $variableName);
return $parameterDocBlockTag !== null ? $parameterDocBlockTag->getDescription()->render() : null; return $parameterDocBlockTag !== null ? $parameterDocBlockTag->getDescription()->render() : null;
} }
// For everything else, get the doc block summary corresponding to the current node. // For everything else, get the doc block summary corresponding to the current node.
$docBlock = $this->getDocBlock($node); $docBlock = $this->cachingDocBlockFactory->getDocBlock($node);
if ($docBlock !== null) { if ($docBlock !== null) {
// check whether we have a description, when true, add a new paragraph // check whether we have a description, when true, add a new paragraph
// with the description // with the description
@ -136,40 +134,6 @@ class DefinitionResolver
return null; return null;
} }
/**
* Gets Doc Block with resolved names for a Node
*
* @param Node $node
* @return DocBlock|null
*/
private function getDocBlock(Node $node)
{
// TODO make more efficient (caching, ensure import table is in right format to begin with)
$docCommentText = $node->getDocCommentText();
if ($docCommentText !== null) {
list($namespaceImportTable,,) = $node->getImportTablesForCurrentScope();
foreach ($namespaceImportTable as $alias => $name) {
$namespaceImportTable[$alias] = (string)$name;
}
$namespaceDefinition = $node->getNamespaceDefinition();
if ($namespaceDefinition !== null && $namespaceDefinition->name !== null) {
$namespaceName = (string)$namespaceDefinition->name->getNamespacedName();
} else {
$namespaceName = 'global';
}
$context = new Types\Context($namespaceName, $namespaceImportTable);
try {
// create() throws when it thinks the doc comment has invalid fields.
// For example, a @see tag that is followed by something that doesn't look like a valid fqsen will throw.
return $this->docBlockFactory->create($docCommentText, $context);
} catch (\InvalidArgumentException $e) {
return null;
}
}
return null;
}
/** /**
* Create a Definition for a definition node * Create a Definition for a definition node
* *
@ -346,6 +310,11 @@ class DefinitionResolver
return null; return null;
} }
public function clearCache()
{
$this->cachingDocBlockFactory->clearCache();
}
private function resolveQualifiedNameNodeToFqn(Node\QualifiedName $node) private function resolveQualifiedNameNodeToFqn(Node\QualifiedName $node)
{ {
$parent = $node->parent; $parent = $node->parent;
@ -1080,7 +1049,7 @@ class DefinitionResolver
// function foo($a) // function foo($a)
$functionLikeDeclaration = ParserHelpers\getFunctionLikeDeclarationFromParameter($node); $functionLikeDeclaration = ParserHelpers\getFunctionLikeDeclarationFromParameter($node);
$variableName = $node->getName(); $variableName = $node->getName();
$docBlock = $this->getDocBlock($functionLikeDeclaration); $docBlock = $this->cachingDocBlockFactory->getDocBlock($functionLikeDeclaration);
$parameterDocBlockTag = $this->tryGetDocBlockTagForParameter($docBlock, $variableName); $parameterDocBlockTag = $this->tryGetDocBlockTagForParameter($docBlock, $variableName);
if ($parameterDocBlockTag !== null && ($type = $parameterDocBlockTag->getType())) { if ($parameterDocBlockTag !== null && ($type = $parameterDocBlockTag->getType())) {
@ -1117,7 +1086,7 @@ class DefinitionResolver
// 3. TODO: infer from return statements // 3. TODO: infer from return statements
if ($node instanceof PhpParser\FunctionLike) { if ($node instanceof PhpParser\FunctionLike) {
// Functions/methods // Functions/methods
$docBlock = $this->getDocBlock($node); $docBlock = $this->cachingDocBlockFactory->getDocBlock($node);
if ( if (
$docBlock !== null $docBlock !== null
&& !empty($returnTags = $docBlock->getTagsByName('return')) && !empty($returnTags = $docBlock->getTagsByName('return'))
@ -1185,7 +1154,7 @@ class DefinitionResolver
// Property, constant or variable // Property, constant or variable
// Use @var tag // Use @var tag
if ( if (
($docBlock = $this->getDocBlock($declarationNode)) ($docBlock = $this->cachingDocBlockFactory->getDocBlock($declarationNode))
&& !empty($varTags = $docBlock->getTagsByName('var')) && !empty($varTags = $docBlock->getTagsByName('var'))
&& ($type = $varTags[0]->getType()) && ($type = $varTags[0]->getType())
) { ) {
@ -1302,7 +1271,7 @@ class DefinitionResolver
// namespace A\B; // namespace A\B;
// const FOO = 5; A\B\FOO // const FOO = 5; A\B\FOO
// class C { // class C {
// const $a, $b = 4 A\B\C::$a(), A\B\C::$b // const $a, $b = 4 A\B\C::$a, A\B\C::$b
// } // }
if (($constDeclaration = ParserHelpers\tryGetConstOrClassConstDeclaration($node)) !== null) { if (($constDeclaration = ParserHelpers\tryGetConstOrClassConstDeclaration($node)) !== null) {
if ($constDeclaration instanceof Node\Statement\ConstDeclaration) { if ($constDeclaration instanceof Node\Statement\ConstDeclaration) {

View File

@ -141,6 +141,10 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
$e $e
); );
} }
// When a request is processed, clear the caches of definition resolver as not to leak memory.
$this->definitionResolver->clearCache();
// Only send a Response for a Request // Only send a Response for a Request
// Notifications do not send Responses // Notifications do not send Responses
if (AdvancedJsonRpc\Request::isRequest($msg->body)) { if (AdvancedJsonRpc\Request::isRequest($msg->body)) {

View File

@ -164,6 +164,8 @@ class PhpDocument
} }
$this->sourceFileNode = $treeAnalyzer->getSourceFileNode(); $this->sourceFileNode = $treeAnalyzer->getSourceFileNode();
$this->definitionResolver->clearCache();
} }
/** /**