diff --git a/src/CachingDocBlockFactory.php b/src/CachingDocBlockFactory.php new file mode 100644 index 00000000..ee693207 --- /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 990c1965..145c6d7f 100644 --- a/src/DefinitionResolver.php +++ b/src/DefinitionResolver.php @@ -8,9 +8,7 @@ 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 { @@ -29,11 +27,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 @@ -49,7 +47,7 @@ public function __construct(ReadableIndex $index) { $this->index = $index; $this->typeResolver = new TypeResolver; - $this->docBlockFactory = DocBlockFactory::createInstance(); + $this->cachingDocBlockFactory = new CachingDocBlockFactory; $this->signatureInformationFactory = new SignatureInformationFactory($this); } @@ -114,14 +112,14 @@ public function getDocumentationFromNode($node) $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 @@ -136,40 +134,6 @@ public function getDocumentationFromNode($node) 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 * @@ -346,6 +310,11 @@ public function resolveReferenceNodeToFqn(Node $node) return null; } + public function clearCache() + { + $this->cachingDocBlockFactory->clearCache(); + } + private function resolveQualifiedNameNodeToFqn(Node\QualifiedName $node) { $parent = $node->parent; @@ -1080,7 +1049,7 @@ public function getTypeFromNode($node) // 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())) { @@ -1117,7 +1086,7 @@ public function getTypeFromNode($node) // 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')) @@ -1185,7 +1154,7 @@ public function getTypeFromNode($node) // 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()) ) { @@ -1302,7 +1271,7 @@ public static function getDefinedFqn($node) // 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 46281f51..48b9b86e 100644 --- a/src/LanguageServer.php +++ b/src/LanguageServer.php @@ -141,6 +141,10 @@ public function __construct(ProtocolReader $reader, ProtocolWriter $writer) $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 0ac84210..d8118dc1 100644 --- a/src/PhpDocument.php +++ b/src/PhpDocument.php @@ -164,6 +164,8 @@ public function updateContent(string $content) } $this->sourceFileNode = $treeAnalyzer->getSourceFileNode(); + + $this->definitionResolver->clearCache(); } /**