diff --git a/src/Index/ProjectIndex.php b/src/Index/ProjectIndex.php index 8adc938..af980f8 100644 --- a/src/Index/ProjectIndex.php +++ b/src/Index/ProjectIndex.php @@ -3,6 +3,8 @@ declare(strict_types = 1); namespace LanguageServer\Index; +use function LanguageServer\getPackageName; + /** * A project index manages the source and dependency indexes */ @@ -22,10 +24,11 @@ class ProjectIndex extends AbstractAggregateIndex */ private $sourceIndex; - public function __construct(Index $sourceIndex, DependenciesIndex $dependenciesIndex) + public function __construct(Index $sourceIndex, DependenciesIndex $dependenciesIndex, \stdClass $composerJson = null) { $this->sourceIndex = $sourceIndex; $this->dependenciesIndex = $dependenciesIndex; + $this->composerJson = $composerJson; parent::__construct(); } @@ -43,8 +46,8 @@ class ProjectIndex extends AbstractAggregateIndex */ public function getIndexForUri(string $uri): Index { - if (preg_match('/\/vendor\/([^\/]+\/[^\/]+)\//', $uri, $matches)) { - $packageName = $matches[1]; + $packageName = getPackageName($uri, $this->composerJson); + if ($packageName) { return $this->dependenciesIndex->getDependencyIndex($packageName); } return $this->sourceIndex; diff --git a/src/Indexer.php b/src/Indexer.php index df9655a..34ad618 100644 --- a/src/Indexer.php +++ b/src/Indexer.php @@ -59,6 +59,11 @@ class Indexer */ private $composerLock; + /** + * @var \stdClasss + */ + private $composerJson; + /** * @param FilesFinder $filesFinder * @param string $rootPath @@ -77,7 +82,8 @@ class Indexer DependenciesIndex $dependenciesIndex, Index $sourceIndex, PhpDocumentLoader $documentLoader, - \stdClass $composerLock = null + \stdClass $composerLock = null, + \stdClass $composerJson = null ) { $this->filesFinder = $filesFinder; $this->rootPath = $rootPath; @@ -87,6 +93,7 @@ class Indexer $this->sourceIndex = $sourceIndex; $this->documentLoader = $documentLoader; $this->composerLock = $composerLock; + $this->composerJson = $composerJson; } /** @@ -109,10 +116,11 @@ class Indexer $source = []; /** @var string[][] */ $deps = []; + foreach ($uris as $uri) { - if ($this->composerLock !== null && preg_match('/\/vendor\/([^\/]+\/[^\/]+)\//', $uri, $matches)) { + $packageName = getPackageName($uri, $this->composerJson); + if ($this->composerLock !== null && $packageName) { // Dependency file - $packageName = $matches[1]; if (!isset($deps[$packageName])) { $deps[$packageName] = []; } @@ -174,6 +182,8 @@ class Indexer if ($cacheKey !== null) { $this->client->window->logMessage(MessageType::INFO, "Storing $packageKey in cache"); $this->cache->set($cacheKey, $index); + } else { + $this->client->window->logMessage(MessageType::WARNING, "Could not compute cache key for $packageName"); } } } @@ -205,7 +215,7 @@ class Indexer $this->client->window->logMessage(MessageType::LOG, "Parsing $uri"); try { $document = yield $this->documentLoader->load($uri); - if (!$document->isVendored()) { + if (!isVendored($document, $this->composerJson)) { $this->client->textDocument->publishDiagnostics($uri, $document->getDiagnostics()); } } catch (ContentTooLargeException $e) { diff --git a/src/LanguageServer.php b/src/LanguageServer.php index 035e663..3c999f2 100644 --- a/src/LanguageServer.php +++ b/src/LanguageServer.php @@ -187,7 +187,7 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher $dependenciesIndex = new DependenciesIndex; $sourceIndex = new Index; - $this->projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex); + $this->projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex, $this->composerJson); $stubsIndex = StubsIndex::read(); $this->globalIndex = new GlobalIndex($stubsIndex, $this->projectIndex); @@ -206,6 +206,8 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher // Find composer.json if ($this->composerJson === null) { $composerJsonFiles = yield $this->filesFinder->find(Path::makeAbsolute('**/composer.json', $rootPath)); + sortUrisLevelOrder($composerJsonFiles); + if (!empty($composerJsonFiles)) { $this->composerJson = json_decode(yield $this->contentRetriever->retrieve($composerJsonFiles[0])); } @@ -214,6 +216,8 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher // Find composer.lock if ($this->composerLock === null) { $composerLockFiles = yield $this->filesFinder->find(Path::makeAbsolute('**/composer.lock', $rootPath)); + sortUrisLevelOrder($composerLockFiles); + if (!empty($composerLockFiles)) { $this->composerLock = json_decode(yield $this->contentRetriever->retrieve($composerLockFiles[0])); } @@ -230,7 +234,8 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher $dependenciesIndex, $sourceIndex, $this->documentLoader, - $this->composerLock + $this->composerLock, + $this->composerJson ); $indexer->index()->otherwise('\\LanguageServer\\crash'); } @@ -252,7 +257,8 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher $dependenciesIndex, $sourceIndex, $this->composerLock, - $this->documentLoader + $this->documentLoader, + $this->composerJson ); } diff --git a/src/PhpDocument.php b/src/PhpDocument.php index b838cd3..3a25c23 100644 --- a/src/PhpDocument.php +++ b/src/PhpDocument.php @@ -220,17 +220,6 @@ class PhpDocument } } - /** - * Returns true if the document is a dependency - * - * @return bool - */ - public function isVendored(): bool - { - $path = Uri\parse($this->uri)['path']; - return strpos($path, '/vendor/') !== false; - } - /** * Returns array of TextEdit changes to format this document. * diff --git a/src/Server/TextDocument.php b/src/Server/TextDocument.php index 624bc67..aa76ec2 100644 --- a/src/Server/TextDocument.php +++ b/src/Server/TextDocument.php @@ -30,7 +30,7 @@ use LanguageServer\Index\ReadableIndex; use Sabre\Event\Promise; use Sabre\Uri; use function Sabre\Event\coroutine; -use function LanguageServer\waitForEvent; +use function LanguageServer\{waitForEvent, isVendored}; /** * Provides method handlers for all textDocument/* methods @@ -134,7 +134,7 @@ class TextDocument public function didOpen(TextDocumentItem $textDocument) { $document = $this->documentLoader->open($textDocument->uri, $textDocument->text); - if (!$document->isVendored()) { + if (!isVendored($document, $this->composerJson)) { $this->client->textDocument->publishDiagnostics($textDocument->uri, $document->getDiagnostics()); } } @@ -409,9 +409,9 @@ class TextDocument $symbol->$prop = $val; } $symbol->fqsen = $def->fqn; - if (preg_match('/\/vendor\/([^\/]+\/[^\/]+)\//', $def->symbolInformation->location->uri, $matches) && $this->composerLock !== null) { + $packageName = getPackageName($def->symbolInformation->location->uri, $this->composerJson); + if ($packageName && $this->composerLock !== null) { // Definition is inside a dependency - $packageName = $matches[1]; foreach (array_merge($this->composerLock->packages, $this->composerLock->{'packages-dev'}) as $package) { if ($package->name === $packageName) { $symbol->package = $package; diff --git a/src/Server/Workspace.php b/src/Server/Workspace.php index 5c0e9fa..b94618c 100644 --- a/src/Server/Workspace.php +++ b/src/Server/Workspace.php @@ -8,7 +8,7 @@ use LanguageServer\Index\{ProjectIndex, DependenciesIndex, Index}; use LanguageServer\Protocol\{SymbolInformation, SymbolDescriptor, ReferenceInformation, DependencyReference, Location}; use Sabre\Event\Promise; use function Sabre\Event\coroutine; -use function LanguageServer\waitForEvent; +use function LanguageServer\{waitForEvent, getPackageName}; /** * Provides method handlers for all workspace/* methods @@ -49,13 +49,14 @@ class Workspace * @param \stdClass $composerLock The parsed composer.lock of the project, if any * @param PhpDocumentLoader $documentLoader PhpDocumentLoader instance to load documents */ - public function __construct(ProjectIndex $index, DependenciesIndex $dependenciesIndex, Index $sourceIndex, \stdClass $composerLock = null, PhpDocumentLoader $documentLoader) + public function __construct(ProjectIndex $index, DependenciesIndex $dependenciesIndex, Index $sourceIndex, \stdClass $composerLock = null, PhpDocumentLoader $documentLoader, \stdClass $composerJson = null) { $this->sourceIndex = $sourceIndex; $this->index = $index; $this->dependenciesIndex = $dependenciesIndex; $this->composerLock = $composerLock; $this->documentLoader = $documentLoader; + $this->composerJson = $composerJson; } /** @@ -122,8 +123,7 @@ class Workspace $symbol->$prop = $val; } // Find out package name - preg_match('/\/vendor\/([^\/]+\/[^\/]+)\//', $def->symbolInformation->location->uri, $matches); - $packageName = $matches[1]; + $packageName = getPackageName($def->symbolInformation->location->uri, $this->composerJson); foreach (array_merge($this->composerLock->packages, $this->composerLock->{'packages-dev'}) as $package) { if ($package->name === $packageName) { $symbol->package = $package; diff --git a/src/utils.php b/src/utils.php index 8ab7beb..c0c5bf7 100644 --- a/src/utils.php +++ b/src/utils.php @@ -7,6 +7,7 @@ use Throwable; use InvalidArgumentException; use PhpParser\Node; use Sabre\Event\{Loop, Promise, EmitterInterface}; +use Sabre\Uri; /** * Transforms an absolute file path into a URI as used by the language server protocol. @@ -131,3 +132,60 @@ function stripStringOverlap(string $a, string $b): string } return $b; } + +/** + * Use for sorting an array of URIs by number of segments + * in ascending order. + * + * @param array $uriList + * @return void + */ +function sortUrisLevelOrder(&$uriList) +{ + usort($uriList, function ($a, $b) { + return substr_count(Uri\parse($a)['path'], '/') - substr_count(Uri\parse($b)['path'], '/'); + }); +} + +/** + * Checks a document against the composer.json to see if it + * is a vendored document + * + * @param PhpDocument $document + * @param \stdClass|null $composerJson + * @return bool + */ +function isVendored(PhpDocument $document, \stdClass $composerJson = null): bool +{ + $path = Uri\parse($document->getUri())['path']; + $vendorDir = getVendorDir($composerJson); + return strpos($path, "/$vendorDir/") !== false; +} + +/** + * Check a given URI against the composer.json to see if it + * is a vendored URI + * + * @param \stdClass|null $composerJson + * @param string $uri + * @param array $matches + * @return string|null + */ +function getPackageName(string $uri, \stdClass $composerJson = null) +{ + $vendorDir = str_replace('/', '\/', getVendorDir($composerJson)); + preg_match("/\/$vendorDir\/([^\/]+\/[^\/]+)\//", $uri, $matches); + return $matches[1] ?? null; +} + +/** + * Helper function to get the vendor directory from composer.json + * or default to 'vendor' + * + * @param \stdClass|null $composerJson + * @return string + */ +function getVendorDir(\stdClass $composerJson = null): string +{ + return $composerJson->config->{'vendor-dir'} ?? 'vendor'; +} diff --git a/tests/PhpDocumentTest.php b/tests/PhpDocumentTest.php index b9b3704..011a231 100644 --- a/tests/PhpDocumentTest.php +++ b/tests/PhpDocumentTest.php @@ -12,6 +12,7 @@ use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\Protocol\{SymbolKind, Position, ClientCapabilities}; use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex}; use PhpParser\Node; +use function LanguageServer\isVendored; class PhpDocumentTest extends TestCase { @@ -42,18 +43,18 @@ class PhpDocumentTest extends TestCase public function testIsVendored() { $document = $this->createDocument('file:///dir/vendor/x.php', "assertEquals(true, $document->isVendored()); + $this->assertEquals(true, isVendored($document)); $document = $this->createDocument('file:///c:/dir/vendor/x.php', "assertEquals(true, $document->isVendored()); + $this->assertEquals(true, isVendored($document)); $document = $this->createDocument('file:///vendor/x.php', "assertEquals(true, $document->isVendored()); + $this->assertEquals(true, isVendored($document)); $document = $this->createDocument('file:///dir/vendor.php', "assertEquals(false, $document->isVendored()); + $this->assertEquals(false, isVendored($document)); $document = $this->createDocument('file:///dir/x.php', "assertEquals(false, $document->isVendored()); + $this->assertEquals(false, isVendored($document)); } }