diff --git a/src/CachingDocBlockFactory.php b/src/CachingDocBlockFactory.php new file mode 100644 index 0000000..ee69320 --- /dev/null +++ b/src/CachingDocBlockFactory.php @@ -0,0 +1,72 @@ +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; + } + } +} diff --git a/src/DefinitionResolver.php b/src/DefinitionResolver.php index adddf77..78f90fa 100644 --- a/src/DefinitionResolver.php +++ b/src/DefinitionResolver.php @@ -9,9 +9,7 @@ use LanguageServerProtocol\SymbolInformation; use Microsoft\PhpParser; use Microsoft\PhpParser\Node; use Microsoft\PhpParser\FunctionLike; -use phpDocumentor\Reflection\{ - DocBlock, DocBlockFactory, Fqsen, Type, TypeResolver, Types -}; +use phpDocumentor\Reflection\{DocBlock, Fqsen, Type, TypeResolver, Types}; class DefinitionResolver { @@ -30,11 +28,11 @@ class DefinitionResolver 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 instances @@ -50,7 +48,7 @@ class DefinitionResolver { $this->index = $index; $this->typeResolver = new TypeResolver; - $this->docBlockFactory = DocBlockFactory::createInstance(); + $this->cachingDocBlockFactory = new CachingDocBlockFactory; $this->signatureInformationFactory = new SignatureInformationFactory($this); } @@ -115,14 +113,14 @@ class DefinitionResolver $variableName = $node->getName(); $functionLikeDeclaration = ParserHelpers\getFunctionLikeDeclarationFromParameter($node); - $docBlock = $this->getDocBlock($functionLikeDeclaration); + $docBlock = $this->cachingDocBlockFactory->getDocBlock($functionLikeDeclaration); $parameterDocBlockTag = $this->tryGetDocBlockTagForParameter($docBlock, $variableName); return $parameterDocBlockTag !== null ? $parameterDocBlockTag->getDescription()->render() : null; } // 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) { // check whether we have a description, when true, add a new paragraph // with the description @@ -137,40 +135,6 @@ class DefinitionResolver 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 * @@ -347,6 +311,11 @@ class DefinitionResolver return null; } + public function clearCache() + { + $this->cachingDocBlockFactory->clearCache(); + } + private function resolveQualifiedNameNodeToFqn(Node\QualifiedName $node) { $parent = $node->parent; @@ -1087,7 +1056,7 @@ class DefinitionResolver // function foo($a) $functionLikeDeclaration = ParserHelpers\getFunctionLikeDeclarationFromParameter($node); $variableName = $node->getName(); - $docBlock = $this->getDocBlock($functionLikeDeclaration); + $docBlock = $this->cachingDocBlockFactory->getDocBlock($functionLikeDeclaration); $parameterDocBlockTag = $this->tryGetDocBlockTagForParameter($docBlock, $variableName); if ($parameterDocBlockTag !== null && ($type = $parameterDocBlockTag->getType())) { @@ -1124,7 +1093,7 @@ class DefinitionResolver // 3. TODO: infer from return statements if ($node instanceof PhpParser\FunctionLike) { // Functions/methods - $docBlock = $this->getDocBlock($node); + $docBlock = $this->cachingDocBlockFactory->getDocBlock($node); if ( $docBlock !== null && !empty($returnTags = $docBlock->getTagsByName('return')) @@ -1192,7 +1161,7 @@ class DefinitionResolver // Property, constant or variable // Use @var tag if ( - ($docBlock = $this->getDocBlock($declarationNode)) + ($docBlock = $this->cachingDocBlockFactory->getDocBlock($declarationNode)) && !empty($varTags = $docBlock->getTagsByName('var')) && ($type = $varTags[0]->getType()) ) { @@ -1315,7 +1284,7 @@ class DefinitionResolver // namespace A\B; // const FOO = 5; A\B\FOO // 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 instanceof Node\Statement\ConstDeclaration) { diff --git a/src/LanguageServer.php b/src/LanguageServer.php index 38dfeb1..d059567 100644 --- a/src/LanguageServer.php +++ b/src/LanguageServer.php @@ -141,6 +141,10 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher $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 // Notifications do not send Responses if (AdvancedJsonRpc\Request::isRequest($msg->body)) { diff --git a/src/PhpDocument.php b/src/PhpDocument.php index d148f9c..4212d92 100644 --- a/src/PhpDocument.php +++ b/src/PhpDocument.php @@ -166,6 +166,8 @@ class PhpDocument } $this->sourceFileNode = $treeAnalyzer->getSourceFileNode(); + + $this->definitionResolver->clearCache(); } /**