perf(parsing): cache docblocks
parent
a8f60c9cf6
commit
9cbd00cb7b
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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) {
|
||||||
|
|
|
@ -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)) {
|
||||||
|
|
|
@ -164,6 +164,8 @@ class PhpDocument
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->sourceFileNode = $treeAnalyzer->getSourceFileNode();
|
$this->sourceFileNode = $treeAnalyzer->getSourceFileNode();
|
||||||
|
|
||||||
|
$this->definitionResolver->clearCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in New Issue