From 87bf7a92667982dc9ec62e93e75bc9ceb61099a2 Mon Sep 17 00:00:00 2001 From: dantleech Date: Sat, 2 Jan 2021 22:04:32 +0000 Subject: [PATCH 001/111] Add common README sections (#27) --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index 669735f0..e1074047 100644 --- a/README.md +++ b/README.md @@ -63,3 +63,16 @@ License ------- This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details + +Contributing +------------ + +This package is open source and welcomes contributions! Feel free to open a +pull request on this repository. + +Support +------- + +- Create an issue on the main [Phpactor](https://github.com/phpactor/phpactor) repository. +- Join the `#phpactor` channel on the Slack [Symfony Devs](https://symfony.com/slack-invite) channel. + From bd8c5241ac1dfc3c0e03d9d8c0fd948a463c3d19 Mon Sep 17 00:00:00 2001 From: dantleech Date: Sat, 2 Jan 2021 22:32:25 +0000 Subject: [PATCH 002/111] Update README.md --- README.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/README.md b/README.md index e1074047..3d4ad10a 100644 --- a/README.md +++ b/README.md @@ -54,11 +54,6 @@ Built With - [Amphp](https://amphp.org/): Event-driven concurrency framework. -Contributing ------------- - -Contributions are welcome. - License ------- From 2cd35422581fc20b2f5715e9f4b4c611f822c494 Mon Sep 17 00:00:00 2001 From: dantleech Date: Wed, 6 Jan 2021 22:10:02 +0000 Subject: [PATCH 003/111] Update gitignore (#28) --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 700af48b..45320e6f 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ /.php_cs.cache /stubs /doc/_build +/.vscode \ No newline at end of file From 267df2e0c1d2c5651b803ff531c4f1266b850adf Mon Sep 17 00:00:00 2001 From: dantleech Date: Sun, 24 Jan 2021 17:48:11 +0000 Subject: [PATCH 004/111] Update coding standard (#29) --- .github/workflows/ci.yml | 2 +- .php_cs.dist | 9 +- .../AggregateDiagnosticsProvider.php | 3 +- lib/Core/Server/LanguageServer.php | 40 +- .../StreamProvider/SocketStreamProvider.php | 2 +- lib/Listener/ServiceListener.php | 2 +- lib/Listener/WorkspaceListener.php | 6 +- lib/Test/LanguageServerTester.php | 20 +- phpstan-baseline.neon | 422 +----------------- .../Core/Command/CommandDispatcherTest.php | 2 +- ...anguageSeverProtocolParamsResolverTest.php | 4 +- .../Factory/ClosureDispatcherFactoryTest.php | 5 +- .../Handler/HandlerMethodResolverTest.php | 6 +- .../Core/Handler/HandlerMethodRunnerTest.php | 4 +- tests/Unit/Core/Handler/HandlersTest.php | 6 +- .../Core/Rpc/RequestMessageFactoryTest.php | 10 +- tests/Unit/Core/Server/ClientApiTest.php | 18 +- .../Server/Parser/LspMessageReaderTest.php | 54 +-- .../Transmitter/LspMessageFormatterTest.php | 2 +- .../Transmitter/LspMessageSerializerTest.php | 4 +- .../Unit/Core/Service/ServiceManagerTest.php | 2 +- tests/Unit/Core/TestLogger.php | 2 +- tests/Unit/Core/Workspace/WorkspaceTest.php | 12 +- tests/Unit/Handler/System/ExitHandlerTest.php | 4 +- .../Handler/System/ServiceHandlerTest.php | 4 +- .../Unit/Handler/System/StatsHandlerTest.php | 2 +- .../TextDocument/TextDocumentHandlerTest.php | 6 +- .../Handler/Workspace/CommandHandlerTest.php | 4 +- tests/Unit/LanguageServerBuilderTest.php | 2 +- .../ErrorHandlingMiddlewareTest.php | 8 +- .../Middleware/InitializeMiddlewareTest.php | 2 +- 31 files changed, 127 insertions(+), 542 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 312b1c94..b30fc14d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,7 +43,7 @@ jobs: - name: "Run friendsofphp/php-cs-fixer" - run: "vendor/bin/php-cs-fixer fix --dry-run --diff --verbose" + run: "vendor/bin/php-cs-fixer fix --dry-run --diff --verbose --allow-risky=yes" phpstan: name: "PHPStan (${{ matrix.php-version }})" diff --git a/.php_cs.dist b/.php_cs.dist index eadf8906..2820b890 100644 --- a/.php_cs.dist +++ b/.php_cs.dist @@ -1,12 +1,10 @@ in('bin') ->in('lib') - ->in('example') ->in('tests') ->exclude([ - 'tests/Workspace' + 'tests/Workspace', ]) ; @@ -15,6 +13,11 @@ return PhpCsFixer\Config::create() '@PSR2' => true, 'no_unused_imports' => true, 'array_syntax' => ['syntax' => 'short'], + 'void_return' => true, + 'ordered_class_elements' => true, + 'single_quote' => true, + 'heredoc_indentation' => true, + 'global_namespace_import' => true, ]) ->setFinder($finder) ; diff --git a/lib/Core/Diagnostics/AggregateDiagnosticsProvider.php b/lib/Core/Diagnostics/AggregateDiagnosticsProvider.php index f9412ce7..9584d07f 100644 --- a/lib/Core/Diagnostics/AggregateDiagnosticsProvider.php +++ b/lib/Core/Diagnostics/AggregateDiagnosticsProvider.php @@ -6,6 +6,7 @@ use Phpactor\LanguageServerProtocol\TextDocumentItem; use Psr\Log\LoggerInterface; use function Amp\call; +use Throwable; class AggregateDiagnosticsProvider implements DiagnosticsProvider { @@ -44,7 +45,7 @@ public function provideDiagnostics(TextDocumentItem $textDocument): Promise number_format(microtime(true) - $start, 2), get_class($provider) )); - } catch (\Throwable $throwable) { + } catch (Throwable $throwable) { $this->logger->error(sprintf( 'Diagnostic error from provider "%s": %s', get_class($provider), diff --git a/lib/Core/Server/LanguageServer.php b/lib/Core/Server/LanguageServer.php index 78c30858..f6d6e0bc 100644 --- a/lib/Core/Server/LanguageServer.php +++ b/lib/Core/Server/LanguageServer.php @@ -103,7 +103,7 @@ public function run(): void yield $this->shutdown(); }); - Loop::setErrorHandler(function (Throwable $error) { + Loop::setErrorHandler(function (Throwable $error): void { $this->logger->critical($error->getMessage()); throw $error; }); @@ -131,6 +131,25 @@ public function address(): ?string return $this->streamProvider->address(); } + /** + * @return Promise + */ + public function shutdown(): Promise + { + return call(function () { + $this->logger->info('Shutting down'); + + $promises = []; + foreach ($this->connections as $connection) { + $promises[] = $connection->stream()->end(); + } + + yield any($promises); + + $this->streamProvider->close(); + }); + } + private function listenForConnections(): Generator { if ($this->streamProvider instanceof SocketStreamProvider) { @@ -220,23 +239,4 @@ private function dispatchRequest(MessageTransmitter $transmitter, Dispatcher $di $transmitter->transmit($response); }); } - - /** - * @return Promise - */ - public function shutdown(): Promise - { - return call(function () { - $this->logger->info('Shutting down'); - - $promises = []; - foreach ($this->connections as $connection) { - $promises[] = $connection->stream()->end(); - } - - yield any($promises); - - $this->streamProvider->close(); - }); - } } diff --git a/lib/Core/Server/StreamProvider/SocketStreamProvider.php b/lib/Core/Server/StreamProvider/SocketStreamProvider.php index 50076483..9d2e024a 100644 --- a/lib/Core/Server/StreamProvider/SocketStreamProvider.php +++ b/lib/Core/Server/StreamProvider/SocketStreamProvider.php @@ -32,7 +32,7 @@ public function accept(): Promise $promise = $this->server->accept(); $deferred = new Deferred(); - $promise->onResolve(function ($reason, ?Socket $socket) use ($deferred) { + $promise->onResolve(function ($reason, ?Socket $socket) use ($deferred): void { if (null === $socket) { return; } diff --git a/lib/Listener/ServiceListener.php b/lib/Listener/ServiceListener.php index 1407e192..27e600b9 100644 --- a/lib/Listener/ServiceListener.php +++ b/lib/Listener/ServiceListener.php @@ -24,7 +24,7 @@ public function __construct(ServiceManager $serviceManager) public function getListenersForEvent(object $event): iterable { if ($event instanceof Initialized) { - yield function (Initialized $closed) { + yield function (Initialized $closed): void { $this->serviceManager->startAll(); }; return; diff --git a/lib/Listener/WorkspaceListener.php b/lib/Listener/WorkspaceListener.php index 2e80af82..faa8cb0b 100644 --- a/lib/Listener/WorkspaceListener.php +++ b/lib/Listener/WorkspaceListener.php @@ -26,21 +26,21 @@ public function __construct(Workspace $workspace) public function getListenersForEvent(object $event): iterable { if ($event instanceof TextDocumentClosed) { - yield function (TextDocumentClosed $closed) { + yield function (TextDocumentClosed $closed): void { $this->workspace->remove($closed->identifier()); }; return; } if ($event instanceof TextDocumentOpened) { - yield function (TextDocumentOpened $opened) { + yield function (TextDocumentOpened $opened): void { $this->workspace->open($opened->textDocument()); }; return; } if ($event instanceof TextDocumentUpdated) { - yield function (TextDocumentUpdated $updated) { + yield function (TextDocumentUpdated $updated): void { $this->workspace->update($updated->identifier(), $updated->updatedText()); }; return; diff --git a/lib/Test/LanguageServerTester.php b/lib/Test/LanguageServerTester.php index 900fcf67..18fd69b4 100644 --- a/lib/Test/LanguageServerTester.php +++ b/lib/Test/LanguageServerTester.php @@ -147,6 +147,16 @@ public function assertSuccess(ResponseMessage $response): void } } + public function cancel(int $requestId): void + { + $this->dispatchAndWait(new NotificationMessage('$/cancelRequest', ['id' => $requestId])); + } + + public function workspace(): WorkspaceTester + { + return new WorkspaceTester($this); + } + /** * @param array|object $params * @return array @@ -159,14 +169,4 @@ private function normalizeParams($params): array return $this->messageSerializer->normalize($params); } - - public function cancel(int $requestId): void - { - $this->dispatchAndWait(new NotificationMessage('$/cancelRequest', ['id' => $requestId])); - } - - public function workspace(): WorkspaceTester - { - return new WorkspaceTester($this); - } } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 703f4d25..de6d9257 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,432 +1,12 @@ parameters: ignoreErrors: - - - message: "#^Unable to resolve the template type T in call to function Amp\\\\call$#" - count: 1 - path: lib/Core/CodeAction/AggregateCodeActionProvider.php - - - - message: "#^Unable to resolve the template type TGenerator in call to function Amp\\\\call$#" - count: 1 - path: lib/Core/CodeAction/AggregateCodeActionProvider.php - - - - message: "#^Unable to resolve the template type TGeneratorPromise in call to function Amp\\\\call$#" - count: 1 - path: lib/Core/CodeAction/AggregateCodeActionProvider.php - - - - message: "#^Unable to resolve the template type TGeneratorReturn in call to function Amp\\\\call$#" - count: 1 - path: lib/Core/CodeAction/AggregateCodeActionProvider.php - - - - message: "#^Unable to resolve the template type TPromise in call to function Amp\\\\call$#" - count: 1 - path: lib/Core/CodeAction/AggregateCodeActionProvider.php - - - - message: "#^Unable to resolve the template type TReturn in call to function Amp\\\\call$#" - count: 1 - path: lib/Core/CodeAction/AggregateCodeActionProvider.php - - - - message: "#^Unable to resolve the template type T in call to function Amp\\\\call$#" - count: 1 - path: lib/Core/Diagnostics/AggregateDiagnosticsProvider.php - - - - message: "#^Unable to resolve the template type TGenerator in call to function Amp\\\\call$#" - count: 1 - path: lib/Core/Diagnostics/AggregateDiagnosticsProvider.php - - - - message: "#^Unable to resolve the template type TGeneratorPromise in call to function Amp\\\\call$#" - count: 1 - path: lib/Core/Diagnostics/AggregateDiagnosticsProvider.php - - - - message: "#^Unable to resolve the template type TGeneratorReturn in call to function Amp\\\\call$#" - count: 1 - path: lib/Core/Diagnostics/AggregateDiagnosticsProvider.php - - - - message: "#^Unable to resolve the template type TPromise in call to function Amp\\\\call$#" - count: 1 - path: lib/Core/Diagnostics/AggregateDiagnosticsProvider.php - - - - message: "#^Unable to resolve the template type TReturn in call to function Amp\\\\call$#" - count: 1 - path: lib/Core/Diagnostics/AggregateDiagnosticsProvider.php - - - - message: "#^Unable to resolve the template type T in call to function Amp\\\\call$#" - count: 1 - path: lib/Core/Diagnostics/DiagnosticsEngine.php - - - - message: "#^Unable to resolve the template type TGenerator in call to function Amp\\\\call$#" - count: 1 - path: lib/Core/Diagnostics/DiagnosticsEngine.php - - - - message: "#^Unable to resolve the template type TGeneratorPromise in call to function Amp\\\\call$#" - count: 1 - path: lib/Core/Diagnostics/DiagnosticsEngine.php - - - - message: "#^Unable to resolve the template type TGeneratorReturn in call to function Amp\\\\call$#" - count: 1 - path: lib/Core/Diagnostics/DiagnosticsEngine.php - - - - message: "#^Unable to resolve the template type TPromise in call to function Amp\\\\call$#" - count: 1 - path: lib/Core/Diagnostics/DiagnosticsEngine.php - - - - message: "#^Unable to resolve the template type TReturn in call to function Amp\\\\call$#" - count: 1 - path: lib/Core/Diagnostics/DiagnosticsEngine.php - - message: "#^Method Phpactor\\\\LanguageServer\\\\Core\\\\Handler\\\\ClosureHandler\\:\\:handle\\(\\) has no return typehint specified\\.$#" count: 1 path: lib/Core/Handler/ClosureHandler.php - - message: "#^Unable to resolve the template type T in call to function Amp\\\\call$#" - count: 1 - path: lib/Core/Handler/HandlerMethodRunner.php - - - - message: "#^Unable to resolve the template type TGenerator in call to function Amp\\\\call$#" - count: 1 - path: lib/Core/Handler/HandlerMethodRunner.php - - - - message: "#^Unable to resolve the template type TGeneratorPromise in call to function Amp\\\\call$#" - count: 1 - path: lib/Core/Handler/HandlerMethodRunner.php - - - - message: "#^Unable to resolve the template type TGeneratorReturn in call to function Amp\\\\call$#" - count: 1 - path: lib/Core/Handler/HandlerMethodRunner.php - - - - message: "#^Unable to resolve the template type TPromise in call to function Amp\\\\call$#" - count: 1 - path: lib/Core/Handler/HandlerMethodRunner.php - - - - message: "#^Unable to resolve the template type TReturn in call to function Amp\\\\call$#" + message: "#^Unable to resolve the template type TValue in call to function Amp\\\\Promise\\\\any$#" count: 1 - path: lib/Core/Handler/HandlerMethodRunner.php - - - - message: "#^Unable to resolve the template type T in call to function Amp\\\\call$#" - count: 1 - path: lib/Core/Server/Client/MessageRequestClient.php - - - - message: "#^Unable to resolve the template type TGenerator in call to function Amp\\\\call$#" - count: 1 - path: lib/Core/Server/Client/MessageRequestClient.php - - - - message: "#^Unable to resolve the template type TGeneratorPromise in call to function Amp\\\\call$#" - count: 1 - path: lib/Core/Server/Client/MessageRequestClient.php - - - - message: "#^Unable to resolve the template type TGeneratorReturn in call to function Amp\\\\call$#" - count: 1 - path: lib/Core/Server/Client/MessageRequestClient.php - - - - message: "#^Unable to resolve the template type TPromise in call to function Amp\\\\call$#" - count: 1 - path: lib/Core/Server/Client/MessageRequestClient.php - - - - message: "#^Unable to resolve the template type TReturn in call to function Amp\\\\call$#" - count: 1 - path: lib/Core/Server/Client/MessageRequestClient.php - - - - message: "#^Unable to resolve the template type T in call to function Amp\\\\call$#" - count: 2 - path: lib/Core/Server/Client/WorkspaceClient.php - - - - message: "#^Unable to resolve the template type TGenerator in call to function Amp\\\\call$#" - count: 2 - path: lib/Core/Server/Client/WorkspaceClient.php - - - - message: "#^Unable to resolve the template type TGeneratorPromise in call to function Amp\\\\call$#" - count: 2 - path: lib/Core/Server/Client/WorkspaceClient.php - - - - message: "#^Unable to resolve the template type TGeneratorReturn in call to function Amp\\\\call$#" - count: 2 - path: lib/Core/Server/Client/WorkspaceClient.php - - - - message: "#^Unable to resolve the template type TPromise in call to function Amp\\\\call$#" - count: 2 - path: lib/Core/Server/Client/WorkspaceClient.php - - - - message: "#^Unable to resolve the template type TReturn in call to function Amp\\\\call$#" - count: 2 - path: lib/Core/Server/Client/WorkspaceClient.php - - - - message: "#^Unable to resolve the template type T in call to function Amp\\\\call$#" - count: 3 - path: lib/Core/Server/LanguageServer.php - - - - message: "#^Unable to resolve the template type TGenerator in call to function Amp\\\\call$#" - count: 3 - path: lib/Core/Server/LanguageServer.php - - - - message: "#^Unable to resolve the template type TGeneratorPromise in call to function Amp\\\\call$#" - count: 3 path: lib/Core/Server/LanguageServer.php - - - message: "#^Unable to resolve the template type TGeneratorReturn in call to function Amp\\\\call$#" - count: 3 - path: lib/Core/Server/LanguageServer.php - - - - message: "#^Unable to resolve the template type TPromise in call to function Amp\\\\call$#" - count: 3 - path: lib/Core/Server/LanguageServer.php - - - - message: "#^Unable to resolve the template type TReturn in call to function Amp\\\\call$#" - count: 3 - path: lib/Core/Server/LanguageServer.php - - - - message: "#^Unable to resolve the template type T in call to function Amp\\\\call$#" - count: 1 - path: lib/Core/Server/Parser/LspMessageReader.php - - - - message: "#^Unable to resolve the template type TGenerator in call to function Amp\\\\call$#" - count: 1 - path: lib/Core/Server/Parser/LspMessageReader.php - - - - message: "#^Unable to resolve the template type TGeneratorPromise in call to function Amp\\\\call$#" - count: 1 - path: lib/Core/Server/Parser/LspMessageReader.php - - - - message: "#^Unable to resolve the template type TGeneratorReturn in call to function Amp\\\\call$#" - count: 1 - path: lib/Core/Server/Parser/LspMessageReader.php - - - - message: "#^Unable to resolve the template type TPromise in call to function Amp\\\\call$#" - count: 1 - path: lib/Core/Server/Parser/LspMessageReader.php - - - - message: "#^Unable to resolve the template type TReturn in call to function Amp\\\\call$#" - count: 1 - path: lib/Core/Server/Parser/LspMessageReader.php - - - - message: "#^Unable to resolve the template type T in call to function Amp\\\\call$#" - count: 1 - path: lib/Diagnostics/CodeActionDiagnosticsProvider.php - - - - message: "#^Unable to resolve the template type TGenerator in call to function Amp\\\\call$#" - count: 1 - path: lib/Diagnostics/CodeActionDiagnosticsProvider.php - - - - message: "#^Unable to resolve the template type TGeneratorPromise in call to function Amp\\\\call$#" - count: 1 - path: lib/Diagnostics/CodeActionDiagnosticsProvider.php - - - - message: "#^Unable to resolve the template type TGeneratorReturn in call to function Amp\\\\call$#" - count: 1 - path: lib/Diagnostics/CodeActionDiagnosticsProvider.php - - - - message: "#^Unable to resolve the template type TPromise in call to function Amp\\\\call$#" - count: 1 - path: lib/Diagnostics/CodeActionDiagnosticsProvider.php - - - - message: "#^Unable to resolve the template type TReturn in call to function Amp\\\\call$#" - count: 1 - path: lib/Diagnostics/CodeActionDiagnosticsProvider.php - - - - message: "#^Unable to resolve the template type T in call to function Amp\\\\call$#" - count: 1 - path: lib/Example/CodeAction/SayHelloCodeActionProvider.php - - - - message: "#^Unable to resolve the template type TGenerator in call to function Amp\\\\call$#" - count: 1 - path: lib/Example/CodeAction/SayHelloCodeActionProvider.php - - - - message: "#^Unable to resolve the template type TGeneratorPromise in call to function Amp\\\\call$#" - count: 1 - path: lib/Example/CodeAction/SayHelloCodeActionProvider.php - - - - message: "#^Unable to resolve the template type TGeneratorReturn in call to function Amp\\\\call$#" - count: 1 - path: lib/Example/CodeAction/SayHelloCodeActionProvider.php - - - - message: "#^Unable to resolve the template type TPromise in call to function Amp\\\\call$#" - count: 1 - path: lib/Example/CodeAction/SayHelloCodeActionProvider.php - - - - message: "#^Unable to resolve the template type TReturn in call to function Amp\\\\call$#" - count: 1 - path: lib/Example/CodeAction/SayHelloCodeActionProvider.php - - - - message: "#^Unable to resolve the template type T in call to function Amp\\\\call$#" - count: 1 - path: lib/Example/Diagnostics/SayHelloDiagnosticsProvider.php - - - - message: "#^Unable to resolve the template type TGenerator in call to function Amp\\\\call$#" - count: 1 - path: lib/Example/Diagnostics/SayHelloDiagnosticsProvider.php - - - - message: "#^Unable to resolve the template type TGeneratorPromise in call to function Amp\\\\call$#" - count: 1 - path: lib/Example/Diagnostics/SayHelloDiagnosticsProvider.php - - - - message: "#^Unable to resolve the template type TGeneratorReturn in call to function Amp\\\\call$#" - count: 1 - path: lib/Example/Diagnostics/SayHelloDiagnosticsProvider.php - - - - message: "#^Unable to resolve the template type TPromise in call to function Amp\\\\call$#" - count: 1 - path: lib/Example/Diagnostics/SayHelloDiagnosticsProvider.php - - - - message: "#^Unable to resolve the template type TReturn in call to function Amp\\\\call$#" - count: 1 - path: lib/Example/Diagnostics/SayHelloDiagnosticsProvider.php - - - - message: "#^Unable to resolve the template type T in call to function Amp\\\\call$#" - count: 1 - path: lib/Example/Service/PingProvider.php - - - - message: "#^Unable to resolve the template type TGenerator in call to function Amp\\\\call$#" - count: 1 - path: lib/Example/Service/PingProvider.php - - - - message: "#^Unable to resolve the template type TGeneratorPromise in call to function Amp\\\\call$#" - count: 1 - path: lib/Example/Service/PingProvider.php - - - - message: "#^Unable to resolve the template type TGeneratorReturn in call to function Amp\\\\call$#" - count: 1 - path: lib/Example/Service/PingProvider.php - - - - message: "#^Unable to resolve the template type TPromise in call to function Amp\\\\call$#" - count: 1 - path: lib/Example/Service/PingProvider.php - - - - message: "#^Unable to resolve the template type TReturn in call to function Amp\\\\call$#" - count: 1 - path: lib/Example/Service/PingProvider.php - - - - message: "#^Unable to resolve the template type T in call to function Amp\\\\call$#" - count: 1 - path: lib/Handler/TextDocument/CodeActionHandler.php - - - - message: "#^Unable to resolve the template type TGenerator in call to function Amp\\\\call$#" - count: 1 - path: lib/Handler/TextDocument/CodeActionHandler.php - - - - message: "#^Unable to resolve the template type TGeneratorPromise in call to function Amp\\\\call$#" - count: 1 - path: lib/Handler/TextDocument/CodeActionHandler.php - - - - message: "#^Unable to resolve the template type TGeneratorReturn in call to function Amp\\\\call$#" - count: 1 - path: lib/Handler/TextDocument/CodeActionHandler.php - - - - message: "#^Unable to resolve the template type TPromise in call to function Amp\\\\call$#" - count: 1 - path: lib/Handler/TextDocument/CodeActionHandler.php - - - - message: "#^Unable to resolve the template type TReturn in call to function Amp\\\\call$#" - count: 1 - path: lib/Handler/TextDocument/CodeActionHandler.php - - - - message: "#^Unable to resolve the template type T in call to function Amp\\\\call$#" - count: 1 - path: lib/Middleware/ErrorHandlingMiddleware.php - - - - message: "#^Unable to resolve the template type TGenerator in call to function Amp\\\\call$#" - count: 1 - path: lib/Middleware/ErrorHandlingMiddleware.php - - - - message: "#^Unable to resolve the template type TGeneratorPromise in call to function Amp\\\\call$#" - count: 1 - path: lib/Middleware/ErrorHandlingMiddleware.php - - - - message: "#^Unable to resolve the template type TGeneratorReturn in call to function Amp\\\\call$#" - count: 1 - path: lib/Middleware/ErrorHandlingMiddleware.php - - - - message: "#^Unable to resolve the template type TPromise in call to function Amp\\\\call$#" - count: 1 - path: lib/Middleware/ErrorHandlingMiddleware.php - - - - message: "#^Unable to resolve the template type TReturn in call to function Amp\\\\call$#" - count: 1 - path: lib/Middleware/ErrorHandlingMiddleware.php - - - - message: "#^Unable to resolve the template type TPromise in call to function Amp\\\\Promise\\\\wait$#" - count: 3 - path: lib/Test/LanguageServerTester.php - diff --git a/tests/Unit/Core/Command/CommandDispatcherTest.php b/tests/Unit/Core/Command/CommandDispatcherTest.php index 7d946822..40c38172 100644 --- a/tests/Unit/Core/Command/CommandDispatcherTest.php +++ b/tests/Unit/Core/Command/CommandDispatcherTest.php @@ -33,7 +33,7 @@ public function testExceptionWhenCommandNotFound(): void $this->createDispatcher([ 'foobar' => new class implements Command { - public function __invoke(string $foobar) + public function __invoke(string $foobar): void { } } diff --git a/tests/Unit/Core/Dispatcher/ArgumentResolver/LanguageSeverProtocolParamsResolverTest.php b/tests/Unit/Core/Dispatcher/ArgumentResolver/LanguageSeverProtocolParamsResolverTest.php index 61076a12..f79ed6a0 100644 --- a/tests/Unit/Core/Dispatcher/ArgumentResolver/LanguageSeverProtocolParamsResolverTest.php +++ b/tests/Unit/Core/Dispatcher/ArgumentResolver/LanguageSeverProtocolParamsResolverTest.php @@ -55,11 +55,11 @@ public function testNotResolvableWhenFirstParamNotProtocolParams(): void class LspHandler { - public function initialize(InitializeParams $params, CancellationToken $c) + public function initialize(InitializeParams $params, CancellationToken $c): void { } - public function initializeWrongOrder(CancellationToken $c, InitializeParams $params) + public function initializeWrongOrder(CancellationToken $c, InitializeParams $params): void { } } diff --git a/tests/Unit/Core/Dispatcher/Factory/ClosureDispatcherFactoryTest.php b/tests/Unit/Core/Dispatcher/Factory/ClosureDispatcherFactoryTest.php index 40e2f726..e6a29d8c 100644 --- a/tests/Unit/Core/Dispatcher/Factory/ClosureDispatcherFactoryTest.php +++ b/tests/Unit/Core/Dispatcher/Factory/ClosureDispatcherFactoryTest.php @@ -10,13 +10,14 @@ use Phpactor\LanguageServer\Core\Server\Transmitter\NullMessageTransmitter; use Phpactor\LanguageServer\Test\ProtocolFactory; use RuntimeException; +use stdClass; class ClosureDispatcherFactoryTest extends TestCase { public function testReturnsDispatcher(): void { $dispatcher = $this->createDispatcherFactory(function () { - return new ClosureDispatcher(function () { + return new ClosureDispatcher(function (): void { }); })->create(new NullMessageTransmitter(), ProtocolFactory::initializeParams()); @@ -27,7 +28,7 @@ public function testExceptionIfNotReturningDispatcher(): void { $this->expectException(RuntimeException::class); $this->createDispatcherFactory(function () { - return new \stdClass(); + return new stdClass(); })->create(new NullMessageTransmitter(), ProtocolFactory::initializeParams()); } diff --git a/tests/Unit/Core/Handler/HandlerMethodResolverTest.php b/tests/Unit/Core/Handler/HandlerMethodResolverTest.php index 047da741..01303862 100644 --- a/tests/Unit/Core/Handler/HandlerMethodResolverTest.php +++ b/tests/Unit/Core/Handler/HandlerMethodResolverTest.php @@ -19,7 +19,7 @@ protected function setUp(): void $this->resolver = new HandlerMethodResolver(); } - public function testThrowsExceptionIfHandlerDidNotDeclaredMethods() + public function testThrowsExceptionIfHandlerDidNotDeclaredMethods(): void { $this->expectException(RuntimeException::class); $this->expectExceptionMessage('has not declared'); @@ -36,7 +36,7 @@ public function methods():array $this->resolver->resolveHandlerMethod($handler, 'foo'); } - public function testThrowsExceptionIfHandlerDoesNotHaveMethod() + public function testThrowsExceptionIfHandlerDoesNotHaveMethod(): void { $this->expectException(RuntimeException::class); $this->expectExceptionMessage('does not have'); @@ -57,7 +57,7 @@ public function foo(): void $this->resolver->resolveHandlerMethod($handler, 'foo'); } - public function testResolvesMethodName() + public function testResolvesMethodName(): void { $handler = new class implements Handler { public function methods():array diff --git a/tests/Unit/Core/Handler/HandlerMethodRunnerTest.php b/tests/Unit/Core/Handler/HandlerMethodRunnerTest.php index f39b851b..d36615d9 100644 --- a/tests/Unit/Core/Handler/HandlerMethodRunnerTest.php +++ b/tests/Unit/Core/Handler/HandlerMethodRunnerTest.php @@ -51,7 +51,7 @@ public function testThrowsExceptionIfHandlerNotReturnPromise() public function testReturnsNullIfMessageIsANotification() { $dispatcher = $this->createRunner([ - new ClosureHandler('foobar', function () { + new ClosureHandler('foobar', function (): void { }) ]); @@ -79,7 +79,7 @@ public function testReturnsValueFromHandler() self::assertEquals(2, $response->id); } - public function testToleratesTryingToCancelNonRunningRequest() + public function testToleratesTryingToCancelNonRunningRequest(): void { $dispatcher = $this->createRunner([ new ClosureHandler('foobar', function () { diff --git a/tests/Unit/Core/Handler/HandlersTest.php b/tests/Unit/Core/Handler/HandlersTest.php index 858c4939..8de251ad 100644 --- a/tests/Unit/Core/Handler/HandlersTest.php +++ b/tests/Unit/Core/Handler/HandlersTest.php @@ -25,7 +25,7 @@ protected function setUp(): void $this->handler2 = $this->prophesize(Handler::class); } - public function testThrowsExceptionNotFound() + public function testThrowsExceptionNotFound(): void { $this->expectException(HandlerNotFound::class); $this->handler1->methods()->willReturn(['barbra']); @@ -33,7 +33,7 @@ public function testThrowsExceptionNotFound() $handlers->get('foobar'); } - public function testReturnsHandler() + public function testReturnsHandler(): void { $this->handler1->methods()->willReturn(['foobar' => 'foobar']); $handlers = $this->create([ $this->handler1->reveal() ]); @@ -41,7 +41,7 @@ public function testReturnsHandler() $this->assertSame($this->handler1->reveal(), $handler); } - public function testMerge() + public function testMerge(): void { $this->handler1->methods()->willReturn(['foobar' => 'foobar']); $this->handler2->methods()->willReturn(['barfoo' => 'barfoo']); diff --git a/tests/Unit/Core/Rpc/RequestMessageFactoryTest.php b/tests/Unit/Core/Rpc/RequestMessageFactoryTest.php index 1c57207d..7b35fa86 100644 --- a/tests/Unit/Core/Rpc/RequestMessageFactoryTest.php +++ b/tests/Unit/Core/Rpc/RequestMessageFactoryTest.php @@ -12,21 +12,21 @@ class RequestMessageFactoryTest extends TestCase { - public function testExceptionOnInvalidKeys() + public function testExceptionOnInvalidKeys(): void { $this->expectException(UnknownKeys::class); $request = new RawMessage([], ['foo' => 'bar']); RequestMessageFactory::fromRequest($request); } - public function testExceptionMissingKeys() + public function testExceptionMissingKeys(): void { $this->expectException(RequiredKeysMissing::class); $request = new RawMessage([], []); RequestMessageFactory::fromRequest($request); } - public function testReturnsRequestMessage() + public function testReturnsRequestMessage(): void { $request = new RawMessage([], [ 'jsonrpc' => 2.0, @@ -41,7 +41,7 @@ public function testReturnsRequestMessage() $this->assertEquals(['one' => 'two'], $request->params); } - public function testReturnsRequestMessageForNotification() + public function testReturnsRequestMessageForNotification(): void { $notification = new RawMessage([], [ 'jsonrpc' => 2.0, @@ -56,7 +56,7 @@ public function testReturnsRequestMessageForNotification() $this->assertEquals(['one' => 'two'], $notification->params); } - public function testReturnsRequestMessageForResponse() + public function testReturnsRequestMessageForResponse(): void { $response = new RawMessage([], [ 'jsonrpc' => 2.0, diff --git a/tests/Unit/Core/Server/ClientApiTest.php b/tests/Unit/Core/Server/ClientApiTest.php index 09a00505..5ed741f8 100644 --- a/tests/Unit/Core/Server/ClientApiTest.php +++ b/tests/Unit/Core/Server/ClientApiTest.php @@ -37,13 +37,13 @@ public function testSend(Closure $executor, Closure $assertions): void public function provideWindowShowMessage(): Generator { yield [ - function (ClientApi $api) { + function (ClientApi $api): void { $api->window()->showMessage()->error('foobar'); $api->window()->showMessage()->log('foobar'); $api->window()->showMessage()->info('foobar'); $api->window()->showMessage()->warning('foobar'); }, - function (TestRpcClient $client) { + function (TestRpcClient $client): void { $message = $client->transmitter()->shiftNotification(); self::assertEquals('window/showMessage', $message->method); self::assertEquals(MessageType::ERROR, $message->params['type']); @@ -70,13 +70,13 @@ function (TestRpcClient $client) { public function provideWindowLogMessage(): Generator { yield [ - function (ClientApi $api) { + function (ClientApi $api): void { $api->window()->logMessage()->error('foobar'); $api->window()->logMessage()->log('foobar'); $api->window()->logMessage()->info('foobar'); $api->window()->logMessage()->warning('foobar'); }, - function (TestRpcClient $client) { + function (TestRpcClient $client): void { $message = $client->transmitter()->shiftNotification(); self::assertEquals('window/logMessage', $message->method); self::assertEquals(MessageType::ERROR, $message->params['type']); @@ -106,7 +106,7 @@ public function provideWindowShowMessageRequest(): Generator function (ClientApi $api) { return $api->window()->showMessageRequest()->info('foobar', new MessageActionItem('foobar')); }, - function (TestRpcClient $client, $result) { + function (TestRpcClient $client, $result): void { $client->responseWatcher()->resolveLastResponse(['title' => 'foobar']); $message = $client->transmitter()->shiftRequest(); self::assertEquals('window/showMessageRequest', $message->method); @@ -129,7 +129,7 @@ public function provideWorkspaceEdit(): Generator function (ClientApi $api) { return $api->workspace()->applyEdit(new WorkspaceEdit([])); }, - function (TestRpcClient $client, $result) { + function (TestRpcClient $client, $result): void { $client->responseWatcher()->resolveLastResponse([ 'applied' => false, 'failureReason' => 'sorry', @@ -154,7 +154,7 @@ public function provideWorkspaceExecuteCommand(): Generator function (ClientApi $api) { return $api->workspace()->executeCommand('one', ['one', 'two']); }, - function (TestRpcClient $client, $result) { + function (TestRpcClient $client, $result): void { $client->responseWatcher()->resolveLastResponse('result'); $message = $client->transmitter()->shiftRequest(); self::assertEquals('workspace/executeCommand', $message->method); @@ -171,7 +171,7 @@ function (TestRpcClient $client, $result) { public function provideDiagnostics(): Generator { yield [ - function (ClientApi $api) { + function (ClientApi $api): void { $api->diagnostics()->publishDiagnostics( 'file://file.php', 1, @@ -179,7 +179,7 @@ function (ClientApi $api) { ] ); }, - function (TestRpcClient $client, $result) { + function (TestRpcClient $client, $result): void { $message = $client->transmitter()->shiftNotification(); self::assertEquals('textDocument/publishDiagnostics', $message->method); } diff --git a/tests/Unit/Core/Server/Parser/LspMessageReaderTest.php b/tests/Unit/Core/Server/Parser/LspMessageReaderTest.php index 96dcf770..662a8591 100644 --- a/tests/Unit/Core/Server/Parser/LspMessageReaderTest.php +++ b/tests/Unit/Core/Server/Parser/LspMessageReaderTest.php @@ -18,19 +18,19 @@ protected function setUp(): void { } - public function testYieldsRequest() + public function testYieldsRequest(): void { $stream = new InMemoryStream( <<assertInstanceOf(RawMessage::class, $result); } - public function testReadsMultipleRequests() + public function testReadsMultipleRequests(): void { $stream = new InMemoryStream( <<wait()); diff --git a/tests/Unit/Core/Server/Transmitter/LspMessageFormatterTest.php b/tests/Unit/Core/Server/Transmitter/LspMessageFormatterTest.php index c418b6d8..2ec7bc60 100644 --- a/tests/Unit/Core/Server/Transmitter/LspMessageFormatterTest.php +++ b/tests/Unit/Core/Server/Transmitter/LspMessageFormatterTest.php @@ -12,7 +12,7 @@ class LspMessageFormatterTest extends TestCase /** * @dataProvider provideFormat */ - public function testFormat(string $serialized, int $expectedContentLength) + public function testFormat(string $serialized, int $expectedContentLength): void { $serializer = $this->prophesize(MessageSerializer::class); $formatter = new LspMessageFormatter($serializer->reveal()); diff --git a/tests/Unit/Core/Server/Transmitter/LspMessageSerializerTest.php b/tests/Unit/Core/Server/Transmitter/LspMessageSerializerTest.php index f895c09b..a21da54c 100644 --- a/tests/Unit/Core/Server/Transmitter/LspMessageSerializerTest.php +++ b/tests/Unit/Core/Server/Transmitter/LspMessageSerializerTest.php @@ -12,7 +12,7 @@ class LspMessageSerializerTest extends TestCase { - public function testExceptionCouldNotEncodeJson() + public function testExceptionCouldNotEncodeJson(): void { $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Could not encode JSON'); @@ -27,7 +27,7 @@ public function testExceptionCouldNotEncodeJson() /** * @dataProvider provideSerializes */ - public function testSerializes(Message $message, string $expected) + public function testSerializes(Message $message, string $expected): void { self::assertEquals($expected, $this->serialize($message)); } diff --git a/tests/Unit/Core/Service/ServiceManagerTest.php b/tests/Unit/Core/Service/ServiceManagerTest.php index d65e25e7..e8add0f7 100644 --- a/tests/Unit/Core/Service/ServiceManagerTest.php +++ b/tests/Unit/Core/Service/ServiceManagerTest.php @@ -268,7 +268,7 @@ public function services(): array public function exception(CancellationToken $cancel): Promise { - return \Amp\call(function () use ($cancel) { + return \Amp\call(function () use ($cancel): void { throw new Exception('No'); }); } diff --git a/tests/Unit/Core/TestLogger.php b/tests/Unit/Core/TestLogger.php index 35032781..6d2ad658 100644 --- a/tests/Unit/Core/TestLogger.php +++ b/tests/Unit/Core/TestLogger.php @@ -11,7 +11,7 @@ class TestLogger extends AbstractLogger /** * {@inheritDoc} */ - public function log($level, $message, array $context = []) + public function log($level, $message, array $context = []): void { $this->messages[] = sprintf('[%s] %s %s', $level, $message, json_encode($context)); } diff --git a/tests/Unit/Core/Workspace/WorkspaceTest.php b/tests/Unit/Core/Workspace/WorkspaceTest.php index b1aed3d1..1b2e9c86 100644 --- a/tests/Unit/Core/Workspace/WorkspaceTest.php +++ b/tests/Unit/Core/Workspace/WorkspaceTest.php @@ -36,7 +36,7 @@ public function testOpensDocument(): void $this->assertSame($expectedDocument, $document); } - public function testThrowsExceptionUpdateUnknown() + public function testThrowsExceptionUpdateUnknown(): void { $this->expectException(UnknownDocument::class); @@ -44,7 +44,7 @@ public function testThrowsExceptionUpdateUnknown() $this->workspace->update($expectedDocument, 'foobar'); } - public function testUpdatesDocument() + public function testUpdatesDocument(): void { $originalDocument = new TextDocumentItem('foobar', 'php', 1, 'foo'); $expectedDocument = new VersionedTextDocumentIdentifier($originalDocument->uri); @@ -59,7 +59,7 @@ public function testUpdatesDocument() /** * @dataProvider provideDoesNotUpdateDocumentWithLowerVersionThanExistingDocument */ - public function testDoesNotUpdateDocumentWithLowerVersionThanExistingDocument(int $originalVersion, ?int $newVersion, bool $shouldBeNewer) + public function testDoesNotUpdateDocumentWithLowerVersionThanExistingDocument(int $originalVersion, ?int $newVersion, bool $shouldBeNewer): void { $originalDocument = new TextDocumentItem('foobar', 'php', $originalVersion, 'original document'); @@ -96,7 +96,7 @@ public function provideDoesNotUpdateDocumentWithLowerVersionThanExistingDocument ]; } - public function testReturnsNumberOfOpenFiles() + public function testReturnsNumberOfOpenFiles(): void { $originalDocument = new TextDocumentItem('foobar', 'php', 1, 'foo'); $originalDocument->uri = 'foobar'; @@ -105,7 +105,7 @@ public function testReturnsNumberOfOpenFiles() $this->assertCount(1, $this->workspace); } - public function testRemoveDocument() + public function testRemoveDocument(): void { $originalDocument = new TextDocumentItem('foobar', 'php', 1, 'foo'); $originalDocument->uri = 'foobar'; @@ -119,7 +119,7 @@ public function testRemoveDocument() $this->assertCount(0, $this->workspace); } - public function testIteratesOverDocuments() + public function testIteratesOverDocuments(): void { $doc1 = new TextDocumentItem('foobar1', 'php', 1, 'foo'); $doc2 = new TextDocumentItem('foobar2', 'php', 1, 'foo'); diff --git a/tests/Unit/Handler/System/ExitHandlerTest.php b/tests/Unit/Handler/System/ExitHandlerTest.php index c94e6af9..20edf65b 100644 --- a/tests/Unit/Handler/System/ExitHandlerTest.php +++ b/tests/Unit/Handler/System/ExitHandlerTest.php @@ -14,13 +14,13 @@ public function handler(): Handler return new ExitHandler(); } - public function testThrowsExitSessionException() + public function testThrowsExitSessionException(): void { $this->expectException(ExitSession::class); $this->dispatch('exit', []); } - public function testShutdownDoesNothing() + public function testShutdownDoesNothing(): void { $this->dispatch('shutdown', []); $this->addToAssertionCount(1); diff --git a/tests/Unit/Handler/System/ServiceHandlerTest.php b/tests/Unit/Handler/System/ServiceHandlerTest.php index e0a0fe82..93080eae 100644 --- a/tests/Unit/Handler/System/ServiceHandlerTest.php +++ b/tests/Unit/Handler/System/ServiceHandlerTest.php @@ -57,7 +57,7 @@ public function testItStartsAService(): void ]); } - public function testItStopsAService() + public function testItStopsAService(): void { $this->serviceManager->stop('foobar')->shouldBeCalled(); $this->dispatch('phpactor/service/stop', [ @@ -66,7 +66,7 @@ public function testItStopsAService() ]); } - public function testReturnsRunningServices() + public function testReturnsRunningServices(): void { $this->serviceManager->runningServices()->willReturn([ 'one', 'two' diff --git a/tests/Unit/Handler/System/StatsHandlerTest.php b/tests/Unit/Handler/System/StatsHandlerTest.php index 2f456a89..ab5e8fd2 100644 --- a/tests/Unit/Handler/System/StatsHandlerTest.php +++ b/tests/Unit/Handler/System/StatsHandlerTest.php @@ -40,7 +40,7 @@ public function handler(): Handler return new StatsHandler($this->clientApi, $this->stats); } - public function testItReturnsTheCurrentSessionStatus() + public function testItReturnsTheCurrentSessionStatus(): void { $tester = LanguageServerTesterBuilder::create()->addHandler($this->handler())->build(); diff --git a/tests/Unit/Handler/TextDocument/TextDocumentHandlerTest.php b/tests/Unit/Handler/TextDocument/TextDocumentHandlerTest.php index 1683bd4c..8d39d1f5 100644 --- a/tests/Unit/Handler/TextDocument/TextDocumentHandlerTest.php +++ b/tests/Unit/Handler/TextDocument/TextDocumentHandlerTest.php @@ -51,7 +51,7 @@ public function testOpensDocument(): void $this->dispatcher->dispatch(new TextDocumentOpened($textDocument))->shouldHaveBeenCalled(); } - public function testUpdatesDocument() + public function testUpdatesDocument(): void { $textDocument = ProtocolFactory::textDocumentItem('foobar', 'foo'); $identifier = ProtocolFactory::versionedTextDocumentIdentifier('foobar'); @@ -78,7 +78,7 @@ public function testWillSave(): void self::assertNull($response->result); } - public function testClosesDocument() + public function testClosesDocument(): void { $document = new TextDocumentItem('foobar', 'php', 1, 'foo'); $document->uri = 'foobar'; @@ -95,7 +95,7 @@ public function testClosesDocument() $this->dispatcher->dispatch(new TextDocumentClosed($identifier))->shouldHaveBeenCalled(); } - public function testSavesDocument() + public function testSavesDocument(): void { $identifier = ProtocolFactory::versionedTextDocumentIdentifier('foobar'); $this->dispatch('textDocument/didSave', [ diff --git a/tests/Unit/Handler/Workspace/CommandHandlerTest.php b/tests/Unit/Handler/Workspace/CommandHandlerTest.php index 60fc724e..36710e29 100644 --- a/tests/Unit/Handler/Workspace/CommandHandlerTest.php +++ b/tests/Unit/Handler/Workspace/CommandHandlerTest.php @@ -17,7 +17,7 @@ public function handler(): Handler return $this->createHandler(); } - public function testExecutesCommand() + public function testExecutesCommand(): void { $result = $this->dispatch('workspace/executeCommand', [ 'command' => 'foobar', @@ -28,7 +28,7 @@ public function testExecutesCommand() self::assertEquals('barfoo', $result->result); } - public function testRegistersCapabilities() + public function testRegistersCapabilities(): void { $capabilities = new ServerCapabilities(); $this->createHandler()->registerCapabiltiies($capabilities); diff --git a/tests/Unit/LanguageServerBuilderTest.php b/tests/Unit/LanguageServerBuilderTest.php index deba2091..a3cc1645 100644 --- a/tests/Unit/LanguageServerBuilderTest.php +++ b/tests/Unit/LanguageServerBuilderTest.php @@ -11,7 +11,7 @@ class LanguageServerBuilderTest extends TestCase { public function testBuild(): void { - $server = LanguageServerBuilder::create(new ClosureDispatcherFactory(function () { + $server = LanguageServerBuilder::create(new ClosureDispatcherFactory(function (): void { })) ->tcpServer('127.0.0.1:8888') ->build(); diff --git a/tests/Unit/Middleware/ErrorHandlingMiddlewareTest.php b/tests/Unit/Middleware/ErrorHandlingMiddlewareTest.php index e9d4a511..7af8b223 100644 --- a/tests/Unit/Middleware/ErrorHandlingMiddlewareTest.php +++ b/tests/Unit/Middleware/ErrorHandlingMiddlewareTest.php @@ -54,7 +54,7 @@ public function testRethrowsServerControlException(): Generator yield $this->createMiddleware()->process( new NotificationMessage('foobar'), new RequestHandler([ - new ClosureMiddleware(function () { + new ClosureMiddleware(function (): void { throw new ExitSession('please'); }) ]) @@ -66,7 +66,7 @@ public function testLogsAndReturnsNullForNoticationException(): Generator $response = yield $this->createMiddleware()->process( new NotificationMessage('foobar'), new RequestHandler([ - new ClosureMiddleware(function () { + new ClosureMiddleware(function (): void { throw new RuntimeException('please'); }) ]) @@ -81,7 +81,7 @@ public function testDoesNotLogButReturnsErrorResponseForRequest(): Generator $response = yield $this->createMiddleware()->process( new RequestMessage(1, 'foobar', []), new RequestHandler([ - new ClosureMiddleware(function () { + new ClosureMiddleware(function (): void { throw new RuntimeException('please'); }) ]) @@ -99,7 +99,7 @@ public function testMethodNotFoundCodeForHandlerNotFound(): Generator $response = yield $this->createMiddleware()->process( new RequestMessage(1, 'foobar', []), new RequestHandler([ - new ClosureMiddleware(function () { + new ClosureMiddleware(function (): void { throw new HandlerNotFound('please'); }) ]) diff --git a/tests/Unit/Middleware/InitializeMiddlewareTest.php b/tests/Unit/Middleware/InitializeMiddlewareTest.php index 4452cd8c..ae577b24 100644 --- a/tests/Unit/Middleware/InitializeMiddlewareTest.php +++ b/tests/Unit/Middleware/InitializeMiddlewareTest.php @@ -106,7 +106,7 @@ public function testHandlersCanRegisterCapabiltiies(): Generator $handler->methods()->willReturn([ 'foo' => 'bar', ]); - $handler->registerCapabiltiies(Argument::type(ServerCapabilities::class))->will(function (array $args) { + $handler->registerCapabiltiies(Argument::type(ServerCapabilities::class))->will(function (array $args): void { $capabilities = $args[0]; assert($capabilities instanceof ServerCapabilities); $capabilities->hoverProvider = true; From 5537279c3ddebc24d141e088908266edfaa51420 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 31 Jan 2021 15:26:41 +0000 Subject: [PATCH 005/111] Use phpactor fork of event dispatcher --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 4214ac45..e1050de8 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "ergebnis/composer-normalize": "^2.0", "friendsofphp/php-cs-fixer": "^2.17", "jangregor/phpstan-prophecy": "^0.8.0", - "phly/phly-event-dispatcher": "^1.0", + "phpactor/phly-event-dispatcher": "^2.0", "phpactor/test-utils": "^1.1", "phpspec/prophecy-phpunit": "^2.0", "phpstan/phpstan": "~0.12.0", From 43da24b62f55e0042bafbd546df618bea0a61257 Mon Sep 17 00:00:00 2001 From: dantleech Date: Sun, 31 Jan 2021 16:45:14 +0000 Subject: [PATCH 006/111] Maestro adds support for PHP 8.0 (#26) --- composer.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index e1050de8..349cdcee 100644 --- a/composer.json +++ b/composer.json @@ -9,11 +9,11 @@ } ], "require": { - "php": "^7.3", + "php": "^7.3 || ^8.0", "amphp/socket": "^1.1", "dantleech/argument-resolver": "^1.1", "dantleech/invoke": "^1.1.2", - "phpactor/language-server-protocol": "~0.1", + "phpactor/language-server-protocol": "~0.1.1", "psr/event-dispatcher": "^1.0", "psr/log": "^1.0", "ramsey/uuid": "^4.0", @@ -24,8 +24,8 @@ "ergebnis/composer-normalize": "^2.0", "friendsofphp/php-cs-fixer": "^2.17", "jangregor/phpstan-prophecy": "^0.8.0", - "phpactor/phly-event-dispatcher": "^2.0", - "phpactor/test-utils": "^1.1", + "phpactor/phly-event-dispatcher": "~2.0.0", + "phpactor/test-utils": "~1.1.3", "phpspec/prophecy-phpunit": "^2.0", "phpstan/phpstan": "~0.12.0", "phpunit/phpunit": "^9.0", From d62d4494314443364d94efaddf5f371a15575b83 Mon Sep 17 00:00:00 2001 From: dantleech Date: Sun, 31 Jan 2021 16:48:52 +0000 Subject: [PATCH 007/111] Maestro adds support for PHP 8.0 (#30) --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b30fc14d..1b5084f4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,6 +87,7 @@ jobs: php-version: - '7.3' - '7.4' + - '8.0' steps: - From b595bcfc5fa3e733c6b11dd7cd885f1ed62dce31 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sat, 6 Feb 2021 14:38:53 +0000 Subject: [PATCH 008/111] Update deps --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 45320e6f..6abec623 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ /.php_cs.cache /stubs /doc/_build -/.vscode \ No newline at end of file +/.vscode +.php_cs.cache \ No newline at end of file From 29e3239691407a06c249a431edde37c80599db16 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sat, 6 Mar 2021 13:24:21 +0000 Subject: [PATCH 009/111] Ensure document version is updated on update --- lib/Core/Workspace/Workspace.php | 2 ++ tests/Unit/Core/Workspace/WorkspaceTest.php | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/lib/Core/Workspace/Workspace.php b/lib/Core/Workspace/Workspace.php index 21340cc9..28835363 100644 --- a/lib/Core/Workspace/Workspace.php +++ b/lib/Core/Workspace/Workspace.php @@ -87,6 +87,8 @@ public function update(VersionedTextDocumentIdentifier $textDocument, string $up } $this->documents[$textDocument->uri]->text = $updatedText; + $this->documents[$textDocument->uri]->version = $textDocument->version; + $this->documentVersions[$textDocument->uri] = $textDocument->version; } public function openFiles(): int diff --git a/tests/Unit/Core/Workspace/WorkspaceTest.php b/tests/Unit/Core/Workspace/WorkspaceTest.php index 1b2e9c86..2858927c 100644 --- a/tests/Unit/Core/Workspace/WorkspaceTest.php +++ b/tests/Unit/Core/Workspace/WorkspaceTest.php @@ -56,6 +56,17 @@ public function testUpdatesDocument(): void $this->assertEquals('my new text', $document->text); } + public function testUpdatesDocumentVersion(): void + { + $originalDocument = new TextDocumentItem('foobar', 'php', 1, 'foo'); + $expectedDocument = new VersionedTextDocumentIdentifier($originalDocument->uri, 5); + $this->workspace->open($originalDocument); + $this->workspace->update($expectedDocument, 'my new text'); + $document = $this->workspace->get('foobar'); + + $this->assertEquals(5, $document->version); + } + /** * @dataProvider provideDoesNotUpdateDocumentWithLowerVersionThanExistingDocument */ From d00472c50711f91091e6c93df89b435ecf386d6e Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 21 Mar 2021 09:36:06 +0000 Subject: [PATCH 010/111] Tolerate NULL version in diagnostics --- lib/Service/DiagnosticsService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Service/DiagnosticsService.php b/lib/Service/DiagnosticsService.php index 24c5f0c8..3e303007 100644 --- a/lib/Service/DiagnosticsService.php +++ b/lib/Service/DiagnosticsService.php @@ -95,7 +95,7 @@ public function enqueueSave(TextDocumentSaved $save): void $item = new TextDocumentItem( $save->identifier()->uri, 'php', - $save->identifier()->version, + $save->identifier()->version ?? 1, // VIM lsp client seems delivers NULL here, so just use an arbitrary identifier $save->text() ?: $this->workspace->get($save->identifier()->uri)->text ); From a3a552fde02ec6523270c9e222c7740d32aabd30 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 18 Apr 2021 10:32:18 +0100 Subject: [PATCH 011/111] Add support for client/[un]register capability --- lib/Core/Server/Client/ClientClient.php | 34 ++++++++++++++++++++++ lib/Core/Server/ClientApi.php | 6 ++++ tests/Unit/Core/Server/ClientApiTest.php | 37 ++++++++++++++++++++++++ 3 files changed, 77 insertions(+) create mode 100644 lib/Core/Server/Client/ClientClient.php diff --git a/lib/Core/Server/Client/ClientClient.php b/lib/Core/Server/Client/ClientClient.php new file mode 100644 index 00000000..959652d5 --- /dev/null +++ b/lib/Core/Server/Client/ClientClient.php @@ -0,0 +1,34 @@ +client = $client; + } + + public function registerCapability(Registration ...$registrations): void + { + $this->client->notification('client/registerCapability', [ + 'registrations' => $registrations + ]); + } + + public function unregisterCapability(Unregistration ...$unregistrations): void + { + $this->client->notification('client/unregisterCapability', [ + 'unregistrations' => $unregistrations + ]); + } +} diff --git a/lib/Core/Server/ClientApi.php b/lib/Core/Server/ClientApi.php index 578aaa0f..ca008baa 100644 --- a/lib/Core/Server/ClientApi.php +++ b/lib/Core/Server/ClientApi.php @@ -2,6 +2,7 @@ namespace Phpactor\LanguageServer\Core\Server; +use Phpactor\LanguageServer\Core\Server\Client\ClientClient; use Phpactor\LanguageServer\Core\Server\Client\DiagnosticsClient; use Phpactor\LanguageServer\Core\Server\Client\WindowClient; use Phpactor\LanguageServer\Core\Server\Client\WorkspaceClient; @@ -18,6 +19,11 @@ public function __construct(RpcClient $client) $this->client = $client; } + public function client(): ClientClient + { + return new ClientClient($this->client); + } + public function window(): WindowClient { return new WindowClient($this->client); diff --git a/tests/Unit/Core/Server/ClientApiTest.php b/tests/Unit/Core/Server/ClientApiTest.php index 5ed741f8..f676fd66 100644 --- a/tests/Unit/Core/Server/ClientApiTest.php +++ b/tests/Unit/Core/Server/ClientApiTest.php @@ -6,8 +6,12 @@ use Closure; use Generator; use Phpactor\LanguageServerProtocol\ApplyWorkspaceEditResponse; +use Phpactor\LanguageServerProtocol\DidChangeWatchedFilesRegistrationOptions; +use Phpactor\LanguageServerProtocol\FileSystemWatcher; use Phpactor\LanguageServerProtocol\MessageActionItem; use Phpactor\LanguageServerProtocol\MessageType; +use Phpactor\LanguageServerProtocol\Registration; +use Phpactor\LanguageServerProtocol\Unregistration; use Phpactor\LanguageServerProtocol\WorkspaceEdit; use Phpactor\LanguageServer\Core\Server\ClientApi; use Phpactor\LanguageServer\Core\Server\RpcClient\TestRpcClient; @@ -21,6 +25,7 @@ class ClientApiTest extends AsyncTestCase * @dataProvider provideWorkspaceEdit * @dataProvider provideWorkspaceExecuteCommand * @dataProvider provideDiagnostics + * @dataProvider provideRegisterCapability */ public function testSend(Closure $executor, Closure $assertions): void { @@ -185,4 +190,36 @@ function (TestRpcClient $client, $result): void { } ]; } + + /** + * @reuturn Generator + */ + public function provideRegisterCapability(): Generator + { + yield [ + function (ClientApi $api): void { + $api->client()->registerCapability( + new Registration('foobar', 'workspace/didChangeWatchedFiles', new DidChangeWatchedFilesRegistrationOptions([ + new FileSystemWatcher('**/*.php') + ])) + ); + }, + function (TestRpcClient $client, $result): void { + $message = $client->transmitter()->shiftNotification(); + self::assertEquals('client/registerCapability', $message->method); + } + ]; + + yield [ + function (ClientApi $api): void { + $api->client()->unregisterCapability( + new Unregistration('10', 'foo') + ); + }, + function (TestRpcClient $client, $result): void { + $message = $client->transmitter()->shiftNotification(); + self::assertEquals('client/unregisterCapability', $message->method); + } + ]; + } } From 58ba9fae03271993cc123d0b959d66c6a8bb0849 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 18 Apr 2021 10:38:06 +0100 Subject: [PATCH 012/111] Allow risky --- .php_cs.dist | 1 + 1 file changed, 1 insertion(+) diff --git a/.php_cs.dist b/.php_cs.dist index 2820b890..5cbd7d7a 100644 --- a/.php_cs.dist +++ b/.php_cs.dist @@ -9,6 +9,7 @@ $finder = PhpCsFixer\Finder::create() ; return PhpCsFixer\Config::create() + ->setRiskyAllowed(true) ->setRules([ '@PSR2' => true, 'no_unused_imports' => true, From 39cc4529acfbebf94db2de3cda6197029c37731d Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 18 Apr 2021 10:39:17 +0100 Subject: [PATCH 013/111] Fixed baseline --- phpstan-baseline.neon | 6 ------ 1 file changed, 6 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index de6d9257..a4df7731 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -4,9 +4,3 @@ parameters: message: "#^Method Phpactor\\\\LanguageServer\\\\Core\\\\Handler\\\\ClosureHandler\\:\\:handle\\(\\) has no return typehint specified\\.$#" count: 1 path: lib/Core/Handler/ClosureHandler.php - - - - message: "#^Unable to resolve the template type TValue in call to function Amp\\\\Promise\\\\any$#" - count: 1 - path: lib/Core/Server/LanguageServer.php - From 8a7b905625515ce16951d740285f3eb724ba6d47 Mon Sep 17 00:00:00 2001 From: dantleech Date: Sun, 18 Apr 2021 13:14:06 +0100 Subject: [PATCH 014/111] File events (#32) * Add support for file events * Seperate listener / handler for file events * Updated CL * SA fixes * Fix CS --- CHANGELOG.md | 5 ++ bin/serve.php | 6 ++ lib/Core/Server/Client/ClientClient.php | 16 +++-- lib/Event/FilesChanged.php | 23 ++++++++ .../DidChangeWatchedFilesHandler.php | 36 ++++++++++++ lib/LanguageServerTesterBuilder.php | 35 +++++++++++ .../DidChangeWatchedFilesListener.php | 58 +++++++++++++++++++ .../RecordingListenerProvider.php | 48 +++++++++++++++ tests/Unit/Core/Server/ClientApiTest.php | 4 +- tests/Unit/Handler/HandlerTestCase.php | 7 +++ .../DidChangeWatchedFilesHandlerTest.php | 40 +++++++++++++ 11 files changed, 272 insertions(+), 6 deletions(-) create mode 100644 lib/Event/FilesChanged.php create mode 100644 lib/Handler/Workspace/DidChangeWatchedFilesHandler.php create mode 100644 lib/Listener/DidChangeWatchedFilesListener.php create mode 100644 lib/Test/ListenerProvider/RecordingListenerProvider.php create mode 100644 tests/Unit/Handler/Workspace/DidChangeWatchedFilesHandlerTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d15f9af..93b9bcb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +1.0.2 +----- + +- Add support for file events + 0.2.0 ----- diff --git a/bin/serve.php b/bin/serve.php index a9009d49..0aa1a583 100755 --- a/bin/serve.php +++ b/bin/serve.php @@ -23,6 +23,8 @@ use Phpactor\LanguageServer\Example\Command\SayHelloCommand; use Phpactor\LanguageServer\Example\Diagnostics\SayHelloDiagnosticsProvider; use Phpactor\LanguageServer\Handler\TextDocument\CodeActionHandler; +use Phpactor\LanguageServer\Handler\Workspace\DidChangeWatchedFilesHandler; +use Phpactor\LanguageServer\Listener\DidChangeWatchedFilesListener; use Phpactor\LanguageServer\Listener\ServiceListener; use Phpactor\LanguageServer\Core\Service\ServiceManager; use Phpactor\LanguageServer\Core\Service\ServiceProviders; @@ -38,6 +40,7 @@ use Phpactor\LanguageServer\Middleware\HandlerMiddleware; use Phpactor\LanguageServer\Middleware\InitializeMiddleware; use Phpactor\LanguageServer\Core\Command\CommandDispatcher; +use Phpactor\LanguageServer\Middleware\ResponseHandlingMiddleware; use Phpactor\LanguageServer\Service\DiagnosticsService; use Psr\Log\AbstractLogger; use function Safe\fopen; @@ -107,6 +110,7 @@ function (MessageTransmitter $transmitter, InitializeParams $params) use ($logge $eventDispatcher = new AggregateEventDispatcher( new ServiceListener($serviceManager), new WorkspaceListener($workspace), + new DidChangeWatchedFilesListener($clientApi, ['**/*.php']), $diagnosticsService ); @@ -117,6 +121,7 @@ function (MessageTransmitter $transmitter, InitializeParams $params) use ($logge new CommandHandler(new CommandDispatcher([ 'phpactor.say_hello' => new SayHelloCommand($clientApi) ])), + new DidChangeWatchedFilesHandler($eventDispatcher), new CodeActionHandler(new AggregateCodeActionProvider( new SayHelloCodeActionProvider() ), $workspace), @@ -137,6 +142,7 @@ function (MessageTransmitter $transmitter, InitializeParams $params) use ($logge 'version' => 1, ]), new CancellationMiddleware($runner), + new ResponseHandlingMiddleware($responseWatcher), new HandlerMiddleware($runner) ); } diff --git a/lib/Core/Server/Client/ClientClient.php b/lib/Core/Server/Client/ClientClient.php index 959652d5..392e6214 100644 --- a/lib/Core/Server/Client/ClientClient.php +++ b/lib/Core/Server/Client/ClientClient.php @@ -2,8 +2,10 @@ namespace Phpactor\LanguageServer\Core\Server\Client; +use Amp\Promise; use Phpactor\LanguageServerProtocol\Registration; use Phpactor\LanguageServerProtocol\Unregistration; +use Phpactor\LanguageServer\Core\Rpc\ResponseMessage; use Phpactor\LanguageServer\Core\Server\RpcClient; final class ClientClient @@ -18,16 +20,22 @@ public function __construct(RpcClient $client) $this->client = $client; } - public function registerCapability(Registration ...$registrations): void + /** + * @return Promise + */ + public function registerCapability(Registration ...$registrations): Promise { - $this->client->notification('client/registerCapability', [ + return $this->client->request('client/registerCapability', [ 'registrations' => $registrations ]); } - public function unregisterCapability(Unregistration ...$unregistrations): void + /** + * @return Promise + */ + public function unregisterCapability(Unregistration ...$unregistrations): Promise { - $this->client->notification('client/unregisterCapability', [ + return $this->client->request('client/unregisterCapability', [ 'unregistrations' => $unregistrations ]); } diff --git a/lib/Event/FilesChanged.php b/lib/Event/FilesChanged.php new file mode 100644 index 00000000..8498190f --- /dev/null +++ b/lib/Event/FilesChanged.php @@ -0,0 +1,23 @@ +events = $events; + } + + public function events(): array + { + return $this->events; + } +} diff --git a/lib/Handler/Workspace/DidChangeWatchedFilesHandler.php b/lib/Handler/Workspace/DidChangeWatchedFilesHandler.php new file mode 100644 index 00000000..cc8995c5 --- /dev/null +++ b/lib/Handler/Workspace/DidChangeWatchedFilesHandler.php @@ -0,0 +1,36 @@ +dispatcher = $dispatcher; + } + + /** + * {@inheritDoc} + */ + public function methods(): array + { + return [ + 'workspace/didChangeWatchedFiles' => 'didChange' + ]; + } + + public function didChange(DidChangeWatchedFilesParams $params): void + { + $this->dispatcher->dispatch(new FilesChanged(...$params->changes)); + } +} diff --git a/lib/LanguageServerTesterBuilder.php b/lib/LanguageServerTesterBuilder.php index 2f6ea815..97f42401 100644 --- a/lib/LanguageServerTesterBuilder.php +++ b/lib/LanguageServerTesterBuilder.php @@ -25,6 +25,8 @@ use Phpactor\LanguageServer\Handler\System\ServiceHandler; use Phpactor\LanguageServer\Handler\TextDocument\TextDocumentHandler; use Phpactor\LanguageServer\Handler\Workspace\CommandHandler; +use Phpactor\LanguageServer\Handler\Workspace\DidChangeWatchedFilesHandler; +use Phpactor\LanguageServer\Listener\DidChangeWatchedFilesListener; use Phpactor\LanguageServer\Listener\ServiceListener; use Phpactor\LanguageServer\Listener\WorkspaceListener; use Phpactor\LanguageServer\Middleware\HandlerMiddleware; @@ -108,6 +110,11 @@ final class LanguageServerTesterBuilder */ private $enableServices = false; + /** + * @var bool + */ + private $enableFileEvents = false; + /** * @var bool */ @@ -128,6 +135,11 @@ final class LanguageServerTesterBuilder */ private $diagnosticsProvider = []; + /** + * @var string[] + */ + private $fileEventGlobs = ['**/*.php']; + private function __construct() { $this->initializeParams = new InitializeParams(new ClientCapabilities()); @@ -224,6 +236,21 @@ public function enableTextDocuments(): self return $this; } + /** + * Enable file events + * @param string[] $globs + */ + public function enableFileEvents(?array $globs = null): self + { + $this->enableFileEvents = true; + + if (null !==$globs) { + $this->fileEventGlobs = $globs; + } + + return $this; + } + /** * Enable the services (enabled by default with ::create) */ @@ -313,6 +340,10 @@ function (MessageTransmitter $transmitter, InitializeParams $params) { $this->listeners[] = $service; } + if ($this->enableFileEvents) { + $this->listeners[] = new DidChangeWatchedFilesListener($this->clientApi, $this->fileEventGlobs); + } + $serviceManager = new ServiceManager(new ServiceProviders(...$serviceProviders), $logger); $eventDispatcher = $this->buildEventDispatcher($serviceManager); @@ -326,6 +357,10 @@ function (MessageTransmitter $transmitter, InitializeParams $params) { $handlers[] = new ServiceHandler($serviceManager, $this->clientApi); } + if ($this->enableFileEvents) { + $handlers[] = new DidChangeWatchedFilesHandler($eventDispatcher); + } + if ($this->enableCommands) { $handlers[] = new CommandHandler(new CommandDispatcher($this->commands)); } diff --git a/lib/Listener/DidChangeWatchedFilesListener.php b/lib/Listener/DidChangeWatchedFilesListener.php new file mode 100644 index 00000000..2922dd92 --- /dev/null +++ b/lib/Listener/DidChangeWatchedFilesListener.php @@ -0,0 +1,58 @@ +client = $client; + $this->globPatterns = $globPatterns; + } + + /** + * {@inheritDoc} + */ + public function getListenersForEvent(object $event): iterable + { + if ($event instanceof Initialized) { + return [[$this, 'registerCapability']]; + } + + return []; + } + + public function registerCapability(Initialized $initialized): void + { + asyncCall(function () { + yield $this->client->client()->registerCapability( + new Registration( + Uuid::uuid4()->__toString(), + 'workspace/didChangeWatchedFiles', + new DidChangeWatchedFilesRegistrationOptions(array_map(function (string $glob) { + return new FileSystemWatcher($glob); + }, $this->globPatterns)) + ) + ); + }); + } +} diff --git a/lib/Test/ListenerProvider/RecordingListenerProvider.php b/lib/Test/ListenerProvider/RecordingListenerProvider.php new file mode 100644 index 00000000..b12e656e --- /dev/null +++ b/lib/Test/ListenerProvider/RecordingListenerProvider.php @@ -0,0 +1,48 @@ +recieved[] = $event; + } + ]; + } + + /** + * @param string $type + */ + public function shift(string $type): object + { + $next = array_shift($this->recieved); + + if (null === $next) { + throw new RuntimeException('No more events'); + } + + if (!$next instanceof $type) { + throw new RuntimeException(sprintf( + 'Expected event of type "%s" but got "%s"', + $type, + get_class($next) + )); + } + + return $next; + } +} diff --git a/tests/Unit/Core/Server/ClientApiTest.php b/tests/Unit/Core/Server/ClientApiTest.php index f676fd66..f13019e9 100644 --- a/tests/Unit/Core/Server/ClientApiTest.php +++ b/tests/Unit/Core/Server/ClientApiTest.php @@ -205,7 +205,7 @@ function (ClientApi $api): void { ); }, function (TestRpcClient $client, $result): void { - $message = $client->transmitter()->shiftNotification(); + $message = $client->transmitter()->shiftRequest(); self::assertEquals('client/registerCapability', $message->method); } ]; @@ -217,7 +217,7 @@ function (ClientApi $api): void { ); }, function (TestRpcClient $client, $result): void { - $message = $client->transmitter()->shiftNotification(); + $message = $client->transmitter()->shiftRequest(); self::assertEquals('client/unregisterCapability', $message->method); } ]; diff --git a/tests/Unit/Handler/HandlerTestCase.php b/tests/Unit/Handler/HandlerTestCase.php index fb5a4a6f..30189d67 100644 --- a/tests/Unit/Handler/HandlerTestCase.php +++ b/tests/Unit/Handler/HandlerTestCase.php @@ -16,4 +16,11 @@ public function dispatch(string $method, array $params) return $tester->requestAndWait($method, $params); } + + public function notify(string $method, array $params) + { + $tester = LanguageServerTesterBuilder::createBare()->addHandler($this->handler())->build(); + + return $tester->notifyAndWait($method, $params); + } } diff --git a/tests/Unit/Handler/Workspace/DidChangeWatchedFilesHandlerTest.php b/tests/Unit/Handler/Workspace/DidChangeWatchedFilesHandlerTest.php new file mode 100644 index 00000000..6bb3500e --- /dev/null +++ b/tests/Unit/Handler/Workspace/DidChangeWatchedFilesHandlerTest.php @@ -0,0 +1,40 @@ +enableFileEvents() + ->build(); + $tester->initialize(); + + // capability registration happens after the intiialization request has been returned + $this->addToAssertionCount(1); + } + + public function testEmitsFileChangedEvents(): void + { + $events = new RecordingListenerProvider(); + $tester = LanguageServerTesterBuilder::create() + ->enableFileEvents() + ->addListenerProvider($events) + ->build(); + + $tester->notifyAndWait('workspace/didChangeWatchedFiles', new DidChangeWatchedFilesParams([ + new FileEvent('file://foobar', FileChangeType::CREATED) + ])); + $event = $events->shift(FilesChanged::class); + self::assertEquals(new FilesChanged(new FileEvent('file://foobar', FileChangeType::CREATED)), $event); + } +} From 85fcf27be6ab2db16b942fd67a46e3ea4b188277 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 18 Apr 2021 13:28:38 +0100 Subject: [PATCH 015/111] Bump dev alias --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 349cdcee..8ee31b54 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "1.x-dev" } }, "autoload": { From a4fb3321241838bbe0d037a67243feb668afb05a Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sat, 24 Apr 2021 11:03:29 +0100 Subject: [PATCH 016/111] Add collection methods to files changed --- lib/Event/FilesChanged.php | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/lib/Event/FilesChanged.php b/lib/Event/FilesChanged.php index 8498190f..1404541e 100644 --- a/lib/Event/FilesChanged.php +++ b/lib/Event/FilesChanged.php @@ -2,7 +2,10 @@ namespace Phpactor\LanguageServer\Event; +use Phpactor\LanguageServerProtocol\FileChangeType; use Phpactor\LanguageServerProtocol\FileEvent; +use RuntimeException; +use function array_shift; final class FilesChanged { @@ -20,4 +23,24 @@ public function events(): array { return $this->events; } + + public function byType(int $type): self + { + return new self(...array_filter($this->events, function (FileEvent $event) use ($type) { + return $event->type === $type; + })); + } + + public function first(): FileEvent + { + $first = reset($this->events); + + if (null === $first) { + throw new RuntimeException( + 'No file events, cannot get the first one', + ); + } + + return $first; + } } From 2485314f7580b3e2b0ceb2bfb9318c4b0cccb5c3 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 25 Apr 2021 13:59:52 +0100 Subject: [PATCH 017/111] Allow minor versions of LSP --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 8ee31b54..5db84215 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ "amphp/socket": "^1.1", "dantleech/argument-resolver": "^1.1", "dantleech/invoke": "^1.1.2", - "phpactor/language-server-protocol": "~0.1.1", + "phpactor/language-server-protocol": "~0.1", "psr/event-dispatcher": "^1.0", "psr/log": "^1.0", "ramsey/uuid": "^4.0", From 22ff306b602617a310cfc8025c0d21f2220e2fce Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 25 Apr 2021 14:03:11 +0100 Subject: [PATCH 018/111] Fix check --- lib/Event/FilesChanged.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Event/FilesChanged.php b/lib/Event/FilesChanged.php index 1404541e..384827c1 100644 --- a/lib/Event/FilesChanged.php +++ b/lib/Event/FilesChanged.php @@ -35,7 +35,7 @@ public function first(): FileEvent { $first = reset($this->events); - if (null === $first) { + if (false === $first) { throw new RuntimeException( 'No file events, cannot get the first one', ); From 3475c59d76d8f5a7eb3d8745387f734dc057c8e4 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 25 Apr 2021 14:47:44 +0100 Subject: [PATCH 019/111] Fix CS --- lib/Event/FilesChanged.php | 2 -- lib/test.php | 0 2 files changed, 2 deletions(-) create mode 100644 lib/test.php diff --git a/lib/Event/FilesChanged.php b/lib/Event/FilesChanged.php index 384827c1..4a1b2f91 100644 --- a/lib/Event/FilesChanged.php +++ b/lib/Event/FilesChanged.php @@ -2,10 +2,8 @@ namespace Phpactor\LanguageServer\Event; -use Phpactor\LanguageServerProtocol\FileChangeType; use Phpactor\LanguageServerProtocol\FileEvent; use RuntimeException; -use function array_shift; final class FilesChanged { diff --git a/lib/test.php b/lib/test.php new file mode 100644 index 00000000..e69de29b From ac37100b331c9b79b4cbb9095c13c71976325782 Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Thu, 15 Jul 2021 09:18:50 +0200 Subject: [PATCH 020/111] stop loop on ShutdownServer (#34) --- lib/Core/Server/LanguageServer.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/Core/Server/LanguageServer.php b/lib/Core/Server/LanguageServer.php index f6d6e0bc..d3b47648 100644 --- a/lib/Core/Server/LanguageServer.php +++ b/lib/Core/Server/LanguageServer.php @@ -104,6 +104,11 @@ public function run(): void }); Loop::setErrorHandler(function (Throwable $error): void { + if ($error instanceof ShutdownServer) { + Loop::stop(); + return; + } + $this->logger->critical($error->getMessage()); throw $error; }); From b7c4068fe439ff26fb5b115d0e8dee3fd3400d42 Mon Sep 17 00:00:00 2001 From: Camille Dejoye Date: Sun, 19 Sep 2021 19:05:17 +0200 Subject: [PATCH 021/111] Handle progress messages (#36) * gitignore: add .phpunit.result.cache * php-cs-fixer: upgrade to ^3.0 * WorkDoneProgress: create a client with a basic implementation * WorkDoneProgress: add WorkDoneToken Just a VO to represent a token and hide the detail of its generation. It will be easier to maintain in time if there is changes in the spec. * WorkDoneProgress: add Notifier interface to handle clinet capability --- .gitignore | 4 +- .php_cs.dist => .php-cs-fixer.dist.php | 2 +- composer.json | 2 +- .../Server/Client/WorkDoneProgressClient.php | 95 ++++++++++++++ lib/Core/Server/ClientApi.php | 6 + lib/Core/Server/RpcClient/TestRpcClient.php | 1 - ...entCapabilityDependentProgressNotifier.php | 62 +++++++++ .../MessageProgressNotifier.php | 60 +++++++++ lib/WorkDoneProgress/ProgressNotifier.php | 37 ++++++ .../WorkDoneProgressNotifier.php | 58 +++++++++ lib/WorkDoneProgress/WorkDoneToken.php | 28 +++++ tests/Unit/Core/Server/ClientApiTest.php | 81 ++++++++++-- ...apabilityDependentProgressNotifierTest.php | 118 ++++++++++++++++++ 13 files changed, 543 insertions(+), 11 deletions(-) rename .php_cs.dist => .php-cs-fixer.dist.php (93%) create mode 100644 lib/Core/Server/Client/WorkDoneProgressClient.php create mode 100644 lib/WorkDoneProgress/ClientCapabilityDependentProgressNotifier.php create mode 100644 lib/WorkDoneProgress/MessageProgressNotifier.php create mode 100644 lib/WorkDoneProgress/ProgressNotifier.php create mode 100644 lib/WorkDoneProgress/WorkDoneProgressNotifier.php create mode 100644 lib/WorkDoneProgress/WorkDoneToken.php create mode 100644 tests/Unit/WorkDoneProgress/ClientCapabilityDependentProgressNotifierTest.php diff --git a/.gitignore b/.gitignore index 6abec623..a71328bb 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,6 @@ /stubs /doc/_build /.vscode -.php_cs.cache \ No newline at end of file +.php_cs.cache +.phpunit.result.cache +.php-cs-fixer.cache diff --git a/.php_cs.dist b/.php-cs-fixer.dist.php similarity index 93% rename from .php_cs.dist rename to .php-cs-fixer.dist.php index 5cbd7d7a..2c205486 100644 --- a/.php_cs.dist +++ b/.php-cs-fixer.dist.php @@ -8,7 +8,7 @@ ]) ; -return PhpCsFixer\Config::create() +return (new PhpCsFixer\Config()) ->setRiskyAllowed(true) ->setRules([ '@PSR2' => true, diff --git a/composer.json b/composer.json index 5db84215..079500f1 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ "require-dev": { "amphp/phpunit-util": "^1.3", "ergebnis/composer-normalize": "^2.0", - "friendsofphp/php-cs-fixer": "^2.17", + "friendsofphp/php-cs-fixer": "^3.0", "jangregor/phpstan-prophecy": "^0.8.0", "phpactor/phly-event-dispatcher": "~2.0.0", "phpactor/test-utils": "~1.1.3", diff --git a/lib/Core/Server/Client/WorkDoneProgressClient.php b/lib/Core/Server/Client/WorkDoneProgressClient.php new file mode 100644 index 00000000..5e6f0aff --- /dev/null +++ b/lib/Core/Server/Client/WorkDoneProgressClient.php @@ -0,0 +1,95 @@ +client = $client; + } + + /** + * @return Promise + */ + public function create(WorkDoneToken $token): Promise + { + return \Amp\call(function () use ($token) { + return yield $this->client->request('window/workDoneProgress/create', [ + 'token' => (string) $token, + ]); + }); + } + + public function begin( + WorkDoneToken $token, + string $title, + ?string $message = null, + ?int $percentage = null, + ?bool $cancellable = null + ): void { + self::assertIsValidPercentage($percentage); + + $this->notify($token, [ + 'kind' => 'begin', + 'title' => $title, + 'message' => $message, + 'percentage' => $percentage, + 'cancellable' => $cancellable, + ]); + } + + public function report( + WorkDoneToken $token, + ?string $message = null, + ?int $percentage = null, + ?bool $cancellable = null + ): void { + self::assertIsValidPercentage($percentage); + + $this->notify($token, [ + 'kind' => 'report', + 'message' => $message, + 'percentage' => $percentage, + 'cancellable' => $cancellable, + ]); + } + + public function end(WorkDoneToken $token, ?string $message = null): void + { + $this->notify($token, [ + 'kind' => 'end', + 'message' => $message, + ]); + } + + private static function assertIsValidPercentage(?int $percentage): void + { + if (!(null === $percentage || 0 <= $percentage && $percentage <= 100)) { + throw new InvalidArgumentException( + 'The percentage must be an integer comprised between 0 and 100.', + ); + } + } + + private function notify(WorkDoneToken $token, array $value): void + { + assert(in_array($value['kind'], ['begin', 'report', 'end'])); + + $this->client->notification('$/progress', [ + 'token' => (string) $token, + 'value' => $value, + ]); + } +} diff --git a/lib/Core/Server/ClientApi.php b/lib/Core/Server/ClientApi.php index ca008baa..ab29ec27 100644 --- a/lib/Core/Server/ClientApi.php +++ b/lib/Core/Server/ClientApi.php @@ -5,6 +5,7 @@ use Phpactor\LanguageServer\Core\Server\Client\ClientClient; use Phpactor\LanguageServer\Core\Server\Client\DiagnosticsClient; use Phpactor\LanguageServer\Core\Server\Client\WindowClient; +use Phpactor\LanguageServer\Core\Server\Client\WorkDoneProgressClient; use Phpactor\LanguageServer\Core\Server\Client\WorkspaceClient; final class ClientApi @@ -38,4 +39,9 @@ public function diagnostics(): DiagnosticsClient { return new DiagnosticsClient($this->client); } + + public function workDoneProgress(): WorkDoneProgressClient + { + return new WorkDoneProgressClient($this->client); + } } diff --git a/lib/Core/Server/RpcClient/TestRpcClient.php b/lib/Core/Server/RpcClient/TestRpcClient.php index ee530be3..40a71b86 100644 --- a/lib/Core/Server/RpcClient/TestRpcClient.php +++ b/lib/Core/Server/RpcClient/TestRpcClient.php @@ -3,7 +3,6 @@ namespace Phpactor\LanguageServer\Core\Server\RpcClient; use Amp\Promise; -use Phpactor\LanguageServer\Core\Server\ResponseWatcher; use Phpactor\LanguageServer\Core\Server\ResponseWatcher\TestResponseWatcher; use Phpactor\LanguageServer\Core\Server\RpcClient; use Phpactor\LanguageServer\Core\Server\Transmitter\TestMessageTransmitter; diff --git a/lib/WorkDoneProgress/ClientCapabilityDependentProgressNotifier.php b/lib/WorkDoneProgress/ClientCapabilityDependentProgressNotifier.php new file mode 100644 index 00000000..9a53ac2c --- /dev/null +++ b/lib/WorkDoneProgress/ClientCapabilityDependentProgressNotifier.php @@ -0,0 +1,62 @@ +window['workDoneProgress'] ?? false) { + $this->notifier = new WorkDoneProgressNotifier($api); + } else { + $this->notifier = new MessageProgressNotifier($api); + } + } + + /** + * {@inheritDoc} + */ + public function create(WorkDoneToken $token): Promise + { + return $this->notifier->create($token); + } + + /** + * {@inheritDoc} + */ + public function begin( + WorkDoneToken $token, + string $title, + ?string $message = null, + ?int $percentage = null, + ?bool $cancellable = null + ): void { + $this->notifier->begin($token, $title, $message, $percentage, $cancellable); + } + + /** + * {@inheritDoc} + */ + public function report( + WorkDoneToken $token, + ?string $message = null, + ?int $percentage = null, + ?bool $cancellable = null + ): void { + $this->notifier->report($token, $message, $percentage, $cancellable); + } + + public function end(WorkDoneToken $token, ?string $message = null): void + { + $this->notifier->end($token, $message); + } +} diff --git a/lib/WorkDoneProgress/MessageProgressNotifier.php b/lib/WorkDoneProgress/MessageProgressNotifier.php new file mode 100644 index 00000000..d7dccf44 --- /dev/null +++ b/lib/WorkDoneProgress/MessageProgressNotifier.php @@ -0,0 +1,60 @@ +api = $api->window()->showMessage(); + } + + /** + * {@inheritDoc} + */ + public function create(WorkDoneToken $token): Promise + { + return new Success(new ResponseMessage( + Uuid::uuid4(), + null, + )); + } + + /** + * {@inheritDoc} + */ + public function begin( + WorkDoneToken $token, + string $title, + ?string $message = null, + ?int $percentage = null, + ?bool $cancellable = null + ): void { + $this->api->info($message); + } + + /** + * {@inheritDoc} + */ + public function report(WorkDoneToken $token, ?string $message = null, ?int $percentage = null, ?bool $cancellable = null): void + { + $this->api->info(sprintf('%s - %d%%', $message, $percentage)); + } + + public function end(WorkDoneToken $token, ?string $message = null): void + { + $this->api->info($message); + } +} diff --git a/lib/WorkDoneProgress/ProgressNotifier.php b/lib/WorkDoneProgress/ProgressNotifier.php new file mode 100644 index 00000000..b69b0918 --- /dev/null +++ b/lib/WorkDoneProgress/ProgressNotifier.php @@ -0,0 +1,37 @@ + + */ + public function create(WorkDoneToken $token): Promise; + + /** + * @param int|null $percentage Percentage comprised between 0 and 100 + */ + public function begin( + WorkDoneToken $token, + string $title, + ?string $message = null, + ?int $percentage = null, + ?bool $cancellable = null + ): void; + + /** + * @param int|null $percentage Percentage comprised between 0 and 100 + */ + public function report( + WorkDoneToken $token, + ?string $message = null, + ?int $percentage = null, + ?bool $cancellable = null + ): void; + + public function end(WorkDoneToken $token, ?string $message = null): void; +} diff --git a/lib/WorkDoneProgress/WorkDoneProgressNotifier.php b/lib/WorkDoneProgress/WorkDoneProgressNotifier.php new file mode 100644 index 00000000..8a6973cf --- /dev/null +++ b/lib/WorkDoneProgress/WorkDoneProgressNotifier.php @@ -0,0 +1,58 @@ +api = $api->workDoneProgress(); + } + + /** + * {@inheritDoc} + */ + public function create(WorkDoneToken $token): Promise + { + return $this->api->create($token); + } + + /** + * {@inheritDoc} + */ + public function begin( + WorkDoneToken $token, + string $title, + ?string $message = null, + ?int $percentage = null, + ?bool $cancellable = null + ): void { + $this->api->begin($token, $title, $message, $percentage, $cancellable); + } + + /** + * {@inheritDoc} + */ + public function report( + WorkDoneToken $token, + ?string $message = null, + ?int $percentage = null, + ?bool $cancellable = null + ): void { + $this->api->report($token, $message, $percentage, $cancellable); + } + + public function end(WorkDoneToken $token, ?string $message = null): void + { + $this->api->end($token, $message); + } +} diff --git a/lib/WorkDoneProgress/WorkDoneToken.php b/lib/WorkDoneProgress/WorkDoneToken.php new file mode 100644 index 00000000..1ac3599b --- /dev/null +++ b/lib/WorkDoneProgress/WorkDoneToken.php @@ -0,0 +1,28 @@ +token = $token; + } + + public function __toString(): string + { + return $this->token; + } + + public static function generate(): self + { + return new self((string) Uuid::uuid4()); + } +} diff --git a/tests/Unit/Core/Server/ClientApiTest.php b/tests/Unit/Core/Server/ClientApiTest.php index f13019e9..0a8d6cf1 100644 --- a/tests/Unit/Core/Server/ClientApiTest.php +++ b/tests/Unit/Core/Server/ClientApiTest.php @@ -15,6 +15,7 @@ use Phpactor\LanguageServerProtocol\WorkspaceEdit; use Phpactor\LanguageServer\Core\Server\ClientApi; use Phpactor\LanguageServer\Core\Server\RpcClient\TestRpcClient; +use Phpactor\LanguageServer\WorkDoneProgress\WorkDoneToken; class ClientApiTest extends AsyncTestCase { @@ -26,6 +27,7 @@ class ClientApiTest extends AsyncTestCase * @dataProvider provideWorkspaceExecuteCommand * @dataProvider provideDiagnostics * @dataProvider provideRegisterCapability + * @dataProvider provideWorkDoneProgress */ public function testSend(Closure $executor, Closure $assertions): void { @@ -37,7 +39,7 @@ public function testSend(Closure $executor, Closure $assertions): void } /** - * @reuturn Generator + * @return Generator */ public function provideWindowShowMessage(): Generator { @@ -70,7 +72,7 @@ function (TestRpcClient $client): void { } /** - * @reuturn Generator + * @return Generator */ public function provideWindowLogMessage(): Generator { @@ -103,7 +105,7 @@ function (TestRpcClient $client): void { } /** - * @reuturn Generator + * @return Generator */ public function provideWindowShowMessageRequest(): Generator { @@ -126,7 +128,7 @@ function (TestRpcClient $client, $result): void { } /** - * @reuturn Generator + * @return Generator */ public function provideWorkspaceEdit(): Generator { @@ -151,7 +153,7 @@ function (TestRpcClient $client, $result): void { } /** - * @reuturn Generator + * @return Generator */ public function provideWorkspaceExecuteCommand(): Generator { @@ -171,7 +173,7 @@ function (TestRpcClient $client, $result): void { } /** - * @reuturn Generator + * @return Generator */ public function provideDiagnostics(): Generator { @@ -192,7 +194,7 @@ function (TestRpcClient $client, $result): void { } /** - * @reuturn Generator + * @return Generator */ public function provideRegisterCapability(): Generator { @@ -222,4 +224,69 @@ function (TestRpcClient $client, $result): void { } ]; } + + /** + * @return Generator + */ + public function provideWorkDoneProgress(): Generator + { + yield 'Creating Work Done Progress' => [ + function (ClientApi $api): void { + $token = new WorkDoneToken('4ef439b3-3c6a-4c98-ae0a-af2b4503cb15'); + $api->workDoneProgress()->create($token); + }, + function (TestRpcClient $client): void { + $message = $client->transmitter()->shiftRequest(); + self::assertEquals('window/workDoneProgress/create', $message->method); + self::assertEquals('4ef439b3-3c6a-4c98-ae0a-af2b4503cb15', $message->params['token']); + } + ]; + + yield 'Work Done Progress Begin' => [ + function (ClientApi $api): void { + $token = new WorkDoneToken('4ef439b3-3c6a-4c98-ae0a-af2b4503cb15'); + $api->workDoneProgress()->begin($token, 'title', 'message'); + }, + function (TestRpcClient $client, $result): void { + $message = $client->transmitter()->shiftNotification(); + self::assertEquals('$/progress', $message->method); + self::assertEquals('4ef439b3-3c6a-4c98-ae0a-af2b4503cb15', $message->params['token']); + self::assertEquals('begin', $message->params['value']['kind']); + self::assertEquals('title', $message->params['value']['title']); + self::assertEquals('message', $message->params['value']['message']); + self::assertNull($message->params['value']['percentage']); + self::assertNull($message->params['value']['cancellable']); + } + ]; + + yield 'Work Done Progress Report' => [ + function (ClientApi $api): void { + $token = new WorkDoneToken('4ef439b3-3c6a-4c98-ae0a-af2b4503cb15'); + $api->workDoneProgress()->report($token, 'message', 20); + }, + function (TestRpcClient $client, $result): void { + $message = $client->transmitter()->shiftNotification(); + self::assertEquals('$/progress', $message->method); + self::assertEquals('4ef439b3-3c6a-4c98-ae0a-af2b4503cb15', $message->params['token']); + self::assertEquals('report', $message->params['value']['kind']); + self::assertEquals('message', $message->params['value']['message']); + self::assertEquals(20, $message->params['value']['percentage']); + self::assertNull($message->params['value']['cancellable']); + } + ]; + + yield 'Work Done Progress End' => [ + function (ClientApi $api): void { + $token = new WorkDoneToken('4ef439b3-3c6a-4c98-ae0a-af2b4503cb15'); + $api->workDoneProgress()->end($token, 'message'); + }, + function (TestRpcClient $client, $result): void { + $message = $client->transmitter()->shiftNotification(); + self::assertEquals('$/progress', $message->method); + self::assertEquals('4ef439b3-3c6a-4c98-ae0a-af2b4503cb15', $message->params['token']); + self::assertEquals('end', $message->params['value']['kind']); + self::assertEquals('message', $message->params['value']['message']); + } + ]; + } } diff --git a/tests/Unit/WorkDoneProgress/ClientCapabilityDependentProgressNotifierTest.php b/tests/Unit/WorkDoneProgress/ClientCapabilityDependentProgressNotifierTest.php new file mode 100644 index 00000000..5ae9ac10 --- /dev/null +++ b/tests/Unit/WorkDoneProgress/ClientCapabilityDependentProgressNotifierTest.php @@ -0,0 +1,118 @@ +client = TestRpcClient::create(); + $this->transmitter = $this->client->transmitter(); + } + + public function testNotifyWithWorkDoneProgressCapability(): void + { + $token = WorkDoneToken::generate(); + $notifier = $this->createNotifierWithProgressCapability(); + + $notifier->create($token); + $message = $this->transmitter->shiftRequest(); + $this->assertEquals('window/workDoneProgress/create', $message->method); + $this->assertEquals((string) $token, $message->params['token']); + + $notifier->begin($token, 'title', 'begin message'); + $message = $this->transmitter->shiftNotification(); + $this->assertEquals('$/progress', $message->method); + $this->assertEquals((string) $token, $message->params['token']); + $this->assertEquals('begin', $message->params['value']['kind']); + $this->assertEquals('title', $message->params['value']['title']); + $this->assertEquals('begin message', $message->params['value']['message']); + $this->assertNull($message->params['value']['percentage']); + $this->assertNull($message->params['value']['cancellable']); + + $notifier->report($token, 'report message', 30); + $message = $this->transmitter->shiftNotification(); + $this->assertEquals('$/progress', $message->method); + $this->assertEquals((string) $token, $message->params['token']); + $this->assertEquals('report', $message->params['value']['kind']); + $this->assertEquals('report message', $message->params['value']['message']); + $this->assertEquals(30, $message->params['value']['percentage']); + $this->assertNull($message->params['value']['cancellable']); + + $notifier->end($token, 'end message'); + $message = $this->transmitter->shiftNotification(); + $this->assertEquals('$/progress', $message->method); + $this->assertEquals((string) $token, $message->params['token']); + $this->assertEquals('end', $message->params['value']['kind']); + $this->assertEquals('end message', $message->params['value']['message']); + } + + public function testNotifyWithoutWorkDoneProgressCapability(): void + { + $token = WorkDoneToken::generate(); + $notifier = $this->createNotifierWithoutProgressCapability(); + + $notifier->create($token); + $message = $this->transmitter->shiftNotification(); + $this->assertNull($message); // Fake response so no message sent + + $notifier->begin($token, 'title', 'begin message'); + $message = $this->transmitter->shiftNotification(); + $this->assertEquals('window/showMessage', $message->method); + $this->assertEquals(MessageType::INFO, $message->params['type']); + $this->assertEquals('begin message', $message->params['message']); + + $notifier->report($token, 'report message', 30); + $message = $this->transmitter->shiftNotification(); + $this->assertEquals('window/showMessage', $message->method); + $this->assertEquals(MessageType::INFO, $message->params['type']); + $this->assertEquals('report message - 30%', $message->params['message']); + + $notifier->end($token, 'end message'); + $message = $this->transmitter->shiftNotification(); + $this->assertEquals('window/showMessage', $message->method); + $this->assertEquals(MessageType::INFO, $message->params['type']); + $this->assertEquals('end message', $message->params['message']); + } + + private function createNotifierWithProgressCapability() : ClientCapabilityDependentProgressNotifier + { + return $this->createNotifier(true); + } + + private function createNotifierWithoutProgressCapability() : ClientCapabilityDependentProgressNotifier + { + return $this->createNotifier(false); + } + + private function createNotifier(bool $WorkDoneProgress) : ClientCapabilityDependentProgressNotifier + { + $api = new ClientApi($this->client); + $capabilities = ClientCapabilities::fromArray([ + 'window' => ['workDoneProgress' => $WorkDoneProgress], + ]); + + return new ClientCapabilityDependentProgressNotifier($api, $capabilities); + } +} From d0e034e33da14ce5c1d4cbd2ab2ef9c6d7363898 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 19 Sep 2021 18:09:38 +0100 Subject: [PATCH 022/111] Style fixes --- ...entCapabilityDependentProgressNotifier.php | 15 +++-- ...apabilityDependentProgressNotifierTest.php | 58 +++++++++---------- 2 files changed, 39 insertions(+), 34 deletions(-) diff --git a/lib/WorkDoneProgress/ClientCapabilityDependentProgressNotifier.php b/lib/WorkDoneProgress/ClientCapabilityDependentProgressNotifier.php index 9a53ac2c..b38f65ef 100644 --- a/lib/WorkDoneProgress/ClientCapabilityDependentProgressNotifier.php +++ b/lib/WorkDoneProgress/ClientCapabilityDependentProgressNotifier.php @@ -15,11 +15,7 @@ final class ClientCapabilityDependentProgressNotifier implements ProgressNotifie public function __construct(ClientApi $api, ClientCapabilities $capabilities) { - if ($capabilities->window['workDoneProgress'] ?? false) { - $this->notifier = new WorkDoneProgressNotifier($api); - } else { - $this->notifier = new MessageProgressNotifier($api); - } + $this->notifier = $this->createNotifier($api, $capabilities); } /** @@ -59,4 +55,13 @@ public function end(WorkDoneToken $token, ?string $message = null): void { $this->notifier->end($token, $message); } + + private function createNotifier(ClientApi $api, ClientCapabilities $capabilities): ProgressNotifier + { + if ($capabilities->window['workDoneProgress'] ?? false) { + return new WorkDoneProgressNotifier($api); + } + + return new MessageProgressNotifier($api); + } } diff --git a/tests/Unit/WorkDoneProgress/ClientCapabilityDependentProgressNotifierTest.php b/tests/Unit/WorkDoneProgress/ClientCapabilityDependentProgressNotifierTest.php index 5ae9ac10..4f7d7c95 100644 --- a/tests/Unit/WorkDoneProgress/ClientCapabilityDependentProgressNotifierTest.php +++ b/tests/Unit/WorkDoneProgress/ClientCapabilityDependentProgressNotifierTest.php @@ -38,34 +38,34 @@ public function testNotifyWithWorkDoneProgressCapability(): void $notifier->create($token); $message = $this->transmitter->shiftRequest(); - $this->assertEquals('window/workDoneProgress/create', $message->method); - $this->assertEquals((string) $token, $message->params['token']); + self::assertEquals('window/workDoneProgress/create', $message->method); + self::assertEquals((string) $token, $message->params['token']); $notifier->begin($token, 'title', 'begin message'); $message = $this->transmitter->shiftNotification(); - $this->assertEquals('$/progress', $message->method); - $this->assertEquals((string) $token, $message->params['token']); - $this->assertEquals('begin', $message->params['value']['kind']); - $this->assertEquals('title', $message->params['value']['title']); - $this->assertEquals('begin message', $message->params['value']['message']); - $this->assertNull($message->params['value']['percentage']); - $this->assertNull($message->params['value']['cancellable']); + self::assertEquals('$/progress', $message->method); + self::assertEquals((string) $token, $message->params['token']); + self::assertEquals('begin', $message->params['value']['kind']); + self::assertEquals('title', $message->params['value']['title']); + self::assertEquals('begin message', $message->params['value']['message']); + self::assertNull($message->params['value']['percentage']); + self::assertNull($message->params['value']['cancellable']); $notifier->report($token, 'report message', 30); $message = $this->transmitter->shiftNotification(); - $this->assertEquals('$/progress', $message->method); - $this->assertEquals((string) $token, $message->params['token']); - $this->assertEquals('report', $message->params['value']['kind']); - $this->assertEquals('report message', $message->params['value']['message']); - $this->assertEquals(30, $message->params['value']['percentage']); - $this->assertNull($message->params['value']['cancellable']); + self::assertEquals('$/progress', $message->method); + self::assertEquals((string) $token, $message->params['token']); + self::assertEquals('report', $message->params['value']['kind']); + self::assertEquals('report message', $message->params['value']['message']); + self::assertEquals(30, $message->params['value']['percentage']); + self::assertNull($message->params['value']['cancellable']); $notifier->end($token, 'end message'); $message = $this->transmitter->shiftNotification(); - $this->assertEquals('$/progress', $message->method); - $this->assertEquals((string) $token, $message->params['token']); - $this->assertEquals('end', $message->params['value']['kind']); - $this->assertEquals('end message', $message->params['value']['message']); + self::assertEquals('$/progress', $message->method); + self::assertEquals((string) $token, $message->params['token']); + self::assertEquals('end', $message->params['value']['kind']); + self::assertEquals('end message', $message->params['value']['message']); } public function testNotifyWithoutWorkDoneProgressCapability(): void @@ -75,25 +75,25 @@ public function testNotifyWithoutWorkDoneProgressCapability(): void $notifier->create($token); $message = $this->transmitter->shiftNotification(); - $this->assertNull($message); // Fake response so no message sent + self::assertNull($message); // Fake response so no message sent $notifier->begin($token, 'title', 'begin message'); $message = $this->transmitter->shiftNotification(); - $this->assertEquals('window/showMessage', $message->method); - $this->assertEquals(MessageType::INFO, $message->params['type']); - $this->assertEquals('begin message', $message->params['message']); + self::assertEquals('window/showMessage', $message->method); + self::assertEquals(MessageType::INFO, $message->params['type']); + self::assertEquals('begin message', $message->params['message']); $notifier->report($token, 'report message', 30); $message = $this->transmitter->shiftNotification(); - $this->assertEquals('window/showMessage', $message->method); - $this->assertEquals(MessageType::INFO, $message->params['type']); - $this->assertEquals('report message - 30%', $message->params['message']); + self::assertEquals('window/showMessage', $message->method); + self::assertEquals(MessageType::INFO, $message->params['type']); + self::assertEquals('report message - 30%', $message->params['message']); $notifier->end($token, 'end message'); $message = $this->transmitter->shiftNotification(); - $this->assertEquals('window/showMessage', $message->method); - $this->assertEquals(MessageType::INFO, $message->params['type']); - $this->assertEquals('end message', $message->params['message']); + self::assertEquals('window/showMessage', $message->method); + self::assertEquals(MessageType::INFO, $message->params['type']); + self::assertEquals('end message', $message->params['message']); } private function createNotifierWithProgressCapability() : ClientCapabilityDependentProgressNotifier From ed60d0f6a511acd1754457d53cbc4f9245c04ee4 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 19 Sep 2021 18:31:33 +0100 Subject: [PATCH 023/111] Adde ClosureCommand class for testing --- lib/Core/Command/ClosureCommand.php | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 lib/Core/Command/ClosureCommand.php diff --git a/lib/Core/Command/ClosureCommand.php b/lib/Core/Command/ClosureCommand.php new file mode 100644 index 00000000..1d22e139 --- /dev/null +++ b/lib/Core/Command/ClosureCommand.php @@ -0,0 +1,27 @@ +closure = $closure; + } + + /** + * @param mixed[] $args + * @return mixed + */ + public function __invoke(...$args) { + $closure = $this->closure; + return $closure(...$args); + } +} From 4484ff7a423bec2179b5c1e089cabad409b60c89 Mon Sep 17 00:00:00 2001 From: Camille Dejoye Date: Mon, 20 Sep 2021 13:37:12 +0200 Subject: [PATCH 024/111] Fix #1304 - Dynamic registration only if supported (#38) * DidChangeWatchedFilesListener: register only if supported Fixes https://github.com/phpactor/phpactor/issues/1304 * fix style --- bin/serve.php | 2 +- lib/Core/Command/ClosureCommand.php | 3 +- lib/LanguageServerTesterBuilder.php | 2 +- .../DidChangeWatchedFilesListener.php | 13 ++- .../DidChangeWatchedFilesListenerTest.php | 83 +++++++++++++++++++ 5 files changed, 99 insertions(+), 4 deletions(-) create mode 100644 tests/Unit/Listener/DidChangeWatchedFilesListenerTest.php diff --git a/bin/serve.php b/bin/serve.php index 0aa1a583..ab68661e 100755 --- a/bin/serve.php +++ b/bin/serve.php @@ -110,7 +110,7 @@ function (MessageTransmitter $transmitter, InitializeParams $params) use ($logge $eventDispatcher = new AggregateEventDispatcher( new ServiceListener($serviceManager), new WorkspaceListener($workspace), - new DidChangeWatchedFilesListener($clientApi, ['**/*.php']), + new DidChangeWatchedFilesListener($clientApi, ['**/*.php'], $params->capabilities), $diagnosticsService ); diff --git a/lib/Core/Command/ClosureCommand.php b/lib/Core/Command/ClosureCommand.php index 1d22e139..bd745078 100644 --- a/lib/Core/Command/ClosureCommand.php +++ b/lib/Core/Command/ClosureCommand.php @@ -20,7 +20,8 @@ public function __construct(Closure $closure) * @param mixed[] $args * @return mixed */ - public function __invoke(...$args) { + public function __invoke(...$args) + { $closure = $this->closure; return $closure(...$args); } diff --git a/lib/LanguageServerTesterBuilder.php b/lib/LanguageServerTesterBuilder.php index 97f42401..0247997d 100644 --- a/lib/LanguageServerTesterBuilder.php +++ b/lib/LanguageServerTesterBuilder.php @@ -341,7 +341,7 @@ function (MessageTransmitter $transmitter, InitializeParams $params) { } if ($this->enableFileEvents) { - $this->listeners[] = new DidChangeWatchedFilesListener($this->clientApi, $this->fileEventGlobs); + $this->listeners[] = new DidChangeWatchedFilesListener($this->clientApi, $this->fileEventGlobs, $params->capabilities); } $serviceManager = new ServiceManager(new ServiceProviders(...$serviceProviders), $logger); diff --git a/lib/Listener/DidChangeWatchedFilesListener.php b/lib/Listener/DidChangeWatchedFilesListener.php index 2922dd92..6a79e074 100644 --- a/lib/Listener/DidChangeWatchedFilesListener.php +++ b/lib/Listener/DidChangeWatchedFilesListener.php @@ -2,6 +2,7 @@ namespace Phpactor\LanguageServer\Listener; +use Phpactor\LanguageServerProtocol\ClientCapabilities; use Phpactor\LanguageServerProtocol\DidChangeWatchedFilesRegistrationOptions; use Phpactor\LanguageServerProtocol\FileSystemWatcher; use Phpactor\LanguageServerProtocol\Registration; @@ -23,10 +24,16 @@ class DidChangeWatchedFilesListener implements ListenerProviderInterface */ private $globPatterns; - public function __construct(ClientApi $client, array $globPatterns) + /** + * @var ClientCapabilities + */ + private $clientCapabilities; + + public function __construct(ClientApi $client, array $globPatterns, ClientCapabilities $clientCapabilities) { $this->client = $client; $this->globPatterns = $globPatterns; + $this->clientCapabilities = $clientCapabilities; } /** @@ -43,6 +50,10 @@ public function getListenersForEvent(object $event): iterable public function registerCapability(Initialized $initialized): void { + if (!($this->clientCapabilities->workspace['didChangeWatchedFiles']['dynamicRegistration'] ?? false)) { + return; + } + asyncCall(function () { yield $this->client->client()->registerCapability( new Registration( diff --git a/tests/Unit/Listener/DidChangeWatchedFilesListenerTest.php b/tests/Unit/Listener/DidChangeWatchedFilesListenerTest.php new file mode 100644 index 00000000..69a57ee4 --- /dev/null +++ b/tests/Unit/Listener/DidChangeWatchedFilesListenerTest.php @@ -0,0 +1,83 @@ +initListener(new ClientCapabilities([ + 'didChangeWatchedFiles' => ['dynamicRegistration' => true], + ])); + $this->dispatch( + new Initialized(), + ); + + self::assertCount(1, $this->transmitter); + + $request = $this->transmitter->shiftRequest(); + self::assertEquals('client/registerCapability', $request->method); + $registrations = $request->params['registrations'] ?? null; + self::assertIsArray($registrations); + self::assertCount(1, $registrations); + + $registration = $registrations[0]; + self::assertInstanceOf(Registration::class, $registration); + self::assertEquals('workspace/didChangeWatchedFiles', $registration->method); + + $options = $registration->registerOptions; + self::assertInstanceOf(DidChangeWatchedFilesRegistrationOptions::class, $options); + self::assertCount(1, $options->watchers); + + $watcher = $options->watchers[0]; + self::assertInstanceOf(FileSystemWatcher::class, $watcher); + self::assertEquals('*.php', $watcher->globPattern); + self::assertNull($watcher->kind); + } + + public function testDoesNotDynamicallyRegisterIfNotSupported(): void + { + $this->initListener(new ClientCapabilities()); + $this->dispatch( + new Initialized(), + ); + + self::assertCount(0, $this->transmitter); + } + + protected function initListener(ClientCapabilities $clientCapabilities): void + { + $client = TestRpcClient::create(); + $api = new ClientApi($client); + $this->transmitter = $client->transmitter(); + $this->listener = new DidChangeWatchedFilesListener($api, ['*.php'], $clientCapabilities); + } + + private function dispatch(object $event): void + { + foreach ($this->listener->getListenersForEvent($event) as $listener) { + $listener($event); + }; + } +} From 302e70d85b39ad0d5c368900b94b7cd6d3b77a05 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Thu, 10 Feb 2022 20:14:44 +0100 Subject: [PATCH 025/111] Bump inovoke dep --- composer.json | 2 +- .../Core/Server/Transmitter/LspMessageSerializerTest.php | 2 +- .../ListenerProvider/RecordingListenerProviderTest.php | 9 +++++++++ 3 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 tests/Unit/Test/ListenerProvider/RecordingListenerProviderTest.php diff --git a/composer.json b/composer.json index 079500f1..37c8564b 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,7 @@ "php": "^7.3 || ^8.0", "amphp/socket": "^1.1", "dantleech/argument-resolver": "^1.1", - "dantleech/invoke": "^1.1.2", + "dantleech/invoke": "^2.0", "phpactor/language-server-protocol": "~0.1", "psr/event-dispatcher": "^1.0", "psr/log": "^1.0", diff --git a/tests/Unit/Core/Server/Transmitter/LspMessageSerializerTest.php b/tests/Unit/Core/Server/Transmitter/LspMessageSerializerTest.php index a21da54c..010105c4 100644 --- a/tests/Unit/Core/Server/Transmitter/LspMessageSerializerTest.php +++ b/tests/Unit/Core/Server/Transmitter/LspMessageSerializerTest.php @@ -29,7 +29,7 @@ public function testExceptionCouldNotEncodeJson(): void */ public function testSerializes(Message $message, string $expected): void { - self::assertEquals($expected, $this->serialize($message)); + self::assertJsonStringEqualsJsonString($expected, $this->serialize($message)); } public function provideSerializes() diff --git a/tests/Unit/Test/ListenerProvider/RecordingListenerProviderTest.php b/tests/Unit/Test/ListenerProvider/RecordingListenerProviderTest.php new file mode 100644 index 00000000..94d3bf19 --- /dev/null +++ b/tests/Unit/Test/ListenerProvider/RecordingListenerProviderTest.php @@ -0,0 +1,9 @@ + Date: Tue, 8 Mar 2022 13:16:18 +0100 Subject: [PATCH 026/111] Bump to 7.4 --- composer.json | 2 +- lib/Core/Service/ServiceProviders.php | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 37c8564b..0c978bbc 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,7 @@ } ], "require": { - "php": "^7.3 || ^8.0", + "php": "^7.4 || ^8.0", "amphp/socket": "^1.1", "dantleech/argument-resolver": "^1.1", "dantleech/invoke": "^2.0", diff --git a/lib/Core/Service/ServiceProviders.php b/lib/Core/Service/ServiceProviders.php index 9174df41..4f3aa44a 100644 --- a/lib/Core/Service/ServiceProviders.php +++ b/lib/Core/Service/ServiceProviders.php @@ -7,6 +7,7 @@ use Countable; use Phpactor\LanguageServer\Core\Service\Exception\UnknownService; +use Traversable; /** * @implements IteratorAggregate @@ -30,12 +31,12 @@ public function __construct(ServiceProvider ...$serviceProviders) /** * {@inheritDoc} */ - public function count() + public function count(): int { return count($this->services); } - public function getIterator() + public function getIterator(): Traversable { return new ArrayIterator($this->services); } From c4cde68ae3e40d551975dfb757fcde258961f05b Mon Sep 17 00:00:00 2001 From: dantleech Date: Sun, 17 Apr 2022 23:41:47 +0200 Subject: [PATCH 027/111] Bump PHP version in CI (#43) --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1b5084f4..a4692f6f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: strategy: matrix: php-version: - - '7.3' + - '7.4' steps: - @@ -52,7 +52,7 @@ jobs: strategy: matrix: php-version: - - '7.3' + - '7.4' steps: - @@ -85,9 +85,9 @@ jobs: strategy: matrix: php-version: - - '7.3' - '7.4' - '8.0' + - '8.1' steps: - From 36eaee364f78e126a2d17343275930e6e2302d89 Mon Sep 17 00:00:00 2001 From: mamazu <14860264+mamazu@users.noreply.github.com> Date: Mon, 18 Apr 2022 11:36:32 +0200 Subject: [PATCH 028/111] Adding a test for invalid messages in LSP reader (#42) --- .../Server/Parser/LspMessageReaderTest.php | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/Unit/Core/Server/Parser/LspMessageReaderTest.php b/tests/Unit/Core/Server/Parser/LspMessageReaderTest.php index 662a8591..6ac7a3a2 100644 --- a/tests/Unit/Core/Server/Parser/LspMessageReaderTest.php +++ b/tests/Unit/Core/Server/Parser/LspMessageReaderTest.php @@ -3,6 +3,7 @@ namespace Phpactor\LanguageServer\Tests\Unit\Core\Server\Parser; use Amp\ByteStream\InMemoryStream; +use Phpactor\LanguageServer\Core\Server\Parser\Exception\CouldNotDecodeBody; use Phpactor\TestUtils\PHPUnit\TestCase; use Phpactor\LanguageServer\Core\Server\Parser\LspMessageReader; use Phpactor\LanguageServer\Core\Rpc\RawMessage; @@ -65,4 +66,24 @@ public function testReadsMultipleRequests(): void $result = \Amp\Promise\wait($reader->wait()); $this->assertInstanceOf(RawMessage::class, $result, 'second'); } + + public function testReadingAnInvalidStream(): void + { + $stream = new InMemoryStream( + <<expectException(CouldNotDecodeBody::class); + + $reader = new LspMessageReader($stream); + $result = \Amp\Promise\wait($reader->wait()); + } } From 27eb17c96967e54cd7063855b9a02424fda4e4c5 Mon Sep 17 00:00:00 2001 From: dantleech Date: Wed, 20 Apr 2022 22:26:06 +0200 Subject: [PATCH 029/111] Will shutdown (#44) * Dispatch willShutdown event * UPdate phpstan * Add test for stop all * Add test for listener * Give diagnostic provider names * Removed baseline * Fix * CS --- composer.json | 5 +- .../AggregateDiagnosticsProvider.php | 8 +++ .../ClosureDiagnosticsProvider.php | 13 ++-- lib/Core/Diagnostics/DiagnosticsProvider.php | 2 + lib/Core/Handler/ClosureHandler.php | 3 + lib/Core/Server/LanguageServer.php | 6 -- lib/Core/Service/ServiceManager.php | 7 ++ lib/Core/Service/ServiceProviders.php | 2 +- lib/Core/Workspace/Workspace.php | 5 -- .../CodeActionDiagnosticsProvider.php | 5 ++ lib/Event/WillShutdown.php | 7 ++ .../SayHelloDiagnosticsProvider.php | 5 ++ lib/Handler/System/ExitHandler.php | 19 +++++- .../TextDocument/TextDocumentHandler.php | 6 -- lib/LanguageServerTesterBuilder.php | 6 -- lib/Listener/ServiceListener.php | 8 +++ phpstan-baseline.neon | 6 -- phpstan.neon | 2 - .../Unit/Core/Service/ServiceManagerTest.php | 22 ++++++- .../AggregateDiagnosticsProviderTest.php | 65 +++++++++++++++++++ tests/Unit/Handler/System/ExitHandlerTest.php | 21 +++++- .../TextDocument/TextDocumentHandlerTest.php | 3 + tests/Unit/Listener/ServiceListenerTest.php | 8 +++ 23 files changed, 192 insertions(+), 42 deletions(-) create mode 100644 lib/Event/WillShutdown.php delete mode 100644 phpstan-baseline.neon create mode 100644 tests/Unit/Diagnostics/AggregateDiagnosticsProviderTest.php diff --git a/composer.json b/composer.json index 0c978bbc..7e7092e5 100644 --- a/composer.json +++ b/composer.json @@ -23,12 +23,13 @@ "amphp/phpunit-util": "^1.3", "ergebnis/composer-normalize": "^2.0", "friendsofphp/php-cs-fixer": "^3.0", - "jangregor/phpstan-prophecy": "^0.8.0", + "jangregor/phpstan-prophecy": "^1.0", "phpactor/phly-event-dispatcher": "~2.0.0", "phpactor/test-utils": "~1.1.3", "phpspec/prophecy-phpunit": "^2.0", - "phpstan/phpstan": "~0.12.0", + "phpstan/phpstan": "^1.0", "phpunit/phpunit": "^9.0", + "phpstan/extension-installer": "^1.1", "symfony/var-dumper": "^5.1" }, "extra": { diff --git a/lib/Core/Diagnostics/AggregateDiagnosticsProvider.php b/lib/Core/Diagnostics/AggregateDiagnosticsProvider.php index 9584d07f..0d5c501a 100644 --- a/lib/Core/Diagnostics/AggregateDiagnosticsProvider.php +++ b/lib/Core/Diagnostics/AggregateDiagnosticsProvider.php @@ -59,4 +59,12 @@ public function provideDiagnostics(TextDocumentItem $textDocument): Promise return $diagnostics; }); } + + public function name(): string + { + return implode(', ', array_map( + fn (DiagnosticsProvider $provider) => $provider->name(), + $this->providers + )); + } } diff --git a/lib/Core/Diagnostics/ClosureDiagnosticsProvider.php b/lib/Core/Diagnostics/ClosureDiagnosticsProvider.php index ba84b67c..86df8384 100644 --- a/lib/Core/Diagnostics/ClosureDiagnosticsProvider.php +++ b/lib/Core/Diagnostics/ClosureDiagnosticsProvider.php @@ -13,18 +13,23 @@ class ClosureDiagnosticsProvider implements DiagnosticsProvider */ private $closure; - public function __construct(Closure $closure) + private string $name; + + public function __construct(Closure $closure, string $name = 'closure') { $this->closure = $closure; + $this->name = $name; } - /** - * {@inheritDoc} - */ public function provideDiagnostics(TextDocumentItem $textDocument): Promise { $closure = $this->closure; return $closure($textDocument); } + + public function name(): string + { + return $this->name; + } } diff --git a/lib/Core/Diagnostics/DiagnosticsProvider.php b/lib/Core/Diagnostics/DiagnosticsProvider.php index 6c603db7..e8c9518e 100644 --- a/lib/Core/Diagnostics/DiagnosticsProvider.php +++ b/lib/Core/Diagnostics/DiagnosticsProvider.php @@ -12,4 +12,6 @@ interface DiagnosticsProvider * @return Promise> */ public function provideDiagnostics(TextDocumentItem $textDocument): Promise; + + public function name(): string; } diff --git a/lib/Core/Handler/ClosureHandler.php b/lib/Core/Handler/ClosureHandler.php index 0a5cacd9..2675ee57 100644 --- a/lib/Core/Handler/ClosureHandler.php +++ b/lib/Core/Handler/ClosureHandler.php @@ -32,6 +32,9 @@ public function methods(): array ]; } + /** + * @return mixed + */ public function handle() { $args = func_get_args(); diff --git a/lib/Core/Server/LanguageServer.php b/lib/Core/Server/LanguageServer.php index d3b47648..d3329bb0 100644 --- a/lib/Core/Server/LanguageServer.php +++ b/lib/Core/Server/LanguageServer.php @@ -9,7 +9,6 @@ use Phpactor\LanguageServer\Core\Rpc\Exception\CouldNotCreateMessage; use Phpactor\LanguageServer\Core\Rpc\Message; use Phpactor\LanguageServer\Core\Rpc\ResponseMessage; -use Phpactor\LanguageServer\Core\Server\Parser\RequestReader; use Phpactor\LanguageServer\Core\Server\Transmitter\ConnectionMessageTransmitter; use Phpactor\LanguageServer\Core\Server\Transmitter\MessageTransmitter; use Phpactor\LanguageServer\Core\Dispatcher\DispatcherFactory; @@ -30,11 +29,6 @@ final class LanguageServer { - /** - * @var RequestReader - */ - private $parser; - /** * @var LoggerInterface */ diff --git a/lib/Core/Service/ServiceManager.php b/lib/Core/Service/ServiceManager.php index 1405b6e3..301de2bc 100644 --- a/lib/Core/Service/ServiceManager.php +++ b/lib/Core/Service/ServiceManager.php @@ -125,4 +125,11 @@ public function isRunning(string $serviceName): bool return isset($this->cancellations[$serviceName]); } + + public function stopAll(): void + { + foreach ($this->serviceProviders->names() as $serviceName) { + $this->stop($serviceName); + } + } } diff --git a/lib/Core/Service/ServiceProviders.php b/lib/Core/Service/ServiceProviders.php index 4f3aa44a..2bb9bff2 100644 --- a/lib/Core/Service/ServiceProviders.php +++ b/lib/Core/Service/ServiceProviders.php @@ -23,7 +23,7 @@ public function __construct(ServiceProvider ...$serviceProviders) { foreach ($serviceProviders as $serviceProvider) { foreach ($serviceProvider->services() as $methodName) { - $this->services[$methodName] = $serviceProvider; + $this->services[(string)$methodName] = $serviceProvider; } } } diff --git a/lib/Core/Workspace/Workspace.php b/lib/Core/Workspace/Workspace.php index 28835363..46b2eb64 100644 --- a/lib/Core/Workspace/Workspace.php +++ b/lib/Core/Workspace/Workspace.php @@ -27,11 +27,6 @@ class Workspace implements Countable, IteratorAggregate */ private $documentVersions = []; - /** - * @var int - */ - private $processId; - /** * @var LoggerInterface|null */ diff --git a/lib/Diagnostics/CodeActionDiagnosticsProvider.php b/lib/Diagnostics/CodeActionDiagnosticsProvider.php index d1803bff..7fc057a7 100644 --- a/lib/Diagnostics/CodeActionDiagnosticsProvider.php +++ b/lib/Diagnostics/CodeActionDiagnosticsProvider.php @@ -45,4 +45,9 @@ public function provideDiagnostics(TextDocumentItem $textDocument): Promise return $diagnostics; }); } + + public function name(): string + { + return 'code-action'; + } } diff --git a/lib/Event/WillShutdown.php b/lib/Event/WillShutdown.php new file mode 100644 index 00000000..e51289a7 --- /dev/null +++ b/lib/Event/WillShutdown.php @@ -0,0 +1,7 @@ +eventDispatcher = $eventDispatcher ?: new NullEventDispatcher(); + } + public function methods(): array { return [ @@ -15,8 +27,13 @@ public function methods(): array ]; } - public function shutdown(): void + /** + * @return Promise + */ + public function shutdown(): Promise { + $this->eventDispatcher->dispatch(new WillShutdown()); + return new Success(null); } public function exit(): void diff --git a/lib/Handler/TextDocument/TextDocumentHandler.php b/lib/Handler/TextDocument/TextDocumentHandler.php index ae6b9e16..09efc108 100644 --- a/lib/Handler/TextDocument/TextDocumentHandler.php +++ b/lib/Handler/TextDocument/TextDocumentHandler.php @@ -11,7 +11,6 @@ use Phpactor\LanguageServerProtocol\WillSaveTextDocumentParams; use Phpactor\LanguageServer\Core\Handler\CanRegisterCapabilities; use Phpactor\LanguageServer\Core\Handler\Handler; -use Phpactor\LanguageServer\Core\Workspace\Workspace; use Phpactor\LanguageServer\Event\TextDocumentClosed; use Phpactor\LanguageServer\Event\TextDocumentOpened; use Phpactor\LanguageServer\Event\TextDocumentSaved; @@ -20,11 +19,6 @@ final class TextDocumentHandler implements Handler, CanRegisterCapabilities { - /** - * @var Workspace - */ - private $workspace; - /** * @var EventDispatcherInterface */ diff --git a/lib/LanguageServerTesterBuilder.php b/lib/LanguageServerTesterBuilder.php index 0247997d..f8f6d817 100644 --- a/lib/LanguageServerTesterBuilder.php +++ b/lib/LanguageServerTesterBuilder.php @@ -15,7 +15,6 @@ use Phpactor\LanguageServer\Core\Dispatcher\ArgumentResolver\LanguageSeverProtocolParamsResolver; use Phpactor\LanguageServer\Core\Dispatcher\ArgumentResolver\ChainArgumentResolver; use Phpactor\LanguageServer\Core\Server\ClientApi; -use Phpactor\LanguageServer\Core\Server\ResponseWatcher; use Phpactor\LanguageServer\Core\Server\ResponseWatcher\DeferredResponseWatcher; use Phpactor\LanguageServer\Core\Server\ResponseWatcher\TestResponseWatcher; use Phpactor\LanguageServer\Core\Server\RpcClient; @@ -85,11 +84,6 @@ final class LanguageServerTesterBuilder */ private $enableTextDocuments = false; - /** - * @var ResponseWatcher - */ - private $responseHandler; - /** * @var RpcClient */ diff --git a/lib/Listener/ServiceListener.php b/lib/Listener/ServiceListener.php index 27e600b9..e526547b 100644 --- a/lib/Listener/ServiceListener.php +++ b/lib/Listener/ServiceListener.php @@ -4,6 +4,7 @@ use Phpactor\LanguageServer\Core\Service\ServiceManager; use Phpactor\LanguageServer\Event\Initialized; +use Phpactor\LanguageServer\Event\WillShutdown; use Psr\EventDispatcher\ListenerProviderInterface; class ServiceListener implements ListenerProviderInterface @@ -29,5 +30,12 @@ public function getListenersForEvent(object $event): iterable }; return; } + + if ($event instanceof WillShutdown) { + yield function (WillShutdown $closed): void { + $this->serviceManager->stopAll(); + }; + return; + } } } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon deleted file mode 100644 index a4df7731..00000000 --- a/phpstan-baseline.neon +++ /dev/null @@ -1,6 +0,0 @@ -parameters: - ignoreErrors: - - - message: "#^Method Phpactor\\\\LanguageServer\\\\Core\\\\Handler\\\\ClosureHandler\\:\\:handle\\(\\) has no return typehint specified\\.$#" - count: 1 - path: lib/Core/Handler/ClosureHandler.php diff --git a/phpstan.neon b/phpstan.neon index 2f8d34a2..8f4bfc52 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -6,5 +6,3 @@ parameters: - lib - example - bin -includes: - - phpstan-baseline.neon diff --git a/tests/Unit/Core/Service/ServiceManagerTest.php b/tests/Unit/Core/Service/ServiceManagerTest.php index e8add0f7..38e5fed9 100644 --- a/tests/Unit/Core/Service/ServiceManagerTest.php +++ b/tests/Unit/Core/Service/ServiceManagerTest.php @@ -47,6 +47,19 @@ public function testStartAllServices(): void self::assertTrue($service->called); } + public function testStopAll(): void + { + $service = new PingService(); + $serviceManager = $this->createServiceManager($service); + $serviceManager->startAll(); + + self::assertFalse($service->cancellationToken->isRequested()); + + $serviceManager->stopAll(); + + self::assertTrue($service->cancellationToken->isRequested()); + } + public function testStartService(): void { $service = new PingService(); @@ -140,7 +153,10 @@ private function createServiceManager(ServiceProvider ...$services): ServiceMana class PingService implements ServiceProvider { + /** @var bool */ public $called = false; + public CancellationToken $cancellationToken; + /** * {@inheritDoc} */ @@ -159,9 +175,13 @@ public function services(): array ]; } - public function ping(): Promise + /** + * @return Promise + */ + public function ping(CancellationToken $token): Promise { $this->called = true; + $this->cancellationToken = $token; return new Success(); } } diff --git a/tests/Unit/Diagnostics/AggregateDiagnosticsProviderTest.php b/tests/Unit/Diagnostics/AggregateDiagnosticsProviderTest.php new file mode 100644 index 00000000..2aedbac1 --- /dev/null +++ b/tests/Unit/Diagnostics/AggregateDiagnosticsProviderTest.php @@ -0,0 +1,65 @@ +createProvider([ + ProtocolFactory::diagnostic( + ProtocolFactory::range(1, 1, 2, 2), + 'one' + ), + ProtocolFactory::diagnostic( + ProtocolFactory::range(1, 1, 2, 2), + 'two' + ) + ]), + $this->createProvider([ + ProtocolFactory::diagnostic( + ProtocolFactory::range(1, 1, 2, 2), + 'three' + ), + ]), + ]; + + $aggregate = $this->createAggregate(...$providers); + $diagnostics = wait($aggregate->provideDiagnostics(ProtocolFactory::textDocumentItem('file:///', 'text'))); + self::assertCount(3, $diagnostics); + } + + public function testReturnsAggregateName(): void + { + $aggregate = $this->createAggregate(...[ + $this->createProvider([], 'one'), + $this->createProvider([], 'two'), + ]); + self::assertEquals('one, two', $aggregate->name()); + } + + private function createAggregate(ClosureDiagnosticsProvider ...$providers): AggregateDiagnosticsProvider + { + return new AggregateDiagnosticsProvider(new NullLogger(), ...$providers); + } + + /** + * @param Diagnostic[] $diagnostics + */ + private function createProvider(array $diagnostics, string $name = 'test'): ClosureDiagnosticsProvider + { + return new ClosureDiagnosticsProvider(function () use ($diagnostics) { + return new Success($diagnostics); + }, $name); + } +} diff --git a/tests/Unit/Handler/System/ExitHandlerTest.php b/tests/Unit/Handler/System/ExitHandlerTest.php index 20edf65b..fceebbec 100644 --- a/tests/Unit/Handler/System/ExitHandlerTest.php +++ b/tests/Unit/Handler/System/ExitHandlerTest.php @@ -4,14 +4,31 @@ use Phpactor\LanguageServer\Core\Handler\Handler; use Phpactor\LanguageServer\Core\Server\Exception\ExitSession; +use Phpactor\LanguageServer\Event\WillShutdown; use Phpactor\LanguageServer\Handler\System\ExitHandler; use Phpactor\LanguageServer\Tests\Unit\Handler\HandlerTestCase; +use Prophecy\PhpUnit\ProphecyTrait; +use Prophecy\Prophecy\ObjectProphecy; +use Psr\EventDispatcher\EventDispatcherInterface; class ExitHandlerTest extends HandlerTestCase { + use ProphecyTrait; + + /** + * @var ObjectProphecy + */ + private ObjectProphecy $eventDispatcher; + + public function setUp(): void + { + $this->eventDispatcher = $this->prophesize(EventDispatcherInterface::class); + } + + public function handler(): Handler { - return new ExitHandler(); + return new ExitHandler($this->eventDispatcher->reveal()); } public function testThrowsExitSessionException(): void @@ -23,6 +40,6 @@ public function testThrowsExitSessionException(): void public function testShutdownDoesNothing(): void { $this->dispatch('shutdown', []); - $this->addToAssertionCount(1); + $this->eventDispatcher->dispatch(new WillShutdown())->shouldHaveBeenCalled(); } } diff --git a/tests/Unit/Handler/TextDocument/TextDocumentHandlerTest.php b/tests/Unit/Handler/TextDocument/TextDocumentHandlerTest.php index 8d39d1f5..491336b7 100644 --- a/tests/Unit/Handler/TextDocument/TextDocumentHandlerTest.php +++ b/tests/Unit/Handler/TextDocument/TextDocumentHandlerTest.php @@ -14,11 +14,14 @@ use Phpactor\LanguageServer\Core\Workspace\Workspace; use Phpactor\LanguageServer\Test\ProtocolFactory; use Phpactor\LanguageServer\Tests\Unit\Handler\HandlerTestCase; +use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; use Psr\EventDispatcher\EventDispatcherInterface; class TextDocumentHandlerTest extends HandlerTestCase { + use ProphecyTrait; + /** * @var Workspace */ diff --git a/tests/Unit/Listener/ServiceListenerTest.php b/tests/Unit/Listener/ServiceListenerTest.php index ba1f6c5c..972a883e 100644 --- a/tests/Unit/Listener/ServiceListenerTest.php +++ b/tests/Unit/Listener/ServiceListenerTest.php @@ -3,6 +3,7 @@ namespace Phpactor\LanguageServer\Tests\Unit\Listener; use PHPUnit\Framework\TestCase; +use Phpactor\LanguageServer\Event\WillShutdown; use Phpactor\LanguageServer\Listener\ServiceListener; use Phpactor\LanguageServer\Core\Service\ServiceManager; use Phpactor\LanguageServer\Event\Initialized; @@ -37,6 +38,13 @@ public function testStartsServicesonInitialized(): void $this->serviceManager->startAll()->shouldHaveBeenCalled(); } + public function testStopsAllServicesOnShutdown(): void + { + $event = new WillShutdown(); + $this->dispatch($event); + $this->serviceManager->stopAll()->shouldHaveBeenCalled(); + } + public function testDoesNotStartServicesOnOtherEvent(): void { $event = new stdClass(); From a4f693a74e187e3cc5ffbfa1b8ffa9205bd75ee6 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Wed, 20 Apr 2022 23:04:54 +0200 Subject: [PATCH 030/111] Allow shutdown grace period --- .../server/acme-ls/AcmeLsDispatcherFactory.php | 2 +- lib/Handler/System/ExitHandler.php | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/example/server/acme-ls/AcmeLsDispatcherFactory.php b/example/server/acme-ls/AcmeLsDispatcherFactory.php index be7435d3..e0fb6f49 100644 --- a/example/server/acme-ls/AcmeLsDispatcherFactory.php +++ b/example/server/acme-ls/AcmeLsDispatcherFactory.php @@ -67,7 +67,7 @@ public function create(MessageTransmitter $transmitter, InitializeParams $initia new TextDocumentHandler($eventDispatcher), new ServiceHandler($serviceManager, $clientApi), new CommandHandler(new CommandDispatcher([])), - new ExitHandler() + new ExitHandler($eventDispatcher) ); $runner = new HandlerMethodRunner( diff --git a/lib/Handler/System/ExitHandler.php b/lib/Handler/System/ExitHandler.php index d56f0f83..e6751384 100644 --- a/lib/Handler/System/ExitHandler.php +++ b/lib/Handler/System/ExitHandler.php @@ -2,21 +2,26 @@ namespace Phpactor\LanguageServer\Handler\System; +use Amp\Delayed; use Amp\Promise; -use Amp\Success; use Phpactor\LanguageServer\Adapter\Psr\NullEventDispatcher; use Phpactor\LanguageServer\Core\Handler\Handler; use Phpactor\LanguageServer\Core\Server\Exception\ExitSession; use Phpactor\LanguageServer\Event\WillShutdown; use Psr\EventDispatcher\EventDispatcherInterface; +use function Amp\call; class ExitHandler implements Handler { private EventDispatcherInterface $eventDispatcher; - public function __construct(?EventDispatcherInterface $eventDispatcher = null) + private int $gracePeriod; + + + public function __construct(?EventDispatcherInterface $eventDispatcher = null, int $gracePeriod = 500) { $this->eventDispatcher = $eventDispatcher ?: new NullEventDispatcher(); + $this->gracePeriod = $gracePeriod; } public function methods(): array @@ -32,8 +37,10 @@ public function methods(): array */ public function shutdown(): Promise { - $this->eventDispatcher->dispatch(new WillShutdown()); - return new Success(null); + return call(function () { + $this->eventDispatcher->dispatch(new WillShutdown()); + yield new Delayed($this->gracePeriod); + }); } public function exit(): void From 555756be8f6875da1ef03a20e7248524b251d4e2 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Wed, 20 Apr 2022 23:08:10 +0200 Subject: [PATCH 031/111] Add docblock --- lib/Core/Handler/Handler.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/Core/Handler/Handler.php b/lib/Core/Handler/Handler.php index e0f3358b..faba5b8c 100644 --- a/lib/Core/Handler/Handler.php +++ b/lib/Core/Handler/Handler.php @@ -38,6 +38,8 @@ interface Handler * * The arguments passed by the RPC call depend on the `ArgumentResolver` * implementation used by the `HandlerMethodRunner`. + * + * @return array */ public function methods(): array; } From f9656685f2ad9a083b8a4b09949ef7abb56ea164 Mon Sep 17 00:00:00 2001 From: dantleech Date: Thu, 21 Apr 2022 23:05:09 +0200 Subject: [PATCH 032/111] Shutdown middleware (#45) * Add shutdown middleware * CS and example update * Update to return response --- .../acme-ls/AcmeLsDispatcherFactory.php | 3 +- lib/Middleware/ShutdownMiddleware.php | 85 +++++++++++++++++++ .../Unit/Middleware/ShutdownMiddlwareTest.php | 75 ++++++++++++++++ 3 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 lib/Middleware/ShutdownMiddleware.php create mode 100644 tests/Unit/Middleware/ShutdownMiddlwareTest.php diff --git a/example/server/acme-ls/AcmeLsDispatcherFactory.php b/example/server/acme-ls/AcmeLsDispatcherFactory.php index e0fb6f49..f4610829 100644 --- a/example/server/acme-ls/AcmeLsDispatcherFactory.php +++ b/example/server/acme-ls/AcmeLsDispatcherFactory.php @@ -32,6 +32,7 @@ use Phpactor\LanguageServer\Core\Server\ResponseWatcher\DeferredResponseWatcher; use Phpactor\LanguageServer\Core\Server\Transmitter\MessageTransmitter; use Phpactor\LanguageServer\Middleware\HandlerMiddleware; +use Phpactor\LanguageServer\Middleware\ShutdownMiddleware; use Psr\Log\LoggerInterface; class AcmeLsDispatcherFactory implements DispatcherFactory @@ -67,7 +68,6 @@ public function create(MessageTransmitter $transmitter, InitializeParams $initia new TextDocumentHandler($eventDispatcher), new ServiceHandler($serviceManager, $clientApi), new CommandHandler(new CommandDispatcher([])), - new ExitHandler($eventDispatcher) ); $runner = new HandlerMethodRunner( @@ -83,6 +83,7 @@ public function create(MessageTransmitter $transmitter, InitializeParams $initia new InitializeMiddleware($handlers, $eventDispatcher, [ 'version' => 1, ]), + new ShutdownMiddleware($eventDispatcher), new ResponseHandlingMiddleware($responseWatcher), new CancellationMiddleware($runner), new HandlerMiddleware($runner) diff --git a/lib/Middleware/ShutdownMiddleware.php b/lib/Middleware/ShutdownMiddleware.php new file mode 100644 index 00000000..abd3cbb0 --- /dev/null +++ b/lib/Middleware/ShutdownMiddleware.php @@ -0,0 +1,85 @@ +eventDispatcher = $eventDispatcher ?: new NullEventDispatcher(); + $this->gracePeriod = $gracePeriod; + } + + public function process(Message $request, RequestHandler $handler): Promise + { + if ($request instanceof NotificationMessage) { + if ($request->method === self::METHOD_EXIT) { + throw new ExitSession('Exit method invoked by client'); + } + + if ($this->shuttingDown) { + return new Success(null); + } + } + + if ($request instanceof RequestMessage) { + if ($this->shuttingDown) { + return new Success(new ResponseMessage( + $request->id, + null, + new ResponseError( + ErrorCodes::InvalidRequest, + 'Server is currently shutting down, cannot serve requests' + ) + )); + } + + if ($request->method === self::METHOD_SHUTDOWN) { + $this->shuttingDown = true; + return $this->shutdown($request); + } + } + + return $handler->handle($request); + } + + /** + * @return Promise + */ + public function shutdown(RequestMessage $request): Promise + { + return call(function () use ($request) { + $this->eventDispatcher->dispatch(new WillShutdown()); + yield new Delayed($this->gracePeriod); + return new ResponseMessage( + $request->id, + null + ); + }); + } +} diff --git a/tests/Unit/Middleware/ShutdownMiddlwareTest.php b/tests/Unit/Middleware/ShutdownMiddlwareTest.php new file mode 100644 index 00000000..f0b3c6f4 --- /dev/null +++ b/tests/Unit/Middleware/ShutdownMiddlwareTest.php @@ -0,0 +1,75 @@ + + */ + private ObjectProphecy $eventDispatcher; + + public function setUp(): void + { + parent::setUp(); + $this->eventDispatcher = $this->prophesize(EventDispatcherInterface::class); + } + + public function testDelegatesToNextHandlerIfMessageIsNotRequest(): Generator + { + $this->expectException(MiddlewareTerminated::class); + self::assertNull( + yield $this->createMiddleware()->process( + new NotificationMessage('foobar'), + new RequestHandler() + ) + ); + } + + public function testShutdownEmitsEvent(): Generator + { + $response = yield $this->createMiddleware()->process( + new RequestMessage(1, ShutdownMiddleware::METHOD_SHUTDOWN, []), + new RequestHandler() + ); + $this->eventDispatcher->dispatch(new WillShutdown())->shouldHaveBeenCalled(); + self::assertEquals(new ResponseMessage(1, null), $response); + } + + public function testShutdownTwiceReturnsError(): Generator + { + $middleware = $this->createMiddleware(); + yield $middleware->process( + new RequestMessage(1, ShutdownMiddleware::METHOD_SHUTDOWN, []), + new RequestHandler() + ); + $response = yield $middleware->process( + new RequestMessage(1, ShutdownMiddleware::METHOD_SHUTDOWN, []), + new RequestHandler() + ); + self::assertInstanceOf(ResponseMessage::class, $response); + self::assertInstanceOf(ResponseError::class, $response->error); + self::assertStringContainsString('Server is currently shutting down', $response->error->message); + } + + private function createMiddleware(): ShutdownMiddleware + { + return new ShutdownMiddleware($this->eventDispatcher->reveal()); + } +} From 2067f7055151c1dc148152409b757873c9c1169c Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 24 Apr 2022 10:34:05 +0200 Subject: [PATCH 033/111] Log line and file --- lib/Middleware/ErrorHandlingMiddleware.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Middleware/ErrorHandlingMiddleware.php b/lib/Middleware/ErrorHandlingMiddleware.php index b12e6194..e96b2315 100644 --- a/lib/Middleware/ErrorHandlingMiddleware.php +++ b/lib/Middleware/ErrorHandlingMiddleware.php @@ -56,7 +56,7 @@ public function process(Message $request, RequestHandler $handler): Promise null, new ResponseError( $this->resolveErrorCode($error), - $message, + sprintf('%s at %s#%s', $message, $error->getFile(), $error->getLine()), $error->getTraceAsString() ) )); From 27e8240eff06c8bf54bcef836c6fc122b5107d64 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 24 Apr 2022 10:34:25 +0200 Subject: [PATCH 034/111] Do not unconditionally log in and out --- lib/Core/Server/LanguageServer.php | 3 +-- .../Transmitter/ConnectionMessageTransmitter.php | 11 +---------- tests/Unit/Middleware/ErrorHandlingMiddlewareTest.php | 4 ++-- 3 files changed, 4 insertions(+), 14 deletions(-) diff --git a/lib/Core/Server/LanguageServer.php b/lib/Core/Server/LanguageServer.php index d3329bb0..07b61cab 100644 --- a/lib/Core/Server/LanguageServer.php +++ b/lib/Core/Server/LanguageServer.php @@ -181,13 +181,12 @@ private function listenForConnections(): Generator private function handle(Connection $connection): Promise { return \Amp\call(function () use ($connection) { - $transmitter = new ConnectionMessageTransmitter($connection, $this->logger); + $transmitter = new ConnectionMessageTransmitter($connection); $reader = new LspMessageReader($connection->stream()); $dispatcher = null; // wait for the next request while (null !== $request = yield $reader->wait()) { - $this->logger->info('IN:', $request->body()); $this->stats->incRequestCount(); try { diff --git a/lib/Core/Server/Transmitter/ConnectionMessageTransmitter.php b/lib/Core/Server/Transmitter/ConnectionMessageTransmitter.php index 34ee4dd6..15d9e802 100644 --- a/lib/Core/Server/Transmitter/ConnectionMessageTransmitter.php +++ b/lib/Core/Server/Transmitter/ConnectionMessageTransmitter.php @@ -4,7 +4,6 @@ use Phpactor\LanguageServer\Core\Rpc\Message; use Phpactor\LanguageServer\Core\Server\StreamProvider\Connection; -use Psr\Log\LoggerInterface; final class ConnectionMessageTransmitter implements MessageTransmitter { @@ -20,22 +19,14 @@ final class ConnectionMessageTransmitter implements MessageTransmitter */ private $formatter; - /** - * @var LoggerInterface - */ - private $logger; - - public function __construct(Connection $connection, LoggerInterface $logger, MessageFormatter $formatter = null) + public function __construct(Connection $connection, MessageFormatter $formatter = null) { $this->connection = $connection; $this->formatter = $formatter ?: new LspMessageFormatter(); - $this->logger = $logger; } public function transmit(Message $response): void { - $this->logger->info('OUT: ', (array) $response); - $responseBody = $this->formatter->format($response); foreach (str_split($responseBody, self::WRITE_CHUNK_SIZE) as $chunk) { diff --git a/tests/Unit/Middleware/ErrorHandlingMiddlewareTest.php b/tests/Unit/Middleware/ErrorHandlingMiddlewareTest.php index 7af8b223..853fda45 100644 --- a/tests/Unit/Middleware/ErrorHandlingMiddlewareTest.php +++ b/tests/Unit/Middleware/ErrorHandlingMiddlewareTest.php @@ -90,7 +90,7 @@ public function testDoesNotLogButReturnsErrorResponseForRequest(): Generator self::assertInstanceOf(ResponseMessage::class, $response); assert($response instanceof ResponseMessage); self::assertInstanceOf(ResponseError::class, $response->error); - self::assertEquals('Exception [RuntimeException] please', $response->error->message); + self::assertStringContainsString('Exception [RuntimeException] please', $response->error->message); self::assertEquals(ErrorCodes::InternalError, $response->error->code); } @@ -108,7 +108,7 @@ public function testMethodNotFoundCodeForHandlerNotFound(): Generator self::assertInstanceOf(ResponseMessage::class, $response); assert($response instanceof ResponseMessage); self::assertInstanceOf(ResponseError::class, $response->error); - self::assertEquals('Exception [Phpactor\LanguageServer\Core\Handler\HandlerNotFound] please', $response->error->message); + self::assertStringContainsString('Exception [Phpactor\LanguageServer\Core\Handler\HandlerNotFound] please', $response->error->message); self::assertEquals(ErrorCodes::MethodNotFound, $response->error->code); } From a4f1e81a6f1f22700c9f46f94de0dff24499830a Mon Sep 17 00:00:00 2001 From: mamazu <14860264+mamazu@users.noreply.github.com> Date: Sun, 8 May 2022 07:59:22 +0200 Subject: [PATCH 035/111] Fixing the command line arguments (#46) * Fixing the command line arguments and adding documentation on how to invoke the serve.php * Delete phpactor-lsp.log Co-authored-by: dantleech --- bin/serve.php | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/bin/serve.php b/bin/serve.php index ab68661e..2baaa9a3 100755 --- a/bin/serve.php +++ b/bin/serve.php @@ -47,14 +47,23 @@ require __DIR__ . '/../vendor/autoload.php'; -$options = [ - 'type' => 'tcp', - 'address' => null, -]; - -$options = array_merge($options, getopt('t::a::', ['type::', 'address::'])); -$type = $options['type']; -$address = $options['address']; +if ($argc === 1) { + echo << Date: Sat, 9 Jul 2022 15:14:38 +0200 Subject: [PATCH 036/111] Cancellation --- .../CodeAction/AggregateCodeActionProvider.php | 16 +++++++++++----- lib/Handler/TextDocument/CodeActionHandler.php | 7 ++++--- lib/Middleware/ErrorHandlingMiddleware.php | 15 +++++++++++++++ 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/lib/Core/CodeAction/AggregateCodeActionProvider.php b/lib/Core/CodeAction/AggregateCodeActionProvider.php index 68d05e31..b5e365f8 100644 --- a/lib/Core/CodeAction/AggregateCodeActionProvider.php +++ b/lib/Core/CodeAction/AggregateCodeActionProvider.php @@ -2,17 +2,19 @@ namespace Phpactor\LanguageServer\Core\CodeAction; +use Amp\CancellationToken; use Amp\Promise; use Phpactor\LanguageServerProtocol\Range; use Phpactor\LanguageServerProtocol\TextDocumentItem; use function Amp\call; +use function Amp\delay; class AggregateCodeActionProvider implements CodeActionProvider { /** - * @var array + * @var CodeActionProvider[] */ - private $providers; + private array $providers; public function __construct(CodeActionProvider ...$providers) { @@ -22,12 +24,16 @@ public function __construct(CodeActionProvider ...$providers) /** * {@inheritDoc} */ - public function provideActionsFor(TextDocumentItem $textDocument, Range $range): Promise + public function provideActionsFor(TextDocumentItem $textDocument, Range $range, CancellationToken $cancel): Promise { - return call(function () use ($textDocument, $range) { + return call(function () use ($textDocument, $range, $cancel) { $actions = []; foreach ($this->providers as $provider) { - $actions = array_merge($actions, yield $provider->provideActionsFor($textDocument, $range)); + $actions = array_merge($actions, yield $provider->provideActionsFor($textDocument, $range, $cancel)); + + yield delay(0); + + $cancel->throwIfRequested(); } return $actions; diff --git a/lib/Handler/TextDocument/CodeActionHandler.php b/lib/Handler/TextDocument/CodeActionHandler.php index 3e22b761..52fafddf 100644 --- a/lib/Handler/TextDocument/CodeActionHandler.php +++ b/lib/Handler/TextDocument/CodeActionHandler.php @@ -2,6 +2,7 @@ namespace Phpactor\LanguageServer\Handler\TextDocument; +use Amp\CancellationToken; use Amp\Promise; use Phpactor\LanguageServerProtocol\CodeAction; use Phpactor\LanguageServerProtocol\CodeActionOptions; @@ -52,11 +53,11 @@ public function registerCapabiltiies(ServerCapabilities $capabilities): void /** * @return Promise> */ - public function codeAction(CodeActionParams $params): Promise + public function codeAction(CodeActionParams $params, CancellationToken $cancel): Promise { - return call(function () use ($params) { + return call(function () use ($params, $cancel) { $document = $this->workspace->get($params->textDocument->uri); - return $this->provider->provideActionsFor($document, $params->range); + return $this->provider->provideActionsFor($document, $params->range, $cancel); }); } } diff --git a/lib/Middleware/ErrorHandlingMiddleware.php b/lib/Middleware/ErrorHandlingMiddleware.php index e96b2315..be0f92d8 100644 --- a/lib/Middleware/ErrorHandlingMiddleware.php +++ b/lib/Middleware/ErrorHandlingMiddleware.php @@ -2,6 +2,8 @@ namespace Phpactor\LanguageServer\Middleware; +use Amp\CancellationToken; +use Amp\CancelledException; use Amp\Promise; use Amp\Success; use Phpactor\LanguageServer\Core\Handler\HandlerNotFound; @@ -39,6 +41,19 @@ public function process(Message $request, RequestHandler $handler): Promise return yield $handler->handle($request); } catch (ServerControl $exception) { throw $exception; + } catch (CancelledException $cancelled) { + if (!$request instanceof RequestMessage) { + return new Success(null); + } + + return new Success(new ResponseMessage( + $request->id, + null, + new ResponseError( + ErrorCodes::RequestCancelled, + sprintf('Request %d (%s) cancelled', $request->id, $request->method), + ) + )); } catch (Throwable $error) { $message = sprintf('Exception [%s] %s', get_class($error), $error->getMessage()); if (!$request instanceof RequestMessage) { From 565469826be887fbb9e2b14a7bf998fb1358c1f5 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sat, 9 Jul 2022 15:18:30 +0200 Subject: [PATCH 037/111] Interface --- lib/Core/CodeAction/CodeActionProvider.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/Core/CodeAction/CodeActionProvider.php b/lib/Core/CodeAction/CodeActionProvider.php index cff441fc..567c4637 100644 --- a/lib/Core/CodeAction/CodeActionProvider.php +++ b/lib/Core/CodeAction/CodeActionProvider.php @@ -2,6 +2,7 @@ namespace Phpactor\LanguageServer\Core\CodeAction; +use Amp\CancellationToken; use Amp\Promise; use Phpactor\LanguageServerProtocol\CodeAction; use Phpactor\LanguageServerProtocol\Range; @@ -12,7 +13,7 @@ interface CodeActionProvider /** * @return Promise> */ - public function provideActionsFor(TextDocumentItem $textDocument, Range $range): Promise; + public function provideActionsFor(TextDocumentItem $textDocument, Range $range, CancellationToken $cancel): Promise; /** * Return the kinds of actions that this provider can return, for example From d55338118630b07ab25156c43544f54c2de1bc82 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sat, 9 Jul 2022 15:22:36 +0200 Subject: [PATCH 038/111] Fully support cancellation token --- lib/Example/CodeAction/SayHelloCodeActionProvider.php | 3 ++- .../Core/CodeAction/AggregateCodeActionProviderTest.php | 9 ++++++--- .../Diagnostics/CodeActionDiagnosticsProviderTest.php | 3 ++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/Example/CodeAction/SayHelloCodeActionProvider.php b/lib/Example/CodeAction/SayHelloCodeActionProvider.php index 05e8abf5..26d1a6cb 100644 --- a/lib/Example/CodeAction/SayHelloCodeActionProvider.php +++ b/lib/Example/CodeAction/SayHelloCodeActionProvider.php @@ -2,6 +2,7 @@ namespace Phpactor\LanguageServer\Example\CodeAction; +use Amp\CancellationToken; use Amp\Promise; use Phpactor\LanguageServerProtocol\CodeAction; use Phpactor\LanguageServerProtocol\Command; @@ -12,7 +13,7 @@ class SayHelloCodeActionProvider implements CodeActionProvider { - public function provideActionsFor(TextDocumentItem $textDocument, Range $range): Promise + public function provideActionsFor(TextDocumentItem $textDocument, Range $range, CancellationToken $cancel): Promise { return call(function () { return [ diff --git a/tests/Unit/Core/CodeAction/AggregateCodeActionProviderTest.php b/tests/Unit/Core/CodeAction/AggregateCodeActionProviderTest.php index 039e2e4f..5079d173 100644 --- a/tests/Unit/Core/CodeAction/AggregateCodeActionProviderTest.php +++ b/tests/Unit/Core/CodeAction/AggregateCodeActionProviderTest.php @@ -2,6 +2,7 @@ namespace Phpactor\LanguageServer\Tests\Unit\Core\CodeAction; +use Amp\CancellationTokenSource; use Amp\Success; use PHPUnit\Framework\TestCase; use Phpactor\LanguageServerProtocol\CodeAction; @@ -39,13 +40,15 @@ public function testProvidesCodeActionsFromProviders(): void $provider1 = $this->prophesize(CodeActionProvider::class); $provider2 = $this->prophesize(CodeActionProvider::class); - $provider1->provideActionsFor($item, $range)->willReturn(new Success([$action1])); - $provider2->provideActionsFor($item, $range)->willReturn(new Success([$action2])); + $source = new CancellationTokenSource(); + $token = $source->getToken(); + $provider1->provideActionsFor($item, $range, $token)->willReturn(new Success([$action1])); + $provider2->provideActionsFor($item, $range, $token)->willReturn(new Success([$action2])); $aggregate = new AggregateCodeActionProvider($provider1->reveal(), $provider2->reveal()); $actions = []; - foreach (wait($aggregate->provideActionsFor($item, $range)) as $action) { + foreach (wait($aggregate->provideActionsFor($item, $range, $token)) as $action) { $actions[] = $action; } diff --git a/tests/Unit/Diagnostics/CodeActionDiagnosticsProviderTest.php b/tests/Unit/Diagnostics/CodeActionDiagnosticsProviderTest.php index 2ef13c09..d56d1579 100644 --- a/tests/Unit/Diagnostics/CodeActionDiagnosticsProviderTest.php +++ b/tests/Unit/Diagnostics/CodeActionDiagnosticsProviderTest.php @@ -2,6 +2,7 @@ namespace Phpactor\LanguageServer\Tests\Unit\Diagnostics; +use Amp\CancellationToken; use Amp\Delayed; use Amp\Promise; use PHPUnit\Framework\TestCase; @@ -39,7 +40,7 @@ public function testProvidesDiagnostics(): void class TestCodeActionProvider implements CodeActionProvider { - public function provideActionsFor(TextDocumentItem $textDocument, Range $range): Promise + public function provideActionsFor(TextDocumentItem $textDocument, Range $range, CancellationToken $cancel): Promise { return call(function () { return [ From 051375f6d5a619eebdbc76148fc72b5066b50f6a Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sat, 9 Jul 2022 15:32:56 +0200 Subject: [PATCH 039/111] Support cancel in diagnostic --- lib/Core/Diagnostics/AggregateDiagnosticsProvider.php | 11 ++++++++--- lib/Core/Diagnostics/ClosureDiagnosticsProvider.php | 3 ++- lib/Core/Diagnostics/DiagnosticsEngine.php | 2 +- lib/Core/Diagnostics/DiagnosticsProvider.php | 3 ++- lib/Diagnostics/CodeActionDiagnosticsProvider.php | 9 ++++++--- .../Diagnostics/SayHelloDiagnosticsProvider.php | 3 ++- lib/Middleware/ErrorHandlingMiddleware.php | 1 - .../Diagnostics/AggregateDiagnosticsProviderTest.php | 4 +++- 8 files changed, 24 insertions(+), 12 deletions(-) diff --git a/lib/Core/Diagnostics/AggregateDiagnosticsProvider.php b/lib/Core/Diagnostics/AggregateDiagnosticsProvider.php index 0d5c501a..2eb1c7d7 100644 --- a/lib/Core/Diagnostics/AggregateDiagnosticsProvider.php +++ b/lib/Core/Diagnostics/AggregateDiagnosticsProvider.php @@ -2,6 +2,7 @@ namespace Phpactor\LanguageServer\Core\Diagnostics; +use Amp\CancellationToken; use Amp\Promise; use Phpactor\LanguageServerProtocol\TextDocumentItem; use Psr\Log\LoggerInterface; @@ -29,17 +30,21 @@ public function __construct(LoggerInterface $logger, DiagnosticsProvider ...$pro /** * {@inheritDoc} */ - public function provideDiagnostics(TextDocumentItem $textDocument): Promise + public function provideDiagnostics(TextDocumentItem $textDocument, CancellationToken $cancel): Promise { - return call(function () use ($textDocument) { + return call(function () use ($textDocument, $cancel) { $diagnostics = []; foreach ($this->providers as $provider) { try { $start = microtime(true); $diagnostics = array_merge( $diagnostics, - yield $provider->provideDiagnostics($textDocument) + yield $provider->provideDiagnostics($textDocument, $cancel) ); + if ($cancel->isRequested()) { + $this->logger->info('Diagnostics cancelled'); + return $diagnostics; + } $this->logger->debug(sprintf( 'Diagnostic finsihed in "%s" (%s)', number_format(microtime(true) - $start, 2), diff --git a/lib/Core/Diagnostics/ClosureDiagnosticsProvider.php b/lib/Core/Diagnostics/ClosureDiagnosticsProvider.php index 86df8384..94688c4e 100644 --- a/lib/Core/Diagnostics/ClosureDiagnosticsProvider.php +++ b/lib/Core/Diagnostics/ClosureDiagnosticsProvider.php @@ -2,6 +2,7 @@ namespace Phpactor\LanguageServer\Core\Diagnostics; +use Amp\CancellationToken; use Amp\Promise; use Closure; use Phpactor\LanguageServerProtocol\TextDocumentItem; @@ -21,7 +22,7 @@ public function __construct(Closure $closure, string $name = 'closure') $this->name = $name; } - public function provideDiagnostics(TextDocumentItem $textDocument): Promise + public function provideDiagnostics(TextDocumentItem $textDocument, CancellationToken $cancel): Promise { $closure = $this->closure; diff --git a/lib/Core/Diagnostics/DiagnosticsEngine.php b/lib/Core/Diagnostics/DiagnosticsEngine.php index 845e7708..e0317627 100644 --- a/lib/Core/Diagnostics/DiagnosticsEngine.php +++ b/lib/Core/Diagnostics/DiagnosticsEngine.php @@ -82,7 +82,7 @@ public function run(CancellationToken $token): Promise $this->next = null; } - $diagnostics = yield $this->provider->provideDiagnostics($textDocument); + $diagnostics = yield $this->provider->provideDiagnostics($textDocument, $token); $this->clientApi->diagnostics()->publishDiagnostics( $textDocument->uri, diff --git a/lib/Core/Diagnostics/DiagnosticsProvider.php b/lib/Core/Diagnostics/DiagnosticsProvider.php index e8c9518e..1d568111 100644 --- a/lib/Core/Diagnostics/DiagnosticsProvider.php +++ b/lib/Core/Diagnostics/DiagnosticsProvider.php @@ -2,6 +2,7 @@ namespace Phpactor\LanguageServer\Core\Diagnostics; +use Amp\CancellationToken; use Amp\Promise; use Phpactor\LanguageServerProtocol\Diagnostic; use Phpactor\LanguageServerProtocol\TextDocumentItem; @@ -11,7 +12,7 @@ interface DiagnosticsProvider /** * @return Promise> */ - public function provideDiagnostics(TextDocumentItem $textDocument): Promise; + public function provideDiagnostics(TextDocumentItem $textDocument, CancellationToken $cancel): Promise; public function name(): string; } diff --git a/lib/Diagnostics/CodeActionDiagnosticsProvider.php b/lib/Diagnostics/CodeActionDiagnosticsProvider.php index 7fc057a7..f6f968f8 100644 --- a/lib/Diagnostics/CodeActionDiagnosticsProvider.php +++ b/lib/Diagnostics/CodeActionDiagnosticsProvider.php @@ -2,6 +2,7 @@ namespace Phpactor\LanguageServer\Diagnostics; +use Amp\CancellationToken; use Amp\Promise; use Phpactor\LanguageServerProtocol\Position; use Phpactor\LanguageServerProtocol\Range; @@ -25,21 +26,23 @@ public function __construct(CodeActionProvider ...$providers) /** * {@inheritDoc} */ - public function provideDiagnostics(TextDocumentItem $textDocument): Promise + public function provideDiagnostics(TextDocumentItem $textDocument, CancellationToken $cancel): Promise { - return call(function () use ($textDocument) { + return call(function () use ($textDocument, $cancel) { $diagnostics = []; foreach ($this->providers as $provider) { $codeActions = yield $provider->provideActionsFor($textDocument, new Range( new Position(0, 0), new Position(PHP_INT_MAX, PHP_INT_MAX) - )); + ), $cancel); foreach ($codeActions as $codeAction) { foreach ($codeAction->diagnostics as $diagnostic) { $diagnostics[] = $diagnostic; } } + + $cancel->throwIfRequested(); } return $diagnostics; diff --git a/lib/Example/Diagnostics/SayHelloDiagnosticsProvider.php b/lib/Example/Diagnostics/SayHelloDiagnosticsProvider.php index b8cff291..f8751827 100644 --- a/lib/Example/Diagnostics/SayHelloDiagnosticsProvider.php +++ b/lib/Example/Diagnostics/SayHelloDiagnosticsProvider.php @@ -2,6 +2,7 @@ namespace Phpactor\LanguageServer\Example\Diagnostics; +use Amp\CancellationToken; use Amp\Promise; use Phpactor\LanguageServerProtocol\Diagnostic; use Phpactor\LanguageServerProtocol\DiagnosticSeverity; @@ -16,7 +17,7 @@ class SayHelloDiagnosticsProvider implements DiagnosticsProvider /** * {@inheritDoc} */ - public function provideDiagnostics(TextDocumentItem $textDocument): Promise + public function provideDiagnostics(TextDocumentItem $textDocument, CancellationToken $cancel): Promise { return call(function () { return [ diff --git a/lib/Middleware/ErrorHandlingMiddleware.php b/lib/Middleware/ErrorHandlingMiddleware.php index be0f92d8..22c36c28 100644 --- a/lib/Middleware/ErrorHandlingMiddleware.php +++ b/lib/Middleware/ErrorHandlingMiddleware.php @@ -2,7 +2,6 @@ namespace Phpactor\LanguageServer\Middleware; -use Amp\CancellationToken; use Amp\CancelledException; use Amp\Promise; use Amp\Success; diff --git a/tests/Unit/Diagnostics/AggregateDiagnosticsProviderTest.php b/tests/Unit/Diagnostics/AggregateDiagnosticsProviderTest.php index 2aedbac1..02481e50 100644 --- a/tests/Unit/Diagnostics/AggregateDiagnosticsProviderTest.php +++ b/tests/Unit/Diagnostics/AggregateDiagnosticsProviderTest.php @@ -2,6 +2,7 @@ namespace Phpactor\LanguageServer\Tests\Unit\Diagnostics; +use Amp\CancellationTokenSource; use Amp\Success; use PHPUnit\Framework\TestCase; use Phpactor\LanguageServerProtocol\Diagnostic; @@ -35,7 +36,8 @@ public function testProvidesAggregateDiagnostics(): void ]; $aggregate = $this->createAggregate(...$providers); - $diagnostics = wait($aggregate->provideDiagnostics(ProtocolFactory::textDocumentItem('file:///', 'text'))); + $cancel = (new CancellationTokenSource())->getToken(); + $diagnostics = wait($aggregate->provideDiagnostics(ProtocolFactory::textDocumentItem('file:///', 'text'), $cancel)); self::assertCount(3, $diagnostics); } From 7a4138ea645e959b427d1af2db6011c23539c235 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 10 Jul 2022 08:06:45 +0200 Subject: [PATCH 040/111] Support clear on update and lint on open --- lib/Core/Diagnostics/DiagnosticsEngine.php | 11 ++++++++- lib/Service/DiagnosticsService.php | 24 ++++++++++++++++++- .../CodeActionDiagnosticsProviderTest.php | 8 +++++-- 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/lib/Core/Diagnostics/DiagnosticsEngine.php b/lib/Core/Diagnostics/DiagnosticsEngine.php index e0317627..df6605c8 100644 --- a/lib/Core/Diagnostics/DiagnosticsEngine.php +++ b/lib/Core/Diagnostics/DiagnosticsEngine.php @@ -50,6 +50,15 @@ public function __construct(ClientApi $clientApi, DiagnosticsProvider $provider, $this->sleepTime = $sleepTime; } + public function clear(TextDocumentItem $textDocument): void + { + $this->clientApi->diagnostics()->publishDiagnostics( + $textDocument->uri, + $textDocument->version, + [] + ); + } + /** * @return Promise */ @@ -101,7 +110,7 @@ public function enqueue(TextDocumentItem $textDocument): void $this->next = $textDocument; return; } - + // resolving the promise will start PHPStan $this->running = true; $this->deferred->resolve($textDocument); diff --git a/lib/Service/DiagnosticsService.php b/lib/Service/DiagnosticsService.php index 3e303007..47f17534 100644 --- a/lib/Service/DiagnosticsService.php +++ b/lib/Service/DiagnosticsService.php @@ -8,6 +8,7 @@ use Phpactor\LanguageServer\Core\Diagnostics\DiagnosticsEngine; use Phpactor\LanguageServer\Core\Service\ServiceProvider; use Phpactor\LanguageServer\Core\Workspace\Workspace; +use Phpactor\LanguageServer\Event\TextDocumentOpened; use Phpactor\LanguageServer\Event\TextDocumentSaved; use Phpactor\LanguageServer\Event\TextDocumentUpdated; use Psr\EventDispatcher\ListenerProviderInterface; @@ -34,16 +35,24 @@ class DiagnosticsService implements ServiceProvider, ListenerProviderInterface */ private $lintOnSave; + private bool $clearOnUpdate; + + private bool $lintOnOpen; + public function __construct( DiagnosticsEngine $engine, bool $lintOnUpdate = true, bool $lintOnSave = true, - ?Workspace $workspace = null + ?Workspace $workspace = null, + bool $clearOnUpdate = true, + bool $lintOnOpen = true, ) { $this->engine = $engine; $this->workspace = $workspace ?: new Workspace(); $this->lintOnUpdate = $lintOnUpdate; $this->lintOnSave = $lintOnSave; + $this->clearOnUpdate = $clearOnUpdate; + $this->lintOnOpen = $lintOnOpen; } /** @@ -69,6 +78,10 @@ public function diagnostics(CancellationToken $cancellationToken): Promise */ public function getListenersForEvent(object $event): iterable { + if ($this->lintOnOpen && $event instanceof TextDocumentOpened) { + yield [$this, 'opened']; + } + if ($this->lintOnUpdate && $event instanceof TextDocumentUpdated) { yield [$this, 'enqueueUpdate']; } @@ -78,6 +91,11 @@ public function getListenersForEvent(object $event): iterable } } + public function opened(TextDocumentOpened $opened): void + { + $this->engine->enqueue($opened->textDocument()); + } + public function enqueueUpdate(TextDocumentUpdated $update): void { $item = new TextDocumentItem( @@ -87,6 +105,10 @@ public function enqueueUpdate(TextDocumentUpdated $update): void $update->updatedText() ); + if ($this->clearOnUpdate) { + $this->engine->clear($item); + } + $this->engine->enqueue($item); } diff --git a/tests/Unit/Diagnostics/CodeActionDiagnosticsProviderTest.php b/tests/Unit/Diagnostics/CodeActionDiagnosticsProviderTest.php index d56d1579..6d5762ed 100644 --- a/tests/Unit/Diagnostics/CodeActionDiagnosticsProviderTest.php +++ b/tests/Unit/Diagnostics/CodeActionDiagnosticsProviderTest.php @@ -29,11 +29,15 @@ public function testProvidesDiagnostics(): void $tester->initialize(); $tester->textDocument()->open('file:///foobar', 'barfoo'); - $tester->textDocument()->update('file:///foobar', 'bar'); - wait(new Delayed(100)); + wait(new Delayed(10)); self::assertEquals(1, $tester->transmitter()->count()); + + $tester->textDocument()->update('file:///foobar', 'bar'); + wait(new Delayed(10)); + + self::assertEquals(3, $tester->transmitter()->count()); self::assertEquals('textDocument/publishDiagnostics', $tester->transmitter()->shiftNotification()->method); } } From 570ff3895c2c807a81886012dd1197e332f62457 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 10 Jul 2022 08:08:31 +0200 Subject: [PATCH 041/111] Update plugin config --- composer.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/composer.json b/composer.json index 7e7092e5..b415c193 100644 --- a/composer.json +++ b/composer.json @@ -56,5 +56,10 @@ "./vendor/bin/phpstan analyze", "./vendor/bin/php-cs-fixer fix" ] + }, + "config": { + "allow-plugins": { + "ergebnis/composer-normalize": true + } } } From a86fe2803c39e4e5adb158378e958f5b1ce9e80a Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 10 Jul 2022 08:08:55 +0200 Subject: [PATCH 042/111] Also extension installer --- composer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index b415c193..a1eb8e79 100644 --- a/composer.json +++ b/composer.json @@ -59,7 +59,8 @@ }, "config": { "allow-plugins": { - "ergebnis/composer-normalize": true + "ergebnis/composer-normalize": true, + "phpstan/extension-installer": true } } } From 159cb20030695b05271843d27f76d4659ca43a1a Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 10 Jul 2022 08:09:38 +0200 Subject: [PATCH 043/111] Fix for 7.4 --- lib/Service/DiagnosticsService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Service/DiagnosticsService.php b/lib/Service/DiagnosticsService.php index 47f17534..f2f377f0 100644 --- a/lib/Service/DiagnosticsService.php +++ b/lib/Service/DiagnosticsService.php @@ -45,7 +45,7 @@ public function __construct( bool $lintOnSave = true, ?Workspace $workspace = null, bool $clearOnUpdate = true, - bool $lintOnOpen = true, + bool $lintOnOpen = true ) { $this->engine = $engine; $this->workspace = $workspace ?: new Workspace(); From 094d6fc2f31160840f8962158dd9b51d0f588c6e Mon Sep 17 00:00:00 2001 From: dantleech Date: Sun, 17 Jul 2022 12:52:27 +0200 Subject: [PATCH 044/111] Add "respond" method to LS tester (#47) --- lib/Test/LanguageServerTester.php | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/lib/Test/LanguageServerTester.php b/lib/Test/LanguageServerTester.php index 18fd69b4..b2d31275 100644 --- a/lib/Test/LanguageServerTester.php +++ b/lib/Test/LanguageServerTester.php @@ -84,6 +84,29 @@ public function requestAndWait(string $method, $params, $id = null): ?ResponseMe return wait($this->request($method, $this->normalizeParams($params), $id)); } + /** + * @param mixed $value + * @param string|int $id + * @return Promise + */ + public function respond($id, $value): Promise + { + $requestMessage = new ResponseMessage($id, $value); + + return $this->dispatch($requestMessage); + } + + /** + * @param mixed $value + * @param string|int $id + * @return mixed + */ + public function respondAndWait($id, $value) + { + return wait($this->respond($id, $value)); + } + + /** * @param array|object $params * @return Promise From 221ebc68d865b7ccc3f496755c18fb31f822052e Mon Sep 17 00:00:00 2001 From: dantleech Date: Thu, 4 Aug 2022 16:40:19 +0200 Subject: [PATCH 045/111] Raw request (#48) * Support for getting the raw request message * Ensure an array is always passed --- lib/Adapter/DTL/DTLArgumentResolver.php | 11 ++++- lib/Core/Dispatcher/ArgumentResolver.php | 3 +- .../ChainArgumentResolver.php | 5 ++- .../LanguageSeverProtocolParamsResolver.php | 18 +++++++- .../PassThroughArgumentResolver.php | 11 ++++- lib/Core/Handler/HandlerMethodRunner.php | 2 +- lib/Test/ProtocolFactory.php | 6 +++ .../ChainArgumentResolverTest.php | 9 +++- ...anguageSeverProtocolParamsResolverTest.php | 41 ++++++++++++++++++- 9 files changed, 93 insertions(+), 13 deletions(-) diff --git a/lib/Adapter/DTL/DTLArgumentResolver.php b/lib/Adapter/DTL/DTLArgumentResolver.php index 6f683cdb..7191f9e2 100644 --- a/lib/Adapter/DTL/DTLArgumentResolver.php +++ b/lib/Adapter/DTL/DTLArgumentResolver.php @@ -5,6 +5,9 @@ use DTL\ArgumentResolver\ArgumentResolver as UpstreamArgumentResolver; use DTL\ArgumentResolver\ParamConverter\RecursiveInstantiator; use Phpactor\LanguageServer\Core\Dispatcher\ArgumentResolver; +use Phpactor\LanguageServer\Core\Rpc\Message; +use Phpactor\LanguageServer\Core\Rpc\NotificationMessage; +use Phpactor\LanguageServer\Core\Rpc\RequestMessage; final class DTLArgumentResolver implements ArgumentResolver { @@ -20,8 +23,12 @@ public function __construct(UpstreamArgumentResolver $dtlArgumnetResolver = null ], UpstreamArgumentResolver::ALLOW_UNKNOWN_ARGUMENTS | UpstreamArgumentResolver::MATCH_TYPE); } - public function resolveArguments(object $object, string $method, array $arguments): array + public function resolveArguments(object $object, string $method, Message $message): array { - return $this->dtlArgumnetResolver->resolveArguments(get_class($object), $method, $arguments); + if (!$message instanceof RequestMessage && !$message instanceof NotificationMessage) { + return []; + } + + return $this->dtlArgumnetResolver->resolveArguments(get_class($object), $method, $message->params ?? []); } } diff --git a/lib/Core/Dispatcher/ArgumentResolver.php b/lib/Core/Dispatcher/ArgumentResolver.php index 3ab74f2b..1d3a309f 100644 --- a/lib/Core/Dispatcher/ArgumentResolver.php +++ b/lib/Core/Dispatcher/ArgumentResolver.php @@ -3,11 +3,12 @@ namespace Phpactor\LanguageServer\Core\Dispatcher; use Phpactor\LanguageServer\Core\Dispatcher\Exception\CouldNotResolveArguments; +use Phpactor\LanguageServer\Core\Rpc\Message; interface ArgumentResolver { /** * @throws CouldNotResolveArguments */ - public function resolveArguments(object $object, string $method, array $arguments): array; + public function resolveArguments(object $object, string $method, Message $message): array; } diff --git a/lib/Core/Dispatcher/ArgumentResolver/ChainArgumentResolver.php b/lib/Core/Dispatcher/ArgumentResolver/ChainArgumentResolver.php index f9682842..3cc86064 100644 --- a/lib/Core/Dispatcher/ArgumentResolver/ChainArgumentResolver.php +++ b/lib/Core/Dispatcher/ArgumentResolver/ChainArgumentResolver.php @@ -4,6 +4,7 @@ use Phpactor\LanguageServer\Core\Dispatcher\ArgumentResolver; use Phpactor\LanguageServer\Core\Dispatcher\Exception\CouldNotResolveArguments; +use Phpactor\LanguageServer\Core\Rpc\Message; final class ChainArgumentResolver implements ArgumentResolver { @@ -17,7 +18,7 @@ public function __construct(ArgumentResolver ...$resolvers) $this->resolvers = $resolvers; } - public function resolveArguments(object $object, string $method, array $arguments): array + public function resolveArguments(object $object, string $method, Message $request): array { if (empty($this->resolvers)) { throw new CouldNotResolveArguments('No resolvers defined in chain resolver, chain resolver cannot resolve anything'); @@ -25,7 +26,7 @@ public function resolveArguments(object $object, string $method, array $argument foreach ($this->resolvers as $resolver) { try { - return $resolver->resolveArguments($object, $method, $arguments); + return $resolver->resolveArguments($object, $method, $request); } catch (CouldNotResolveArguments $couldNotResolve) { $lastException = $couldNotResolve; } diff --git a/lib/Core/Dispatcher/ArgumentResolver/LanguageSeverProtocolParamsResolver.php b/lib/Core/Dispatcher/ArgumentResolver/LanguageSeverProtocolParamsResolver.php index 0520d619..b00670f3 100644 --- a/lib/Core/Dispatcher/ArgumentResolver/LanguageSeverProtocolParamsResolver.php +++ b/lib/Core/Dispatcher/ArgumentResolver/LanguageSeverProtocolParamsResolver.php @@ -4,6 +4,9 @@ use Phpactor\LanguageServer\Core\Dispatcher\ArgumentResolver; use Phpactor\LanguageServer\Core\Dispatcher\Exception\CouldNotResolveArguments; +use Phpactor\LanguageServer\Core\Rpc\Message; +use Phpactor\LanguageServer\Core\Rpc\NotificationMessage; +use Phpactor\LanguageServer\Core\Rpc\RequestMessage; use ReflectionClass; use ReflectionNamedType; @@ -12,9 +15,14 @@ final class LanguageSeverProtocolParamsResolver implements ArgumentResolver /** * {@inheritDoc} */ - public function resolveArguments(object $object, string $method, array $arguments): array + public function resolveArguments(object $object, string $method, Message $message): array { + if (!$message instanceof RequestMessage && !$message instanceof NotificationMessage) { + return []; + } + $reflection = new ReflectionClass($object); + $arguments = $message->params; if (!$reflection->hasMethod($method)) { throw new CouldNotResolveArguments(sprintf( @@ -38,6 +46,14 @@ public function resolveArguments(object $object, string $method, array $argument /** @var class-string */ $classFqn = $type->getName(); + if ($classFqn === RequestMessage::class && $message instanceof RequestMessage) { + return [$message]; + } + + if ($classFqn === NotificationMessage::class && $message instanceof NotificationMessage) { + return [$message]; + } + if (preg_match('{^Phpactor\\\LanguageServerProtocol\\\.*Params$}', $classFqn)) { return $this->doResolveArguments($classFqn, $parameter->getName(), $arguments); } diff --git a/lib/Core/Dispatcher/ArgumentResolver/PassThroughArgumentResolver.php b/lib/Core/Dispatcher/ArgumentResolver/PassThroughArgumentResolver.php index bea7005b..2b4ec207 100644 --- a/lib/Core/Dispatcher/ArgumentResolver/PassThroughArgumentResolver.php +++ b/lib/Core/Dispatcher/ArgumentResolver/PassThroughArgumentResolver.php @@ -3,14 +3,21 @@ namespace Phpactor\LanguageServer\Core\Dispatcher\ArgumentResolver; use Phpactor\LanguageServer\Core\Dispatcher\ArgumentResolver; +use Phpactor\LanguageServer\Core\Rpc\Message; +use Phpactor\LanguageServer\Core\Rpc\NotificationMessage; +use Phpactor\LanguageServer\Core\Rpc\RequestMessage; final class PassThroughArgumentResolver implements ArgumentResolver { /** * {@inheritDoc} */ - public function resolveArguments(object $object, string $method, array $arguments): array + public function resolveArguments(object $object, string $method, Message $request): array { - return $arguments; + if ($request instanceof RequestMessage || $request instanceof NotificationMessage) { + return $request->params ?? []; + } + + return []; } } diff --git a/lib/Core/Handler/HandlerMethodRunner.php b/lib/Core/Handler/HandlerMethodRunner.php index 9e6efbcb..597d4631 100644 --- a/lib/Core/Handler/HandlerMethodRunner.php +++ b/lib/Core/Handler/HandlerMethodRunner.php @@ -80,7 +80,7 @@ public function dispatch(Message $request): Promise $this->cancellations[$request->id] = $cancellationTokenSource; } - $args = array_values($this->argumentResolver->resolveArguments($handler, $method, $request->params ?? [])); + $args = array_values($this->argumentResolver->resolveArguments($handler, $method, $request)); $args[] = $cancellationTokenSource->getToken(); $promise = $handler->$method(...$args) ?? new Success(null); diff --git a/lib/Test/ProtocolFactory.php b/lib/Test/ProtocolFactory.php index 50f75c41..47692856 100644 --- a/lib/Test/ProtocolFactory.php +++ b/lib/Test/ProtocolFactory.php @@ -10,6 +10,7 @@ use Phpactor\LanguageServerProtocol\TextDocumentIdentifier; use Phpactor\LanguageServerProtocol\TextDocumentItem; use Phpactor\LanguageServerProtocol\VersionedTextDocumentIdentifier; +use Phpactor\LanguageServer\Core\Rpc\NotificationMessage; use Phpactor\LanguageServer\Core\Rpc\RequestMessage; final class ProtocolFactory @@ -42,6 +43,11 @@ public static function requestMessage(string $method, array $params): RequestMes return new RequestMessage(uniqid(), $method, $params); } + public static function notificationMessage(string $method, array $params): NotificationMessage + { + return new NotificationMessage($method, $params); + } + public static function range(int $line1, int $col1, int $line2, int $col2): Range { return new Range( diff --git a/tests/Unit/Core/Dispatcher/ArgumentResolver/ChainArgumentResolverTest.php b/tests/Unit/Core/Dispatcher/ArgumentResolver/ChainArgumentResolverTest.php index 803d15ad..ba34ab0d 100644 --- a/tests/Unit/Core/Dispatcher/ArgumentResolver/ChainArgumentResolverTest.php +++ b/tests/Unit/Core/Dispatcher/ArgumentResolver/ChainArgumentResolverTest.php @@ -6,6 +6,7 @@ use Phpactor\LanguageServer\Core\Dispatcher\ArgumentResolver; use Phpactor\LanguageServer\Core\Dispatcher\ArgumentResolver\ChainArgumentResolver; use Phpactor\LanguageServer\Core\Dispatcher\Exception\CouldNotResolveArguments; +use Phpactor\LanguageServer\Test\ProtocolFactory; use Prophecy\PhpUnit\ProphecyTrait; use stdClass; @@ -16,7 +17,7 @@ class ChainArgumentResolverTest extends TestCase public function testExceptionIfNoResolvers(): void { $this->expectException(CouldNotResolveArguments::class); - (new ChainArgumentResolver())->resolveArguments(new stdClass(), 'foo', [], []); + (new ChainArgumentResolver())->resolveArguments(new stdClass(), 'foo', ProtocolFactory::requestMessage('foo', [])); } public function testResolvesFirstThatReturns(): void @@ -26,6 +27,10 @@ public function testResolvesFirstThatReturns(): void $resolver1->resolveArguments(new stdClass(), 'foo', [], [])->willThrow(new CouldNotResolveArguments('foo')); $resolver1->resolveArguments(new stdClass(), 'foo', [], [])->willReturn(['foo' => 'bar']); $this->expectException(CouldNotResolveArguments::class); - self::assertEquals(['foo' => 'bar'], (new ChainArgumentResolver())->resolveArguments(new stdClass(), 'foo', [], [])); + self::assertEquals(['foo' => 'bar'], (new ChainArgumentResolver())->resolveArguments( + new stdClass(), + 'foo', + ProtocolFactory::requestMessage('foo', []), + )); } } diff --git a/tests/Unit/Core/Dispatcher/ArgumentResolver/LanguageSeverProtocolParamsResolverTest.php b/tests/Unit/Core/Dispatcher/ArgumentResolver/LanguageSeverProtocolParamsResolverTest.php index f79ed6a0..b68e2f83 100644 --- a/tests/Unit/Core/Dispatcher/ArgumentResolver/LanguageSeverProtocolParamsResolverTest.php +++ b/tests/Unit/Core/Dispatcher/ArgumentResolver/LanguageSeverProtocolParamsResolverTest.php @@ -8,6 +8,9 @@ use Phpactor\LanguageServerProtocol\InitializeParams; use Phpactor\LanguageServer\Core\Dispatcher\ArgumentResolver\LanguageSeverProtocolParamsResolver; use Phpactor\LanguageServer\Core\Dispatcher\Exception\CouldNotResolveArguments; +use Phpactor\LanguageServer\Core\Rpc\NotificationMessage; +use Phpactor\LanguageServer\Core\Rpc\RequestMessage; +use Phpactor\LanguageServer\Test\ProtocolFactory; class LanguageSeverProtocolParamsResolverTest extends TestCase { @@ -20,13 +23,39 @@ public function testResolvesLspParams(): void ], 'rootUri' => 'file://tmp/foo', ]; - $resolvedArgs = $resolver->resolveArguments($handler, 'initialize', $args); + $resolvedArgs = $resolver->resolveArguments($handler, 'initialize', ProtocolFactory::requestMessage('foo', $args)); self::assertEquals([ InitializeParams::fromArray($args), ], $resolvedArgs); } + public function testResolvesRawRequestMessage(): void + { + $handler = new LspHandler(); + $resolver = new LanguageSeverProtocolParamsResolver(); + $args = [ + 'foo' => 'bar', + ]; + $message = ProtocolFactory::requestMessage('foo', $args); + $resolvedArgs = $resolver->resolveArguments($handler, 'rawRequest', $message); + + self::assertEquals([$message], $resolvedArgs); + } + + public function testResolvesRawNotification(): void + { + $handler = new LspHandler(); + $resolver = new LanguageSeverProtocolParamsResolver(); + $args = [ + 'foo' => 'bar', + ]; + $message = ProtocolFactory::notificationMessage('foo', $args); + $resolvedArgs = $resolver->resolveArguments($handler, 'rawNotification', $message); + + self::assertEquals([$message], $resolvedArgs); + } + public function testNotResolvableWhenFirstParamNotProtocolParams(): void { $this->expectException(CouldNotResolveArguments::class); @@ -44,7 +73,7 @@ public function testNotResolvableWhenFirstParamNotProtocolParams(): void 'cancel' => $cancellationToken ]; - $resolvedArgs = $resolver->resolveArguments($handler, 'initializeWrongOrder', $args); + $resolvedArgs = $resolver->resolveArguments($handler, 'initializeWrongOrder', ProtocolFactory::requestMessage('foo', $args)); self::assertEquals([ InitializeParams::fromArray($args), @@ -62,4 +91,12 @@ public function initialize(InitializeParams $params, CancellationToken $c): void public function initializeWrongOrder(CancellationToken $c, InitializeParams $params): void { } + + public function rawRequest(RequestMessage $request): void + { + } + + public function rawNotification(NotificationMessage $notification): void + { + } } From 67a5a4fa5a8264181048d76b918f684c2c092976 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 2 Oct 2022 17:50:11 +0200 Subject: [PATCH 046/111] Add formatting infrastructure --- lib/Core/Formatting/Formatter.php | 15 +++++ .../TextDocument/FormattingHandler.php | 51 +++++++++++++++++ lib/Test/ProtocolFactory.php | 16 ++++-- .../TextDocument/FormattingHandlerTest.php | 56 +++++++++++++++++++ 4 files changed, 133 insertions(+), 5 deletions(-) create mode 100644 lib/Core/Formatting/Formatter.php create mode 100644 lib/Handler/TextDocument/FormattingHandler.php create mode 100644 tests/Unit/Handler/TextDocument/FormattingHandlerTest.php diff --git a/lib/Core/Formatting/Formatter.php b/lib/Core/Formatting/Formatter.php new file mode 100644 index 00000000..75580eac --- /dev/null +++ b/lib/Core/Formatting/Formatter.php @@ -0,0 +1,15 @@ +> + */ + public function format(TextDocumentItem $textDocument): Promise; +} diff --git a/lib/Handler/TextDocument/FormattingHandler.php b/lib/Handler/TextDocument/FormattingHandler.php new file mode 100644 index 00000000..32fdbad6 --- /dev/null +++ b/lib/Handler/TextDocument/FormattingHandler.php @@ -0,0 +1,51 @@ +workspace = $workspace; + $this->formatter = $formatter; + } + + public function methods(): array + { + return ['textDocument/formatting' => 'formatting']; + } + + /** + * @return Promise|null> + */ + public function formatting(TextDocumentIdentifier $textDocument, FormattingOptions $options): Promise + { + return call(function () use ($textDocument) { + $document = $this->workspace->get($textDocument->uri); + $formatted = yield $this->formatter->format($document); + + return $formatted; + }); + } + + public function registerCapabiltiies(ServerCapabilities $capabilities): void + { + $capabilities->documentFormattingProvider = true; + } +} diff --git a/lib/Test/ProtocolFactory.php b/lib/Test/ProtocolFactory.php index 47692856..2202b261 100644 --- a/lib/Test/ProtocolFactory.php +++ b/lib/Test/ProtocolFactory.php @@ -9,6 +9,7 @@ use Phpactor\LanguageServerProtocol\Range; use Phpactor\LanguageServerProtocol\TextDocumentIdentifier; use Phpactor\LanguageServerProtocol\TextDocumentItem; +use Phpactor\LanguageServerProtocol\TextEdit; use Phpactor\LanguageServerProtocol\VersionedTextDocumentIdentifier; use Phpactor\LanguageServer\Core\Rpc\NotificationMessage; use Phpactor\LanguageServer\Core\Rpc\RequestMessage; @@ -48,21 +49,26 @@ public static function notificationMessage(string $method, array $params): Notif return new NotificationMessage($method, $params); } - public static function range(int $line1, int $col1, int $line2, int $col2): Range + public static function range(int $line1, int $char1, int $line2, int $char2): Range { return new Range( - self::position($line1, $col1), - self::position($line2, $col2) + self::position($line1, $char1), + self::position($line2, $char2) ); } - public static function position(int $lineNb, int $colNb): Position + public static function position(int $line, int $char): Position { - return new Position($lineNb, $colNb); + return new Position($line, $char); } public static function diagnostic(Range $range, string $message): Diagnostic { return new Diagnostic($range, $message); } + + public static function textEdit(int $line1, int $char1, int $line2, int $char2, string $text): TextEdit + { + return new TextEdit(self::range($line1, $char1, $line2, $char2), $text); + } } diff --git a/tests/Unit/Handler/TextDocument/FormattingHandlerTest.php b/tests/Unit/Handler/TextDocument/FormattingHandlerTest.php new file mode 100644 index 00000000..8a023d4b --- /dev/null +++ b/tests/Unit/Handler/TextDocument/FormattingHandlerTest.php @@ -0,0 +1,56 @@ +addHandler(new FormattingHandler( + $tester->workspace(), + new TestFormatter( + ProtocolFactory::textEdit(0, 0, 0, 0, 'Hello'), + ) + )); + + $tester = $tester->build(); + + $tester->textDocument()->open('file://foobar', 'barfoo'); + + $response = $tester->requestAndWait(DocumentFormattingRequest::METHOD, new DocumentFormattingParams( + ProtocolFactory::textDocumentIdentifier('file://foobar'), + new FormattingOptions(4, false), + )); + + self::assertCount(1, $response->result, 'Example formatter provided results'); + } +} + +class TestFormatter implements Formatter +{ + private TextEdit $textEdit; + + public function __construct(TextEdit $textEdit) + { + $this->textEdit = $textEdit; + } + + public function format(TextDocumentItem $textDocument): Promise + { + return new Success([$this->textEdit]); + } +} From d20395c182c492ab606f9135ad1fe707620e9306 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 2 Oct 2022 18:25:31 +0200 Subject: [PATCH 047/111] Allow returning NULL --- lib/Core/Formatting/Formatter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Core/Formatting/Formatter.php b/lib/Core/Formatting/Formatter.php index 75580eac..e1da11d6 100644 --- a/lib/Core/Formatting/Formatter.php +++ b/lib/Core/Formatting/Formatter.php @@ -9,7 +9,7 @@ interface Formatter { /** - * @return Promise> + * @return Promise|null> */ public function format(TextDocumentItem $textDocument): Promise; } From 4cf9e8e3765ac8c6a26ad11da53ec966095fd0b1 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sat, 19 Nov 2022 11:35:30 +0100 Subject: [PATCH 048/111] ADd disabled workdonw progress notifier --- .../NullWorkDoneProgressNotifier.php | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 lib/WorkDoneProgress/NullWorkDoneProgressNotifier.php diff --git a/lib/WorkDoneProgress/NullWorkDoneProgressNotifier.php b/lib/WorkDoneProgress/NullWorkDoneProgressNotifier.php new file mode 100644 index 00000000..a44e6e51 --- /dev/null +++ b/lib/WorkDoneProgress/NullWorkDoneProgressNotifier.php @@ -0,0 +1,31 @@ + Date: Sat, 19 Nov 2022 11:36:23 +0100 Subject: [PATCH 049/111] Rename --- ...eProgressNotifier.php => SilentWorkDoneProgressNotifier.php} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename lib/WorkDoneProgress/{NullWorkDoneProgressNotifier.php => SilentWorkDoneProgressNotifier.php} (91%) diff --git a/lib/WorkDoneProgress/NullWorkDoneProgressNotifier.php b/lib/WorkDoneProgress/SilentWorkDoneProgressNotifier.php similarity index 91% rename from lib/WorkDoneProgress/NullWorkDoneProgressNotifier.php rename to lib/WorkDoneProgress/SilentWorkDoneProgressNotifier.php index a44e6e51..8740b0dc 100644 --- a/lib/WorkDoneProgress/NullWorkDoneProgressNotifier.php +++ b/lib/WorkDoneProgress/SilentWorkDoneProgressNotifier.php @@ -7,7 +7,7 @@ use Ramsey\Uuid\Uuid; use Phpactor\LanguageServer\Core\Rpc\ResponseMessage; -class NullWorkDoneProgressNotifier implements ProgressNotifier +class SilentWorkDoneProgressNotifier implements ProgressNotifier { public function create(WorkDoneToken $token): Promise { From 413b6580bef77bbb46b824760105dd0aec7919d7 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sat, 19 Nov 2022 11:43:25 +0100 Subject: [PATCH 050/111] ADd silent notifier --- .../MessageProgressNotifier.php | 3 +-- .../SilentWorkDoneProgressNotifier.php | 5 ++-- .../SilentProgressNotifierTest.php | 23 +++++++++++++++++++ 3 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 tests/Unit/WorkDoneProgress/SilentProgressNotifierTest.php diff --git a/lib/WorkDoneProgress/MessageProgressNotifier.php b/lib/WorkDoneProgress/MessageProgressNotifier.php index d7dccf44..48426b07 100644 --- a/lib/WorkDoneProgress/MessageProgressNotifier.php +++ b/lib/WorkDoneProgress/MessageProgressNotifier.php @@ -7,7 +7,6 @@ use Phpactor\LanguageServer\Core\Rpc\ResponseMessage; use Phpactor\LanguageServer\Core\Server\ClientApi; use Phpactor\LanguageServer\Core\Server\Client\MessageClient; -use Ramsey\Uuid\Uuid; final class MessageProgressNotifier implements ProgressNotifier { @@ -27,7 +26,7 @@ public function __construct(ClientApi $api) public function create(WorkDoneToken $token): Promise { return new Success(new ResponseMessage( - Uuid::uuid4(), + $token->__toString(), null, )); } diff --git a/lib/WorkDoneProgress/SilentWorkDoneProgressNotifier.php b/lib/WorkDoneProgress/SilentWorkDoneProgressNotifier.php index 8740b0dc..72614de9 100644 --- a/lib/WorkDoneProgress/SilentWorkDoneProgressNotifier.php +++ b/lib/WorkDoneProgress/SilentWorkDoneProgressNotifier.php @@ -4,15 +4,16 @@ use Amp\Promise; use Amp\Success; -use Ramsey\Uuid\Uuid; use Phpactor\LanguageServer\Core\Rpc\ResponseMessage; class SilentWorkDoneProgressNotifier implements ProgressNotifier { public function create(WorkDoneToken $token): Promise { + // yield a response _as if_ a message were sent to the client to start + // the progress. return new Success(new ResponseMessage( - Uuid::uuid4(), + $token->__toString(), null, )); } diff --git a/tests/Unit/WorkDoneProgress/SilentProgressNotifierTest.php b/tests/Unit/WorkDoneProgress/SilentProgressNotifierTest.php new file mode 100644 index 00000000..a7fa4e2d --- /dev/null +++ b/tests/Unit/WorkDoneProgress/SilentProgressNotifierTest.php @@ -0,0 +1,23 @@ +create($token)); + self::assertInstanceOf(ResponseMessage::class, $response); + $notifier->begin($token, 'Foobar'); + $notifier->report($token); + $notifier->end($token); + } +} From 9e2c9adaa90fcff7e3ebb025bfa67468617fb0c3 Mon Sep 17 00:00:00 2001 From: dantleech Date: Tue, 29 Nov 2022 21:37:21 +0100 Subject: [PATCH 051/111] Fix rpc message (#51) * Fix error response objects not conforming to JSON-RPC 2.0 * Drop 7.4 support * Fix CS Co-authored-by: Kamil --- .github/workflows/ci.yml | 6 +++--- composer.json | 2 +- lib/Core/Rpc/ResponseMessage.php | 5 +++-- lib/Core/Server/Transmitter/LspMessageSerializer.php | 11 ++++++++--- lib/Core/Service/ServiceManager.php | 1 - .../Server/Transmitter/LspMessageSerializerTest.php | 2 +- 6 files changed, 16 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a4692f6f..4d086580 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: strategy: matrix: php-version: - - '7.4' + - '8.0' steps: - @@ -52,7 +52,7 @@ jobs: strategy: matrix: php-version: - - '7.4' + - '8.0' steps: - @@ -85,9 +85,9 @@ jobs: strategy: matrix: php-version: - - '7.4' - '8.0' - '8.1' + - '8.2' steps: - diff --git a/composer.json b/composer.json index a1eb8e79..4a01ba9a 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,7 @@ } ], "require": { - "php": "^7.4 || ^8.0", + "php": "^8.0", "amphp/socket": "^1.1", "dantleech/argument-resolver": "^1.1", "dantleech/invoke": "^2.0", diff --git a/lib/Core/Rpc/ResponseMessage.php b/lib/Core/Rpc/ResponseMessage.php index 29ae0adc..78b60343 100644 --- a/lib/Core/Rpc/ResponseMessage.php +++ b/lib/Core/Rpc/ResponseMessage.php @@ -36,12 +36,13 @@ public function jsonSerialize(): array { $response = [ 'jsonrpc' => $this->jsonrpc, - 'id' => $this->id, - 'result' => $this->result, + 'id' => $this->id ]; if (null !== $this->error) { $response['error'] = $this->error; + } else { + $response['result'] = $this->result; } return $response; diff --git a/lib/Core/Server/Transmitter/LspMessageSerializer.php b/lib/Core/Server/Transmitter/LspMessageSerializer.php index 967063eb..45c5b39d 100644 --- a/lib/Core/Server/Transmitter/LspMessageSerializer.php +++ b/lib/Core/Server/Transmitter/LspMessageSerializer.php @@ -12,7 +12,7 @@ public function serialize(Message $message): string { $data = $this->normalize($message); if ($message instanceof ResponseMessage) { - $data = $this->ensureResultIsSet($data); + $data = $this->ensureOnlyResultOrErrorSet($data); } $decoded = json_encode($data); @@ -49,9 +49,14 @@ public function normalize($message) }); } - private function ensureResultIsSet(array $data): array + private function ensureOnlyResultOrErrorSet(array $data): array { - if (!array_key_exists('result', $data)) { + if (array_key_exists('error', $data) && array_key_exists('result', $data)) { + unset($data['result']); + return $data; + } + + if (!array_key_exists('error', $data) && !array_key_exists('result', $data)) { $data['result'] = null; } diff --git a/lib/Core/Service/ServiceManager.php b/lib/Core/Service/ServiceManager.php index 301de2bc..91e6f644 100644 --- a/lib/Core/Service/ServiceManager.php +++ b/lib/Core/Service/ServiceManager.php @@ -12,7 +12,6 @@ class ServiceManager { - /** * @var array */ diff --git a/tests/Unit/Core/Server/Transmitter/LspMessageSerializerTest.php b/tests/Unit/Core/Server/Transmitter/LspMessageSerializerTest.php index 010105c4..eae5f1af 100644 --- a/tests/Unit/Core/Server/Transmitter/LspMessageSerializerTest.php +++ b/tests/Unit/Core/Server/Transmitter/LspMessageSerializerTest.php @@ -36,7 +36,7 @@ public function provideSerializes() { yield 'response message' => [ new ResponseMessage(1, [], new ResponseError(1, 'foobar', [])), - '{"id":1,"result":[],"error":{"code":1,"message":"foobar","data":[]},"jsonrpc":"2.0"}', + '{"id":1,"error":{"code":1,"message":"foobar","data":[]},"jsonrpc":"2.0"}', ]; yield 'response message with null result' => [ From 7f68f100f44d3649e393a27d2d5e8640209567d0 Mon Sep 17 00:00:00 2001 From: Kamil Date: Tue, 29 Nov 2022 16:46:03 +0100 Subject: [PATCH 052/111] Make all ErrorHandlingMiddleware log all exceptions --- lib/Middleware/ErrorHandlingMiddleware.php | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/lib/Middleware/ErrorHandlingMiddleware.php b/lib/Middleware/ErrorHandlingMiddleware.php index 22c36c28..0dd05fc3 100644 --- a/lib/Middleware/ErrorHandlingMiddleware.php +++ b/lib/Middleware/ErrorHandlingMiddleware.php @@ -20,19 +20,13 @@ class ErrorHandlingMiddleware implements Middleware { - /** - * @var LoggerInterface - */ - private $logger; + private LoggerInterface $logger; public function __construct(LoggerInterface $logger) { $this->logger = $logger; } - /** - * {@inheritDoc} - */ public function process(Message $request, RequestHandler $handler): Promise { return call(function () use ($request, $handler) { @@ -55,13 +49,14 @@ public function process(Message $request, RequestHandler $handler): Promise )); } catch (Throwable $error) { $message = sprintf('Exception [%s] %s', get_class($error), $error->getMessage()); + $this->logger->error(sprintf( + 'Error when handling "%s" (%s): %s', + get_class($request), + json_encode($request), + $message + )); + if (!$request instanceof RequestMessage) { - $this->logger->error(sprintf( - 'Error when handling "%s" (%s): %s', - get_class($request), - json_encode($request), - $message - )); return new Success(null); } From e83805b952a77d97039cd288720e96fb5e994ad0 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sat, 10 Dec 2022 11:25:26 +0100 Subject: [PATCH 053/111] Fix race condition --- lib/Core/Diagnostics/DiagnosticsEngine.php | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/Core/Diagnostics/DiagnosticsEngine.php b/lib/Core/Diagnostics/DiagnosticsEngine.php index df6605c8..f5aa1fee 100644 --- a/lib/Core/Diagnostics/DiagnosticsEngine.php +++ b/lib/Core/Diagnostics/DiagnosticsEngine.php @@ -74,6 +74,11 @@ public function run(CancellationToken $token): Promise $textDocument = yield $this->deferred->promise(); + if ($this->next) { + $textDocument = $this->next; + $this->next = null; + } + $this->deferred = new Deferred(); // after we have reset deferred, we can safely set linting to @@ -86,11 +91,7 @@ public function run(CancellationToken $token): Promise yield delay($this->sleepTime); } - if ($this->next) { - $textDocument = $this->next; - $this->next = null; - } - + dump('ANALUZE: ' . $textDocument->text); $diagnostics = yield $this->provider->provideDiagnostics($textDocument, $token); $this->clientApi->diagnostics()->publishDiagnostics( @@ -104,12 +105,15 @@ public function run(CancellationToken $token): Promise public function enqueue(TextDocumentItem $textDocument): void { + dump('Pre: ' . $textDocument->text); // if we are already linting then store whatever comes afterwards in // next, overwriting the redundant update if ($this->running === true) { + dump('Deferred'); $this->next = $textDocument; return; } + dump('Immediate'); // resolving the promise will start PHPStan $this->running = true; From 57197aa71daf19ba50fe954c3d973b4b94ad101d Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sat, 10 Dec 2022 12:02:21 +0100 Subject: [PATCH 054/111] Remove debug statements --- lib/Core/Diagnostics/DiagnosticsEngine.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/Core/Diagnostics/DiagnosticsEngine.php b/lib/Core/Diagnostics/DiagnosticsEngine.php index f5aa1fee..82f0aa03 100644 --- a/lib/Core/Diagnostics/DiagnosticsEngine.php +++ b/lib/Core/Diagnostics/DiagnosticsEngine.php @@ -91,7 +91,6 @@ public function run(CancellationToken $token): Promise yield delay($this->sleepTime); } - dump('ANALUZE: ' . $textDocument->text); $diagnostics = yield $this->provider->provideDiagnostics($textDocument, $token); $this->clientApi->diagnostics()->publishDiagnostics( @@ -105,15 +104,12 @@ public function run(CancellationToken $token): Promise public function enqueue(TextDocumentItem $textDocument): void { - dump('Pre: ' . $textDocument->text); // if we are already linting then store whatever comes afterwards in // next, overwriting the redundant update if ($this->running === true) { - dump('Deferred'); $this->next = $textDocument; return; } - dump('Immediate'); // resolving the promise will start PHPStan $this->running = true; From 3cbbaa3ed4e2c7b2e8c9799038beddae2c4019e5 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sat, 10 Dec 2022 12:04:54 +0100 Subject: [PATCH 055/111] Add missing test --- .../Diagnostics/DiagnosticsEngineTest.php | 48 +++++++++++++++++-- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/tests/Unit/Core/Diagnostics/DiagnosticsEngineTest.php b/tests/Unit/Core/Diagnostics/DiagnosticsEngineTest.php index 0898e43b..a0043e08 100644 --- a/tests/Unit/Core/Diagnostics/DiagnosticsEngineTest.php +++ b/tests/Unit/Core/Diagnostics/DiagnosticsEngineTest.php @@ -11,6 +11,7 @@ use Phpactor\LanguageServer\Core\Diagnostics\DiagnosticsEngine; use Phpactor\LanguageServer\LanguageServerTesterBuilder; use Phpactor\LanguageServer\Test\ProtocolFactory; +use function Amp\asyncCall; use function Amp\call; use function Amp\delay; @@ -74,7 +75,6 @@ public function testDeduplicatesSuccessiveChangesToSameFile(): Generator $token = new CancellationTokenSource(); $promise = $engine->run($token->getToken()); - $engine->enqueue(ProtocolFactory::textDocumentItem('file:///foobar', 'bazboo')); $engine->enqueue(ProtocolFactory::textDocumentItem('file:///foobar', 'foobar')); $engine->enqueue(ProtocolFactory::textDocumentItem('file:///foobar', 'foobar')); @@ -107,13 +107,53 @@ public function testSleepPreventsSeige(): Generator self::assertEquals(2, $tester->transmitter()->count()); } - private function createEngine(LanguageServerTesterBuilder $tester, int $delay = 0, int $sleepTime = 0): DiagnosticsEngine + /** + * Note that this test was added in relation to the race condition in + * https://github.com/phpactor/phpactor/issues/1974 + * + * It DOES NOT reproduce the race condition sadly. + * + * See the commit this change was introduced to see what it SHOULD have covered. + * + * @return Generator + */ + public function testAlwaysAnalyzesTheLastChangeLast(): Generator + { + $tester = LanguageServerTesterBuilder::create(); + $lastDocument = ''; + $engine = $this->createEngine($tester, 5, 0, $lastDocument); + + $token = new CancellationTokenSource(); + asyncCall(function () use ($engine, $token) { + yield $engine->run($token->getToken()); + }); + yield new Delayed(1); + + $engine->enqueue(ProtocolFactory::textDocumentItem('file:///foobar', '1')); + yield new Delayed(1); + $engine->enqueue(ProtocolFactory::textDocumentItem('file:///foobar', '2')); + yield new Delayed(10); + $engine->enqueue(ProtocolFactory::textDocumentItem('file:///foobar', '3')); + yield new Delayed(1); + $engine->enqueue(ProtocolFactory::textDocumentItem('file:///foobar', '4')); + yield new Delayed(0); + $engine->enqueue(ProtocolFactory::textDocumentItem('file:///foobar', '5')); + + yield new Delayed(100); + + $token->cancel(); + + self::assertEquals('5', $lastDocument); + } + + private function createEngine(LanguageServerTesterBuilder $tester, int $delay = 0, int $sleepTime = 0, string &$lastDocument = null): DiagnosticsEngine { - $engine = new DiagnosticsEngine($tester->clientApi(), new ClosureDiagnosticsProvider(function (TextDocumentItem $item) use ($delay) { - return call(function () use ($delay) { + $engine = new DiagnosticsEngine($tester->clientApi(), new ClosureDiagnosticsProvider(function (TextDocumentItem $item) use ($delay, &$lastDocument) { + return call(function () use ($delay, $item, &$lastDocument) { if ($delay) { yield delay($delay); } + $lastDocument = $item->text; return [ ProtocolFactory::diagnostic( ProtocolFactory::range(0, 0, 0, 0), From 6c29ede39d90a7a56828c356e70bc58bae44b794 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sat, 31 Dec 2022 12:09:01 +0000 Subject: [PATCH 056/111] Revert "Fix race condition" This reverts commit e83805b952a77d97039cd288720e96fb5e994ad0. --- lib/Core/Diagnostics/DiagnosticsEngine.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/Core/Diagnostics/DiagnosticsEngine.php b/lib/Core/Diagnostics/DiagnosticsEngine.php index 82f0aa03..df6605c8 100644 --- a/lib/Core/Diagnostics/DiagnosticsEngine.php +++ b/lib/Core/Diagnostics/DiagnosticsEngine.php @@ -74,11 +74,6 @@ public function run(CancellationToken $token): Promise $textDocument = yield $this->deferred->promise(); - if ($this->next) { - $textDocument = $this->next; - $this->next = null; - } - $this->deferred = new Deferred(); // after we have reset deferred, we can safely set linting to @@ -91,6 +86,11 @@ public function run(CancellationToken $token): Promise yield delay($this->sleepTime); } + if ($this->next) { + $textDocument = $this->next; + $this->next = null; + } + $diagnostics = yield $this->provider->provideDiagnostics($textDocument, $token); $this->clientApi->diagnostics()->publishDiagnostics( From f0bcb5126a477c382b0ab5f18be7c42a5ac0d316 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Fri, 10 Feb 2023 22:30:31 +0000 Subject: [PATCH 057/111] Updating to new version --- lib/Core/Server/Client/WorkspaceClient.php | 6 +++--- lib/Test/ProtocolFactory.php | 4 ++-- tests/Unit/Core/Server/ClientApiTest.php | 4 ++-- tests/Unit/Core/Workspace/WorkspaceTest.php | 21 ++++++++----------- .../TextDocument/TextDocumentHandlerTest.php | 4 ++-- 5 files changed, 18 insertions(+), 21 deletions(-) diff --git a/lib/Core/Server/Client/WorkspaceClient.php b/lib/Core/Server/Client/WorkspaceClient.php index 49e850f9..852e59b6 100644 --- a/lib/Core/Server/Client/WorkspaceClient.php +++ b/lib/Core/Server/Client/WorkspaceClient.php @@ -4,7 +4,7 @@ use Amp\Promise; use DTL\Invoke\Invoke; -use Phpactor\LanguageServerProtocol\ApplyWorkspaceEditResponse; +use Phpactor\LanguageServerProtocol\ApplyWorkspaceEditResult; use Phpactor\LanguageServerProtocol\WorkspaceEdit; use Phpactor\LanguageServer\Core\Server\RpcClient; @@ -21,7 +21,7 @@ public function __construct(RpcClient $client) } /** - * @return Promise + * @return Promise */ public function applyEdit(WorkspaceEdit $edit, ?string $label = null): Promise { @@ -34,7 +34,7 @@ public function applyEdit(WorkspaceEdit $edit, ?string $label = null): Promise ] ); - return Invoke::new(ApplyWorkspaceEditResponse::class, (array)$response->result); + return Invoke::new(ApplyWorkspaceEditResult::class, (array)$response->result); }); } diff --git a/lib/Test/ProtocolFactory.php b/lib/Test/ProtocolFactory.php index 2202b261..6db758d6 100644 --- a/lib/Test/ProtocolFactory.php +++ b/lib/Test/ProtocolFactory.php @@ -21,9 +21,9 @@ public static function textDocumentItem(string $uri, string $content): TextDocum return new TextDocumentItem($uri, 'php', 1, $content); } - public static function versionedTextDocumentIdentifier(?string $uri = 'foobar', ?int $version = null): VersionedTextDocumentIdentifier + public static function versionedTextDocumentIdentifier(string $uri, int $version): VersionedTextDocumentIdentifier { - return new VersionedTextDocumentIdentifier($uri, $version); + return new VersionedTextDocumentIdentifier($version, $uri); } public static function textDocumentIdentifier(string $uri): TextDocumentIdentifier diff --git a/tests/Unit/Core/Server/ClientApiTest.php b/tests/Unit/Core/Server/ClientApiTest.php index 0a8d6cf1..45438eac 100644 --- a/tests/Unit/Core/Server/ClientApiTest.php +++ b/tests/Unit/Core/Server/ClientApiTest.php @@ -5,7 +5,7 @@ use Amp\PHPUnit\AsyncTestCase; use Closure; use Generator; -use Phpactor\LanguageServerProtocol\ApplyWorkspaceEditResponse; +use Phpactor\LanguageServerProtocol\ApplyWorkspaceEditResult; use Phpactor\LanguageServerProtocol\DidChangeWatchedFilesRegistrationOptions; use Phpactor\LanguageServerProtocol\FileSystemWatcher; use Phpactor\LanguageServerProtocol\MessageActionItem; @@ -145,7 +145,7 @@ function (TestRpcClient $client, $result): void { self::assertEquals('workspace/applyEdit', $message->method); $result = \Amp\Promise\wait($result); - self::assertInstanceOf(ApplyWorkspaceEditResponse::class, $result); + self::assertInstanceOf(ApplyWorkspaceEditResult::class, $result); self::assertFalse($result->applied); self::assertEquals('sorry', $result->failureReason); } diff --git a/tests/Unit/Core/Workspace/WorkspaceTest.php b/tests/Unit/Core/Workspace/WorkspaceTest.php index 2858927c..e47498d0 100644 --- a/tests/Unit/Core/Workspace/WorkspaceTest.php +++ b/tests/Unit/Core/Workspace/WorkspaceTest.php @@ -2,6 +2,7 @@ namespace Phpactor\LanguageServer\Tests\Unit\Core\Workspace; +use Generator; use Phpactor\LanguageServerProtocol\TextDocumentIdentifier; use Phpactor\LanguageServerProtocol\TextDocumentItem; use Phpactor\LanguageServerProtocol\VersionedTextDocumentIdentifier; @@ -40,14 +41,14 @@ public function testThrowsExceptionUpdateUnknown(): void { $this->expectException(UnknownDocument::class); - $expectedDocument = new VersionedTextDocumentIdentifier('foobar'); + $expectedDocument = new VersionedTextDocumentIdentifier(version: 1, uri: 'foobar'); $this->workspace->update($expectedDocument, 'foobar'); } public function testUpdatesDocument(): void { $originalDocument = new TextDocumentItem('foobar', 'php', 1, 'foo'); - $expectedDocument = new VersionedTextDocumentIdentifier($originalDocument->uri); + $expectedDocument = new VersionedTextDocumentIdentifier(1, $originalDocument->uri); $this->workspace->open($originalDocument); $this->workspace->update($expectedDocument, 'my new text'); $document = $this->workspace->get('foobar'); @@ -59,7 +60,7 @@ public function testUpdatesDocument(): void public function testUpdatesDocumentVersion(): void { $originalDocument = new TextDocumentItem('foobar', 'php', 1, 'foo'); - $expectedDocument = new VersionedTextDocumentIdentifier($originalDocument->uri, 5); + $expectedDocument = new VersionedTextDocumentIdentifier(5, $originalDocument->uri); $this->workspace->open($originalDocument); $this->workspace->update($expectedDocument, 'my new text'); $document = $this->workspace->get('foobar'); @@ -70,13 +71,13 @@ public function testUpdatesDocumentVersion(): void /** * @dataProvider provideDoesNotUpdateDocumentWithLowerVersionThanExistingDocument */ - public function testDoesNotUpdateDocumentWithLowerVersionThanExistingDocument(int $originalVersion, ?int $newVersion, bool $shouldBeNewer): void + public function testDoesNotUpdateDocumentWithLowerVersionThanExistingDocument(int $originalVersion, int $newVersion, bool $shouldBeNewer): void { $originalDocument = new TextDocumentItem('foobar', 'php', $originalVersion, 'original document'); $this->workspace->open($originalDocument); - $oldDocument = new VersionedTextDocumentIdentifier($originalDocument->uri, $newVersion); + $oldDocument = new VersionedTextDocumentIdentifier($newVersion, $originalDocument->uri); $this->workspace->update($oldDocument, 'new document'); @@ -85,7 +86,9 @@ public function testDoesNotUpdateDocumentWithLowerVersionThanExistingDocument(in $this->assertEquals($oldDocument->uri, $document->uri); $this->assertEquals($shouldBeNewer ? 'new document' : 'original document', $document->text); } - + /** + * @return Generator + */ public function provideDoesNotUpdateDocumentWithLowerVersionThanExistingDocument() { yield 'older document does not overwrite' => [ @@ -99,12 +102,6 @@ public function provideDoesNotUpdateDocumentWithLowerVersionThanExistingDocument 5, true ]; - - yield 'null overwrites the document' => [ - 5, - null, - true - ]; } public function testReturnsNumberOfOpenFiles(): void diff --git a/tests/Unit/Handler/TextDocument/TextDocumentHandlerTest.php b/tests/Unit/Handler/TextDocument/TextDocumentHandlerTest.php index 491336b7..3ca9d848 100644 --- a/tests/Unit/Handler/TextDocument/TextDocumentHandlerTest.php +++ b/tests/Unit/Handler/TextDocument/TextDocumentHandlerTest.php @@ -57,7 +57,7 @@ public function testOpensDocument(): void public function testUpdatesDocument(): void { $textDocument = ProtocolFactory::textDocumentItem('foobar', 'foo'); - $identifier = ProtocolFactory::versionedTextDocumentIdentifier('foobar'); + $identifier = ProtocolFactory::versionedTextDocumentIdentifier('foobar', 1); $this->dispatch('textDocument/didChange', [ 'textDocument' => $identifier, @@ -100,7 +100,7 @@ public function testClosesDocument(): void public function testSavesDocument(): void { - $identifier = ProtocolFactory::versionedTextDocumentIdentifier('foobar'); + $identifier = ProtocolFactory::versionedTextDocumentIdentifier('foobar', 1); $this->dispatch('textDocument/didSave', [ 'textDocument' => $identifier, 'text' => 'hello', From 0043aa94a531274e4260e8213f432ea0068e1a64 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Fri, 10 Feb 2023 22:48:08 +0000 Subject: [PATCH 058/111] Fix type of saved document --- lib/Event/TextDocumentSaved.php | 10 ++++------ lib/Listener/DidChangeWatchedFilesListener.php | 2 +- .../Listener/DidChangeWatchedFilesListenerTest.php | 10 +++++++--- tests/Unit/Listener/WorkspaceListenerTest.php | 3 +-- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/lib/Event/TextDocumentSaved.php b/lib/Event/TextDocumentSaved.php index e701206c..27c301e9 100644 --- a/lib/Event/TextDocumentSaved.php +++ b/lib/Event/TextDocumentSaved.php @@ -2,27 +2,25 @@ namespace Phpactor\LanguageServer\Event; +use Phpactor\LanguageServerProtocol\TextDocumentIdentifier; use Phpactor\LanguageServerProtocol\VersionedTextDocumentIdentifier; class TextDocumentSaved { - /** - * @var VersionedTextDocumentIdentifier - */ - private $identifier; + private TextDocumentIdentifier $identifier; /** * @var string|null */ private $text; - public function __construct(VersionedTextDocumentIdentifier $identifier, ?string $text = null) + public function __construct(TextDocumentIdentifier $identifier, ?string $text = null) { $this->identifier = $identifier; $this->text = $text; } - public function identifier(): VersionedTextDocumentIdentifier + public function identifier(): TextDocumentIdentifier { return $this->identifier; } diff --git a/lib/Listener/DidChangeWatchedFilesListener.php b/lib/Listener/DidChangeWatchedFilesListener.php index 6a79e074..fdef55b1 100644 --- a/lib/Listener/DidChangeWatchedFilesListener.php +++ b/lib/Listener/DidChangeWatchedFilesListener.php @@ -50,7 +50,7 @@ public function getListenersForEvent(object $event): iterable public function registerCapability(Initialized $initialized): void { - if (!($this->clientCapabilities->workspace['didChangeWatchedFiles']['dynamicRegistration'] ?? false)) { + if (!($this->clientCapabilities?->workspace?->didChangeWatchedFiles?->dynamicRegistration ?? false)) { return; } diff --git a/tests/Unit/Listener/DidChangeWatchedFilesListenerTest.php b/tests/Unit/Listener/DidChangeWatchedFilesListenerTest.php index 69a57ee4..81416af3 100644 --- a/tests/Unit/Listener/DidChangeWatchedFilesListenerTest.php +++ b/tests/Unit/Listener/DidChangeWatchedFilesListenerTest.php @@ -3,9 +3,11 @@ namespace Phpactor\LanguageServer\Tests\Unit\Listener; use Phpactor\LanguageServerProtocol\ClientCapabilities; +use Phpactor\LanguageServerProtocol\DidChangeWatchedFilesClientCapabilities; use Phpactor\LanguageServerProtocol\DidChangeWatchedFilesRegistrationOptions; use Phpactor\LanguageServerProtocol\FileSystemWatcher; use Phpactor\LanguageServerProtocol\Registration; +use Phpactor\LanguageServerProtocol\WorkspaceClientCapabilities; use Phpactor\LanguageServer\Core\Server\ClientApi; use Phpactor\LanguageServer\Core\Server\RpcClient\TestRpcClient; use Phpactor\LanguageServer\Core\Server\Transmitter\TestMessageTransmitter; @@ -27,9 +29,11 @@ class DidChangeWatchedFilesListenerTest extends TestCase public function testDynamicallyRegisterIfSupported(): void { - $this->initListener(new ClientCapabilities([ - 'didChangeWatchedFiles' => ['dynamicRegistration' => true], - ])); + $this->initListener(new ClientCapabilities( + workspace: new WorkspaceClientCapabilities( + didChangeWatchedFiles: new DidChangeWatchedFilesClientCapabilities(dynamicRegistration: true), + ) + )); $this->dispatch( new Initialized(), ); diff --git a/tests/Unit/Listener/WorkspaceListenerTest.php b/tests/Unit/Listener/WorkspaceListenerTest.php index bb2dda34..eb3f813f 100644 --- a/tests/Unit/Listener/WorkspaceListenerTest.php +++ b/tests/Unit/Listener/WorkspaceListenerTest.php @@ -3,7 +3,6 @@ namespace Phpactor\LanguageServer\Tests\Unit\Listener; use Phpactor\LanguageServerProtocol\TextDocumentItem; -use Phpactor\LanguageServerProtocol\VersionedTextDocumentIdentifier; use Phpactor\LanguageServer\Core\Workspace\Workspace; use Phpactor\LanguageServer\Listener\WorkspaceListener; use Phpactor\LanguageServer\Event\TextDocumentClosed; @@ -54,7 +53,7 @@ public function testOpened(): void public function testUpdated(): void { - $identifier = new VersionedTextDocumentIdentifier('file://test', 1); + $identifier = ProtocolFactory::versionedTextDocumentIdentifier('file://test', 1); $this->workspace->update( $identifier, 'new text' From 390d112bbc000f8bac6cd5dde8f6d1388330dfa1 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Fri, 10 Feb 2023 22:48:53 +0000 Subject: [PATCH 059/111] Fix array access --- .../ClientCapabilityDependentProgressNotifier.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/WorkDoneProgress/ClientCapabilityDependentProgressNotifier.php b/lib/WorkDoneProgress/ClientCapabilityDependentProgressNotifier.php index b38f65ef..6483d0e1 100644 --- a/lib/WorkDoneProgress/ClientCapabilityDependentProgressNotifier.php +++ b/lib/WorkDoneProgress/ClientCapabilityDependentProgressNotifier.php @@ -58,7 +58,7 @@ public function end(WorkDoneToken $token, ?string $message = null): void private function createNotifier(ClientApi $api, ClientCapabilities $capabilities): ProgressNotifier { - if ($capabilities->window['workDoneProgress'] ?? false) { + if ($capabilities->window->workDoneProgress ?? false) { return new WorkDoneProgressNotifier($api); } From a50210b67df70ecdd675cfbb9478b9b5a8a774f7 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Fri, 10 Feb 2023 22:49:01 +0000 Subject: [PATCH 060/111] Update composer --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 4a01ba9a..65bdc50a 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ "amphp/socket": "^1.1", "dantleech/argument-resolver": "^1.1", "dantleech/invoke": "^2.0", - "phpactor/language-server-protocol": "~0.1", + "phpactor/language-server-protocol": "dev-3.17-2", "psr/event-dispatcher": "^1.0", "psr/log": "^1.0", "ramsey/uuid": "^4.0", From bd9343829890fa53ceedfb4e2c8af397dd5960e2 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Fri, 10 Feb 2023 22:50:44 +0000 Subject: [PATCH 061/111] Update --- lib/Core/CodeAction/CodeActionProvider.php | 3 ++- lib/Event/TextDocumentSaved.php | 1 - lib/Example/CodeAction/SayHelloCodeActionProvider.php | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/Core/CodeAction/CodeActionProvider.php b/lib/Core/CodeAction/CodeActionProvider.php index 567c4637..fe029379 100644 --- a/lib/Core/CodeAction/CodeActionProvider.php +++ b/lib/Core/CodeAction/CodeActionProvider.php @@ -5,6 +5,7 @@ use Amp\CancellationToken; use Amp\Promise; use Phpactor\LanguageServerProtocol\CodeAction; +use Phpactor\LanguageServerProtocol\CodeActionKind; use Phpactor\LanguageServerProtocol\Range; use Phpactor\LanguageServerProtocol\TextDocumentItem; @@ -21,7 +22,7 @@ public function provideActionsFor(TextDocumentItem $textDocument, Range $range, * * @see Phpactor\LanguageServerProtocol\CodeAction * - * @return array + * @return list */ public function kinds(): array; } diff --git a/lib/Event/TextDocumentSaved.php b/lib/Event/TextDocumentSaved.php index 27c301e9..e409d0d6 100644 --- a/lib/Event/TextDocumentSaved.php +++ b/lib/Event/TextDocumentSaved.php @@ -3,7 +3,6 @@ namespace Phpactor\LanguageServer\Event; use Phpactor\LanguageServerProtocol\TextDocumentIdentifier; -use Phpactor\LanguageServerProtocol\VersionedTextDocumentIdentifier; class TextDocumentSaved { diff --git a/lib/Example/CodeAction/SayHelloCodeActionProvider.php b/lib/Example/CodeAction/SayHelloCodeActionProvider.php index 26d1a6cb..e91ace7e 100644 --- a/lib/Example/CodeAction/SayHelloCodeActionProvider.php +++ b/lib/Example/CodeAction/SayHelloCodeActionProvider.php @@ -5,6 +5,7 @@ use Amp\CancellationToken; use Amp\Promise; use Phpactor\LanguageServerProtocol\CodeAction; +use Phpactor\LanguageServerProtocol\CodeActionKind; use Phpactor\LanguageServerProtocol\Command; use Phpactor\LanguageServerProtocol\Range; use Phpactor\LanguageServerProtocol\TextDocumentItem; @@ -38,6 +39,6 @@ public function provideActionsFor(TextDocumentItem $textDocument, Range $range, */ public function kinds(): array { - return ['example']; + return [CodeActionKind::QUICK_FIX]; } } From e6485adfdab6d0f129a069247c2438bc32c94c59 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sat, 11 Feb 2023 13:32:56 +0000 Subject: [PATCH 062/111] Update dep --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 65bdc50a..a515386a 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ "amphp/socket": "^1.1", "dantleech/argument-resolver": "^1.1", "dantleech/invoke": "^2.0", - "phpactor/language-server-protocol": "dev-3.17-2", + "phpactor/language-server-protocol": "^3.17", "psr/event-dispatcher": "^1.0", "psr/log": "^1.0", "ramsey/uuid": "^4.0", From af911fed14f30bcc35ffdca00378e100580c5f59 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sat, 11 Feb 2023 13:41:48 +0000 Subject: [PATCH 063/111] Fix server info --- bin/serve.php | 3 ++- .../acme-ls/AcmeLsDispatcherFactory.php | 3 ++- lib/Middleware/InitializeMiddleware.php | 21 ++++++------------- .../Middleware/InitializeMiddlewareTest.php | 6 ++++-- 4 files changed, 14 insertions(+), 19 deletions(-) diff --git a/bin/serve.php b/bin/serve.php index 2baaa9a3..db7c8d40 100755 --- a/bin/serve.php +++ b/bin/serve.php @@ -148,7 +148,8 @@ function (MessageTransmitter $transmitter, InitializeParams $params) use ($logge return new MiddlewareDispatcher( new ErrorHandlingMiddleware($logger), new InitializeMiddleware($handlers, $eventDispatcher, [ - 'version' => 1, + 'name' => 'phpactor', + 'version' => '1', ]), new CancellationMiddleware($runner), new ResponseHandlingMiddleware($responseWatcher), diff --git a/example/server/acme-ls/AcmeLsDispatcherFactory.php b/example/server/acme-ls/AcmeLsDispatcherFactory.php index f4610829..0e384485 100644 --- a/example/server/acme-ls/AcmeLsDispatcherFactory.php +++ b/example/server/acme-ls/AcmeLsDispatcherFactory.php @@ -81,7 +81,8 @@ public function create(MessageTransmitter $transmitter, InitializeParams $initia return new MiddlewareDispatcher( new ErrorHandlingMiddleware($this->logger), new InitializeMiddleware($handlers, $eventDispatcher, [ - 'version' => 1, + 'name' => 'acme', + 'version' => '1', ]), new ShutdownMiddleware($eventDispatcher), new ResponseHandlingMiddleware($responseWatcher), diff --git a/lib/Middleware/InitializeMiddleware.php b/lib/Middleware/InitializeMiddleware.php index 1e06b80c..27d663f0 100644 --- a/lib/Middleware/InitializeMiddleware.php +++ b/lib/Middleware/InitializeMiddleware.php @@ -23,27 +23,15 @@ class InitializeMiddleware implements Middleware private const METHOD_INITIALIZED = 'initialized'; private const METHOD_INITIALIZE = 'initialize'; - /** - * @var Handlers - */ - private $handlers; - /** * @var bool */ private $initialized = false; /** - * @var EventDispatcherInterface + * @param array{name?:string,version?:string} $serverInfo */ - private $dispatcher; - - /** - * @var array - */ - private $serverInfo; - - public function __construct(Handlers $handlers, EventDispatcherInterface $dispatcher, array $serverInfo = []) + public function __construct(private Handlers $handlers, private EventDispatcherInterface $dispatcher, private array $serverInfo = []) { $this->handlers = $handlers; $this->dispatcher = $dispatcher; @@ -88,7 +76,10 @@ public function process(Message $request, RequestHandler $handler): Promise return new Success( new ResponseMessage( $request->id, - new InitializeResult($serverCapabilities, $this->serverInfo) + new InitializeResult($serverCapabilities, array_merge([ + 'name' => 'unspecified', + 'version' => 'unspecified', + ], $this->serverInfo)) ) ); } diff --git a/tests/Unit/Middleware/InitializeMiddlewareTest.php b/tests/Unit/Middleware/InitializeMiddlewareTest.php index ae577b24..bd9f56da 100644 --- a/tests/Unit/Middleware/InitializeMiddlewareTest.php +++ b/tests/Unit/Middleware/InitializeMiddlewareTest.php @@ -83,7 +83,8 @@ public function testExceptionIfInitializedTwice(): Generator public function testReturnsInitializedResponse(): Generator { $middleware = $this->createMiddleware([], [ - 'server_info' => 'please', + 'name' => 'test', + 'version' => '12a' ]); $response = yield $middleware->process( @@ -95,7 +96,8 @@ public function testReturnsInitializedResponse(): Generator self::assertInstanceOf(ResponseMessage::class, $response); self::assertInstanceOf(InitializeResult::class, $response->result); self::assertEquals([ - 'server_info' => 'please', + 'name' => 'test', + 'version' => '12a' ], $response->result->serverInfo); } From d8f29dcf139a71c5a9a9f7a51ec83f1c17a5612f Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sat, 11 Feb 2023 17:39:48 +0000 Subject: [PATCH 064/111] PHPStan level 9 --- .../LanguageSeverProtocolParamsResolver.php | 2 +- .../Dispatcher/ClosureDispatcher.php | 5 +++ lib/Core/Handler/HandlerMethodRunner.php | 2 +- lib/Core/Rpc/NotificationMessage.php | 2 +- lib/Core/Rpc/RequestMessage.php | 5 ++- .../Server/Initializer/RequestInitializer.php | 2 +- lib/Core/Server/Parser/LspMessageReader.php | 7 ++++ .../StreamProvider/SocketStreamProvider.php | 5 ++- .../Transmitter/LspMessageSerializer.php | 7 ++-- lib/Core/Workspace/Workspace.php | 5 +-- .../CodeAction/SayHelloCodeActionProvider.php | 3 +- .../SayHelloDiagnosticsProvider.php | 1 + .../TextDocument/CodeActionHandler.php | 1 + lib/Handler/Workspace/CommandHandler.php | 2 +- lib/Middleware/CancellationMiddleware.php | 2 +- lib/Middleware/ClosureMiddleware.php | 1 + lib/Test/LanguageServerTester.php | 39 +++++++++++++++++-- .../LanguageServerTester/ServicesTester.php | 10 ++++- .../MessageProgressNotifier.php | 4 +- phpstan.neon | 2 +- 20 files changed, 82 insertions(+), 25 deletions(-) diff --git a/lib/Core/Dispatcher/ArgumentResolver/LanguageSeverProtocolParamsResolver.php b/lib/Core/Dispatcher/ArgumentResolver/LanguageSeverProtocolParamsResolver.php index b00670f3..658ad6d9 100644 --- a/lib/Core/Dispatcher/ArgumentResolver/LanguageSeverProtocolParamsResolver.php +++ b/lib/Core/Dispatcher/ArgumentResolver/LanguageSeverProtocolParamsResolver.php @@ -55,7 +55,7 @@ public function resolveArguments(object $object, string $method, Message $messag } if (preg_match('{^Phpactor\\\LanguageServerProtocol\\\.*Params$}', $classFqn)) { - return $this->doResolveArguments($classFqn, $parameter->getName(), $arguments); + return $this->doResolveArguments($classFqn, $parameter->getName(), $arguments ?: []); } throw new CouldNotResolveArguments(sprintf( diff --git a/lib/Core/Dispatcher/Dispatcher/ClosureDispatcher.php b/lib/Core/Dispatcher/Dispatcher/ClosureDispatcher.php index a7b3e36c..96f6f6f8 100644 --- a/lib/Core/Dispatcher/Dispatcher/ClosureDispatcher.php +++ b/lib/Core/Dispatcher/Dispatcher/ClosureDispatcher.php @@ -6,6 +6,7 @@ use Closure; use Phpactor\LanguageServer\Core\Dispatcher\Dispatcher; use Phpactor\LanguageServer\Core\Rpc\Message; +use Phpactor\LanguageServer\Core\Rpc\ResponseMessage; final class ClosureDispatcher implements Dispatcher { @@ -14,6 +15,9 @@ final class ClosureDispatcher implements Dispatcher */ private $closure; + /** + * @param Closure(Message): Promise $closure + */ public function __construct(Closure $closure) { $this->closure = $closure; @@ -24,6 +28,7 @@ public function __construct(Closure $closure) */ public function dispatch(Message $request): Promise { + /** @phpstan-ignore-next-line */ return $this->closure->__invoke($request); } } diff --git a/lib/Core/Handler/HandlerMethodRunner.php b/lib/Core/Handler/HandlerMethodRunner.php index 597d4631..67b7cdc3 100644 --- a/lib/Core/Handler/HandlerMethodRunner.php +++ b/lib/Core/Handler/HandlerMethodRunner.php @@ -38,7 +38,7 @@ final class HandlerMethodRunner implements MethodRunner private $logger; /** - * @var ArgumentResolver|null + * @var ArgumentResolver */ private $argumentResolver; diff --git a/lib/Core/Rpc/NotificationMessage.php b/lib/Core/Rpc/NotificationMessage.php index 2fef6d42..ffc8d8ff 100644 --- a/lib/Core/Rpc/NotificationMessage.php +++ b/lib/Core/Rpc/NotificationMessage.php @@ -10,7 +10,7 @@ final class NotificationMessage extends Message public $method; /** - * @var array + * @var array|null */ public $params; diff --git a/lib/Core/Rpc/RequestMessage.php b/lib/Core/Rpc/RequestMessage.php index e4d4308b..9bbad79f 100644 --- a/lib/Core/Rpc/RequestMessage.php +++ b/lib/Core/Rpc/RequestMessage.php @@ -15,7 +15,7 @@ final class RequestMessage extends Message public $method; /** - * @var array + * @var array|null */ public $params; @@ -30,6 +30,9 @@ public function __construct($id, string $method, ?array $params) $this->params = $params; } + /** + * @return array + */ public function toArray(): array { return [ diff --git a/lib/Core/Server/Initializer/RequestInitializer.php b/lib/Core/Server/Initializer/RequestInitializer.php index 5b8f5543..0b415294 100644 --- a/lib/Core/Server/Initializer/RequestInitializer.php +++ b/lib/Core/Server/Initializer/RequestInitializer.php @@ -26,6 +26,6 @@ public function provideInitializeParams(Message $request): InitializeParams )); } - return InitializeParams::fromArray($request->params, true); + return InitializeParams::fromArray($request->params ?? [], true); } } diff --git a/lib/Core/Server/Parser/LspMessageReader.php b/lib/Core/Server/Parser/LspMessageReader.php index 97f131d8..9f24d0c8 100644 --- a/lib/Core/Server/Parser/LspMessageReader.php +++ b/lib/Core/Server/Parser/LspMessageReader.php @@ -132,6 +132,13 @@ private function decodeBody(string $string): array )); } + if (!is_array($array)) { + throw new CouldNotDecodeBody(sprintf( + 'Expected an array got "%s"', + gettype($array) + )); + } + return $array; } } diff --git a/lib/Core/Server/StreamProvider/SocketStreamProvider.php b/lib/Core/Server/StreamProvider/SocketStreamProvider.php index 9d2e024a..b0576288 100644 --- a/lib/Core/Server/StreamProvider/SocketStreamProvider.php +++ b/lib/Core/Server/StreamProvider/SocketStreamProvider.php @@ -8,6 +8,7 @@ use Amp\Socket\Socket; use Phpactor\LanguageServer\Core\Server\Stream\SocketDuplexStream; use Psr\Log\LoggerInterface; +use Throwable; final class SocketStreamProvider implements StreamProvider { @@ -32,8 +33,8 @@ public function accept(): Promise $promise = $this->server->accept(); $deferred = new Deferred(); - $promise->onResolve(function ($reason, ?Socket $socket) use ($deferred): void { - if (null === $socket) { + $promise->onResolve(function (?Throwable $reason, mixed $socket) use ($deferred): void { + if (!$socket instanceof Socket) { return; } diff --git a/lib/Core/Server/Transmitter/LspMessageSerializer.php b/lib/Core/Server/Transmitter/LspMessageSerializer.php index 45c5b39d..784a66fe 100644 --- a/lib/Core/Server/Transmitter/LspMessageSerializer.php +++ b/lib/Core/Server/Transmitter/LspMessageSerializer.php @@ -11,6 +11,9 @@ final class LspMessageSerializer implements MessageSerializer public function serialize(Message $message): string { $data = $this->normalize($message); + if (!is_array($data)) { + throw new RuntimeException('Expected an array'); + } if ($message instanceof ResponseMessage) { $data = $this->ensureOnlyResultOrErrorSet($data); } @@ -31,10 +34,8 @@ public function serialize(Message $message): string * and removing null values * * @param mixed $message - * - * @return mixed */ - public function normalize($message) + public function normalize($message): mixed { if (is_object($message)) { $message = (array) $message; diff --git a/lib/Core/Workspace/Workspace.php b/lib/Core/Workspace/Workspace.php index 46b2eb64..da3f1823 100644 --- a/lib/Core/Workspace/Workspace.php +++ b/lib/Core/Workspace/Workspace.php @@ -27,10 +27,7 @@ class Workspace implements Countable, IteratorAggregate */ private $documentVersions = []; - /** - * @var LoggerInterface|null - */ - private $logger; + private LoggerInterface $logger; public function __construct(?LoggerInterface $logger = null) { diff --git a/lib/Example/CodeAction/SayHelloCodeActionProvider.php b/lib/Example/CodeAction/SayHelloCodeActionProvider.php index e91ace7e..9df9d1ea 100644 --- a/lib/Example/CodeAction/SayHelloCodeActionProvider.php +++ b/lib/Example/CodeAction/SayHelloCodeActionProvider.php @@ -16,7 +16,8 @@ class SayHelloCodeActionProvider implements CodeActionProvider { public function provideActionsFor(TextDocumentItem $textDocument, Range $range, CancellationToken $cancel): Promise { - return call(function () { + /** @phpstan-ignore-next-line */ + return call(function (): array { return [ CodeAction::fromArray([ 'title' => 'Alice', diff --git a/lib/Example/Diagnostics/SayHelloDiagnosticsProvider.php b/lib/Example/Diagnostics/SayHelloDiagnosticsProvider.php index f8751827..9e516e51 100644 --- a/lib/Example/Diagnostics/SayHelloDiagnosticsProvider.php +++ b/lib/Example/Diagnostics/SayHelloDiagnosticsProvider.php @@ -19,6 +19,7 @@ class SayHelloDiagnosticsProvider implements DiagnosticsProvider */ public function provideDiagnostics(TextDocumentItem $textDocument, CancellationToken $cancel): Promise { + /** @phpstan-ignore-next-line */ return call(function () { return [ new Diagnostic( diff --git a/lib/Handler/TextDocument/CodeActionHandler.php b/lib/Handler/TextDocument/CodeActionHandler.php index 52fafddf..08b18edb 100644 --- a/lib/Handler/TextDocument/CodeActionHandler.php +++ b/lib/Handler/TextDocument/CodeActionHandler.php @@ -55,6 +55,7 @@ public function registerCapabiltiies(ServerCapabilities $capabilities): void */ public function codeAction(CodeActionParams $params, CancellationToken $cancel): Promise { + /** @phpstan-ignore-next-line */ return call(function () use ($params, $cancel) { $document = $this->workspace->get($params->textDocument->uri); return $this->provider->provideActionsFor($document, $params->range, $cancel); diff --git a/lib/Handler/Workspace/CommandHandler.php b/lib/Handler/Workspace/CommandHandler.php index 355eebae..f56cf005 100644 --- a/lib/Handler/Workspace/CommandHandler.php +++ b/lib/Handler/Workspace/CommandHandler.php @@ -37,7 +37,7 @@ public function methods(): array */ public function executeCommand(ExecuteCommandParams $params): Promise { - return $this->dispatcher->dispatch($params->command, $params->arguments); + return $this->dispatcher->dispatch($params->command, $params->arguments ?? []); } public function registerCapabiltiies(ServerCapabilities $capabilities): void diff --git a/lib/Middleware/CancellationMiddleware.php b/lib/Middleware/CancellationMiddleware.php index c25c3f2d..7992c921 100644 --- a/lib/Middleware/CancellationMiddleware.php +++ b/lib/Middleware/CancellationMiddleware.php @@ -36,7 +36,7 @@ public function process(Message $message, RequestHandler $handler): Promise ) { $id = $message->params['id'] ?? null; - if (null === $id) { + if (!is_int($id) && !is_string($id)) { throw new RuntimeException( 'ID parameter not present in cancel notification' ); diff --git a/lib/Middleware/ClosureMiddleware.php b/lib/Middleware/ClosureMiddleware.php index 1902d980..a4b586f0 100644 --- a/lib/Middleware/ClosureMiddleware.php +++ b/lib/Middleware/ClosureMiddleware.php @@ -25,6 +25,7 @@ public function __construct(Closure $closure) */ public function process(Message $request, RequestHandler $handler): Promise { + /** @phpstan-ignore-next-line */ return $this->closure->__invoke($request, $handler); } } diff --git a/lib/Test/LanguageServerTester.php b/lib/Test/LanguageServerTester.php index b2d31275..6cf1ada3 100644 --- a/lib/Test/LanguageServerTester.php +++ b/lib/Test/LanguageServerTester.php @@ -84,6 +84,23 @@ public function requestAndWait(string $method, $params, $id = null): ?ResponseMe return wait($this->request($method, $this->normalizeParams($params), $id)); } + /** + * @param array|object $params + */ + public function mustRequestAndWait(string $method, mixed $params, int|string $id = null): ResponseMessage + { + $response = $this->requestAndWait($method, $params); + + if (null === $response) { + throw new RuntimeException(sprintf( + 'Expected request to method "%s" to return a response, but it did not!', + $method + )); + } + + return $response; + } + /** * @param mixed $value * @param string|int $id @@ -137,11 +154,19 @@ public function transmitter(): TestMessageTransmitter */ public function initialize(): InitializeResult { - $response = $this->requestAndWait('initialize', $this->initializeParams); + $response = $this->mustRequestAndWait('initialize', $this->initializeParams); $this->assertSuccess($response); $this->notifyAndWait('initialized', []); - return $response->result; + $result = $response->result; + if (!$result instanceof InitializeResult) { + throw new RuntimeException(sprintf( + 'Initialize did not return an InitializeResult, got "%s"', + is_object($result) ? get_class($result) : gettype($result) + )); + } + + return $result; } public function services(): ServicesTester @@ -190,6 +215,14 @@ private function normalizeParams($params): array return $params; } - return $this->messageSerializer->normalize($params); + $params = $this->messageSerializer->normalize($params); + if (!is_array($params)) { + throw new RuntimeException(sprintf( + 'Could not normalize params, returned as type %s', + gettype($params) + )); + } + + return $params; } } diff --git a/lib/Test/LanguageServerTester/ServicesTester.php b/lib/Test/LanguageServerTester/ServicesTester.php index cb759a09..b76827b3 100644 --- a/lib/Test/LanguageServerTester/ServicesTester.php +++ b/lib/Test/LanguageServerTester/ServicesTester.php @@ -18,11 +18,17 @@ public function __construct(LanguageServerTester $tester) /** * Return running services + * + * @return list */ public function listRunning(): array { - $response = $this->tester->requestAndWait('phpactor/service/running', []); - return $response->result; + $response = $this->tester->mustRequestAndWait('phpactor/service/running', []); + $running = $response->result; + if (!is_array($running)) { + return []; + } + return $running; } /** diff --git a/lib/WorkDoneProgress/MessageProgressNotifier.php b/lib/WorkDoneProgress/MessageProgressNotifier.php index 48426b07..1b6406d7 100644 --- a/lib/WorkDoneProgress/MessageProgressNotifier.php +++ b/lib/WorkDoneProgress/MessageProgressNotifier.php @@ -41,7 +41,7 @@ public function begin( ?int $percentage = null, ?bool $cancellable = null ): void { - $this->api->info($message); + $this->api->info($message ?? ''); } /** @@ -54,6 +54,6 @@ public function report(WorkDoneToken $token, ?string $message = null, ?int $perc public function end(WorkDoneToken $token, ?string $message = null): void { - $this->api->info($message); + $this->api->info($message ?? ''); } } diff --git a/phpstan.neon b/phpstan.neon index 8f4bfc52..61cde2c5 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,5 +1,5 @@ parameters: - level: 7 + level: 9 inferPrivatePropertyTypeFromConstructor: true checkMissingIterableValueType: false paths: From 0b04d4805ba8ead778df06db8e92a97a836d11ab Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sat, 11 Feb 2023 17:40:45 +0000 Subject: [PATCH 065/111] Add baseline for iterable types --- phpstan-baseline.neon | 306 ++++++++++++++++++++++++++++++++++++++++++ phpstan.neon | 2 +- 2 files changed, 307 insertions(+), 1 deletion(-) create mode 100644 phpstan-baseline.neon diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 00000000..c17241eb --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,306 @@ +parameters: + ignoreErrors: + - + message: "#^Method Phpactor\\\\LanguageServer\\\\Adapter\\\\DTL\\\\DTLArgumentResolver\\:\\:resolveArguments\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Adapter/DTL/DTLArgumentResolver.php + + - + message: "#^Property Phpactor\\\\LanguageServer\\\\Core\\\\Command\\\\CommandDispatcher\\:\\:\\$commandMap type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Core/Command/CommandDispatcher.php + + - + message: "#^Method Phpactor\\\\LanguageServer\\\\Core\\\\Dispatcher\\\\ArgumentResolver\\:\\:resolveArguments\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Core/Dispatcher/ArgumentResolver.php + + - + message: "#^Method Phpactor\\\\LanguageServer\\\\Core\\\\Dispatcher\\\\ArgumentResolver\\\\ChainArgumentResolver\\:\\:resolveArguments\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Core/Dispatcher/ArgumentResolver/ChainArgumentResolver.php + + - + message: "#^Method Phpactor\\\\LanguageServer\\\\Core\\\\Dispatcher\\\\ArgumentResolver\\\\LanguageSeverProtocolParamsResolver\\:\\:doResolveArguments\\(\\) has parameter \\$arguments with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Core/Dispatcher/ArgumentResolver/LanguageSeverProtocolParamsResolver.php + + - + message: "#^Method Phpactor\\\\LanguageServer\\\\Core\\\\Dispatcher\\\\ArgumentResolver\\\\LanguageSeverProtocolParamsResolver\\:\\:doResolveArguments\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Core/Dispatcher/ArgumentResolver/LanguageSeverProtocolParamsResolver.php + + - + message: "#^Method Phpactor\\\\LanguageServer\\\\Core\\\\Dispatcher\\\\ArgumentResolver\\\\LanguageSeverProtocolParamsResolver\\:\\:resolveArguments\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Core/Dispatcher/ArgumentResolver/LanguageSeverProtocolParamsResolver.php + + - + message: "#^Method Phpactor\\\\LanguageServer\\\\Core\\\\Dispatcher\\\\ArgumentResolver\\\\PassThroughArgumentResolver\\:\\:resolveArguments\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Core/Dispatcher/ArgumentResolver/PassThroughArgumentResolver.php + + - + message: "#^Property Phpactor\\\\LanguageServer\\\\Core\\\\Dispatcher\\\\Dispatcher\\\\MiddlewareDispatcher\\:\\:\\$middleware type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Core/Dispatcher/Dispatcher/MiddlewareDispatcher.php + + - + message: "#^Property Phpactor\\\\LanguageServer\\\\Core\\\\Handler\\\\Handlers\\:\\:\\$methods type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Core/Handler/Handlers.php + + - + message: "#^Method Phpactor\\\\LanguageServer\\\\Core\\\\Middleware\\\\RequestHandler\\:\\:__construct\\(\\) has parameter \\$queue with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Core/Middleware/RequestHandler.php + + - + message: "#^Method Phpactor\\\\LanguageServer\\\\Core\\\\Rpc\\\\NotificationMessage\\:\\:__construct\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Core/Rpc/NotificationMessage.php + + - + message: "#^Method Phpactor\\\\LanguageServer\\\\Core\\\\Rpc\\\\RawMessage\\:\\:__construct\\(\\) has parameter \\$body with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Core/Rpc/RawMessage.php + + - + message: "#^Method Phpactor\\\\LanguageServer\\\\Core\\\\Rpc\\\\RawMessage\\:\\:__construct\\(\\) has parameter \\$headers with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Core/Rpc/RawMessage.php + + - + message: "#^Method Phpactor\\\\LanguageServer\\\\Core\\\\Rpc\\\\RawMessage\\:\\:body\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Core/Rpc/RawMessage.php + + - + message: "#^Method Phpactor\\\\LanguageServer\\\\Core\\\\Rpc\\\\RawMessage\\:\\:headers\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Core/Rpc/RawMessage.php + + - + message: "#^Property Phpactor\\\\LanguageServer\\\\Core\\\\Rpc\\\\RawMessage\\:\\:\\$body type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Core/Rpc/RawMessage.php + + - + message: "#^Property Phpactor\\\\LanguageServer\\\\Core\\\\Rpc\\\\RawMessage\\:\\:\\$headers type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Core/Rpc/RawMessage.php + + - + message: "#^Method Phpactor\\\\LanguageServer\\\\Core\\\\Rpc\\\\RequestMessage\\:\\:__construct\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Core/Rpc/RequestMessage.php + + - + message: "#^Method Phpactor\\\\LanguageServer\\\\Core\\\\Rpc\\\\ResponseMessage\\:\\:jsonSerialize\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Core/Rpc/ResponseMessage.php + + - + message: "#^Method Phpactor\\\\LanguageServer\\\\Core\\\\Server\\\\Client\\\\DiagnosticsClient\\:\\:publishDiagnostics\\(\\) has parameter \\$diagnostics with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Core/Server/Client/DiagnosticsClient.php + + - + message: "#^Method Phpactor\\\\LanguageServer\\\\Core\\\\Server\\\\Client\\\\WorkDoneProgressClient\\:\\:notify\\(\\) has parameter \\$value with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Core/Server/Client/WorkDoneProgressClient.php + + - + message: "#^Method Phpactor\\\\LanguageServer\\\\Core\\\\Server\\\\Client\\\\WorkspaceClient\\:\\:executeCommand\\(\\) has parameter \\$arguments with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Core/Server/Client/WorkspaceClient.php + + - + message: "#^Method Phpactor\\\\LanguageServer\\\\Core\\\\Server\\\\Parser\\\\LspMessageReader\\:\\:decodeBody\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Core/Server/Parser/LspMessageReader.php + + - + message: "#^Method Phpactor\\\\LanguageServer\\\\Core\\\\Server\\\\Parser\\\\LspMessageReader\\:\\:parseHeaders\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Core/Server/Parser/LspMessageReader.php + + - + message: "#^Property Phpactor\\\\LanguageServer\\\\Core\\\\Server\\\\ResponseWatcher\\\\TestResponseWatcher\\:\\:\\$requestIds type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Core/Server/ResponseWatcher/TestResponseWatcher.php + + - + message: "#^Method Phpactor\\\\LanguageServer\\\\Core\\\\Server\\\\RpcClient\\:\\:notification\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Core/Server/RpcClient.php + + - + message: "#^Method Phpactor\\\\LanguageServer\\\\Core\\\\Server\\\\RpcClient\\:\\:request\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Core/Server/RpcClient.php + + - + message: "#^Method Phpactor\\\\LanguageServer\\\\Core\\\\Server\\\\RpcClient\\\\JsonRpcClient\\:\\:notification\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Core/Server/RpcClient/JsonRpcClient.php + + - + message: "#^Method Phpactor\\\\LanguageServer\\\\Core\\\\Server\\\\RpcClient\\\\JsonRpcClient\\:\\:request\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Core/Server/RpcClient/JsonRpcClient.php + + - + message: "#^Method Phpactor\\\\LanguageServer\\\\Core\\\\Server\\\\RpcClient\\\\TestRpcClient\\:\\:notification\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Core/Server/RpcClient/TestRpcClient.php + + - + message: "#^Method Phpactor\\\\LanguageServer\\\\Core\\\\Server\\\\RpcClient\\\\TestRpcClient\\:\\:request\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Core/Server/RpcClient/TestRpcClient.php + + - + message: "#^Method Phpactor\\\\LanguageServer\\\\Core\\\\Server\\\\Transmitter\\\\LspMessageSerializer\\:\\:ensureOnlyResultOrErrorSet\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Core/Server/Transmitter/LspMessageSerializer.php + + - + message: "#^Method Phpactor\\\\LanguageServer\\\\Core\\\\Server\\\\Transmitter\\\\LspMessageSerializer\\:\\:ensureOnlyResultOrErrorSet\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Core/Server/Transmitter/LspMessageSerializer.php + + - + message: "#^Property Phpactor\\\\LanguageServer\\\\Core\\\\Service\\\\ServiceManager\\:\\:\\$cancellations type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Core/Service/ServiceManager.php + + - + message: "#^Method Phpactor\\\\LanguageServer\\\\Core\\\\Service\\\\ServiceProvider\\:\\:services\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Core/Service/ServiceProvider.php + + - + message: "#^Method Phpactor\\\\LanguageServer\\\\Event\\\\FilesChanged\\:\\:events\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Event/FilesChanged.php + + - + message: "#^Property Phpactor\\\\LanguageServer\\\\Event\\\\FilesChanged\\:\\:\\$events type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Event/FilesChanged.php + + - + message: "#^Method Phpactor\\\\LanguageServer\\\\Example\\\\Service\\\\PingProvider\\:\\:services\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Example/Service/PingProvider.php + + - + message: "#^Method Phpactor\\\\LanguageServer\\\\Handler\\\\System\\\\ServiceHandler\\:\\:runningServices\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Handler/System/ServiceHandler.php + + - + message: "#^Property Phpactor\\\\LanguageServer\\\\LanguageServerTesterBuilder\\:\\:\\$commands type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/LanguageServerTesterBuilder.php + + - + message: "#^Property Phpactor\\\\LanguageServer\\\\LanguageServerTesterBuilder\\:\\:\\$serviceProviders type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/LanguageServerTesterBuilder.php + + - + message: "#^Method Phpactor\\\\LanguageServer\\\\Listener\\\\DidChangeWatchedFilesListener\\:\\:__construct\\(\\) has parameter \\$globPatterns with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Listener/DidChangeWatchedFilesListener.php + + - + message: "#^Method Phpactor\\\\LanguageServer\\\\Listener\\\\DidChangeWatchedFilesListener\\:\\:getListenersForEvent\\(\\) return type has no value type specified in iterable type iterable\\.$#" + count: 1 + path: lib/Listener/DidChangeWatchedFilesListener.php + + - + message: "#^Property Phpactor\\\\LanguageServer\\\\Listener\\\\DidChangeWatchedFilesListener\\:\\:\\$globPatterns type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Listener/DidChangeWatchedFilesListener.php + + - + message: "#^Method Phpactor\\\\LanguageServer\\\\Listener\\\\ServiceListener\\:\\:getListenersForEvent\\(\\) return type has no value type specified in iterable type iterable\\.$#" + count: 1 + path: lib/Listener/ServiceListener.php + + - + message: "#^Method Phpactor\\\\LanguageServer\\\\Listener\\\\WorkspaceListener\\:\\:getListenersForEvent\\(\\) return type has no value type specified in iterable type iterable\\.$#" + count: 1 + path: lib/Listener/WorkspaceListener.php + + - + message: "#^Method Phpactor\\\\LanguageServer\\\\Middleware\\\\MethodAliasMiddleware\\:\\:__construct\\(\\) has parameter \\$aliasMap with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Middleware/MethodAliasMiddleware.php + + - + message: "#^Property Phpactor\\\\LanguageServer\\\\Middleware\\\\MethodAliasMiddleware\\:\\:\\$aliasMap type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Middleware/MethodAliasMiddleware.php + + - + message: "#^Method Phpactor\\\\LanguageServer\\\\Service\\\\DiagnosticsService\\:\\:getListenersForEvent\\(\\) return type has no value type specified in iterable type iterable\\.$#" + count: 1 + path: lib/Service/DiagnosticsService.php + + - + message: "#^Method Phpactor\\\\LanguageServer\\\\Service\\\\DiagnosticsService\\:\\:services\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Service/DiagnosticsService.php + + - + message: "#^Method Phpactor\\\\LanguageServer\\\\Test\\\\LanguageServerTester\\:\\:mustRequestAndWait\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Test/LanguageServerTester.php + + - + message: "#^Method Phpactor\\\\LanguageServer\\\\Test\\\\LanguageServerTester\\:\\:normalizeParams\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Test/LanguageServerTester.php + + - + message: "#^Method Phpactor\\\\LanguageServer\\\\Test\\\\LanguageServerTester\\:\\:notify\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Test/LanguageServerTester.php + + - + message: "#^Method Phpactor\\\\LanguageServer\\\\Test\\\\LanguageServerTester\\:\\:notifyAndWait\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Test/LanguageServerTester.php + + - + message: "#^Method Phpactor\\\\LanguageServer\\\\Test\\\\LanguageServerTester\\:\\:request\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Test/LanguageServerTester.php + + - + message: "#^Method Phpactor\\\\LanguageServer\\\\Test\\\\LanguageServerTester\\:\\:requestAndWait\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Test/LanguageServerTester.php + + - + message: "#^Method Phpactor\\\\LanguageServer\\\\Test\\\\LanguageServerTester\\\\WorkspaceTester\\:\\:executeCommand\\(\\) has parameter \\$args with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Test/LanguageServerTester/WorkspaceTester.php + + - + message: "#^Method Phpactor\\\\LanguageServer\\\\Test\\\\ListenerProvider\\\\RecordingListenerProvider\\:\\:getListenersForEvent\\(\\) return type has no value type specified in iterable type iterable\\.$#" + count: 1 + path: lib/Test/ListenerProvider/RecordingListenerProvider.php + + - + message: "#^Method Phpactor\\\\LanguageServer\\\\Test\\\\ProtocolFactory\\:\\:notificationMessage\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Test/ProtocolFactory.php + + - + message: "#^Method Phpactor\\\\LanguageServer\\\\Test\\\\ProtocolFactory\\:\\:requestMessage\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Test/ProtocolFactory.php diff --git a/phpstan.neon b/phpstan.neon index 61cde2c5..1b77674c 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,7 +1,7 @@ parameters: level: 9 inferPrivatePropertyTypeFromConstructor: true - checkMissingIterableValueType: false + checkMissingIterableValueType: true paths: - lib - example From 323f7565b16b47bc10b5bddd88390b977cb65487 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sat, 11 Feb 2023 17:42:20 +0000 Subject: [PATCH 066/111] Update --- lib/Core/Service/ServiceProvider.php | 2 ++ phpstan-baseline.neon | 15 --------------- phpstan.neon | 3 +++ 3 files changed, 5 insertions(+), 15 deletions(-) diff --git a/lib/Core/Service/ServiceProvider.php b/lib/Core/Service/ServiceProvider.php index dec4e8af..2c4d5e3f 100644 --- a/lib/Core/Service/ServiceProvider.php +++ b/lib/Core/Service/ServiceProvider.php @@ -25,6 +25,8 @@ interface ServiceProvider * // exit immediately * return new Success(); * } + * + * @return list */ public function services(): array; } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index c17241eb..59031ae1 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -175,11 +175,6 @@ parameters: count: 1 path: lib/Core/Service/ServiceManager.php - - - message: "#^Method Phpactor\\\\LanguageServer\\\\Core\\\\Service\\\\ServiceProvider\\:\\:services\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: lib/Core/Service/ServiceProvider.php - - message: "#^Method Phpactor\\\\LanguageServer\\\\Event\\\\FilesChanged\\:\\:events\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 @@ -190,11 +185,6 @@ parameters: count: 1 path: lib/Event/FilesChanged.php - - - message: "#^Method Phpactor\\\\LanguageServer\\\\Example\\\\Service\\\\PingProvider\\:\\:services\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: lib/Example/Service/PingProvider.php - - message: "#^Method Phpactor\\\\LanguageServer\\\\Handler\\\\System\\\\ServiceHandler\\:\\:runningServices\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 @@ -250,11 +240,6 @@ parameters: count: 1 path: lib/Service/DiagnosticsService.php - - - message: "#^Method Phpactor\\\\LanguageServer\\\\Service\\\\DiagnosticsService\\:\\:services\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: lib/Service/DiagnosticsService.php - - message: "#^Method Phpactor\\\\LanguageServer\\\\Test\\\\LanguageServerTester\\:\\:mustRequestAndWait\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#" count: 1 diff --git a/phpstan.neon b/phpstan.neon index 1b77674c..65942c83 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -6,3 +6,6 @@ parameters: - lib - example - bin + +includes: + - phpstan-baseline.neon From b68058e7b1561d88ddbaad729c861dfa3bb9c2c1 Mon Sep 17 00:00:00 2001 From: Kamil Date: Sun, 12 Feb 2023 12:40:50 +0100 Subject: [PATCH 067/111] Allow any string in CodeActionProvider::kinds method As per specification this list is open - https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#codeActionKind --- lib/Core/CodeAction/CodeActionProvider.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/Core/CodeAction/CodeActionProvider.php b/lib/Core/CodeAction/CodeActionProvider.php index fe029379..13b897f1 100644 --- a/lib/Core/CodeAction/CodeActionProvider.php +++ b/lib/Core/CodeAction/CodeActionProvider.php @@ -5,7 +5,6 @@ use Amp\CancellationToken; use Amp\Promise; use Phpactor\LanguageServerProtocol\CodeAction; -use Phpactor\LanguageServerProtocol\CodeActionKind; use Phpactor\LanguageServerProtocol\Range; use Phpactor\LanguageServerProtocol\TextDocumentItem; @@ -22,7 +21,7 @@ public function provideActionsFor(TextDocumentItem $textDocument, Range $range, * * @see Phpactor\LanguageServerProtocol\CodeAction * - * @return list + * @return string[] */ public function kinds(): array; } From 4a0de9fd8315323e93b06fbb33921dca9e5cfea6 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 12 Mar 2023 11:49:11 +0000 Subject: [PATCH 068/111] Add names() accessor --- .../Diagnostics/AggregateDiagnosticsProvider.php | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/Core/Diagnostics/AggregateDiagnosticsProvider.php b/lib/Core/Diagnostics/AggregateDiagnosticsProvider.php index 2eb1c7d7..0f6f6604 100644 --- a/lib/Core/Diagnostics/AggregateDiagnosticsProvider.php +++ b/lib/Core/Diagnostics/AggregateDiagnosticsProvider.php @@ -65,11 +65,19 @@ public function provideDiagnostics(TextDocumentItem $textDocument, CancellationT }); } - public function name(): string + /** + * @return list + */ + public function names(): array { - return implode(', ', array_map( + return array_map( fn (DiagnosticsProvider $provider) => $provider->name(), $this->providers - )); + ); + } + + public function name(): string + { + return implode(', ', $this->names()); } } From e921d1d945e9040b86ed8d6a21c00908c45487bd Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 9 Apr 2023 10:33:54 +0100 Subject: [PATCH 069/111] Require that code actions describe themselves --- lib/Core/CodeAction/AggregateCodeActionProvider.php | 5 +++++ lib/Core/CodeAction/CodeActionProvider.php | 2 ++ lib/Example/CodeAction/SayHelloCodeActionProvider.php | 5 +++++ tests/Unit/Diagnostics/CodeActionDiagnosticsProviderTest.php | 5 +++++ 4 files changed, 17 insertions(+) diff --git a/lib/Core/CodeAction/AggregateCodeActionProvider.php b/lib/Core/CodeAction/AggregateCodeActionProvider.php index b5e365f8..401cbd15 100644 --- a/lib/Core/CodeAction/AggregateCodeActionProvider.php +++ b/lib/Core/CodeAction/AggregateCodeActionProvider.php @@ -58,4 +58,9 @@ function (array $kinds, CodeActionProvider $provider) { ) ); } + + public function describe(): string + { + return sprintf('aggregate code action proivder with %s providers', count($this->providers)); + } } diff --git a/lib/Core/CodeAction/CodeActionProvider.php b/lib/Core/CodeAction/CodeActionProvider.php index 13b897f1..78263da4 100644 --- a/lib/Core/CodeAction/CodeActionProvider.php +++ b/lib/Core/CodeAction/CodeActionProvider.php @@ -24,4 +24,6 @@ public function provideActionsFor(TextDocumentItem $textDocument, Range $range, * @return string[] */ public function kinds(): array; + + public function describe(): string; } diff --git a/lib/Example/CodeAction/SayHelloCodeActionProvider.php b/lib/Example/CodeAction/SayHelloCodeActionProvider.php index 9df9d1ea..b46a2216 100644 --- a/lib/Example/CodeAction/SayHelloCodeActionProvider.php +++ b/lib/Example/CodeAction/SayHelloCodeActionProvider.php @@ -42,4 +42,9 @@ public function kinds(): array { return [CodeActionKind::QUICK_FIX]; } + + public function describe(): string + { + return 'says hello!'; + } } diff --git a/tests/Unit/Diagnostics/CodeActionDiagnosticsProviderTest.php b/tests/Unit/Diagnostics/CodeActionDiagnosticsProviderTest.php index 6d5762ed..9472262c 100644 --- a/tests/Unit/Diagnostics/CodeActionDiagnosticsProviderTest.php +++ b/tests/Unit/Diagnostics/CodeActionDiagnosticsProviderTest.php @@ -68,4 +68,9 @@ public function kinds(): array { return ['foo']; } + + public function describe(): string + { + return 'test'; + } } From 7ea31688877bbdf29822408d02ad9231a9dbbb91 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 9 Apr 2023 10:34:45 +0100 Subject: [PATCH 070/111] Update CL --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93b9bcb4..a87e7fdc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +5.0 +--- + +- Code actions must implement the `describe(): string` method. + 1.0.2 ----- From 378783b37ccdef2d1727353f6a4467a167faae52 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 9 Apr 2023 10:34:45 +0100 Subject: [PATCH 071/111] Update CL --- lib/Core/Rpc/RequestMessageFactory.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/Core/Rpc/RequestMessageFactory.php b/lib/Core/Rpc/RequestMessageFactory.php index e27651ac..c10e0119 100644 --- a/lib/Core/Rpc/RequestMessageFactory.php +++ b/lib/Core/Rpc/RequestMessageFactory.php @@ -26,6 +26,9 @@ private static function doFromRequest(RawMessage $request): Message return Invoke::new(ResponseMessage::class, $body); } + /** + * @phpstan-ignore-next-line + */ if (!isset($body['id']) || is_null($body['id'])) { unset($body['id']); return Invoke::new(NotificationMessage::class, $body); From 47f5738520de7c88d213ac538cbee288f95b4f9b Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 9 Apr 2023 12:51:59 +0100 Subject: [PATCH 072/111] Show progress notification for code action handler --- .../TextDocument/CodeActionHandler.php | 25 +++++++------------ .../TextDocument/CodeActionHandlerTest.php | 3 ++- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/lib/Handler/TextDocument/CodeActionHandler.php b/lib/Handler/TextDocument/CodeActionHandler.php index 08b18edb..67275267 100644 --- a/lib/Handler/TextDocument/CodeActionHandler.php +++ b/lib/Handler/TextDocument/CodeActionHandler.php @@ -12,26 +12,15 @@ use Phpactor\LanguageServer\Core\CodeAction\CodeActionProvider; use Phpactor\LanguageServer\Core\Handler\CanRegisterCapabilities; use Phpactor\LanguageServer\Core\Handler\Handler; +use Phpactor\LanguageServer\Core\Server\ClientApi; use Phpactor\LanguageServer\Core\Workspace\Workspace; +use Phpactor\LanguageServer\WorkDoneProgress\WorkDoneToken; use function Amp\call; class CodeActionHandler implements Handler, CanRegisterCapabilities { - /** - * @var CodeActionProvider - */ - private $provider; - - /** - * @var Workspace - */ - private $workspace; - - - public function __construct(CodeActionProvider $provider, Workspace $workspace) + public function __construct(private CodeActionProvider $provider, private Workspace $workspace, private ClientApi $client) { - $this->provider = $provider; - $this->workspace = $workspace; } /** @@ -55,10 +44,14 @@ public function registerCapabiltiies(ServerCapabilities $capabilities): void */ public function codeAction(CodeActionParams $params, CancellationToken $cancel): Promise { - /** @phpstan-ignore-next-line */ return call(function () use ($params, $cancel) { + $token = WorkDoneToken::generate(); + $this->client->workDoneProgress()->create($token); $document = $this->workspace->get($params->textDocument->uri); - return $this->provider->provideActionsFor($document, $params->range, $cancel); + $this->client->workDoneProgress()->begin($token, title: 'Resolving code actions'); + $actions = yield $this->provider->provideActionsFor($document, $params->range, $cancel); + $this->client->workDoneProgress()->end($token); + return $actions; }); } } diff --git a/tests/Unit/Handler/TextDocument/CodeActionHandlerTest.php b/tests/Unit/Handler/TextDocument/CodeActionHandlerTest.php index 7237e7b8..b31b4bf4 100644 --- a/tests/Unit/Handler/TextDocument/CodeActionHandlerTest.php +++ b/tests/Unit/Handler/TextDocument/CodeActionHandlerTest.php @@ -21,7 +21,8 @@ public function testProvidesCodeActions(): void new AggregateCodeActionProvider( new SayHelloCodeActionProvider() ), - $tester->workspace() + $tester->workspace(), + $tester->clientApi(), )); $tester = $tester->build(); From 87416f508aef8361ce09b9d42331d8ecec043db3 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 9 Apr 2023 12:59:07 +0100 Subject: [PATCH 073/111] Add progres notification to formatting handler --- CHANGELOG.md | 6 ++++++ bin/serve.php | 2 +- lib/Handler/TextDocument/FormattingHandler.php | 15 +++++++-------- .../TextDocument/FormattingHandlerTest.php | 3 ++- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a87e7fdc..1fd0cb77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ CHANGELOG ========= +6.0 +--- + +- Code action and formatting handlers now require the `ClientApi` in order to + provide progress notification. + 5.0 --- diff --git a/bin/serve.php b/bin/serve.php index db7c8d40..3494de33 100755 --- a/bin/serve.php +++ b/bin/serve.php @@ -133,7 +133,7 @@ function (MessageTransmitter $transmitter, InitializeParams $params) use ($logge new DidChangeWatchedFilesHandler($eventDispatcher), new CodeActionHandler(new AggregateCodeActionProvider( new SayHelloCodeActionProvider() - ), $workspace), + ), $workspace, $clientApi), new ExitHandler() ); diff --git a/lib/Handler/TextDocument/FormattingHandler.php b/lib/Handler/TextDocument/FormattingHandler.php index 32fdbad6..69cc7c48 100644 --- a/lib/Handler/TextDocument/FormattingHandler.php +++ b/lib/Handler/TextDocument/FormattingHandler.php @@ -10,20 +10,15 @@ use Phpactor\LanguageServer\Core\Formatting\Formatter; use Phpactor\LanguageServer\Core\Handler\CanRegisterCapabilities; use Phpactor\LanguageServer\Core\Handler\Handler; +use Phpactor\LanguageServer\Core\Server\ClientApi; use Phpactor\LanguageServer\Core\Workspace\Workspace; +use Phpactor\LanguageServer\WorkDoneProgress\WorkDoneToken; use function Amp\call; class FormattingHandler implements Handler, CanRegisterCapabilities { - private Workspace $workspace; - - private Formatter $formatter; - - - public function __construct(Workspace $workspace, Formatter $formatter) + public function __construct(private Workspace $workspace, private Formatter $formatter, private ClientApi $client) { - $this->workspace = $workspace; - $this->formatter = $formatter; } public function methods(): array @@ -37,8 +32,12 @@ public function methods(): array public function formatting(TextDocumentIdentifier $textDocument, FormattingOptions $options): Promise { return call(function () use ($textDocument) { + $token = WorkDoneToken::generate(); + $this->client->workDoneProgress()->create($token); $document = $this->workspace->get($textDocument->uri); + $this->client->workDoneProgress()->begin($token, 'Formatting document'); $formatted = yield $this->formatter->format($document); + $this->client->workDoneProgress()->end($token); return $formatted; }); diff --git a/tests/Unit/Handler/TextDocument/FormattingHandlerTest.php b/tests/Unit/Handler/TextDocument/FormattingHandlerTest.php index 8a023d4b..8f130943 100644 --- a/tests/Unit/Handler/TextDocument/FormattingHandlerTest.php +++ b/tests/Unit/Handler/TextDocument/FormattingHandlerTest.php @@ -24,7 +24,8 @@ public function testProvidesFormatting(): void $tester->workspace(), new TestFormatter( ProtocolFactory::textEdit(0, 0, 0, 0, 'Hello'), - ) + ), + $tester->clientApi(), )); $tester = $tester->build(); From 39bc0648cda3a3f2c68f71d77dc410c92790c3ea Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 9 Apr 2023 13:05:55 +0100 Subject: [PATCH 074/111] Refactor to use progress notifier class --- CHANGELOG.md | 6 ------ bin/serve.php | 2 +- lib/Handler/TextDocument/CodeActionHandler.php | 14 +++++++++----- lib/Handler/TextDocument/FormattingHandler.php | 14 +++++++++----- .../Handler/TextDocument/CodeActionHandlerTest.php | 1 - .../Handler/TextDocument/FormattingHandlerTest.php | 1 - 6 files changed, 19 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fd0cb77..a87e7fdc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,6 @@ CHANGELOG ========= -6.0 ---- - -- Code action and formatting handlers now require the `ClientApi` in order to - provide progress notification. - 5.0 --- diff --git a/bin/serve.php b/bin/serve.php index 3494de33..db7c8d40 100755 --- a/bin/serve.php +++ b/bin/serve.php @@ -133,7 +133,7 @@ function (MessageTransmitter $transmitter, InitializeParams $params) use ($logge new DidChangeWatchedFilesHandler($eventDispatcher), new CodeActionHandler(new AggregateCodeActionProvider( new SayHelloCodeActionProvider() - ), $workspace, $clientApi), + ), $workspace), new ExitHandler() ); diff --git a/lib/Handler/TextDocument/CodeActionHandler.php b/lib/Handler/TextDocument/CodeActionHandler.php index 67275267..01ce9159 100644 --- a/lib/Handler/TextDocument/CodeActionHandler.php +++ b/lib/Handler/TextDocument/CodeActionHandler.php @@ -12,15 +12,19 @@ use Phpactor\LanguageServer\Core\CodeAction\CodeActionProvider; use Phpactor\LanguageServer\Core\Handler\CanRegisterCapabilities; use Phpactor\LanguageServer\Core\Handler\Handler; -use Phpactor\LanguageServer\Core\Server\ClientApi; use Phpactor\LanguageServer\Core\Workspace\Workspace; +use Phpactor\LanguageServer\WorkDoneProgress\ProgressNotifier; +use Phpactor\LanguageServer\WorkDoneProgress\SilentWorkDoneProgressNotifier; use Phpactor\LanguageServer\WorkDoneProgress\WorkDoneToken; use function Amp\call; class CodeActionHandler implements Handler, CanRegisterCapabilities { - public function __construct(private CodeActionProvider $provider, private Workspace $workspace, private ClientApi $client) + private ProgressNotifier $notifier; + + public function __construct(private CodeActionProvider $provider, private Workspace $workspace, ?ProgressNotifier $notifier = null) { + $this->notifier = $notifier ?: new SilentWorkDoneProgressNotifier(); } /** @@ -46,11 +50,11 @@ public function codeAction(CodeActionParams $params, CancellationToken $cancel): { return call(function () use ($params, $cancel) { $token = WorkDoneToken::generate(); - $this->client->workDoneProgress()->create($token); + $this->notifier->create($token); $document = $this->workspace->get($params->textDocument->uri); - $this->client->workDoneProgress()->begin($token, title: 'Resolving code actions'); + $this->notifier->begin($token, title: 'Resolving code actions'); $actions = yield $this->provider->provideActionsFor($document, $params->range, $cancel); - $this->client->workDoneProgress()->end($token); + $this->notifier->end($token); return $actions; }); } diff --git a/lib/Handler/TextDocument/FormattingHandler.php b/lib/Handler/TextDocument/FormattingHandler.php index 69cc7c48..75ddf40c 100644 --- a/lib/Handler/TextDocument/FormattingHandler.php +++ b/lib/Handler/TextDocument/FormattingHandler.php @@ -10,15 +10,19 @@ use Phpactor\LanguageServer\Core\Formatting\Formatter; use Phpactor\LanguageServer\Core\Handler\CanRegisterCapabilities; use Phpactor\LanguageServer\Core\Handler\Handler; -use Phpactor\LanguageServer\Core\Server\ClientApi; use Phpactor\LanguageServer\Core\Workspace\Workspace; +use Phpactor\LanguageServer\WorkDoneProgress\ProgressNotifier; +use Phpactor\LanguageServer\WorkDoneProgress\SilentWorkDoneProgressNotifier; use Phpactor\LanguageServer\WorkDoneProgress\WorkDoneToken; use function Amp\call; class FormattingHandler implements Handler, CanRegisterCapabilities { - public function __construct(private Workspace $workspace, private Formatter $formatter, private ClientApi $client) + private ProgressNotifier $notifier; + + public function __construct(private Workspace $workspace, private Formatter $formatter, ?ProgressNotifier $notifier = null) { + $this->notifier = $notifier ?: new SilentWorkDoneProgressNotifier(); } public function methods(): array @@ -33,11 +37,11 @@ public function formatting(TextDocumentIdentifier $textDocument, FormattingOptio { return call(function () use ($textDocument) { $token = WorkDoneToken::generate(); - $this->client->workDoneProgress()->create($token); + $this->notifier->create($token); $document = $this->workspace->get($textDocument->uri); - $this->client->workDoneProgress()->begin($token, 'Formatting document'); + $this->notifier->begin($token, 'Formatting document'); $formatted = yield $this->formatter->format($document); - $this->client->workDoneProgress()->end($token); + $this->notifier->end($token); return $formatted; }); diff --git a/tests/Unit/Handler/TextDocument/CodeActionHandlerTest.php b/tests/Unit/Handler/TextDocument/CodeActionHandlerTest.php index b31b4bf4..a64265aa 100644 --- a/tests/Unit/Handler/TextDocument/CodeActionHandlerTest.php +++ b/tests/Unit/Handler/TextDocument/CodeActionHandlerTest.php @@ -22,7 +22,6 @@ public function testProvidesCodeActions(): void new SayHelloCodeActionProvider() ), $tester->workspace(), - $tester->clientApi(), )); $tester = $tester->build(); diff --git a/tests/Unit/Handler/TextDocument/FormattingHandlerTest.php b/tests/Unit/Handler/TextDocument/FormattingHandlerTest.php index 8f130943..e9614725 100644 --- a/tests/Unit/Handler/TextDocument/FormattingHandlerTest.php +++ b/tests/Unit/Handler/TextDocument/FormattingHandlerTest.php @@ -25,7 +25,6 @@ public function testProvidesFormatting(): void new TestFormatter( ProtocolFactory::textEdit(0, 0, 0, 0, 'Hello'), ), - $tester->clientApi(), )); $tester = $tester->build(); From 9c04e403089461e2ed5c8e00d234a8625fbe36cb Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 9 Apr 2023 20:16:31 +0100 Subject: [PATCH 075/111] Wait for response to create workDone progress --- lib/Handler/TextDocument/CodeActionHandler.php | 2 +- lib/Handler/TextDocument/FormattingHandler.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Handler/TextDocument/CodeActionHandler.php b/lib/Handler/TextDocument/CodeActionHandler.php index 01ce9159..548dafc0 100644 --- a/lib/Handler/TextDocument/CodeActionHandler.php +++ b/lib/Handler/TextDocument/CodeActionHandler.php @@ -50,7 +50,7 @@ public function codeAction(CodeActionParams $params, CancellationToken $cancel): { return call(function () use ($params, $cancel) { $token = WorkDoneToken::generate(); - $this->notifier->create($token); + yield $this->notifier->create($token); $document = $this->workspace->get($params->textDocument->uri); $this->notifier->begin($token, title: 'Resolving code actions'); $actions = yield $this->provider->provideActionsFor($document, $params->range, $cancel); diff --git a/lib/Handler/TextDocument/FormattingHandler.php b/lib/Handler/TextDocument/FormattingHandler.php index 75ddf40c..9b49e14e 100644 --- a/lib/Handler/TextDocument/FormattingHandler.php +++ b/lib/Handler/TextDocument/FormattingHandler.php @@ -37,7 +37,7 @@ public function formatting(TextDocumentIdentifier $textDocument, FormattingOptio { return call(function () use ($textDocument) { $token = WorkDoneToken::generate(); - $this->notifier->create($token); + yield $this->notifier->create($token); $document = $this->workspace->get($textDocument->uri); $this->notifier->begin($token, 'Formatting document'); $formatted = yield $this->formatter->format($document); From 051a20e003492cde92be01e54303aeb2fa9ee5f2 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Mon, 10 Apr 2023 10:13:32 +0100 Subject: [PATCH 076/111] End notification when exception is thrown --- .../Server/Transmitter/TestMessageTransmitter.php | 12 ++++++++++++ lib/Handler/TextDocument/CodeActionHandler.php | 7 +++++-- lib/Handler/TextDocument/FormattingHandler.php | 7 +++++-- lib/test.php | 0 4 files changed, 22 insertions(+), 4 deletions(-) delete mode 100644 lib/test.php diff --git a/lib/Core/Server/Transmitter/TestMessageTransmitter.php b/lib/Core/Server/Transmitter/TestMessageTransmitter.php index e602f63f..53792692 100644 --- a/lib/Core/Server/Transmitter/TestMessageTransmitter.php +++ b/lib/Core/Server/Transmitter/TestMessageTransmitter.php @@ -80,6 +80,18 @@ public function shiftRequest(): ?RequestMessage return $message; } + public function mustShiftRequest(): RequestMessage + { + $message = $this->shiftRequest(); + if (null === $message) { + + throw new RuntimeException( + 'No request messages left to shift!' + ); + } + return $message; + } + public function clear(): void { $this->buffer = []; diff --git a/lib/Handler/TextDocument/CodeActionHandler.php b/lib/Handler/TextDocument/CodeActionHandler.php index 548dafc0..b99896ef 100644 --- a/lib/Handler/TextDocument/CodeActionHandler.php +++ b/lib/Handler/TextDocument/CodeActionHandler.php @@ -53,8 +53,11 @@ public function codeAction(CodeActionParams $params, CancellationToken $cancel): yield $this->notifier->create($token); $document = $this->workspace->get($params->textDocument->uri); $this->notifier->begin($token, title: 'Resolving code actions'); - $actions = yield $this->provider->provideActionsFor($document, $params->range, $cancel); - $this->notifier->end($token); + try { + $actions = yield $this->provider->provideActionsFor($document, $params->range, $cancel); + } finally { + $this->notifier->end($token); + } return $actions; }); } diff --git a/lib/Handler/TextDocument/FormattingHandler.php b/lib/Handler/TextDocument/FormattingHandler.php index 9b49e14e..74f75874 100644 --- a/lib/Handler/TextDocument/FormattingHandler.php +++ b/lib/Handler/TextDocument/FormattingHandler.php @@ -40,8 +40,11 @@ public function formatting(TextDocumentIdentifier $textDocument, FormattingOptio yield $this->notifier->create($token); $document = $this->workspace->get($textDocument->uri); $this->notifier->begin($token, 'Formatting document'); - $formatted = yield $this->formatter->format($document); - $this->notifier->end($token); + try { + $formatted = yield $this->formatter->format($document); + } finally { + $this->notifier->end($token); + } return $formatted; }); diff --git a/lib/test.php b/lib/test.php deleted file mode 100644 index e69de29b..00000000 From ac6246b2abd3976bd22f2439f56b75b71a331c59 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sun, 4 Jun 2023 10:21:51 +0100 Subject: [PATCH 077/111] Failing test --- lib/Core/Rpc/RequestMessageFactory.php | 3 +- lib/Core/Server/LanguageServer.php | 2 ++ .../Transmitter/TestMessageSerializer.php | 25 ++++++++++++++ .../Core/Server/LanguageServerTest.php | 33 +++++++++++++++++-- 4 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 lib/Core/Server/Transmitter/TestMessageSerializer.php diff --git a/lib/Core/Rpc/RequestMessageFactory.php b/lib/Core/Rpc/RequestMessageFactory.php index c10e0119..37de1194 100644 --- a/lib/Core/Rpc/RequestMessageFactory.php +++ b/lib/Core/Rpc/RequestMessageFactory.php @@ -3,6 +3,7 @@ namespace Phpactor\LanguageServer\Core\Rpc; use DTL\Invoke\Invoke; +use Error; use Phpactor\LanguageServer\Core\Rpc\Exception\CouldNotCreateMessage; use RuntimeException; @@ -12,7 +13,7 @@ public static function fromRequest(RawMessage $request): Message { try { return self::doFromRequest($request); - } catch (RuntimeException $error) { + } catch (Error $error) { throw new CouldNotCreateMessage($error->getMessage(), 0, $error); } } diff --git a/lib/Core/Server/LanguageServer.php b/lib/Core/Server/LanguageServer.php index 07b61cab..d1f7dfbd 100644 --- a/lib/Core/Server/LanguageServer.php +++ b/lib/Core/Server/LanguageServer.php @@ -8,6 +8,7 @@ use Generator; use Phpactor\LanguageServer\Core\Rpc\Exception\CouldNotCreateMessage; use Phpactor\LanguageServer\Core\Rpc\Message; +use Phpactor\LanguageServer\Core\Rpc\ResponseError; use Phpactor\LanguageServer\Core\Rpc\ResponseMessage; use Phpactor\LanguageServer\Core\Server\Transmitter\ConnectionMessageTransmitter; use Phpactor\LanguageServer\Core\Server\Transmitter\MessageTransmitter; @@ -195,6 +196,7 @@ private function handle(Connection $connection): Promise $transmitter->transmit(new ResponseMessage( $request->body()['id'] ?? 0, [], + new ResponseError(255, $e->getMessage()), )); continue; } diff --git a/lib/Core/Server/Transmitter/TestMessageSerializer.php b/lib/Core/Server/Transmitter/TestMessageSerializer.php new file mode 100644 index 00000000..e42371be --- /dev/null +++ b/lib/Core/Server/Transmitter/TestMessageSerializer.php @@ -0,0 +1,25 @@ +serialized = $serialized; + } + + public function serialize(Message $message): string + { + return $this->serialized; + } + + public function normalize($message) + { + return $message; + } +} diff --git a/tests/Integration/Core/Server/LanguageServerTest.php b/tests/Integration/Core/Server/LanguageServerTest.php index 52a09507..6bd2ffd7 100644 --- a/tests/Integration/Core/Server/LanguageServerTest.php +++ b/tests/Integration/Core/Server/LanguageServerTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\Assert; use Phpactor\LanguageServer\Core\Dispatcher\Dispatcher\ClosureDispatcher; use Phpactor\LanguageServer\Core\Dispatcher\Factory\ClosureDispatcherFactory; +use Phpactor\LanguageServer\Core\Rpc\Message; use Phpactor\LanguageServer\Core\Rpc\RawMessage; use Phpactor\LanguageServer\Core\Rpc\RequestMessage; use Phpactor\LanguageServer\Core\Rpc\ResponseMessage; @@ -23,12 +24,18 @@ use Phpactor\LanguageServer\Core\Server\StreamProvider\ResourceStreamProvider; use Phpactor\LanguageServer\Core\Server\Stream\ResourceDuplexStream; use Phpactor\LanguageServer\Core\Server\Transmitter\LspMessageFormatter; +use Phpactor\LanguageServer\Core\Server\Transmitter\LspMessageSerializer; +use Phpactor\LanguageServer\Core\Server\Transmitter\MessageSerializer; +use Phpactor\LanguageServer\Core\Server\Transmitter\TestMessageSerializer; use Psr\Log\NullLogger; use function Amp\Iterator\fromIterable; use function Amp\call; class LanguageServerTest extends AsyncTestCase { + /** + * @return Generator> + */ public function testDispatchesRequest(): Generator { $response = yield $this->dispatchRequest( @@ -44,16 +51,36 @@ function (RequestMessage $message) { Assert::assertInstanceOf(RawMessage::class, $response); Assert::assertEquals(['foo' => 'bar'], $response->body()['result']); } + /** + * @return Generator> + */ + public function testHandlesMalformedRequest(): Generator + { + $serializer = new TestMessageSerializer('{"foo":"bar"}'); + $response = yield $this->dispatchRequest( + new RequestMessage(1, 'foobar', []), + function (RequestMessage $message) { + return new Success(new ResponseMessage(1, [ + 'foo' => 'bar', + ])); + }, + $serializer + ); + Assert::assertInstanceOf(RawMessage::class, $response); + Assert::assertEquals(['foo' => 'bar'], $response->body()['result']); + } /** * @return Promise + * @param Closure(Message): Promise $handler */ - private function dispatchRequest(RequestMessage $request, Closure $handler): Promise + private function dispatchRequest(RequestMessage $request, Closure $handler, ?MessageSerializer $serializer = null): Promise { - return call(function () use ($handler, $request) { + $serializer = $serializer ?: new LspMessageSerializer(); + return call(function () use ($handler, $request, $serializer) { $this->setTimeout(100); - $formatter = new LspMessageFormatter(); + $formatter = new LspMessageFormatter($serializer); $message = $formatter->format($request); $output = new OutputBuffer(); From 6e41dfd350244acfb8fbea35c2d0385d22555737 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sun, 4 Jun 2023 10:29:34 +0100 Subject: [PATCH 078/111] Fix erro rhandling --- lib/Core/Rpc/RequestMessageFactory.php | 3 ++- .../Integration/Core/Server/LanguageServerTest.php | 13 +++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/Core/Rpc/RequestMessageFactory.php b/lib/Core/Rpc/RequestMessageFactory.php index 37de1194..89c5ebf7 100644 --- a/lib/Core/Rpc/RequestMessageFactory.php +++ b/lib/Core/Rpc/RequestMessageFactory.php @@ -6,6 +6,7 @@ use Error; use Phpactor\LanguageServer\Core\Rpc\Exception\CouldNotCreateMessage; use RuntimeException; +use Throwable; final class RequestMessageFactory { @@ -13,7 +14,7 @@ public static function fromRequest(RawMessage $request): Message { try { return self::doFromRequest($request); - } catch (Error $error) { + } catch (Throwable $error) { throw new CouldNotCreateMessage($error->getMessage(), 0, $error); } } diff --git a/tests/Integration/Core/Server/LanguageServerTest.php b/tests/Integration/Core/Server/LanguageServerTest.php index 6bd2ffd7..5616612b 100644 --- a/tests/Integration/Core/Server/LanguageServerTest.php +++ b/tests/Integration/Core/Server/LanguageServerTest.php @@ -30,6 +30,7 @@ use Psr\Log\NullLogger; use function Amp\Iterator\fromIterable; use function Amp\call; +use Exception; class LanguageServerTest extends AsyncTestCase { @@ -40,7 +41,10 @@ public function testDispatchesRequest(): Generator { $response = yield $this->dispatchRequest( new RequestMessage(1, 'foobar', []), - function (RequestMessage $message) { + function (Message $message) { + if (!$message instanceof RequestMessage) { + throw new Exception('not a request'); + } Assert::assertEquals('foobar', $message->method); return new Success(new ResponseMessage(1, [ 'foo' => 'bar', @@ -56,10 +60,10 @@ function (RequestMessage $message) { */ public function testHandlesMalformedRequest(): Generator { - $serializer = new TestMessageSerializer('{"foo":"bar"}'); + $serializer = new TestMessageSerializer('{"id":3,"foo":"bar"}'); $response = yield $this->dispatchRequest( new RequestMessage(1, 'foobar', []), - function (RequestMessage $message) { + function (Message $message) { return new Success(new ResponseMessage(1, [ 'foo' => 'bar', ])); @@ -68,7 +72,8 @@ function (RequestMessage $message) { ); Assert::assertInstanceOf(RawMessage::class, $response); - Assert::assertEquals(['foo' => 'bar'], $response->body()['result']); + self::assertEquals(255, $response->body()['error']['code']); + } /** * @return Promise From 08574402bff061a028d276100fb69c15a222d35f Mon Sep 17 00:00:00 2001 From: Daniel Date: Sun, 4 Jun 2023 10:34:50 +0100 Subject: [PATCH 079/111] Fix test --- tests/Unit/Core/Rpc/RequestMessageFactoryTest.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/Unit/Core/Rpc/RequestMessageFactoryTest.php b/tests/Unit/Core/Rpc/RequestMessageFactoryTest.php index 7b35fa86..59b11882 100644 --- a/tests/Unit/Core/Rpc/RequestMessageFactoryTest.php +++ b/tests/Unit/Core/Rpc/RequestMessageFactoryTest.php @@ -4,6 +4,7 @@ use DTL\Invoke\Exception\RequiredKeysMissing; use DTL\Invoke\Exception\UnknownKeys; +use Phpactor\LanguageServer\Core\Rpc\Exception\CouldNotCreateMessage; use Phpactor\LanguageServer\Core\Rpc\NotificationMessage; use Phpactor\LanguageServer\Core\Rpc\ResponseMessage; use Phpactor\TestUtils\PHPUnit\TestCase; @@ -14,14 +15,14 @@ class RequestMessageFactoryTest extends TestCase { public function testExceptionOnInvalidKeys(): void { - $this->expectException(UnknownKeys::class); + $this->expectException(CouldNotCreateMessage::class); $request = new RawMessage([], ['foo' => 'bar']); RequestMessageFactory::fromRequest($request); } public function testExceptionMissingKeys(): void { - $this->expectException(RequiredKeysMissing::class); + $this->expectException(CouldNotCreateMessage::class); $request = new RawMessage([], []); RequestMessageFactory::fromRequest($request); } From 30a517405b959eca9d597ce6d45f66a4feeb01cc Mon Sep 17 00:00:00 2001 From: Daniel Date: Sun, 4 Jun 2023 10:35:58 +0100 Subject: [PATCH 080/111] Fix CS --- lib/Core/Rpc/RequestMessageFactory.php | 2 -- tests/Unit/Core/Rpc/RequestMessageFactoryTest.php | 2 -- 2 files changed, 4 deletions(-) diff --git a/lib/Core/Rpc/RequestMessageFactory.php b/lib/Core/Rpc/RequestMessageFactory.php index 89c5ebf7..34814162 100644 --- a/lib/Core/Rpc/RequestMessageFactory.php +++ b/lib/Core/Rpc/RequestMessageFactory.php @@ -3,9 +3,7 @@ namespace Phpactor\LanguageServer\Core\Rpc; use DTL\Invoke\Invoke; -use Error; use Phpactor\LanguageServer\Core\Rpc\Exception\CouldNotCreateMessage; -use RuntimeException; use Throwable; final class RequestMessageFactory diff --git a/tests/Unit/Core/Rpc/RequestMessageFactoryTest.php b/tests/Unit/Core/Rpc/RequestMessageFactoryTest.php index 59b11882..b7115940 100644 --- a/tests/Unit/Core/Rpc/RequestMessageFactoryTest.php +++ b/tests/Unit/Core/Rpc/RequestMessageFactoryTest.php @@ -2,8 +2,6 @@ namespace Phpactor\LanguageServer\Tests\Unit\Core\Rpc; -use DTL\Invoke\Exception\RequiredKeysMissing; -use DTL\Invoke\Exception\UnknownKeys; use Phpactor\LanguageServer\Core\Rpc\Exception\CouldNotCreateMessage; use Phpactor\LanguageServer\Core\Rpc\NotificationMessage; use Phpactor\LanguageServer\Core\Rpc\ResponseMessage; From a46a67280eeac1ae2ce224b8417418c94992d1bc Mon Sep 17 00:00:00 2001 From: Daniel Date: Sun, 4 Jun 2023 10:42:24 +0100 Subject: [PATCH 081/111] Handle error --- lib/Core/Rpc/RequestMessageFactory.php | 7 ++++++- tests/Unit/Core/Rpc/RequestMessageFactoryTest.php | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/Core/Rpc/RequestMessageFactory.php b/lib/Core/Rpc/RequestMessageFactory.php index 34814162..429039c6 100644 --- a/lib/Core/Rpc/RequestMessageFactory.php +++ b/lib/Core/Rpc/RequestMessageFactory.php @@ -22,7 +22,12 @@ private static function doFromRequest(RawMessage $request): Message $body = $request->body(); unset($body['jsonrpc']); - if (array_key_exists('result', $body)) { + if (array_key_exists('result', $body) || array_key_exists('error', $body)) { + $body['result'] = $body['result'] ?? null; + if ($body['error'] ?? null) { + $body['error'] = Invoke::new(ResponseError::class, $body['error']); + } + return Invoke::new(ResponseMessage::class, $body); } diff --git a/tests/Unit/Core/Rpc/RequestMessageFactoryTest.php b/tests/Unit/Core/Rpc/RequestMessageFactoryTest.php index b7115940..7bfb94ad 100644 --- a/tests/Unit/Core/Rpc/RequestMessageFactoryTest.php +++ b/tests/Unit/Core/Rpc/RequestMessageFactoryTest.php @@ -67,4 +67,18 @@ public function testReturnsRequestMessageForResponse(): void self::assertInstanceOf(ResponseMessage::class, $response); $this->assertEquals('foobar', $response->result); } + public function testReturnsRequestMessageForResponseWithoutResultButWithError(): void + { + $response = new RawMessage([], [ + 'jsonrpc' => 2.0, + 'id' => 123, + 'error' => [ + 'code' => 123, + 'message' => 'foo', + ], + ]); + $response = RequestMessageFactory::fromRequest($response); + self::assertInstanceOf(ResponseMessage::class, $response); + $this->assertEquals(123, $response->error->code); + } } From 1fee1300ed76f35e3980cbd6b78a97c279af1c06 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sun, 4 Jun 2023 10:43:37 +0100 Subject: [PATCH 082/111] Fix --- tests/Unit/Core/Rpc/RequestMessageFactoryTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Unit/Core/Rpc/RequestMessageFactoryTest.php b/tests/Unit/Core/Rpc/RequestMessageFactoryTest.php index 7bfb94ad..69f774ea 100644 --- a/tests/Unit/Core/Rpc/RequestMessageFactoryTest.php +++ b/tests/Unit/Core/Rpc/RequestMessageFactoryTest.php @@ -67,6 +67,7 @@ public function testReturnsRequestMessageForResponse(): void self::assertInstanceOf(ResponseMessage::class, $response); $this->assertEquals('foobar', $response->result); } + public function testReturnsRequestMessageForResponseWithoutResultButWithError(): void { $response = new RawMessage([], [ @@ -79,6 +80,7 @@ public function testReturnsRequestMessageForResponseWithoutResultButWithError(): ]); $response = RequestMessageFactory::fromRequest($response); self::assertInstanceOf(ResponseMessage::class, $response); + assert($response instanceof ResponseMessage); $this->assertEquals(123, $response->error->code); } } From 3e01d002abb0939dad101861b6209724a9474678 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 26 Jul 2023 20:35:31 +0100 Subject: [PATCH 083/111] Fix types --- lib/Core/Diagnostics/DiagnosticsEngine.php | 27 +++++----------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/lib/Core/Diagnostics/DiagnosticsEngine.php b/lib/Core/Diagnostics/DiagnosticsEngine.php index df6605c8..425be155 100644 --- a/lib/Core/Diagnostics/DiagnosticsEngine.php +++ b/lib/Core/Diagnostics/DiagnosticsEngine.php @@ -15,32 +15,17 @@ class DiagnosticsEngine /** * @var Deferred */ - private $deferred; + private Deferred $deferred; - /** - * @var bool - */ - private $running = false; + private bool $running = false; - /** - * @var ?TextDocumentItem - */ - private $next; + private ?TextDocumentItem $next = null; - /** - * @var DiagnosticsProvider - */ - private $provider; + private DiagnosticsProvider $provider; - /** - * @var ClientApi - */ - private $clientApi; + private ClientApi $clientApi; - /** - * @var int - */ - private $sleepTime; + private int $sleepTime; public function __construct(ClientApi $clientApi, DiagnosticsProvider $provider, int $sleepTime = 1000) { From 8d9f1200dee084b68c7895651090a045a2b2b2b0 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 26 Jul 2023 21:03:08 +0100 Subject: [PATCH 084/111] Refactor diagnostic engine to accept multiple providers --- lib/Core/Diagnostics/DiagnosticsEngine.php | 37 ++++++++++++++++------ 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/lib/Core/Diagnostics/DiagnosticsEngine.php b/lib/Core/Diagnostics/DiagnosticsEngine.php index 425be155..ce74d347 100644 --- a/lib/Core/Diagnostics/DiagnosticsEngine.php +++ b/lib/Core/Diagnostics/DiagnosticsEngine.php @@ -8,6 +8,7 @@ use Amp\CancellationToken; use Amp\Deferred; use Phpactor\LanguageServerProtocol\TextDocumentItem; +use function Amp\asyncCall; use function Amp\delay; class DiagnosticsEngine @@ -21,16 +22,24 @@ class DiagnosticsEngine private ?TextDocumentItem $next = null; - private DiagnosticsProvider $provider; + /** + * @var DiagnosticsProvider[] + */ + private array $providers; private ClientApi $clientApi; private int $sleepTime; - public function __construct(ClientApi $clientApi, DiagnosticsProvider $provider, int $sleepTime = 1000) + private array $diagnostics = []; + + /** + * @param DiagnosticsProvider[] $providers + */ + public function __construct(ClientApi $clientApi, array $providers, int $sleepTime = 1000) { $this->deferred = new Deferred(); - $this->provider = $provider; + $this->providers = $providers; $this->clientApi = $clientApi; $this->sleepTime = $sleepTime; } @@ -59,6 +68,7 @@ public function run(CancellationToken $token): Promise $textDocument = yield $this->deferred->promise(); + $this->diagnostics = []; $this->deferred = new Deferred(); // after we have reset deferred, we can safely set linting to @@ -76,13 +86,20 @@ public function run(CancellationToken $token): Promise $this->next = null; } - $diagnostics = yield $this->provider->provideDiagnostics($textDocument, $token); - - $this->clientApi->diagnostics()->publishDiagnostics( - $textDocument->uri, - $textDocument->version, - $diagnostics - ); + foreach ($this->providers as $provider) { + asyncCall(function () use ($provider, $token, $textDocument) { + $this->diagnostics = array_merge( + $this->diagnostics, + yield $provider->provideDiagnostics($textDocument, $token) + ); + + $this->clientApi->diagnostics()->publishDiagnostics( + $textDocument->uri, + $textDocument->version, + $this->diagnostics + ); + }); + } } }); } From 826742ae87982a4d0286d13077cabe3a39789f7a Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 26 Jul 2023 21:05:34 +0100 Subject: [PATCH 085/111] Failing test for dedupe --- .../Diagnostics/DiagnosticsEngineTest.php | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/tests/Unit/Core/Diagnostics/DiagnosticsEngineTest.php b/tests/Unit/Core/Diagnostics/DiagnosticsEngineTest.php index a0043e08..19049bbe 100644 --- a/tests/Unit/Core/Diagnostics/DiagnosticsEngineTest.php +++ b/tests/Unit/Core/Diagnostics/DiagnosticsEngineTest.php @@ -148,20 +148,18 @@ public function testAlwaysAnalyzesTheLastChangeLast(): Generator private function createEngine(LanguageServerTesterBuilder $tester, int $delay = 0, int $sleepTime = 0, string &$lastDocument = null): DiagnosticsEngine { - $engine = new DiagnosticsEngine($tester->clientApi(), new ClosureDiagnosticsProvider(function (TextDocumentItem $item) use ($delay, &$lastDocument) { - return call(function () use ($delay, $item, &$lastDocument) { - if ($delay) { - yield delay($delay); - } - $lastDocument = $item->text; - return [ - ProtocolFactory::diagnostic( - ProtocolFactory::range(0, 0, 0, 0), - 'Foobar is broken' - ) - ]; - }); - }), $sleepTime); - return $engine; + return new DiagnosticsEngine($tester->clientApi(), [ + new ClosureDiagnosticsProvider(function (TextDocumentItem $item) use ($delay, &$lastDocument) { + return call(function () use ($delay, $item, &$lastDocument) { + if ($delay) { + yield delay($delay); + } + $lastDocument = $item->text; + return [ + ProtocolFactory::diagnostic(ProtocolFactory::range(0, 0, 0, 0), 'Foobar is broken') + ]; + }); + }) + ], $sleepTime); } } From bf977617c2e609c4543763e4a26579975596f0b2 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 26 Jul 2023 21:16:41 +0100 Subject: [PATCH 086/111] Fix tests --- lib/Core/Diagnostics/DiagnosticsEngine.php | 2 +- tests/Unit/Core/Diagnostics/DiagnosticsEngineTest.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/Core/Diagnostics/DiagnosticsEngine.php b/lib/Core/Diagnostics/DiagnosticsEngine.php index ce74d347..bae8bb10 100644 --- a/lib/Core/Diagnostics/DiagnosticsEngine.php +++ b/lib/Core/Diagnostics/DiagnosticsEngine.php @@ -86,7 +86,7 @@ public function run(CancellationToken $token): Promise $this->next = null; } - foreach ($this->providers as $provider) { + foreach ($this->providers as $i => $provider) { asyncCall(function () use ($provider, $token, $textDocument) { $this->diagnostics = array_merge( $this->diagnostics, diff --git a/tests/Unit/Core/Diagnostics/DiagnosticsEngineTest.php b/tests/Unit/Core/Diagnostics/DiagnosticsEngineTest.php index 19049bbe..b36b5c10 100644 --- a/tests/Unit/Core/Diagnostics/DiagnosticsEngineTest.php +++ b/tests/Unit/Core/Diagnostics/DiagnosticsEngineTest.php @@ -70,7 +70,7 @@ public function testPublishesForManyFiles(): Generator public function testDeduplicatesSuccessiveChangesToSameFile(): Generator { $tester = LanguageServerTesterBuilder::create(); - $engine = $this->createEngine($tester, 5, 0); + $engine = $this->createEngine($tester, 10, 0); $token = new CancellationTokenSource(); $promise = $engine->run($token->getToken()); @@ -136,7 +136,7 @@ public function testAlwaysAnalyzesTheLastChangeLast(): Generator $engine->enqueue(ProtocolFactory::textDocumentItem('file:///foobar', '3')); yield new Delayed(1); $engine->enqueue(ProtocolFactory::textDocumentItem('file:///foobar', '4')); - yield new Delayed(0); + yield new Delayed(1); $engine->enqueue(ProtocolFactory::textDocumentItem('file:///foobar', '5')); yield new Delayed(100); From cd627e00d7e9fb85f0166013f481c9cf2e833891 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 26 Jul 2023 21:18:53 +0100 Subject: [PATCH 087/111] Tests pass --- lib/LanguageServerTesterBuilder.php | 2 +- tests/Unit/Service/DiagnosticsServiceTest.php | 24 ++++++++++--------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/lib/LanguageServerTesterBuilder.php b/lib/LanguageServerTesterBuilder.php index f8f6d817..24963cbd 100644 --- a/lib/LanguageServerTesterBuilder.php +++ b/lib/LanguageServerTesterBuilder.php @@ -326,7 +326,7 @@ function (MessageTransmitter $transmitter, InitializeParams $params) { $service = new DiagnosticsService( new DiagnosticsEngine( $this->clientApi, - new AggregateDiagnosticsProvider($logger, ...$this->diagnosticsProvider), + [new AggregateDiagnosticsProvider($logger, ...$this->diagnosticsProvider)], 0 ), ); diff --git a/tests/Unit/Service/DiagnosticsServiceTest.php b/tests/Unit/Service/DiagnosticsServiceTest.php index 1947dc17..369b2ae2 100644 --- a/tests/Unit/Service/DiagnosticsServiceTest.php +++ b/tests/Unit/Service/DiagnosticsServiceTest.php @@ -24,16 +24,18 @@ public function testService(): void $service = new DiagnosticsService( new DiagnosticsEngine( $tester->clientApi(), - new ClosureDiagnosticsProvider(function () { - return call(function () { - return [ - ProtocolFactory::diagnostic( - ProtocolFactory::range(0, 0, 0, 0), - 'Foobar is bust' - ) - ]; - }); - }), + [ + new ClosureDiagnosticsProvider(function () { + return call(function () { + return [ + ProtocolFactory::diagnostic( + ProtocolFactory::range(0, 0, 0, 0), + 'Foobar is bust' + ) + ]; + }); + }), + ], 0 ), true, @@ -54,7 +56,7 @@ public function testService(): void assert($notification instanceof NotificationMessage); self::assertEquals('textDocument/publishDiagnostics', $notification->method); - $tester->textDocument()->save('file:///foobar', 'foobar'); + $tester->textDocument()->save('file:///foobar'); wait(new Delayed(100)); $notification = $tester->transmitter()->shift(); assert($notification instanceof NotificationMessage); From 2c4b7624124babc58ff43929a6a0a885042679dc Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 26 Jul 2023 21:55:56 +0100 Subject: [PATCH 088/111] Update --- lib/Core/Diagnostics/DiagnosticsEngine.php | 25 +++++++++++--- .../Diagnostics/DiagnosticsEngineTest.php | 34 +++++++++++++++++-- .../CodeActionDiagnosticsProviderTest.php | 4 +-- 3 files changed, 53 insertions(+), 10 deletions(-) diff --git a/lib/Core/Diagnostics/DiagnosticsEngine.php b/lib/Core/Diagnostics/DiagnosticsEngine.php index bae8bb10..785bd2c9 100644 --- a/lib/Core/Diagnostics/DiagnosticsEngine.php +++ b/lib/Core/Diagnostics/DiagnosticsEngine.php @@ -3,6 +3,7 @@ namespace Phpactor\LanguageServer\Core\Diagnostics; use Amp\CancelledException; +use Phpactor\LanguageServerProtocol\Diagnostic; use Phpactor\LanguageServer\Core\Server\ClientApi; use Amp\Promise; use Amp\CancellationToken; @@ -31,6 +32,9 @@ class DiagnosticsEngine private int $sleepTime; + /** + * @var array> + */ private array $diagnostics = []; /** @@ -68,7 +72,14 @@ public function run(CancellationToken $token): Promise $textDocument = yield $this->deferred->promise(); - $this->diagnostics = []; + // clear diagnostics for document + $this->diagnostics[$textDocument->uri] = []; + $this->clientApi->diagnostics()->publishDiagnostics( + $textDocument->uri, + $textDocument->version, + $this->diagnostics[$textDocument->uri], + ); + $this->deferred = new Deferred(); // after we have reset deferred, we can safely set linting to @@ -88,16 +99,20 @@ public function run(CancellationToken $token): Promise foreach ($this->providers as $i => $provider) { asyncCall(function () use ($provider, $token, $textDocument) { - $this->diagnostics = array_merge( - $this->diagnostics, - yield $provider->provideDiagnostics($textDocument, $token) + /** @var Diagnostic[] $diagnostics */ + $diagnostics =yield $provider->provideDiagnostics($textDocument, $token) ; + + $this->diagnostics[$textDocument->uri] = array_merge( + $this->diagnostics[$textDocument->uri] ?? [], + $diagnostics ); $this->clientApi->diagnostics()->publishDiagnostics( $textDocument->uri, $textDocument->version, - $this->diagnostics + $this->diagnostics[$textDocument->uri] ); + }); } } diff --git a/tests/Unit/Core/Diagnostics/DiagnosticsEngineTest.php b/tests/Unit/Core/Diagnostics/DiagnosticsEngineTest.php index b36b5c10..d209e47e 100644 --- a/tests/Unit/Core/Diagnostics/DiagnosticsEngineTest.php +++ b/tests/Unit/Core/Diagnostics/DiagnosticsEngineTest.php @@ -5,6 +5,7 @@ use Amp\CancellationTokenSource; use Amp\Delayed; use Amp\PHPUnit\AsyncTestCase; +use Amp\Success; use Generator; use Phpactor\LanguageServerProtocol\TextDocumentItem; use Phpactor\LanguageServer\Core\Diagnostics\ClosureDiagnosticsProvider; @@ -61,7 +62,7 @@ public function testPublishesForManyFiles(): Generator $token->cancel(); - self::assertEquals(3, $tester->transmitter()->count()); + self::assertEquals(6, $tester->transmitter()->count()); } /** @@ -82,7 +83,7 @@ public function testDeduplicatesSuccessiveChangesToSameFile(): Generator $token->cancel(); - self::assertEquals(1, $tester->transmitter()->count()); + self::assertEquals(3, $tester->transmitter()->count()); } /** @@ -104,7 +105,34 @@ public function testSleepPreventsSeige(): Generator $token->cancel(); - self::assertEquals(2, $tester->transmitter()->count()); + self::assertEquals(4, $tester->transmitter()->count()); + } + + public function testAggregatesResultsFromMultipleProviders(): Generator + { + $tester = LanguageServerTesterBuilder::create(); + $engine = new DiagnosticsEngine($tester->clientApi(), [ + new ClosureDiagnosticsProvider(function (TextDocumentItem $item) { + return new Success([ + ProtocolFactory::diagnostic(ProtocolFactory::range(0, 0, 0, 0), 'Foobar is broken') + ]); + }), + new ClosureDiagnosticsProvider(function (TextDocumentItem $item) { + return new Success([ + ProtocolFactory::diagnostic(ProtocolFactory::range(0, 0, 0, 0), 'Barfoo is broken') + ]); + }) + ], 10); + + $token = new CancellationTokenSource(); + $promise = $engine->run($token->getToken()); + + $engine->enqueue(ProtocolFactory::textDocumentItem('file:///foobar', 'bazboo')); + + yield new Delayed(100); + + self::assertEquals(3, $tester->transmitter()->count()); + } /** diff --git a/tests/Unit/Diagnostics/CodeActionDiagnosticsProviderTest.php b/tests/Unit/Diagnostics/CodeActionDiagnosticsProviderTest.php index 9472262c..9868aae9 100644 --- a/tests/Unit/Diagnostics/CodeActionDiagnosticsProviderTest.php +++ b/tests/Unit/Diagnostics/CodeActionDiagnosticsProviderTest.php @@ -32,12 +32,12 @@ public function testProvidesDiagnostics(): void wait(new Delayed(10)); - self::assertEquals(1, $tester->transmitter()->count()); + self::assertEquals(2, $tester->transmitter()->count()); $tester->textDocument()->update('file:///foobar', 'bar'); wait(new Delayed(10)); - self::assertEquals(3, $tester->transmitter()->count()); + self::assertEquals(5, $tester->transmitter()->count()); self::assertEquals('textDocument/publishDiagnostics', $tester->transmitter()->shiftNotification()->method); } } From c7a56855fd30cfb1bda478ff0dfd5bf61a895f36 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 26 Jul 2023 21:56:34 +0100 Subject: [PATCH 089/111] Update CL --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a87e7fdc..ddafe409 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +6.0 +--- + +- Refactored diagnostics engine to execute diagnostic providers in parallel + 5.0 --- From 714abb57e1ef3d531cc9ff50aade5ae601a4b459 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 26 Jul 2023 21:59:25 +0100 Subject: [PATCH 090/111] Update --- bin/serve.php | 4 ++-- tests/Unit/Core/Diagnostics/DiagnosticsEngineTest.php | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/bin/serve.php b/bin/serve.php index db7c8d40..23020ffd 100755 --- a/bin/serve.php +++ b/bin/serve.php @@ -106,10 +106,10 @@ function (MessageTransmitter $transmitter, InitializeParams $params) use ($logge $clientApi = new ClientApi(new JsonRpcClient($transmitter, $responseWatcher)); $diagnosticsService = new DiagnosticsService( - new DiagnosticsEngine($clientApi, new AggregateDiagnosticsProvider( + new DiagnosticsEngine($clientApi, [new AggregateDiagnosticsProvider( $logger, new SayHelloDiagnosticsProvider() - )) + )]) ); $serviceProviders = new ServiceProviders($diagnosticsService); diff --git a/tests/Unit/Core/Diagnostics/DiagnosticsEngineTest.php b/tests/Unit/Core/Diagnostics/DiagnosticsEngineTest.php index d209e47e..0c6e9238 100644 --- a/tests/Unit/Core/Diagnostics/DiagnosticsEngineTest.php +++ b/tests/Unit/Core/Diagnostics/DiagnosticsEngineTest.php @@ -76,13 +76,15 @@ public function testDeduplicatesSuccessiveChangesToSameFile(): Generator $token = new CancellationTokenSource(); $promise = $engine->run($token->getToken()); + $engine->enqueue(ProtocolFactory::textDocumentItem('file:///foobar', 'foobar')); $engine->enqueue(ProtocolFactory::textDocumentItem('file:///foobar', 'foobar')); $engine->enqueue(ProtocolFactory::textDocumentItem('file:///foobar', 'foobar')); - yield new Delayed(10); + yield new Delayed(1); $token->cancel(); + // clear three times self::assertEquals(3, $tester->transmitter()->count()); } From 43058bc3df212d046fc5dc857bf3e9db31b30d26 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 29 Jul 2023 16:35:24 +0100 Subject: [PATCH 091/111] Do not publish if document version different --- lib/Core/Diagnostics/DiagnosticsEngine.php | 29 ++++++++++++++++------ 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/lib/Core/Diagnostics/DiagnosticsEngine.php b/lib/Core/Diagnostics/DiagnosticsEngine.php index 785bd2c9..f51a653a 100644 --- a/lib/Core/Diagnostics/DiagnosticsEngine.php +++ b/lib/Core/Diagnostics/DiagnosticsEngine.php @@ -4,6 +4,7 @@ use Amp\CancelledException; use Phpactor\LanguageServerProtocol\Diagnostic; +use Phpactor\LanguageServerProtocol\TextDocument; use Phpactor\LanguageServer\Core\Server\ClientApi; use Amp\Promise; use Amp\CancellationToken; @@ -37,6 +38,11 @@ class DiagnosticsEngine */ private array $diagnostics = []; + /** + * @var array + */ + private array $versions = []; + /** * @param DiagnosticsProvider[] $providers */ @@ -70,7 +76,9 @@ public function run(CancellationToken $token): Promise return; } + /** @var TextDocumentItem $textDocument */ $textDocument = yield $this->deferred->promise(); + $this->versions[$textDocument->uri] = $textDocument->version; // clear diagnostics for document $this->diagnostics[$textDocument->uri] = []; @@ -86,12 +94,6 @@ public function run(CancellationToken $token): Promise // `false` and let another resolve happen $this->running = false; - assert($textDocument instanceof TextDocumentItem); - - if ($this->sleepTime > 0) { - yield delay($this->sleepTime); - } - if ($this->next) { $textDocument = $this->next; $this->next = null; @@ -99,14 +101,27 @@ public function run(CancellationToken $token): Promise foreach ($this->providers as $i => $provider) { asyncCall(function () use ($provider, $token, $textDocument) { + $start = microtime(true); + /** @var Diagnostic[] $diagnostics */ - $diagnostics =yield $provider->provideDiagnostics($textDocument, $token) ; + $diagnostics = yield $provider->provideDiagnostics($textDocument, $token) ; + $elapsed = (int)round((microtime(true) - $start) / 1000); $this->diagnostics[$textDocument->uri] = array_merge( $this->diagnostics[$textDocument->uri] ?? [], $diagnostics ); + $timeToSleep = $this->sleepTime - $elapsed; + + if ($timeToSleep > 0) { + yield delay($timeToSleep); + } + + if ($textDocument->version !== ($this->versions[$textDocument->uri] ?? -1)) { + return; + } + $this->clientApi->diagnostics()->publishDiagnostics( $textDocument->uri, $textDocument->version, From 5924bb4b2e9f79bbf640c518a36deadbacddfa4a Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 29 Jul 2023 18:20:13 +0100 Subject: [PATCH 092/111] Fix diagnostics engine --- lib/Core/Diagnostics/DiagnosticsEngine.php | 47 ++++++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/lib/Core/Diagnostics/DiagnosticsEngine.php b/lib/Core/Diagnostics/DiagnosticsEngine.php index f51a653a..c1d34801 100644 --- a/lib/Core/Diagnostics/DiagnosticsEngine.php +++ b/lib/Core/Diagnostics/DiagnosticsEngine.php @@ -3,6 +3,7 @@ namespace Phpactor\LanguageServer\Core\Diagnostics; use Amp\CancelledException; +use Amp\Success; use Phpactor\LanguageServerProtocol\Diagnostic; use Phpactor\LanguageServerProtocol\TextDocument; use Phpactor\LanguageServer\Core\Server\ClientApi; @@ -43,6 +44,16 @@ class DiagnosticsEngine */ private array $versions = []; + /** + * @var array + */ + private array $locks = []; + + /** + * @var array + */ + private array $concurrencies = []; + /** * @param DiagnosticsProvider[] $providers */ @@ -99,12 +110,27 @@ public function run(CancellationToken $token): Promise $this->next = null; } - foreach ($this->providers as $i => $provider) { - asyncCall(function () use ($provider, $token, $textDocument) { + foreach ($this->providers as $providerId => $provider) { + asyncCall(function () use ($providerId, $provider, $token, $textDocument) { $start = microtime(true); + yield $this->await($providerId); + + if (!$this->isDocumentCurrent($textDocument)) { + return; + } + + $this->locks[$providerId] = new Deferred(); + /** @var Diagnostic[] $diagnostics */ $diagnostics = yield $provider->provideDiagnostics($textDocument, $token) ; + + if (isset($this->locks[$providerId])) { + $lock = $this->locks[$providerId]; + unset($this->locks[$providerId]); + $lock->resolve(true); + } + $elapsed = (int)round((microtime(true) - $start) / 1000); $this->diagnostics[$textDocument->uri] = array_merge( @@ -118,7 +144,7 @@ public function run(CancellationToken $token): Promise yield delay($timeToSleep); } - if ($textDocument->version !== ($this->versions[$textDocument->uri] ?? -1)) { + if (!$this->isDocumentCurrent($textDocument)) { return; } @@ -147,4 +173,19 @@ public function enqueue(TextDocumentItem $textDocument): void $this->running = true; $this->deferred->resolve($textDocument); } + + private function isDocumentCurrent(?TextDocumentItem $textDocument): bool + { + return $textDocument->version === ($this->versions[$textDocument->uri] ?? -1); + } + + private function await($providerId): Promise + { + if (!array_key_exists($providerId, $this->locks)) { + return new Success(true); + } + + return $this->locks[$providerId]->promise(); + + } } From dd2c52f6568dacfab23aa0fd4a566f432f9052d3 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 29 Jul 2023 18:23:09 +0100 Subject: [PATCH 093/111] Fix --- lib/Core/Diagnostics/DiagnosticsEngine.php | 15 ++++++--------- .../Core/Diagnostics/DiagnosticsEngineTest.php | 2 +- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/lib/Core/Diagnostics/DiagnosticsEngine.php b/lib/Core/Diagnostics/DiagnosticsEngine.php index c1d34801..06c88977 100644 --- a/lib/Core/Diagnostics/DiagnosticsEngine.php +++ b/lib/Core/Diagnostics/DiagnosticsEngine.php @@ -5,7 +5,6 @@ use Amp\CancelledException; use Amp\Success; use Phpactor\LanguageServerProtocol\Diagnostic; -use Phpactor\LanguageServerProtocol\TextDocument; use Phpactor\LanguageServer\Core\Server\ClientApi; use Amp\Promise; use Amp\CancellationToken; @@ -45,15 +44,10 @@ class DiagnosticsEngine private array $versions = []; /** - * @var array + * @var array> */ private array $locks = []; - /** - * @var array - */ - private array $concurrencies = []; - /** * @param DiagnosticsProvider[] $providers */ @@ -174,11 +168,14 @@ public function enqueue(TextDocumentItem $textDocument): void $this->deferred->resolve($textDocument); } - private function isDocumentCurrent(?TextDocumentItem $textDocument): bool + private function isDocumentCurrent(TextDocumentItem $textDocument): bool { return $textDocument->version === ($this->versions[$textDocument->uri] ?? -1); } - + /** + * @param string|int $providerId + * @return Promise + */ private function await($providerId): Promise { if (!array_key_exists($providerId, $this->locks)) { diff --git a/tests/Unit/Core/Diagnostics/DiagnosticsEngineTest.php b/tests/Unit/Core/Diagnostics/DiagnosticsEngineTest.php index 0c6e9238..5da260f7 100644 --- a/tests/Unit/Core/Diagnostics/DiagnosticsEngineTest.php +++ b/tests/Unit/Core/Diagnostics/DiagnosticsEngineTest.php @@ -107,7 +107,7 @@ public function testSleepPreventsSeige(): Generator $token->cancel(); - self::assertEquals(4, $tester->transmitter()->count()); + self::assertEquals(6, $tester->transmitter()->count()); } public function testAggregatesResultsFromMultipleProviders(): Generator From 3bc48123502298d2ea2ebb314a93785d505e8095 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 29 Jul 2023 19:20:51 +0100 Subject: [PATCH 094/111] Be more efficient --- lib/Core/Diagnostics/DiagnosticsEngine.php | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/lib/Core/Diagnostics/DiagnosticsEngine.php b/lib/Core/Diagnostics/DiagnosticsEngine.php index 06c88977..01b499bc 100644 --- a/lib/Core/Diagnostics/DiagnosticsEngine.php +++ b/lib/Core/Diagnostics/DiagnosticsEngine.php @@ -85,13 +85,14 @@ public function run(CancellationToken $token): Promise $textDocument = yield $this->deferred->promise(); $this->versions[$textDocument->uri] = $textDocument->version; - // clear diagnostics for document + if (isset($this->diagnostics[$textDocument->uri])) { + $this->clientApi->diagnostics()->publishDiagnostics( + $textDocument->uri, + $textDocument->version, + $this->diagnostics[$textDocument->uri], + ); + } $this->diagnostics[$textDocument->uri] = []; - $this->clientApi->diagnostics()->publishDiagnostics( - $textDocument->uri, - $textDocument->version, - $this->diagnostics[$textDocument->uri], - ); $this->deferred = new Deferred(); @@ -125,6 +126,10 @@ public function run(CancellationToken $token): Promise $lock->resolve(true); } + if (!$diagnostics) { + return; + } + $elapsed = (int)round((microtime(true) - $start) / 1000); $this->diagnostics[$textDocument->uri] = array_merge( @@ -147,7 +152,6 @@ public function run(CancellationToken $token): Promise $textDocument->version, $this->diagnostics[$textDocument->uri] ); - }); } } From 71e2f710db6d49c64822b140e38c3dd88f2e452e Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 29 Jul 2023 19:22:38 +0100 Subject: [PATCH 095/111] Fix testse --- tests/Unit/Core/Diagnostics/DiagnosticsEngineTest.php | 8 ++++---- .../Diagnostics/CodeActionDiagnosticsProviderTest.php | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/Unit/Core/Diagnostics/DiagnosticsEngineTest.php b/tests/Unit/Core/Diagnostics/DiagnosticsEngineTest.php index 5da260f7..56fae074 100644 --- a/tests/Unit/Core/Diagnostics/DiagnosticsEngineTest.php +++ b/tests/Unit/Core/Diagnostics/DiagnosticsEngineTest.php @@ -62,7 +62,7 @@ public function testPublishesForManyFiles(): Generator $token->cancel(); - self::assertEquals(6, $tester->transmitter()->count()); + self::assertEquals(3, $tester->transmitter()->count()); } /** @@ -85,7 +85,7 @@ public function testDeduplicatesSuccessiveChangesToSameFile(): Generator $token->cancel(); // clear three times - self::assertEquals(3, $tester->transmitter()->count()); + self::assertEquals(2, $tester->transmitter()->count()); } /** @@ -107,7 +107,7 @@ public function testSleepPreventsSeige(): Generator $token->cancel(); - self::assertEquals(6, $tester->transmitter()->count()); + self::assertEquals(3, $tester->transmitter()->count()); } public function testAggregatesResultsFromMultipleProviders(): Generator @@ -133,7 +133,7 @@ public function testAggregatesResultsFromMultipleProviders(): Generator yield new Delayed(100); - self::assertEquals(3, $tester->transmitter()->count()); + self::assertEquals(2, $tester->transmitter()->count()); } diff --git a/tests/Unit/Diagnostics/CodeActionDiagnosticsProviderTest.php b/tests/Unit/Diagnostics/CodeActionDiagnosticsProviderTest.php index 9868aae9..d42a3556 100644 --- a/tests/Unit/Diagnostics/CodeActionDiagnosticsProviderTest.php +++ b/tests/Unit/Diagnostics/CodeActionDiagnosticsProviderTest.php @@ -32,12 +32,12 @@ public function testProvidesDiagnostics(): void wait(new Delayed(10)); - self::assertEquals(2, $tester->transmitter()->count()); + self::assertEquals(1, $tester->transmitter()->count()); $tester->textDocument()->update('file:///foobar', 'bar'); wait(new Delayed(10)); - self::assertEquals(5, $tester->transmitter()->count()); + self::assertEquals(4, $tester->transmitter()->count()); self::assertEquals('textDocument/publishDiagnostics', $tester->transmitter()->shiftNotification()->method); } } From 78a27025521ef203b230dabc37f16d308ad73820 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 29 Jul 2023 20:46:20 +0100 Subject: [PATCH 096/111] Fix next document resolution --- lib/Core/Diagnostics/DiagnosticsEngine.php | 23 +++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/lib/Core/Diagnostics/DiagnosticsEngine.php b/lib/Core/Diagnostics/DiagnosticsEngine.php index 01b499bc..b4bf982f 100644 --- a/lib/Core/Diagnostics/DiagnosticsEngine.php +++ b/lib/Core/Diagnostics/DiagnosticsEngine.php @@ -81,15 +81,14 @@ public function run(CancellationToken $token): Promise return; } - /** @var TextDocumentItem $textDocument */ - $textDocument = yield $this->deferred->promise(); + $textDocument = yield $this->nextDocument(); $this->versions[$textDocument->uri] = $textDocument->version; if (isset($this->diagnostics[$textDocument->uri])) { $this->clientApi->diagnostics()->publishDiagnostics( $textDocument->uri, $textDocument->version, - $this->diagnostics[$textDocument->uri], + [], ); } $this->diagnostics[$textDocument->uri] = []; @@ -100,10 +99,6 @@ public function run(CancellationToken $token): Promise // `false` and let another resolve happen $this->running = false; - if ($this->next) { - $textDocument = $this->next; - $this->next = null; - } foreach ($this->providers as $providerId => $provider) { asyncCall(function () use ($providerId, $provider, $token, $textDocument) { @@ -189,4 +184,18 @@ private function await($providerId): Promise return $this->locks[$providerId]->promise(); } + + /** + * @return Promise + */ + private function nextDocument(): Promise + { + if ($this->next) { + $textDocument = $this->next; + $this->next = null; + return new Success($textDocument); + } + + return $this->deferred->promise(); + } } From 7a419a2b1a347fc179a5d7bcf333d1650e72930d Mon Sep 17 00:00:00 2001 From: Daniel Date: Sun, 10 Sep 2023 12:28:54 +0100 Subject: [PATCH 097/111] Add test --- lib/Core/Diagnostics/DiagnosticsEngine.php | 34 ++++++++++++------- lib/LanguageServerTesterBuilder.php | 1 + .../Diagnostics/DiagnosticsEngineTest.php | 30 ++++++++++++++-- tests/Unit/Service/DiagnosticsServiceTest.php | 2 ++ 4 files changed, 51 insertions(+), 16 deletions(-) diff --git a/lib/Core/Diagnostics/DiagnosticsEngine.php b/lib/Core/Diagnostics/DiagnosticsEngine.php index b4bf982f..a22edfe1 100644 --- a/lib/Core/Diagnostics/DiagnosticsEngine.php +++ b/lib/Core/Diagnostics/DiagnosticsEngine.php @@ -10,6 +10,8 @@ use Amp\CancellationToken; use Amp\Deferred; use Phpactor\LanguageServerProtocol\TextDocumentItem; +use Psr\Log\LoggerInterface; +use Throwable; use function Amp\asyncCall; use function Amp\delay; @@ -24,15 +26,6 @@ class DiagnosticsEngine private ?TextDocumentItem $next = null; - /** - * @var DiagnosticsProvider[] - */ - private array $providers; - - private ClientApi $clientApi; - - private int $sleepTime; - /** * @var array> */ @@ -51,7 +44,7 @@ class DiagnosticsEngine /** * @param DiagnosticsProvider[] $providers */ - public function __construct(ClientApi $clientApi, array $providers, int $sleepTime = 1000) + public function __construct(private ClientApi $clientApi, private LoggerInterface $logger, private array $providers, private int $sleepTime = 1000) { $this->deferred = new Deferred(); $this->providers = $providers; @@ -99,9 +92,14 @@ public function run(CancellationToken $token): Promise // `false` and let another resolve happen $this->running = false; + $crashedProviders = []; foreach ($this->providers as $providerId => $provider) { - asyncCall(function () use ($providerId, $provider, $token, $textDocument) { + if (in_array($providerId, $crashedProviders)) { + continue; + } + + asyncCall(function () use ($providerId, $provider, $token, $textDocument, &$crashedProviders) { $start = microtime(true); yield $this->await($providerId); @@ -112,8 +110,18 @@ public function run(CancellationToken $token): Promise $this->locks[$providerId] = new Deferred(); - /** @var Diagnostic[] $diagnostics */ - $diagnostics = yield $provider->provideDiagnostics($textDocument, $token) ; + try { + /** @var Diagnostic[] $diagnostics */ + $diagnostics = yield $provider->provideDiagnostics($textDocument, $token) ; + } catch (Throwable $e) { + $message = sprintf('Diagnostic provider "%s" errored with "%s", removing from pool', $providerId, $e->getMessage()); + $this->clientApi->window()->showMessage()->warning($message); + $this->logger->error($message, [ + 'stack' => (new \Exception())->getTraceAsString() + ]); + $crashedProviders[$providerId] = true; + return; + } if (isset($this->locks[$providerId])) { $lock = $this->locks[$providerId]; diff --git a/lib/LanguageServerTesterBuilder.php b/lib/LanguageServerTesterBuilder.php index 24963cbd..9887863a 100644 --- a/lib/LanguageServerTesterBuilder.php +++ b/lib/LanguageServerTesterBuilder.php @@ -326,6 +326,7 @@ function (MessageTransmitter $transmitter, InitializeParams $params) { $service = new DiagnosticsService( new DiagnosticsEngine( $this->clientApi, + new NullLogger(), [new AggregateDiagnosticsProvider($logger, ...$this->diagnosticsProvider)], 0 ), diff --git a/tests/Unit/Core/Diagnostics/DiagnosticsEngineTest.php b/tests/Unit/Core/Diagnostics/DiagnosticsEngineTest.php index 56fae074..1eb2b85e 100644 --- a/tests/Unit/Core/Diagnostics/DiagnosticsEngineTest.php +++ b/tests/Unit/Core/Diagnostics/DiagnosticsEngineTest.php @@ -12,6 +12,7 @@ use Phpactor\LanguageServer\Core\Diagnostics\DiagnosticsEngine; use Phpactor\LanguageServer\LanguageServerTesterBuilder; use Phpactor\LanguageServer\Test\ProtocolFactory; +use Psr\Log\NullLogger; use function Amp\asyncCall; use function Amp\call; use function Amp\delay; @@ -37,7 +38,7 @@ public function testPublishesDiagnostics(): Generator $token->cancel(); - $notification = $tester->transmitter()->shiftNotification(); + $notification = $tester->transmitter()->shiftnotification(); self::assertNotNull($notification, 'Notification sent'); self::assertEquals('textDocument/publishDiagnostics', $notification->method); @@ -113,7 +114,7 @@ public function testSleepPreventsSeige(): Generator public function testAggregatesResultsFromMultipleProviders(): Generator { $tester = LanguageServerTesterBuilder::create(); - $engine = new DiagnosticsEngine($tester->clientApi(), [ + $engine = new DiagnosticsEngine($tester->clientApi(), new NullLogger(), [ new ClosureDiagnosticsProvider(function (TextDocumentItem $item) { return new Success([ ProtocolFactory::diagnostic(ProtocolFactory::range(0, 0, 0, 0), 'Foobar is broken') @@ -137,6 +138,29 @@ public function testAggregatesResultsFromMultipleProviders(): Generator } + public function testHandlesLinterExceptions(): Generator + { + $tester = LanguageServerTesterBuilder::create(); + $engine = new DiagnosticsEngine($tester->clientApi(), new NullLogger(), [ + new ClosureDiagnosticsProvider(function (TextDocumentItem $item) { + throw new \Exception('oh dear'); + }), + ], 10); + + $token = new CancellationTokenSource(); + $promise = $engine->run($token->getToken()); + + $engine->enqueue(ProtocolFactory::textDocumentItem('file:///foobar', 'bazboo')); + + yield new Delayed(100); + + self::assertEquals(1, $tester->transmitter()->count()); + $notification = $tester->transmitter()->shiftnotification(); + self::assertEquals('window/showMessage', $notification->method); + self::assertStringContainsString('oh dear', $notification->params['message']); + + } + /** * Note that this test was added in relation to the race condition in * https://github.com/phpactor/phpactor/issues/1974 @@ -178,7 +202,7 @@ public function testAlwaysAnalyzesTheLastChangeLast(): Generator private function createEngine(LanguageServerTesterBuilder $tester, int $delay = 0, int $sleepTime = 0, string &$lastDocument = null): DiagnosticsEngine { - return new DiagnosticsEngine($tester->clientApi(), [ + return new DiagnosticsEngine($tester->clientApi(), new NullLogger(), [ new ClosureDiagnosticsProvider(function (TextDocumentItem $item) use ($delay, &$lastDocument) { return call(function () use ($delay, $item, &$lastDocument) { if ($delay) { diff --git a/tests/Unit/Service/DiagnosticsServiceTest.php b/tests/Unit/Service/DiagnosticsServiceTest.php index 369b2ae2..8ae8b22f 100644 --- a/tests/Unit/Service/DiagnosticsServiceTest.php +++ b/tests/Unit/Service/DiagnosticsServiceTest.php @@ -10,6 +10,7 @@ use Phpactor\LanguageServer\LanguageServerTesterBuilder; use Phpactor\LanguageServer\Service\DiagnosticsService; use Phpactor\LanguageServer\Test\ProtocolFactory; +use Psr\Log\NullLogger; use function Amp\Promise\wait; use function Amp\call; @@ -24,6 +25,7 @@ public function testService(): void $service = new DiagnosticsService( new DiagnosticsEngine( $tester->clientApi(), + new NullLogger(), [ new ClosureDiagnosticsProvider(function () { return call(function () { From b12211da450104566aa36aa3127abefe0cdf8bbf Mon Sep 17 00:00:00 2001 From: Daniel Date: Sun, 10 Sep 2023 12:29:43 +0100 Subject: [PATCH 098/111] CS fixes --- lib/Core/Diagnostics/DiagnosticsEngine.php | 3 ++- tests/Unit/Core/Diagnostics/DiagnosticsEngineTest.php | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/Core/Diagnostics/DiagnosticsEngine.php b/lib/Core/Diagnostics/DiagnosticsEngine.php index a22edfe1..e008bed9 100644 --- a/lib/Core/Diagnostics/DiagnosticsEngine.php +++ b/lib/Core/Diagnostics/DiagnosticsEngine.php @@ -14,6 +14,7 @@ use Throwable; use function Amp\asyncCall; use function Amp\delay; +use Exception; class DiagnosticsEngine { @@ -117,7 +118,7 @@ public function run(CancellationToken $token): Promise $message = sprintf('Diagnostic provider "%s" errored with "%s", removing from pool', $providerId, $e->getMessage()); $this->clientApi->window()->showMessage()->warning($message); $this->logger->error($message, [ - 'stack' => (new \Exception())->getTraceAsString() + 'stack' => (new Exception())->getTraceAsString() ]); $crashedProviders[$providerId] = true; return; diff --git a/tests/Unit/Core/Diagnostics/DiagnosticsEngineTest.php b/tests/Unit/Core/Diagnostics/DiagnosticsEngineTest.php index 1eb2b85e..553cebe9 100644 --- a/tests/Unit/Core/Diagnostics/DiagnosticsEngineTest.php +++ b/tests/Unit/Core/Diagnostics/DiagnosticsEngineTest.php @@ -16,6 +16,7 @@ use function Amp\asyncCall; use function Amp\call; use function Amp\delay; +use Exception; class DiagnosticsEngineTest extends AsyncTestCase { @@ -142,8 +143,8 @@ public function testHandlesLinterExceptions(): Generator { $tester = LanguageServerTesterBuilder::create(); $engine = new DiagnosticsEngine($tester->clientApi(), new NullLogger(), [ - new ClosureDiagnosticsProvider(function (TextDocumentItem $item) { - throw new \Exception('oh dear'); + new ClosureDiagnosticsProvider(function (TextDocumentItem $item): void { + throw new Exception('oh dear'); }), ], 10); From 4898945e315d9894d3495736d93793670ec0980e Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 22 Sep 2023 09:49:06 +0100 Subject: [PATCH 099/111] Do not crash if cannot parse header --- bin/serve.php | 2 +- lib/Core/Server/LanguageServer.php | 2 +- lib/Core/Server/Parser/LspMessageReader.php | 18 ++++++++++++++--- .../Server/Parser/LspMessageReaderTest.php | 20 +++++++++++++++++++ 4 files changed, 37 insertions(+), 5 deletions(-) diff --git a/bin/serve.php b/bin/serve.php index 23020ffd..895bcbfa 100755 --- a/bin/serve.php +++ b/bin/serve.php @@ -106,7 +106,7 @@ function (MessageTransmitter $transmitter, InitializeParams $params) use ($logge $clientApi = new ClientApi(new JsonRpcClient($transmitter, $responseWatcher)); $diagnosticsService = new DiagnosticsService( - new DiagnosticsEngine($clientApi, [new AggregateDiagnosticsProvider( + new DiagnosticsEngine($clientApi, $logger, [new AggregateDiagnosticsProvider( $logger, new SayHelloDiagnosticsProvider() )]) diff --git a/lib/Core/Server/LanguageServer.php b/lib/Core/Server/LanguageServer.php index d1f7dfbd..b8042316 100644 --- a/lib/Core/Server/LanguageServer.php +++ b/lib/Core/Server/LanguageServer.php @@ -183,7 +183,7 @@ private function handle(Connection $connection): Promise { return \Amp\call(function () use ($connection) { $transmitter = new ConnectionMessageTransmitter($connection); - $reader = new LspMessageReader($connection->stream()); + $reader = new LspMessageReader($connection->stream(), $this->logger); $dispatcher = null; // wait for the next request diff --git a/lib/Core/Server/Parser/LspMessageReader.php b/lib/Core/Server/Parser/LspMessageReader.php index 9f24d0c8..3a3ec76e 100644 --- a/lib/Core/Server/Parser/LspMessageReader.php +++ b/lib/Core/Server/Parser/LspMessageReader.php @@ -5,6 +5,8 @@ use Amp\ByteStream\InputStream; use Amp\Promise; use Phpactor\LanguageServer\Core\Rpc\RawMessage; +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; use function json_encode; use Phpactor\LanguageServer\Core\Server\Parser\Exception\CouldNotDecodeBody; use Phpactor\LanguageServer\Core\Server\Parser\Exception\CouldNotParseHeader; @@ -34,9 +36,12 @@ final class LspMessageReader implements RequestReader */ private $stream; - public function __construct(InputStream $stream) + private LoggerInterface $logger; + + public function __construct(InputStream $stream, ?LoggerInterface $logger = null) { $this->stream = $stream; + $this->logger = $logger ?: new NullLogger(); } public function wait(): Promise @@ -111,10 +116,17 @@ private function parseHeaders(string $rawHeaders): array $headers = []; foreach ($lines as $line) { - [ $name, $value ] = array_map(function ($value) { + $parts = array_map(function ($value) { return trim($value); }, explode(':', $line)); - $headers[$name] = $value; + if (count($parts) != 2) { + $this->logger->warning(sprintf( + 'Could not parse header: %s', + $line + )); + continue; + } + $headers[$parts[0]] = $parts[1]; } return $headers; diff --git a/tests/Unit/Core/Server/Parser/LspMessageReaderTest.php b/tests/Unit/Core/Server/Parser/LspMessageReaderTest.php index 6ac7a3a2..8e9905bc 100644 --- a/tests/Unit/Core/Server/Parser/LspMessageReaderTest.php +++ b/tests/Unit/Core/Server/Parser/LspMessageReaderTest.php @@ -39,6 +39,26 @@ public function testYieldsRequest(): void $this->assertInstanceOf(RawMessage::class, $result); } + public function testSkipsInvalidHeader(): void + { + $stream = new InMemoryStream( + <<wait()); + $this->assertInstanceOf(RawMessage::class, $result); + } + public function testReadsMultipleRequests(): void { $stream = new InMemoryStream( From 0d1154dbc6158b39b97618eaffe253830eafc56d Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sat, 24 Feb 2024 15:23:07 +0000 Subject: [PATCH 100/111] Do not run diagnostic engine if version is already outdated. --- lib/Core/Diagnostics/DiagnosticsEngine.php | 17 +++++++++++++---- lib/Service/DiagnosticsService.php | 4 +++- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/lib/Core/Diagnostics/DiagnosticsEngine.php b/lib/Core/Diagnostics/DiagnosticsEngine.php index e008bed9..dd9a5c08 100644 --- a/lib/Core/Diagnostics/DiagnosticsEngine.php +++ b/lib/Core/Diagnostics/DiagnosticsEngine.php @@ -67,6 +67,8 @@ public function clear(TextDocumentItem $textDocument): void */ public function run(CancellationToken $token): Promise { + + return \Amp\call(function () use ($token) { while (true) { try { @@ -76,7 +78,6 @@ public function run(CancellationToken $token): Promise } $textDocument = yield $this->nextDocument(); - $this->versions[$textDocument->uri] = $textDocument->version; if (isset($this->diagnostics[$textDocument->uri])) { $this->clientApi->diagnostics()->publishDiagnostics( @@ -85,14 +86,22 @@ public function run(CancellationToken $token): Promise [], ); } - $this->diagnostics[$textDocument->uri] = []; + $this->diagnostics[$textDocument->uri] = []; $this->deferred = new Deferred(); - // after we have reset deferred, we can safely set linting to // `false` and let another resolve happen $this->running = false; + + // if the last processed version of the document is more recent + // than the last then continue. + if (($this->vesions[$textDocument->uri] ?? -1) >= $textDocument->version) { + continue; + } + + $this->versions[$textDocument->uri] = $textDocument->version; + $crashedProviders = []; foreach ($this->providers as $providerId => $provider) { @@ -171,7 +180,7 @@ public function enqueue(TextDocumentItem $textDocument): void return; } - // resolving the promise will start PHPStan + // resolving the promise will start the diagnostc resolving $this->running = true; $this->deferred->resolve($textDocument); } diff --git a/lib/Service/DiagnosticsService.php b/lib/Service/DiagnosticsService.php index f2f377f0..05bec4a6 100644 --- a/lib/Service/DiagnosticsService.php +++ b/lib/Service/DiagnosticsService.php @@ -114,10 +114,12 @@ public function enqueueUpdate(TextDocumentUpdated $update): void public function enqueueSave(TextDocumentSaved $save): void { + $version = $this->workspace->get($save->identifier()->uri)->version; + $item = new TextDocumentItem( $save->identifier()->uri, 'php', - $save->identifier()->version ?? 1, // VIM lsp client seems delivers NULL here, so just use an arbitrary identifier + $version, $save->text() ?: $this->workspace->get($save->identifier()->uri)->text ); From d166de07a7da57180a393d3e0873707a00eed3fa Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sat, 24 Feb 2024 15:46:35 +0000 Subject: [PATCH 101/111] Refactor and add test for ignoring inferior versions --- lib/Core/Diagnostics/DiagnosticsEngine.php | 7 ++-- lib/Test/ProtocolFactory.php | 4 +-- .../Diagnostics/DiagnosticsEngineTest.php | 33 ++++++++++++++++--- 3 files changed, 34 insertions(+), 10 deletions(-) diff --git a/lib/Core/Diagnostics/DiagnosticsEngine.php b/lib/Core/Diagnostics/DiagnosticsEngine.php index dd9a5c08..5be09113 100644 --- a/lib/Core/Diagnostics/DiagnosticsEngine.php +++ b/lib/Core/Diagnostics/DiagnosticsEngine.php @@ -78,8 +78,10 @@ public function run(CancellationToken $token): Promise } $textDocument = yield $this->nextDocument(); + $lastKnownVersion = ($this->versions[$textDocument->uri] ?? -1); - if (isset($this->diagnostics[$textDocument->uri])) { + if ($lastKnownVersion <= $textDocument->version && isset($this->diagnostics[$textDocument->uri])) { + // reset diagnostics for this document $this->clientApi->diagnostics()->publishDiagnostics( $textDocument->uri, $textDocument->version, @@ -93,10 +95,9 @@ public function run(CancellationToken $token): Promise // `false` and let another resolve happen $this->running = false; - // if the last processed version of the document is more recent // than the last then continue. - if (($this->vesions[$textDocument->uri] ?? -1) >= $textDocument->version) { + if ($lastKnownVersion >= $textDocument->version) { continue; } diff --git a/lib/Test/ProtocolFactory.php b/lib/Test/ProtocolFactory.php index 6db758d6..552e755d 100644 --- a/lib/Test/ProtocolFactory.php +++ b/lib/Test/ProtocolFactory.php @@ -16,9 +16,9 @@ final class ProtocolFactory { - public static function textDocumentItem(string $uri, string $content): TextDocumentItem + public static function textDocumentItem(string $uri, string $content, int $version = 1): TextDocumentItem { - return new TextDocumentItem($uri, 'php', 1, $content); + return new TextDocumentItem($uri, 'php', $version, $content); } public static function versionedTextDocumentIdentifier(string $uri, int $version): VersionedTextDocumentIdentifier diff --git a/tests/Unit/Core/Diagnostics/DiagnosticsEngineTest.php b/tests/Unit/Core/Diagnostics/DiagnosticsEngineTest.php index 553cebe9..ca3018d8 100644 --- a/tests/Unit/Core/Diagnostics/DiagnosticsEngineTest.php +++ b/tests/Unit/Core/Diagnostics/DiagnosticsEngineTest.php @@ -90,6 +90,29 @@ public function testDeduplicatesSuccessiveChangesToSameFile(): Generator self::assertEquals(2, $tester->transmitter()->count()); } + /** + * @return Generator + */ + public function testIgnoresTextDocumentsWithInferiorVersions(): Generator + { + $tester = LanguageServerTesterBuilder::create(); + $engine = $this->createEngine($tester, 10, 0); + + $token = new CancellationTokenSource(); + $promise = $engine->run($token->getToken()); + + $engine->enqueue(ProtocolFactory::textDocumentItem('file:///foobar', 'foobar', version: 20)); + $engine->enqueue(ProtocolFactory::textDocumentItem('file:///foobar', 'foobar', version: 1)); + $engine->enqueue(ProtocolFactory::textDocumentItem('file:///foobar', 'foobar', version: 20)); + $engine->enqueue(ProtocolFactory::textDocumentItem('file:///foobar', 'foobar', version: 30)); + + yield new Delayed(1); + + $token->cancel(); + + self::assertEquals(2, $tester->transmitter()->count()); + } + /** * @return Generator */ @@ -184,15 +207,15 @@ public function testAlwaysAnalyzesTheLastChangeLast(): Generator }); yield new Delayed(1); - $engine->enqueue(ProtocolFactory::textDocumentItem('file:///foobar', '1')); + $engine->enqueue(ProtocolFactory::textDocumentItem('file:///foobar', '1', version: 1)); yield new Delayed(1); - $engine->enqueue(ProtocolFactory::textDocumentItem('file:///foobar', '2')); + $engine->enqueue(ProtocolFactory::textDocumentItem('file:///foobar', '2', version: 2)); yield new Delayed(10); - $engine->enqueue(ProtocolFactory::textDocumentItem('file:///foobar', '3')); + $engine->enqueue(ProtocolFactory::textDocumentItem('file:///foobar', '3', version: 3)); yield new Delayed(1); - $engine->enqueue(ProtocolFactory::textDocumentItem('file:///foobar', '4')); + $engine->enqueue(ProtocolFactory::textDocumentItem('file:///foobar', '4', version: 4)); yield new Delayed(1); - $engine->enqueue(ProtocolFactory::textDocumentItem('file:///foobar', '5')); + $engine->enqueue(ProtocolFactory::textDocumentItem('file:///foobar', '5', version: 5)); yield new Delayed(100); From e547efe410e2a3d357d16a9668db197113ce1776 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sat, 24 Feb 2024 15:54:05 +0000 Subject: [PATCH 102/111] Fix versioning in tester --- .../LanguageServerTester/TextDocumentTester.php | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/Test/LanguageServerTester/TextDocumentTester.php b/lib/Test/LanguageServerTester/TextDocumentTester.php index de06f212..4559e936 100644 --- a/lib/Test/LanguageServerTester/TextDocumentTester.php +++ b/lib/Test/LanguageServerTester/TextDocumentTester.php @@ -10,9 +10,15 @@ use Phpactor\LanguageServerProtocol\DidOpenTextDocumentParams; use Phpactor\LanguageServerProtocol\DidOpenTextDocumentNotification; use Phpactor\LanguageServer\Test\LanguageServerTester; +use RuntimeException; class TextDocumentTester { + /** + * @var array + */ + private static $versions = []; + /** * @var LanguageServerTester */ @@ -25,6 +31,7 @@ public function __construct(LanguageServerTester $tester) public function open(string $url, string $content): void { + self::$versions[$url] = 1; $this->tester->notifyAndWait(DidOpenTextDocumentNotification::METHOD, new DidOpenTextDocumentParams( ProtocolFactory::textDocumentItem($url, $content) )); @@ -32,8 +39,15 @@ public function open(string $url, string $content): void public function update(string $uri, string $newText): void { + if (!isset(self::$versions[$uri])) { + throw new RuntimeException(sprintf( + 'Cannot update document that has not been opened: %s', + $uri + )); + } + self::$versions[$uri]++; $this->tester->notifyAndWait(DidChangeTextDocumentNotification::METHOD, new DidChangeTextDocumentParams( - ProtocolFactory::versionedTextDocumentIdentifier($uri, 1), + ProtocolFactory::versionedTextDocumentIdentifier($uri, self::$versions[$uri]), [ [ 'text' => $newText From 18c6336fd3ede98bfe460e051a0a9d6f2456bdc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20Dziewo=C5=84ski?= Date: Tue, 27 Feb 2024 05:00:06 +0100 Subject: [PATCH 103/111] Make the use of POSIX signals optional Signals are not supported on Windows, and gracefully shutting down everything doesn't seem necessary if we're about to exit anyway. --- lib/Core/Server/LanguageServer.php | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/Core/Server/LanguageServer.php b/lib/Core/Server/LanguageServer.php index b8042316..fb36ca01 100644 --- a/lib/Core/Server/LanguageServer.php +++ b/lib/Core/Server/LanguageServer.php @@ -93,10 +93,13 @@ public function start(): Promise */ public function run(): void { - Loop::onSignal(SIGINT, function (string $watcherId) { - Loop::cancel($watcherId); - yield $this->shutdown(); - }); + // Signals are not supported on Windows + if(defined('SIGINT')) { + Loop::onSignal(SIGINT, function (string $watcherId) { + Loop::cancel($watcherId); + yield $this->shutdown(); + }); + } Loop::setErrorHandler(function (Throwable $error): void { if ($error instanceof ShutdownServer) { From 844bcee80be6f5b0c380a8f02030bbccdec76b96 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 3 Mar 2024 11:25:34 +0000 Subject: [PATCH 104/111] Refactor diagnostics provider Only process one document at a time and move grace period to BEFORE we start linting. --- lib/Core/Diagnostics/DiagnosticsEngine.php | 87 +++++++++---------- .../Diagnostics/DiagnosticsEngineTest.php | 57 +++++------- .../CodeActionDiagnosticsProviderTest.php | 4 +- 3 files changed, 67 insertions(+), 81 deletions(-) diff --git a/lib/Core/Diagnostics/DiagnosticsEngine.php b/lib/Core/Diagnostics/DiagnosticsEngine.php index 5be09113..ecab499c 100644 --- a/lib/Core/Diagnostics/DiagnosticsEngine.php +++ b/lib/Core/Diagnostics/DiagnosticsEngine.php @@ -19,13 +19,11 @@ class DiagnosticsEngine { /** - * @var Deferred + * @var Deferred */ private Deferred $deferred; - private bool $running = false; - - private ?TextDocumentItem $next = null; + private ?TextDocumentItem $waiting = null; /** * @var array> @@ -42,6 +40,8 @@ class DiagnosticsEngine */ private array $locks = []; + private float $lastUpdatedAt; + /** * @param DiagnosticsProvider[] $providers */ @@ -51,6 +51,7 @@ public function __construct(private ClientApi $clientApi, private LoggerInterfac $this->providers = $providers; $this->clientApi = $clientApi; $this->sleepTime = $sleepTime; + $this->lastUpdatedAt = 0.0; } public function clear(TextDocumentItem $textDocument): void @@ -67,8 +68,6 @@ public function clear(TextDocumentItem $textDocument): void */ public function run(CancellationToken $token): Promise { - - return \Amp\call(function () use ($token) { while (true) { try { @@ -77,30 +76,29 @@ public function run(CancellationToken $token): Promise return; } - $textDocument = yield $this->nextDocument(); - $lastKnownVersion = ($this->versions[$textDocument->uri] ?? -1); + yield $this->awaitNextDocument(); - if ($lastKnownVersion <= $textDocument->version && isset($this->diagnostics[$textDocument->uri])) { - // reset diagnostics for this document - $this->clientApi->diagnostics()->publishDiagnostics( - $textDocument->uri, - $textDocument->version, - [], - ); - } + $gracePeriod = abs($this->sleepTime - ((microtime(true) - $this->lastUpdatedAt) * 1000)); + yield delay(intval($gracePeriod)); - $this->diagnostics[$textDocument->uri] = []; + $textDocument = $this->waiting; + $this->waiting = null; + // allow the next document update to resolve $this->deferred = new Deferred(); - // after we have reset deferred, we can safely set linting to - // `false` and let another resolve happen - $this->running = false; - // if the last processed version of the document is more recent - // than the last then continue. - if ($lastKnownVersion >= $textDocument->version) { + // should never happen + if ($textDocument === null) { continue; } + // reset diagnostics for this document + $this->clientApi->diagnostics()->publishDiagnostics( + $textDocument->uri, + $textDocument->version, + [], + ); + $this->diagnostics[$textDocument->uri] = []; + $this->versions[$textDocument->uri] = $textDocument->version; $crashedProviders = []; @@ -113,7 +111,7 @@ public function run(CancellationToken $token): Promise asyncCall(function () use ($providerId, $provider, $token, $textDocument, &$crashedProviders) { $start = microtime(true); - yield $this->await($providerId); + yield $this->awaitProviderLock($providerId); if (!$this->isDocumentCurrent($textDocument)) { return; @@ -151,16 +149,11 @@ public function run(CancellationToken $token): Promise $diagnostics ); - $timeToSleep = $this->sleepTime - $elapsed; - - if ($timeToSleep > 0) { - yield delay($timeToSleep); - } - if (!$this->isDocumentCurrent($textDocument)) { return; } + $this->clientApi->diagnostics()->publishDiagnostics( $textDocument->uri, $textDocument->version, @@ -174,27 +167,35 @@ public function run(CancellationToken $token): Promise public function enqueue(TextDocumentItem $textDocument): void { - // if we are already linting then store whatever comes afterwards in - // next, overwriting the redundant update - if ($this->running === true) { - $this->next = $textDocument; + // set the last updated at timestamp - this will be used as the basis of + // the grace period before linting + $this->lastUpdatedAt = microtime(true); + + $waiting = $this->waiting; + + // set the next document + $this->waiting = $textDocument; + + // if we already had a waiting document then do nothing + // it will get resolved next time + if ($waiting !== null) { return; } - // resolving the promise will start the diagnostc resolving - $this->running = true; - $this->deferred->resolve($textDocument); + // otherwise trigger the lint process + $this->deferred->resolve(); } private function isDocumentCurrent(TextDocumentItem $textDocument): bool { return $textDocument->version === ($this->versions[$textDocument->uri] ?? -1); } + /** * @param string|int $providerId * @return Promise */ - private function await($providerId): Promise + private function awaitProviderLock($providerId): Promise { if (!array_key_exists($providerId, $this->locks)) { return new Success(true); @@ -205,14 +206,12 @@ private function await($providerId): Promise } /** - * @return Promise + * @return Promise */ - private function nextDocument(): Promise + private function awaitNextDocument(): Promise { - if ($this->next) { - $textDocument = $this->next; - $this->next = null; - return new Success($textDocument); + if ($this->waiting) { + return new Success(); } return $this->deferred->promise(); diff --git a/tests/Unit/Core/Diagnostics/DiagnosticsEngineTest.php b/tests/Unit/Core/Diagnostics/DiagnosticsEngineTest.php index ca3018d8..1178542a 100644 --- a/tests/Unit/Core/Diagnostics/DiagnosticsEngineTest.php +++ b/tests/Unit/Core/Diagnostics/DiagnosticsEngineTest.php @@ -10,6 +10,7 @@ use Phpactor\LanguageServerProtocol\TextDocumentItem; use Phpactor\LanguageServer\Core\Diagnostics\ClosureDiagnosticsProvider; use Phpactor\LanguageServer\Core\Diagnostics\DiagnosticsEngine; +use Phpactor\LanguageServer\Core\Rpc\NotificationMessage; use Phpactor\LanguageServer\LanguageServerTesterBuilder; use Phpactor\LanguageServer\Test\ProtocolFactory; use Psr\Log\NullLogger; @@ -39,16 +40,22 @@ public function testPublishesDiagnostics(): Generator $token->cancel(); - $notification = $tester->transmitter()->shiftnotification(); + $notification = $tester->transmitter()->shiftNotification(); + self::assertNotNull($notification, 'Notification sent'); + self::assertEquals('textDocument/publishDiagnostics', $notification->method); + self::assertEquals([], $notification->params['diagnostics'] ?? null); + $notification = $tester->transmitter()->shiftNotification(); self::assertNotNull($notification, 'Notification sent'); self::assertEquals('textDocument/publishDiagnostics', $notification->method); + /** @phpstan-ignore-next-line */ + self::assertEquals('Foobar is broken', $notification->params['diagnostics'][0]->message ?? null); } /** * @return Generator */ - public function testPublishesForManyFiles(): Generator + public function testOnlyPublishesForMostRecentFile(): Generator { $tester = LanguageServerTesterBuilder::create(); $engine = $this->createEngine($tester, 0, 0); @@ -64,16 +71,17 @@ public function testPublishesForManyFiles(): Generator $token->cancel(); - self::assertEquals(3, $tester->transmitter()->count()); + // includes reset diagnostic + self::assertEquals(2, $tester->transmitter()->count()); } /** * @return Generator */ - public function testDeduplicatesSuccessiveChangesToSameFile(): Generator + public function testDoesNotProcessMoreThanOneDocument(): Generator { $tester = LanguageServerTesterBuilder::create(); - $engine = $this->createEngine($tester, 10, 0); + $engine = $this->createEngine($tester, 1, 0); $token = new CancellationTokenSource(); $promise = $engine->run($token->getToken()); @@ -82,34 +90,11 @@ public function testDeduplicatesSuccessiveChangesToSameFile(): Generator $engine->enqueue(ProtocolFactory::textDocumentItem('file:///foobar', 'foobar')); $engine->enqueue(ProtocolFactory::textDocumentItem('file:///foobar', 'foobar')); - yield new Delayed(1); - - $token->cancel(); - - // clear three times - self::assertEquals(2, $tester->transmitter()->count()); - } - - /** - * @return Generator - */ - public function testIgnoresTextDocumentsWithInferiorVersions(): Generator - { - $tester = LanguageServerTesterBuilder::create(); - $engine = $this->createEngine($tester, 10, 0); - - $token = new CancellationTokenSource(); - $promise = $engine->run($token->getToken()); - - $engine->enqueue(ProtocolFactory::textDocumentItem('file:///foobar', 'foobar', version: 20)); - $engine->enqueue(ProtocolFactory::textDocumentItem('file:///foobar', 'foobar', version: 1)); - $engine->enqueue(ProtocolFactory::textDocumentItem('file:///foobar', 'foobar', version: 20)); - $engine->enqueue(ProtocolFactory::textDocumentItem('file:///foobar', 'foobar', version: 30)); - - yield new Delayed(1); + yield new Delayed(10); $token->cancel(); + // clear + publish self::assertEquals(2, $tester->transmitter()->count()); } @@ -132,7 +117,7 @@ public function testSleepPreventsSeige(): Generator $token->cancel(); - self::assertEquals(3, $tester->transmitter()->count()); + self::assertEquals(2, $tester->transmitter()->count()); } public function testAggregatesResultsFromMultipleProviders(): Generator @@ -158,7 +143,7 @@ public function testAggregatesResultsFromMultipleProviders(): Generator yield new Delayed(100); - self::assertEquals(2, $tester->transmitter()->count()); + self::assertEquals(3, $tester->transmitter()->count()); } @@ -178,10 +163,12 @@ public function testHandlesLinterExceptions(): Generator yield new Delayed(100); - self::assertEquals(1, $tester->transmitter()->count()); - $notification = $tester->transmitter()->shiftnotification(); + self::assertEquals(2, $tester->transmitter()->count()); + $notification = $tester->transmitter()->shiftNotification(); + $notification = $tester->transmitter()->shiftNotification(); + assert($notification instanceof NotificationMessage); self::assertEquals('window/showMessage', $notification->method); - self::assertStringContainsString('oh dear', $notification->params['message']); + self::assertStringContainsString('oh dear', ((string)($notification->params['message'] ?? ''))); } diff --git a/tests/Unit/Diagnostics/CodeActionDiagnosticsProviderTest.php b/tests/Unit/Diagnostics/CodeActionDiagnosticsProviderTest.php index d42a3556..9868aae9 100644 --- a/tests/Unit/Diagnostics/CodeActionDiagnosticsProviderTest.php +++ b/tests/Unit/Diagnostics/CodeActionDiagnosticsProviderTest.php @@ -32,12 +32,12 @@ public function testProvidesDiagnostics(): void wait(new Delayed(10)); - self::assertEquals(1, $tester->transmitter()->count()); + self::assertEquals(2, $tester->transmitter()->count()); $tester->textDocument()->update('file:///foobar', 'bar'); wait(new Delayed(10)); - self::assertEquals(4, $tester->transmitter()->count()); + self::assertEquals(5, $tester->transmitter()->count()); self::assertEquals('textDocument/publishDiagnostics', $tester->transmitter()->shiftNotification()->method); } } From c46e883649806613e649957b264c7e751bb66e20 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 3 Mar 2024 11:47:31 +0000 Subject: [PATCH 105/111] Remove redundant versioning checks --- lib/Core/Diagnostics/DiagnosticsEngine.php | 23 ++++++++----------- .../Diagnostics/DiagnosticsEngineTest.php | 2 +- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/lib/Core/Diagnostics/DiagnosticsEngine.php b/lib/Core/Diagnostics/DiagnosticsEngine.php index ecab499c..f8c62cf7 100644 --- a/lib/Core/Diagnostics/DiagnosticsEngine.php +++ b/lib/Core/Diagnostics/DiagnosticsEngine.php @@ -30,11 +30,6 @@ class DiagnosticsEngine */ private array $diagnostics = []; - /** - * @var array - */ - private array $versions = []; - /** * @var array> */ @@ -78,9 +73,14 @@ public function run(CancellationToken $token): Promise yield $this->awaitNextDocument(); + $beforeDocument = $this->waiting; $gracePeriod = abs($this->sleepTime - ((microtime(true) - $this->lastUpdatedAt) * 1000)); yield delay(intval($gracePeriod)); + if ($beforeDocument !== $this->waiting) { + continue; + } + $textDocument = $this->waiting; $this->waiting = null; // allow the next document update to resolve @@ -99,8 +99,6 @@ public function run(CancellationToken $token): Promise ); $this->diagnostics[$textDocument->uri] = []; - $this->versions[$textDocument->uri] = $textDocument->version; - $crashedProviders = []; foreach ($this->providers as $providerId => $provider) { @@ -144,15 +142,14 @@ public function run(CancellationToken $token): Promise $elapsed = (int)round((microtime(true) - $start) / 1000); - $this->diagnostics[$textDocument->uri] = array_merge( - $this->diagnostics[$textDocument->uri] ?? [], - $diagnostics - ); - if (!$this->isDocumentCurrent($textDocument)) { return; } + $this->diagnostics[$textDocument->uri] = array_merge( + $this->diagnostics[$textDocument->uri] ?? [], + $diagnostics + ); $this->clientApi->diagnostics()->publishDiagnostics( $textDocument->uri, @@ -188,7 +185,7 @@ public function enqueue(TextDocumentItem $textDocument): void private function isDocumentCurrent(TextDocumentItem $textDocument): bool { - return $textDocument->version === ($this->versions[$textDocument->uri] ?? -1); + return $this->waiting === null; } /** diff --git a/tests/Unit/Core/Diagnostics/DiagnosticsEngineTest.php b/tests/Unit/Core/Diagnostics/DiagnosticsEngineTest.php index 1178542a..c1d46589 100644 --- a/tests/Unit/Core/Diagnostics/DiagnosticsEngineTest.php +++ b/tests/Unit/Core/Diagnostics/DiagnosticsEngineTest.php @@ -67,7 +67,7 @@ public function testOnlyPublishesForMostRecentFile(): Generator $engine->enqueue(ProtocolFactory::textDocumentItem('file:///barfoo', 'foobar')); $engine->enqueue(ProtocolFactory::textDocumentItem('file:///bazbar', 'foobar')); - yield new Delayed(0); + yield new Delayed(1); $token->cancel(); From 01ad22debad055d20d6e574d6b941831cb8d457f Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 26 Jan 2025 20:10:57 +0000 Subject: [PATCH 106/111] Add git attributes --- .gitattributes | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..c307ec2b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,15 @@ +# Handle line endings automatically for files detected as text and leave all +# files detected as binary untouched. +* text=auto + +# Files and directories with the attribute export-ignore won’t be added to +# archive files. See http://git-scm.com/docs/gitattributes for details. +.gitattributes export-ignore +.gitignore export-ignore +/*.neon export-ignore +/.github export-ignore +/.php-cs-fixer.dist.php export-ignore +/doc/ export-ignore +/phpunit.* export-ignore +/phpstan.* export-ignore +/tests/ export-ignore From 866e5e29e79e011367db3d3d294ac08e76c1935a Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 26 Jan 2025 20:13:36 +0000 Subject: [PATCH 107/111] Drop safe --- bin/serve.php | 1 - composer.json | 5 ++--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/bin/serve.php b/bin/serve.php index 895bcbfa..253b9a3c 100755 --- a/bin/serve.php +++ b/bin/serve.php @@ -43,7 +43,6 @@ use Phpactor\LanguageServer\Middleware\ResponseHandlingMiddleware; use Phpactor\LanguageServer\Service\DiagnosticsService; use Psr\Log\AbstractLogger; -use function Safe\fopen; require __DIR__ . '/../vendor/autoload.php'; diff --git a/composer.json b/composer.json index a515386a..5652a4d4 100644 --- a/composer.json +++ b/composer.json @@ -9,15 +9,14 @@ } ], "require": { - "php": "^8.0", + "php": "^8.1", "amphp/socket": "^1.1", "dantleech/argument-resolver": "^1.1", "dantleech/invoke": "^2.0", "phpactor/language-server-protocol": "^3.17", "psr/event-dispatcher": "^1.0", "psr/log": "^1.0", - "ramsey/uuid": "^4.0", - "thecodingmachine/safe": "^1.1" + "ramsey/uuid": "^4.0" }, "require-dev": { "amphp/phpunit-util": "^1.3", From 52e155ef5c46bb5404a7a3d1d04d1ee572c1ada6 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 26 Jan 2025 20:16:00 +0000 Subject: [PATCH 108/111] Update --- bin/serve.php | 2 ++ lib/Core/Server/LanguageServer.php | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/bin/serve.php b/bin/serve.php index 253b9a3c..a30e752b 100755 --- a/bin/serve.php +++ b/bin/serve.php @@ -77,7 +77,9 @@ private $log; public function __construct() { + /** @phpstan-ignore assign.propertyType */ $this->err = fopen('php://stderr', 'w'); + /** @phpstan-ignore assign.propertyType */ $this->log = fopen('phpactor-lsp.log', 'w'); } diff --git a/lib/Core/Server/LanguageServer.php b/lib/Core/Server/LanguageServer.php index fb36ca01..e08dedf1 100644 --- a/lib/Core/Server/LanguageServer.php +++ b/lib/Core/Server/LanguageServer.php @@ -94,7 +94,7 @@ public function start(): Promise public function run(): void { // Signals are not supported on Windows - if(defined('SIGINT')) { + if (defined('SIGINT')) { Loop::onSignal(SIGINT, function (string $watcherId) { Loop::cancel($watcherId); yield $this->shutdown(); From 956e233310b1b6c91b0f35838ce385834cadc4c3 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 26 Jan 2025 20:16:00 +0000 Subject: [PATCH 109/111] Update --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ddafe409..e664d4da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ CHANGELOG ========= +7.0 +--- + +- Dropped dependency on `Safe` +- Bumped minimum version of PHP to 8.1 + 6.0 --- From f9b45594e4a7af8b9860d10812fd6d116f1dd957 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 26 Jan 2025 20:20:05 +0000 Subject: [PATCH 110/111] Support all versions --- .github/workflows/ci.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4d086580..e618855e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: strategy: matrix: php-version: - - '8.0' + - '8.1' steps: - @@ -52,7 +52,7 @@ jobs: strategy: matrix: php-version: - - '8.0' + - '8.1' steps: - @@ -85,9 +85,10 @@ jobs: strategy: matrix: php-version: - - '8.0' - '8.1' - '8.2' + - '8.3' + - '8.4' steps: - From e4934195cc1857ec3347a488f3357b0f0df2d2bf Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 26 Jan 2025 23:40:29 +0000 Subject: [PATCH 111/111] Nullable types for 8.4 --- lib/Adapter/DTL/DTLArgumentResolver.php | 2 +- lib/LanguageServerBuilder.php | 2 +- lib/Test/LanguageServerTester.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/Adapter/DTL/DTLArgumentResolver.php b/lib/Adapter/DTL/DTLArgumentResolver.php index 7191f9e2..bb8865f0 100644 --- a/lib/Adapter/DTL/DTLArgumentResolver.php +++ b/lib/Adapter/DTL/DTLArgumentResolver.php @@ -16,7 +16,7 @@ final class DTLArgumentResolver implements ArgumentResolver */ private $dtlArgumnetResolver; - public function __construct(UpstreamArgumentResolver $dtlArgumnetResolver = null) + public function __construct(?UpstreamArgumentResolver $dtlArgumnetResolver = null) { $this->dtlArgumnetResolver = $dtlArgumnetResolver ?: new UpstreamArgumentResolver([ new RecursiveInstantiator() diff --git a/lib/LanguageServerBuilder.php b/lib/LanguageServerBuilder.php index 9c7475a1..6754f90e 100644 --- a/lib/LanguageServerBuilder.php +++ b/lib/LanguageServerBuilder.php @@ -52,7 +52,7 @@ private function __construct( */ public static function create( DispatcherFactory $dispatcherFactory, - LoggerInterface $logger = null + ?LoggerInterface $logger = null ): self { return new self( $dispatcherFactory, diff --git a/lib/Test/LanguageServerTester.php b/lib/Test/LanguageServerTester.php index 6cf1ada3..d5fb58ec 100644 --- a/lib/Test/LanguageServerTester.php +++ b/lib/Test/LanguageServerTester.php @@ -87,7 +87,7 @@ public function requestAndWait(string $method, $params, $id = null): ?ResponseMe /** * @param array|object $params */ - public function mustRequestAndWait(string $method, mixed $params, int|string $id = null): ResponseMessage + public function mustRequestAndWait(string $method, mixed $params, int|string|null $id = null): ResponseMessage { $response = $this->requestAndWait($method, $params);