1
0
Fork 0

Read vendor directory from project's composer.json, if set. (#281)

pull/292/head
Trevor Bortins 2017-02-07 14:20:12 -08:00 committed by Felix Becker
parent 571b26a0c3
commit d5c54ac30f
8 changed files with 101 additions and 34 deletions

View File

@ -3,6 +3,8 @@ declare(strict_types = 1);
namespace LanguageServer\Index; namespace LanguageServer\Index;
use function LanguageServer\getPackageName;
/** /**
* A project index manages the source and dependency indexes * A project index manages the source and dependency indexes
*/ */
@ -22,10 +24,11 @@ class ProjectIndex extends AbstractAggregateIndex
*/ */
private $sourceIndex; private $sourceIndex;
public function __construct(Index $sourceIndex, DependenciesIndex $dependenciesIndex) public function __construct(Index $sourceIndex, DependenciesIndex $dependenciesIndex, \stdClass $composerJson = null)
{ {
$this->sourceIndex = $sourceIndex; $this->sourceIndex = $sourceIndex;
$this->dependenciesIndex = $dependenciesIndex; $this->dependenciesIndex = $dependenciesIndex;
$this->composerJson = $composerJson;
parent::__construct(); parent::__construct();
} }
@ -43,8 +46,8 @@ class ProjectIndex extends AbstractAggregateIndex
*/ */
public function getIndexForUri(string $uri): Index public function getIndexForUri(string $uri): Index
{ {
if (preg_match('/\/vendor\/([^\/]+\/[^\/]+)\//', $uri, $matches)) { $packageName = getPackageName($uri, $this->composerJson);
$packageName = $matches[1]; if ($packageName) {
return $this->dependenciesIndex->getDependencyIndex($packageName); return $this->dependenciesIndex->getDependencyIndex($packageName);
} }
return $this->sourceIndex; return $this->sourceIndex;

View File

@ -59,6 +59,11 @@ class Indexer
*/ */
private $composerLock; private $composerLock;
/**
* @var \stdClasss
*/
private $composerJson;
/** /**
* @param FilesFinder $filesFinder * @param FilesFinder $filesFinder
* @param string $rootPath * @param string $rootPath
@ -77,7 +82,8 @@ class Indexer
DependenciesIndex $dependenciesIndex, DependenciesIndex $dependenciesIndex,
Index $sourceIndex, Index $sourceIndex,
PhpDocumentLoader $documentLoader, PhpDocumentLoader $documentLoader,
\stdClass $composerLock = null \stdClass $composerLock = null,
\stdClass $composerJson = null
) { ) {
$this->filesFinder = $filesFinder; $this->filesFinder = $filesFinder;
$this->rootPath = $rootPath; $this->rootPath = $rootPath;
@ -87,6 +93,7 @@ class Indexer
$this->sourceIndex = $sourceIndex; $this->sourceIndex = $sourceIndex;
$this->documentLoader = $documentLoader; $this->documentLoader = $documentLoader;
$this->composerLock = $composerLock; $this->composerLock = $composerLock;
$this->composerJson = $composerJson;
} }
/** /**
@ -109,10 +116,11 @@ class Indexer
$source = []; $source = [];
/** @var string[][] */ /** @var string[][] */
$deps = []; $deps = [];
foreach ($uris as $uri) { 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 // Dependency file
$packageName = $matches[1];
if (!isset($deps[$packageName])) { if (!isset($deps[$packageName])) {
$deps[$packageName] = []; $deps[$packageName] = [];
} }
@ -174,6 +182,8 @@ class Indexer
if ($cacheKey !== null) { if ($cacheKey !== null) {
$this->client->window->logMessage(MessageType::INFO, "Storing $packageKey in cache"); $this->client->window->logMessage(MessageType::INFO, "Storing $packageKey in cache");
$this->cache->set($cacheKey, $index); $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"); $this->client->window->logMessage(MessageType::LOG, "Parsing $uri");
try { try {
$document = yield $this->documentLoader->load($uri); $document = yield $this->documentLoader->load($uri);
if (!$document->isVendored()) { if (!isVendored($document, $this->composerJson)) {
$this->client->textDocument->publishDiagnostics($uri, $document->getDiagnostics()); $this->client->textDocument->publishDiagnostics($uri, $document->getDiagnostics());
} }
} catch (ContentTooLargeException $e) { } catch (ContentTooLargeException $e) {

View File

@ -187,7 +187,7 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
$dependenciesIndex = new DependenciesIndex; $dependenciesIndex = new DependenciesIndex;
$sourceIndex = new Index; $sourceIndex = new Index;
$this->projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex); $this->projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex, $this->composerJson);
$stubsIndex = StubsIndex::read(); $stubsIndex = StubsIndex::read();
$this->globalIndex = new GlobalIndex($stubsIndex, $this->projectIndex); $this->globalIndex = new GlobalIndex($stubsIndex, $this->projectIndex);
@ -206,6 +206,8 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
// Find composer.json // Find composer.json
if ($this->composerJson === null) { if ($this->composerJson === null) {
$composerJsonFiles = yield $this->filesFinder->find(Path::makeAbsolute('**/composer.json', $rootPath)); $composerJsonFiles = yield $this->filesFinder->find(Path::makeAbsolute('**/composer.json', $rootPath));
sortUrisLevelOrder($composerJsonFiles);
if (!empty($composerJsonFiles)) { if (!empty($composerJsonFiles)) {
$this->composerJson = json_decode(yield $this->contentRetriever->retrieve($composerJsonFiles[0])); $this->composerJson = json_decode(yield $this->contentRetriever->retrieve($composerJsonFiles[0]));
} }
@ -214,6 +216,8 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
// Find composer.lock // Find composer.lock
if ($this->composerLock === null) { if ($this->composerLock === null) {
$composerLockFiles = yield $this->filesFinder->find(Path::makeAbsolute('**/composer.lock', $rootPath)); $composerLockFiles = yield $this->filesFinder->find(Path::makeAbsolute('**/composer.lock', $rootPath));
sortUrisLevelOrder($composerLockFiles);
if (!empty($composerLockFiles)) { if (!empty($composerLockFiles)) {
$this->composerLock = json_decode(yield $this->contentRetriever->retrieve($composerLockFiles[0])); $this->composerLock = json_decode(yield $this->contentRetriever->retrieve($composerLockFiles[0]));
} }
@ -230,7 +234,8 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
$dependenciesIndex, $dependenciesIndex,
$sourceIndex, $sourceIndex,
$this->documentLoader, $this->documentLoader,
$this->composerLock $this->composerLock,
$this->composerJson
); );
$indexer->index()->otherwise('\\LanguageServer\\crash'); $indexer->index()->otherwise('\\LanguageServer\\crash');
} }
@ -252,7 +257,8 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
$dependenciesIndex, $dependenciesIndex,
$sourceIndex, $sourceIndex,
$this->composerLock, $this->composerLock,
$this->documentLoader $this->documentLoader,
$this->composerJson
); );
} }

View File

@ -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. * Returns array of TextEdit changes to format this document.
* *

View File

@ -30,7 +30,7 @@ use LanguageServer\Index\ReadableIndex;
use Sabre\Event\Promise; use Sabre\Event\Promise;
use Sabre\Uri; use Sabre\Uri;
use function Sabre\Event\coroutine; use function Sabre\Event\coroutine;
use function LanguageServer\waitForEvent; use function LanguageServer\{waitForEvent, isVendored};
/** /**
* Provides method handlers for all textDocument/* methods * Provides method handlers for all textDocument/* methods
@ -134,7 +134,7 @@ class TextDocument
public function didOpen(TextDocumentItem $textDocument) public function didOpen(TextDocumentItem $textDocument)
{ {
$document = $this->documentLoader->open($textDocument->uri, $textDocument->text); $document = $this->documentLoader->open($textDocument->uri, $textDocument->text);
if (!$document->isVendored()) { if (!isVendored($document, $this->composerJson)) {
$this->client->textDocument->publishDiagnostics($textDocument->uri, $document->getDiagnostics()); $this->client->textDocument->publishDiagnostics($textDocument->uri, $document->getDiagnostics());
} }
} }
@ -409,9 +409,9 @@ class TextDocument
$symbol->$prop = $val; $symbol->$prop = $val;
} }
$symbol->fqsen = $def->fqn; $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 // Definition is inside a dependency
$packageName = $matches[1];
foreach (array_merge($this->composerLock->packages, $this->composerLock->{'packages-dev'}) as $package) { foreach (array_merge($this->composerLock->packages, $this->composerLock->{'packages-dev'}) as $package) {
if ($package->name === $packageName) { if ($package->name === $packageName) {
$symbol->package = $package; $symbol->package = $package;

View File

@ -8,7 +8,7 @@ use LanguageServer\Index\{ProjectIndex, DependenciesIndex, Index};
use LanguageServer\Protocol\{SymbolInformation, SymbolDescriptor, ReferenceInformation, DependencyReference, Location}; use LanguageServer\Protocol\{SymbolInformation, SymbolDescriptor, ReferenceInformation, DependencyReference, Location};
use Sabre\Event\Promise; use Sabre\Event\Promise;
use function Sabre\Event\coroutine; use function Sabre\Event\coroutine;
use function LanguageServer\waitForEvent; use function LanguageServer\{waitForEvent, getPackageName};
/** /**
* Provides method handlers for all workspace/* methods * 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 \stdClass $composerLock The parsed composer.lock of the project, if any
* @param PhpDocumentLoader $documentLoader PhpDocumentLoader instance to load documents * @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->sourceIndex = $sourceIndex;
$this->index = $index; $this->index = $index;
$this->dependenciesIndex = $dependenciesIndex; $this->dependenciesIndex = $dependenciesIndex;
$this->composerLock = $composerLock; $this->composerLock = $composerLock;
$this->documentLoader = $documentLoader; $this->documentLoader = $documentLoader;
$this->composerJson = $composerJson;
} }
/** /**
@ -122,8 +123,7 @@ class Workspace
$symbol->$prop = $val; $symbol->$prop = $val;
} }
// Find out package name // Find out package name
preg_match('/\/vendor\/([^\/]+\/[^\/]+)\//', $def->symbolInformation->location->uri, $matches); $packageName = getPackageName($def->symbolInformation->location->uri, $this->composerJson);
$packageName = $matches[1];
foreach (array_merge($this->composerLock->packages, $this->composerLock->{'packages-dev'}) as $package) { foreach (array_merge($this->composerLock->packages, $this->composerLock->{'packages-dev'}) as $package) {
if ($package->name === $packageName) { if ($package->name === $packageName) {
$symbol->package = $package; $symbol->package = $package;

View File

@ -7,6 +7,7 @@ use Throwable;
use InvalidArgumentException; use InvalidArgumentException;
use PhpParser\Node; use PhpParser\Node;
use Sabre\Event\{Loop, Promise, EmitterInterface}; use Sabre\Event\{Loop, Promise, EmitterInterface};
use Sabre\Uri;
/** /**
* Transforms an absolute file path into a URI as used by the language server protocol. * 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; 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';
}

View File

@ -12,6 +12,7 @@ use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\Protocol\{SymbolKind, Position, ClientCapabilities}; use LanguageServer\Protocol\{SymbolKind, Position, ClientCapabilities};
use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex}; use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex};
use PhpParser\Node; use PhpParser\Node;
use function LanguageServer\isVendored;
class PhpDocumentTest extends TestCase class PhpDocumentTest extends TestCase
{ {
@ -42,18 +43,18 @@ class PhpDocumentTest extends TestCase
public function testIsVendored() public function testIsVendored()
{ {
$document = $this->createDocument('file:///dir/vendor/x.php', "<?php\n$\$a = new SomeClass;"); $document = $this->createDocument('file:///dir/vendor/x.php', "<?php\n$\$a = new SomeClass;");
$this->assertEquals(true, $document->isVendored()); $this->assertEquals(true, isVendored($document));
$document = $this->createDocument('file:///c:/dir/vendor/x.php', "<?php\n$\$a = new SomeClass;"); $document = $this->createDocument('file:///c:/dir/vendor/x.php', "<?php\n$\$a = new SomeClass;");
$this->assertEquals(true, $document->isVendored()); $this->assertEquals(true, isVendored($document));
$document = $this->createDocument('file:///vendor/x.php', "<?php\n$\$a = new SomeClass;"); $document = $this->createDocument('file:///vendor/x.php', "<?php\n$\$a = new SomeClass;");
$this->assertEquals(true, $document->isVendored()); $this->assertEquals(true, isVendored($document));
$document = $this->createDocument('file:///dir/vendor.php', "<?php\n$\$a = new SomeClass;"); $document = $this->createDocument('file:///dir/vendor.php', "<?php\n$\$a = new SomeClass;");
$this->assertEquals(false, $document->isVendored()); $this->assertEquals(false, isVendored($document));
$document = $this->createDocument('file:///dir/x.php', "<?php\n$\$a = new SomeClass;"); $document = $this->createDocument('file:///dir/x.php', "<?php\n$\$a = new SomeClass;");
$this->assertEquals(false, $document->isVendored()); $this->assertEquals(false, isVendored($document));
} }
} }