From 1e6bf904243ada3e251f80a0e07a464bd8491fc3 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Thu, 8 Dec 2016 13:05:40 +0100 Subject: [PATCH] Dont like this --- composer.json | 21 ++++- src/DefinitionResolver.php | 32 ++++--- src/DocumentLoader.php | 81 +++++++++++++++++ src/Index.php | 11 --- src/LanguageServer.php | 10 +-- src/PhpDocument.php | 102 +++++---------------- src/Project.php | 175 ++++++++++++++++++------------------ src/Server/TextDocument.php | 13 ++- src/parse_stubs.php | 26 ++++++ 9 files changed, 274 insertions(+), 197 deletions(-) create mode 100644 src/DocumentLoader.php create mode 100644 src/parse_stubs.php diff --git a/composer.json b/composer.json index 224d3d0..7d29d0a 100644 --- a/composer.json +++ b/composer.json @@ -32,8 +32,27 @@ "netresearch/jsonmapper": "^1.0", "webmozart/path-util": "^2.3", "webmozart/glob": "^4.1", - "sabre/uri": "^2.0" + "sabre/uri": "^2.0", + "JetBrains/phpstorm-stubs": "dev-master" }, + "repositories": [ + { + "type": "package", + "package": { + "name": "JetBrains/phpstorm-stubs", + "version": "dev-master", + "dist": { + "url": "https://github.com/JetBrains/phpstorm-stubs/archive/master.zip", + "type": "zip" + }, + "source": { + "url": "https://github.com/JetBrains/phpstorm-stubs", + "type": "git", + "reference": "master" + } + } + } + ], "minimum-stability": "dev", "prefer-stable": true, "autoload": { diff --git a/src/DefinitionResolver.php b/src/DefinitionResolver.php index 87c5aa0..7de3f6f 100644 --- a/src/DefinitionResolver.php +++ b/src/DefinitionResolver.php @@ -13,9 +13,9 @@ use function Sabre\Event\coroutine; class DefinitionResolver { /** - * @var \LanguageServer\Project + * @var \LanguageServer\Index[] */ - private $project; + private $indexes; /** * @var \phpDocumentor\Reflection\TypeResolver @@ -27,13 +27,25 @@ class DefinitionResolver */ private $prettyPrinter; - public function __construct(Project $project) + /** + * @param Index[] $indexes + */ + public function __construct(array $indexes) { - $this->project = $project; + $this->indexes = $indexes; $this->typeResolver = new TypeResolver; $this->prettyPrinter = new PrettyPrinter; } + private function getDefinition(string $fqn, bool $globalFallback = false) + { + foreach ($this->indexes as $index) { + if ($def = $index->getDefinition($fqn, $globalFallback)) { + return $def; + } + } + } + /** * Builds the declaration line for a given node * @@ -147,8 +159,8 @@ class DefinitionResolver // http://php.net/manual/en/language.namespaces.fallback.php $parent = $node->getAttribute('parentNode'); $globalFallback = $parent instanceof Node\Expr\ConstFetch || $parent instanceof Node\Expr\FuncCall; - // Return the Definition object from the project index - return $this->project->getDefinition($fqn, $globalFallback); + // Return the Definition object from the index index + return $this->getDefinition($fqn, $globalFallback); } /** @@ -403,7 +415,7 @@ class DefinitionResolver return new Types\Mixed; } $fqn = (string)($expr->getAttribute('namespacedName') ?? $expr->name); - $def = $this->project->getDefinition($fqn, true); + $def = $this->getDefinition($fqn, true); if ($def !== null) { return $def->type; } @@ -414,7 +426,7 @@ class DefinitionResolver } // Resolve constant $fqn = (string)($expr->getAttribute('namespacedName') ?? $expr->name); - $def = $this->project->getDefinition($fqn, true); + $def = $this->getDefinition($fqn, true); if ($def !== null) { return $def->type; } @@ -443,7 +455,7 @@ class DefinitionResolver if ($expr instanceof Node\Expr\MethodCall) { $fqn .= '()'; } - $def = $this->project->getDefinition($fqn); + $def = $this->getDefinition($fqn); if ($def !== null) { return $def->type; } @@ -466,7 +478,7 @@ class DefinitionResolver if ($expr instanceof Node\Expr\StaticCall) { $fqn .= '()'; } - $def = $this->project->getDefinition($fqn); + $def = $this->getDefinition($fqn); if ($def === null) { return new Types\Mixed; } diff --git a/src/DocumentLoader.php b/src/DocumentLoader.php new file mode 100644 index 0000000..fcf2226 --- /dev/null +++ b/src/DocumentLoader.php @@ -0,0 +1,81 @@ +contentRetriever = $contentRetriever; + } + + /** + * Loads a document + * + * @param string $uri + * @return Promise + */ + public function load(string $uri): Promise + { + return coroutine(function () use ($uri) { + + $limit = 150000; + $content = yield $this->contentRetriever->retrieve($uri); + $size = strlen($content); + if ($size > $limit) { + throw new ContentTooLargeException($uri, $size, $limit); + } + + /** The key for the index */ + $key = ''; + + // If the document is part of a dependency + if (preg_match($u['path'], '/vendor\/(\w+\/\w+)/', $matches)) { + if ($this->composerLockFiles === null) { + throw new \Exception('composer.lock files were not read yet'); + } + // Try to find closest composer.lock + $u = Uri\parse($uri); + $packageName = $matches[1]; + do { + $u['path'] = dirname($u['path']); + foreach ($this->composerLockFiles as $lockFileUri => $lockFileContent) { + $lockFileUri = Uri\parse($composerLockFile); + $lockFileUri['path'] = dirname($lockFileUri['path']); + if ($u == $lockFileUri) { + // Found it, find out package version + foreach ($lockFileContent->packages as $package) { + if ($package->name === $packageName) { + $key = $packageName . ':' . $package->version; + break; + } + } + break; + } + } + } while (!empty(trim($u, '/'))); + } + + // If there is no index for the key yet, create one + if (!isset($this->indexes[$key])) { + $this->indexes[$key] = new Index; + } + $index = $this->indexes[$key]; + + if (isset($this->documents[$uri])) { + $document = $this->documents[$uri]; + $document->updateContent($content); + } else { + $document = new PhpDocument( + $uri, + $content, + $index, + $this->parser, + $this->docBlockFactory, + $this->definitionResolver + ); + } + return $document; + }); + } +} diff --git a/src/Index.php b/src/Index.php index baa3a48..1bd4515 100644 --- a/src/Index.php +++ b/src/Index.php @@ -150,15 +150,4 @@ class Index { $this->references = $references; } - - /** - * Returns true if the given FQN is defined in the project - * - * @param string $fqn The fully qualified name of the symbol - * @return bool - */ - public function isDefined(string $fqn): bool - { - return isset($this->definitions[$fqn]); - } } diff --git a/src/LanguageServer.php b/src/LanguageServer.php index 3a7828c..660ec14 100644 --- a/src/LanguageServer.php +++ b/src/LanguageServer.php @@ -22,6 +22,7 @@ use function Sabre\Event\coroutine; use Exception; use Throwable; use Webmozart\PathUtil\Path; +use Webmozart\Glob\Glob; use Sabre\Uri; class LanguageServer extends AdvancedJsonRpc\Dispatcher @@ -92,7 +93,7 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher } catch (Throwable $e) { // If an unexpected error occured, send back an INTERNAL_ERROR error response $error = new AdvancedJsonRpc\Error( - $e->getMessage(), + (string)$e, AdvancedJsonRpc\ErrorCode::INTERNAL_ERROR, null, $e @@ -120,9 +121,9 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher * @param ClientCapabilities $capabilities The capabilities provided by the client (editor) * @param string|null $rootPath The rootPath of the workspace. Is null if no folder is open. * @param int|null $processId The process Id of the parent process that started the server. Is null if the process has not been started by another process. If the parent process is not alive then the server should exit (see exit notification) its process. - * @return InitializeResult + * @return Promise */ - public function initialize(ClientCapabilities $capabilities, string $rootPath = null, int $processId = null): InitializeResult + public function initialize(ClientCapabilities $capabilities, string $rootPath = null, int $processId = null): Promise { return coroutine(function () use ($capabilities, $rootPath, $processId) { @@ -140,11 +141,10 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher $this->contentRetriever = new FileSystemContentRetriever; } - // start building project index if ($rootPath !== null) { $pattern = Path::makeAbsolute('**/{*.php,composer.lock}', $this->rootPath); $composerLockPattern = Path::makeAbsolute('**/composer.lock}', $this->rootPath); - $uris = yield $this->findFiles($pattern); + $uris = yield $this->filesFinder->find($pattern); // Find composer.lock files $composerLockFiles = []; diff --git a/src/PhpDocument.php b/src/PhpDocument.php index e9263dd..f5901d0 100644 --- a/src/PhpDocument.php +++ b/src/PhpDocument.php @@ -22,22 +22,6 @@ use Sabre\Uri; class PhpDocument { - /** - * The LanguageClient instance (to report errors etc) - * - * @var LanguageClient - */ - private $client; - - /** - * The Project this document belongs to (to register definitions etc) - * - * @var Project - */ - public $project; - // for whatever reason I get "cannot access private property" error if $project is not public - // https://github.com/felixfbecker/php-language-server/pull/49#issuecomment-252427359 - /** * The PHPParser instance * @@ -101,28 +85,29 @@ class PhpDocument */ private $referenceNodes; + /** + * Diagnostics for this document that were collected while parsing + * + * @var Diagnostic[] + */ + private $diagnostics; + /** * @param string $uri The URI of the document * @param string $content The content of the document - * @param Project $project The Project this document belongs to (to load other documents) * @param Index $index The Index to register definitions etc - * @param LanguageClient $client The LanguageClient instance (to report errors etc) * @param Parser $parser The PHPParser instance * @param DocBlockFactory $docBlockFactory The DocBlockFactory instance to parse docblocks */ public function __construct( string $uri, string $content, - Project $project, Index $index, - LanguageClient $client, Parser $parser, DocBlockFactory $docBlockFactory, DefinitionResolver $definitionResolver ) { $this->uri = $uri; - $this->project = $project; - $this->client = $client; $this->parser = $parser; $this->docBlockFactory = $docBlockFactory; $this->definitionResolver = $definitionResolver; @@ -156,9 +141,9 @@ class PhpDocument $errorHandler = new ErrorHandler\Collecting; $stmts = $this->parser->parse($content, $errorHandler); - $diagnostics = []; + $this->diagnostics = []; foreach ($errorHandler->getErrors() as $error) { - $diagnostics[] = Diagnostic::fromError($error, $this->content, DiagnosticSeverity::ERROR, 'php'); + $this->diagnostics[] = Diagnostic::fromError($error, $this->content, DiagnosticSeverity::ERROR, 'php'); } // $stmts can be null in case of a fatal parsing error @@ -182,7 +167,7 @@ class PhpDocument // Report errors from parsing docblocks foreach ($docBlockParser->errors as $error) { - $diagnostics[] = Diagnostic::fromError($error, $this->content, DiagnosticSeverity::WARNING, 'php'); + $this->diagnostics[] = Diagnostic::fromError($error, $this->content, DiagnosticSeverity::WARNING, 'php'); } $traverser = new NodeTraverser; @@ -224,10 +209,6 @@ class PhpDocument $this->stmts = $stmts; } - - if (!$this->isVendored()) { - $this->client->textDocument->publishDiagnostics($this->uri, $diagnostics); - } } /** @@ -264,6 +245,16 @@ class PhpDocument return $this->content; } + /** + * Returns this document's diagnostics + * + * @return Diagnostic[] + */ + public function getContent() + { + return $this->diagnostics; + } + /** * Returns the URI of the document * @@ -359,57 +350,4 @@ class PhpDocument { return isset($this->definitions[$fqn]); } - - /** - * Returns the reference nodes for any node - * The references node MAY be in other documents, check the ownerDocument attribute - * - * @param Node $node - * @return Promise - */ - public function getReferenceNodesByNode(Node $node): Promise - { - return coroutine(function () use ($node) { - // Variables always stay in the boundary of the file and need to be searched inside their function scope - // by traversing the AST - if ( - $node instanceof Node\Expr\Variable - || $node instanceof Node\Param - || $node instanceof Node\Expr\ClosureUse - ) { - if ($node->name instanceof Node\Expr) { - return null; - } - // Find function/method/closure scope - $n = $node; - while (isset($n) && !($n instanceof Node\FunctionLike)) { - $n = $n->getAttribute('parentNode'); - } - if (!isset($n)) { - $n = $node->getAttribute('ownerDocument'); - } - $traverser = new NodeTraverser; - $refCollector = new VariableReferencesCollector($node->name); - $traverser->addVisitor($refCollector); - $traverser->traverse($n->getStmts()); - return $refCollector->nodes; - } - // Definition with a global FQN - $fqn = DefinitionResolver::getDefinedFqn($node); - if ($fqn === null) { - return []; - } - $refDocuments = yield $this->project->getReferenceDocuments($fqn); - $nodes = []; - foreach ($refDocuments as $document) { - $refs = $document->getReferenceNodesByFqn($fqn); - if ($refs !== null) { - foreach ($refs as $ref) { - $nodes[] = $ref; - } - } - } - return $nodes; - }); - } } diff --git a/src/Project.php b/src/Project.php index 5217c76..54bf47f 100644 --- a/src/Project.php +++ b/src/Project.php @@ -21,11 +21,24 @@ class Project /** * Associative array from package identifier to index - * The empty string represents the project itself * * @var Index[] */ - private $indexes = []; + private $dependencyIndexes = []; + + /** + * The Index for the project itself + * + * @var Index + */ + private $sourceIndex; + + /** + * The Index for PHP built-ins + * + * @var Index + */ + private $stubIndex; /** * An associative array that maps fully qualified symbol names to Definitions @@ -84,23 +97,24 @@ class Project * @param LanguageClient $client Used for logging and reporting diagnostics * @param ClientCapabilities $clientCapabilities Used for determining the right content/find strategies * @param string|null $rootPath Used for finding files in the project - * @param string[] $composerLockFiles An array of URIs of composer.lock files in the project + * @param string $composerLockFiles An array of URI => parsed composer.lock JSON */ public function __construct( LanguageClient $client, ClientCapabilities $clientCapabilities, array $composerLockFiles, + DefinitionResolver $definitionResolver, string $rootPath = null ) { $this->client = $client; $this->rootPath = $rootPath; $this->parser = new Parser; $this->docBlockFactory = DocBlockFactory::createInstance(); - $this->definitionResolver = new DefinitionResolver($this); + $this->definitionResolver = $definitionResolver; $this->contentRetriever = $contentRetriever; $this->composerLockFiles = $composerLockFiles; // The index for the project itself - $this->indexes[''] = new Index; + $this->projectIndex = new Index; } /** @@ -127,80 +141,6 @@ class Project return isset($this->documents[$uri]) ? Promise\resolve($this->documents[$uri]) : $this->loadDocument($uri); } - /** - * Loads the document by doing a textDocument/xcontent request to the client. - * If the client does not support textDocument/xcontent, tries to read the file from the file system. - * The document is NOT added to the list of open documents, but definitions are registered. - * - * @param string $uri - * @return Promise - */ - public function loadDocument(string $uri): Promise - { - return coroutine(function () use ($uri) { - - $limit = 150000; - $content = yield $this->contentRetriever->retrieve($uri); - $size = strlen($content); - if ($size > $limit) { - throw new ContentTooLargeException($uri, $size, $limit); - } - - /** The key for the index */ - $key = ''; - - // If the document is part of a dependency - if (preg_match($u['path'], '/vendor\/(\w+\/\w+)/', $matches)) { - if ($this->composerLockFiles === null) { - throw new \Exception('composer.lock files were not read yet'); - } - // Try to find closest composer.lock - $u = Uri\parse($uri); - $packageName = $matches[1]; - do { - $u['path'] = dirname($u['path']); - foreach ($this->composerLockFiles as $lockFileUri => $lockFileContent) { - $lockFileUri = Uri\parse($composerLockFile); - $lockFileUri['path'] = dirname($lockFileUri['path']); - if ($u == $lockFileUri) { - // Found it, find out package version - foreach ($lockFileContent->packages as $package) { - if ($package->name === $packageName) { - $key = $packageName . ':' . $package->version; - break; - } - } - break; - } - } - } while (!empty(trim($u, '/'))); - } - - // If there is no index for the key yet, create one - if (!isset($this->indexes[$key])) { - $this->indexes[$key] = new Index; - } - $index = $this->indexes[$key]; - - if (isset($this->documents[$uri])) { - $document = $this->documents[$uri]; - $document->updateContent($content); - } else { - $document = new PhpDocument( - $uri, - $content, - $this, - $index, - $this->client, - $this->parser, - $this->docBlockFactory, - $this->definitionResolver - ); - } - return $document; - }); - } - /** * Ensures a document is loaded and added to the list of open documents. * @@ -217,8 +157,6 @@ class Project $document = new PhpDocument( $uri, $content, - $this, - $this->client, $this->parser, $this->docBlockFactory, $this->definitionResolver @@ -258,7 +196,19 @@ class Project */ public function getDefinitions() { - return $this->definitions; + $defs = []; + foreach ($this->sourceIndex->getDefinitions() as $def) { + $defs[] = $def; + } + foreach ($this->dependenciesIndexes as $dependencyIndex) { + foreach ($dependencyIndex->getDefinitions() as $def) { + $defs[] = $def; + } + } + foreach ($this->stubIndex->getDefinitions() as $def) { + $defs[] = $def; + } + return $defs; } /** @@ -270,9 +220,12 @@ class Project */ public function getDefinition(string $fqn, $globalFallback = false) { - if (isset($this->definitions[$fqn])) { - return $this->definitions[$fqn]; - } else if ($globalFallback) { + foreach (array_merge([$this->sourceIndex, $this->stubsIndex], ...$this->dependencyIndexes) as $index) { + if ($index->isDefined($fqn)) { + return $index->getDefinition($fqn); + } + } + if ($globalFallback) { $parts = explode('\\', $fqn); $fqn = end($parts); return $this->getDefinition($fqn); @@ -411,4 +364,56 @@ class Project { return isset($this->definitions[$fqn]); } + + /** + * Returns the reference nodes for any node + * + * @param Node $node + * @return Promise + */ + public function getReferenceNodesByNode(Node $node): Promise + { + return coroutine(function () use ($node) { + // Variables always stay in the boundary of the file and need to be searched inside their function scope + // by traversing the AST + if ( + $node instanceof Node\Expr\Variable + || $node instanceof Node\Param + || $node instanceof Node\Expr\ClosureUse + ) { + if ($node->name instanceof Node\Expr) { + return null; + } + // Find function/method/closure scope + $n = $node; + while (isset($n) && !($n instanceof Node\FunctionLike)) { + $n = $n->getAttribute('parentNode'); + } + if (!isset($n)) { + $n = $node->getAttribute('ownerDocument'); + } + $traverser = new NodeTraverser; + $refCollector = new VariableReferencesCollector($node->name); + $traverser->addVisitor($refCollector); + $traverser->traverse($n->getStmts()); + return $refCollector->nodes; + } + // Definition with a global FQN + $fqn = DefinitionResolver::getDefinedFqn($node); + if ($fqn === null) { + return []; + } + $refDocuments = yield $this->getReferenceDocuments($fqn); + $nodes = []; + foreach ($refDocuments as $document) { + $refs = $document->getReferenceNodesByFqn($fqn); + if ($refs !== null) { + foreach ($refs as $ref) { + $nodes[] = $ref; + } + } + } + return $nodes; + }); + } } diff --git a/src/Server/TextDocument.php b/src/Server/TextDocument.php index 5998acc..6d424f8 100644 --- a/src/Server/TextDocument.php +++ b/src/Server/TextDocument.php @@ -58,12 +58,14 @@ class TextDocument */ private $completionProvider; + private $openDocuments = []; + public function __construct(Project $project, LanguageClient $client) { $this->project = $project; $this->client = $client; $this->prettyPrinter = new PrettyPrinter(); - $this->definitionResolver = new DefinitionResolver($project); + $this->definitionResolver = new DefinitionResolver(); $this->completionProvider = new CompletionProvider($this->definitionResolver, $project); } @@ -95,7 +97,10 @@ class TextDocument */ public function didOpen(TextDocumentItem $textDocument) { - $this->project->openDocument($textDocument->uri, $textDocument->text); + $document = $this->project->openDocument($textDocument->uri, $textDocument->text); + if (!$document->isVendored()) { + $this->client->textDocument->publishDiagnostics($uri, $document->getDiagnostics()); + } } /** @@ -107,7 +112,9 @@ class TextDocument */ public function didChange(VersionedTextDocumentIdentifier $textDocument, array $contentChanges) { - $this->project->getDocument($textDocument->uri)->updateContent($contentChanges[0]->text); + $document = $this->project->getDocument($textDocument->uri); + $document->updateContent($contentChanges[0]->text); + $this->client->publishDiagnostics($document->getDiagnostics()); } /** diff --git a/src/parse_stubs.php b/src/parse_stubs.php new file mode 100644 index 0000000..2e8955a --- /dev/null +++ b/src/parse_stubs.php @@ -0,0 +1,26 @@ +find(__DIR__ . '/../vendor/JetBrains/phpstorm-stubs'); + + foreach ($uris as $uri) { + $content = $contentRetriever->retrieve($uri); + $document = new PhpDocument($uri, $content, $index, $docBlockFactory, $definitionResolver); + } + +})->wait();