diff --git a/fixtures/different_extension.inc b/fixtures/different_extension.inc new file mode 100644 index 00000000..b3d9bbc7 --- /dev/null +++ b/fixtures/different_extension.inc @@ -0,0 +1 @@ +dependenciesIndex = $dependenciesIndex; $this->sourceIndex = $sourceIndex; $this->documentLoader = $documentLoader; + $this->options = $options; $this->composerLock = $composerLock; $this->composerJson = $composerJson; } @@ -103,8 +111,8 @@ public function __construct( public function index(): Promise { return coroutine(function () { - - $pattern = Path::makeAbsolute('**/*.php', $this->rootPath); + $fileTypes = implode(',', $this->options->fileTypes); + $pattern = Path::makeAbsolute('**/*{' . $fileTypes . '}', $this->rootPath); $uris = yield $this->filesFinder->find($pattern); $count = count($uris); @@ -213,7 +221,7 @@ private function indexFiles(array $files): Promise yield timeout(); $this->client->window->logMessage(MessageType::LOG, "Parsing $uri"); try { - $document = yield $this->documentLoader->load($uri); + $document = yield $this->documentLoader->load($uri, $this->options->fileSizeLimit); if (!isVendored($document, $this->composerJson)) { $this->client->textDocument->publishDiagnostics($uri, $document->getDiagnostics()); } diff --git a/src/LanguageServer.php b/src/LanguageServer.php index 46281f51..b29cb1da 100644 --- a/src/LanguageServer.php +++ b/src/LanguageServer.php @@ -163,11 +163,12 @@ public function __construct(ProtocolReader $reader, ProtocolWriter $writer) * @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. + * @param Options $initializationOptions The options send from client to initialize the server * @return Promise */ - public function initialize(ClientCapabilities $capabilities, string $rootPath = null, int $processId = null): Promise + public function initialize(ClientCapabilities $capabilities, string $rootPath = null, int $processId = null, Options $initializationOptions = null): Promise { - return coroutine(function () use ($capabilities, $rootPath, $processId) { + return coroutine(function () use ($capabilities, $rootPath, $processId, $initializationOptions) { if ($capabilities->xfilesProvider) { $this->filesFinder = new ClientFilesFinder($this->client); @@ -186,6 +187,7 @@ public function initialize(ClientCapabilities $capabilities, string $rootPath = $this->projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex, $this->composerJson); $stubsIndex = StubsIndex::read(); $this->globalIndex = new GlobalIndex($stubsIndex, $this->projectIndex); + $initializationOptions = $initializationOptions ?? new Options; // The DefinitionResolver should look in stubs, the project source and dependencies $this->definitionResolver = new DefinitionResolver($this->globalIndex); @@ -230,8 +232,10 @@ public function initialize(ClientCapabilities $capabilities, string $rootPath = $dependenciesIndex, $sourceIndex, $this->documentLoader, + $initializationOptions, $this->composerLock, - $this->composerJson + $this->composerJson, + $initializationOptions ); $indexer->index()->otherwise('\\LanguageServer\\crash'); } @@ -255,7 +259,9 @@ public function initialize(ClientCapabilities $capabilities, string $rootPath = $sourceIndex, $this->composerLock, $this->documentLoader, - $this->composerJson + $this->composerJson, + $indexer, + $initializationOptions ); } diff --git a/src/Options.php b/src/Options.php new file mode 100644 index 00000000..81a0fa55 --- /dev/null +++ b/src/Options.php @@ -0,0 +1,89 @@ + [$this, 'filterFileTypes']]); + $fileTypes = array_filter($fileTypes, 'strlen'); + $fileTypes = array_values($fileTypes); + + $this->fileTypes = !empty($fileTypes) ? $fileTypes : $this->fileTypes; + } + + /** + * Validate/Filter input and set option for file size limit + * + * @param string $fileSizeLimit Size in human readable format or -1 for unlimited + */ + public function setFileSizeLimit(string $fileSizeLimit) + { + $fileSizeLimit = filter_var($fileSizeLimit, FILTER_SANITIZE_STRING); + + if ($fileSizeLimit === '-1') { + $this->fileSizeLimit = PHP_INT_MAX; + } else { + $this->fileSizeLimit = $this->convertFileSize($fileSizeLimit); + } + } + + /** + * Filter valid file type + * + * @param string $fileType The file type to filter + * @return string|bool If valid it returns the file type, otherwise false + */ + private function filterFileTypes(string $fileType) + { + $fileType = trim($fileType); + + if (empty($fileType)) { + return $fileType; + } + + if (substr($fileType, 0, 1) !== '.') { + return false; + } + + return $fileType; + } + + /** + * Convert human readable file size to byte + * + * @param string $fileSize + * @return int + */ + private function convertFileSize(string $fileSize) + { + preg_match('/(\d+)(\w)/', $fileSize, $match); + $sizes = 'KMG'; + $size = (int) $match[1]; + $factor = strpos($sizes, strtoupper($match[2])) + 1; + + return $size * pow(1000, $factor); + } +} diff --git a/src/PhpDocumentLoader.php b/src/PhpDocumentLoader.php index 57a7e9c9..7bcb067e 100644 --- a/src/PhpDocumentLoader.php +++ b/src/PhpDocumentLoader.php @@ -100,13 +100,12 @@ public function getOrLoad(string $uri): Promise * The document is NOT added to the list of open documents, but definitions are registered. * * @param string $uri + * @param int $limit * @return Promise */ - public function load(string $uri): Promise + public function load(string $uri, int $limit): Promise { - return coroutine(function () use ($uri) { - - $limit = 150000; + return coroutine(function () use ($uri, $limit) { $content = yield $this->contentRetriever->retrieve($uri); $size = strlen($content); if ($size > $limit) { diff --git a/tests/LanguageServerTest.php b/tests/LanguageServerTest.php index 52963e6c..9fe890d4 100644 --- a/tests/LanguageServerTest.php +++ b/tests/LanguageServerTest.php @@ -5,6 +5,7 @@ use PHPUnit\Framework\TestCase; use LanguageServer\LanguageServer; +use LanguageServer\Options; use LanguageServer\Protocol\{ Message, ClientCapabilities, @@ -29,7 +30,7 @@ class LanguageServerTest extends TestCase public function testInitialize() { $server = new LanguageServer(new MockProtocolStream, new MockProtocolStream); - $result = $server->initialize(new ClientCapabilities, __DIR__, getmypid())->wait(); + $result = $server->initialize(new ClientCapabilities, __DIR__, getmypid(), new Options)->wait(); $serverCapabilities = new ServerCapabilities(); $serverCapabilities->textDocumentSync = TextDocumentSyncKind::FULL; @@ -60,14 +61,14 @@ public function testIndexingWithDirectFileAccess() if ($msg->body->params->type === MessageType::ERROR) { $promise->reject(new Exception($msg->body->params->message)); } else if (preg_match('/All \d+ PHP files parsed/', $msg->body->params->message)) { - $promise->fulfill(); + $promise->fulfill(true); } } }); $server = new LanguageServer($input, $output); $capabilities = new ClientCapabilities; - $server->initialize($capabilities, realpath(__DIR__ . '/../fixtures'), getmypid()); - $promise->wait(); + $server->initialize($capabilities, realpath(__DIR__ . '/../fixtures'), getmypid(), new Options); + $this->assertTrue($promise->wait()); } public function testIndexingWithFilesAndContentRequests() @@ -114,9 +115,34 @@ public function testIndexingWithFilesAndContentRequests() $capabilities = new ClientCapabilities; $capabilities->xfilesProvider = true; $capabilities->xcontentProvider = true; - $server->initialize($capabilities, $rootPath, getmypid()); + $server->initialize($capabilities, $rootPath, getmypid(), new Options); $promise->wait(); $this->assertTrue($filesCalled); $this->assertTrue($contentCalled); } + + public function testIndexingMultipleFileTypes() + { + $promise = new Promise; + $input = new MockProtocolStream; + $output = new MockProtocolStream; + $options = new Options; + $options->setFileTypes([ + '.php', + '.inc' + ]); + $output->on('message', function (Message $msg) use ($promise, &$allFilesParsed) { + if ($msg->body->method === 'window/logMessage' && $promise->state === Promise::PENDING) { + if ($msg->body->params->type === MessageType::ERROR) { + $promise->reject(new Exception($msg->body->params->message)); + } elseif (preg_match('/All \d+ PHP files parsed/', $msg->body->params->message)) { + $promise->fulfill(true); + } + } + }); + $server = new LanguageServer($input, $output); + $capabilities = new ClientCapabilities; + $server->initialize($capabilities, realpath(__DIR__ . '/../fixtures'), getmypid(), $options); + $this->assertTrue($promise->wait()); + } } diff --git a/tests/OptionsTest.php b/tests/OptionsTest.php new file mode 100644 index 00000000..04b2869a --- /dev/null +++ b/tests/OptionsTest.php @@ -0,0 +1,45 @@ +setFileTypes([ + '.php', + false, + 12345, + '.valid' + ]); + + $this->assertSame($expected, $options->fileTypes); + } + + public function testConvertFileSize() + { + $options = new Options(); + + $options->setFileSizeLimit('150K'); + $this->assertEquals(150000, $options->fileSizeLimit); + + $options->setFileSizeLimit('15M'); + $this->assertEquals(15000000, $options->fileSizeLimit); + + $options->setFileSizeLimit('15G'); + $this->assertEquals(15000000000, $options->fileSizeLimit); + + $options->setFileSizeLimit('-1'); + $this->assertEquals(PHP_INT_MAX, $options->fileSizeLimit); + } +}