diff --git a/src/Linters/ASTLintError.php b/src/Linters/ASTLintError.php index c92dccca..74f69337 100644 --- a/src/Linters/ASTLintError.php +++ b/src/Linters/ASTLintError.php @@ -50,7 +50,6 @@ final public function getRange(): ((int, int), (int, int)) { ); } - <<__Override>> final public function getBlameCode(): string { return $this->node->getCode(); } diff --git a/src/Linters/AutoFixingASTLinter.php b/src/Linters/AutoFixingASTLinter.php index 55daed22..dfe808c5 100644 --- a/src/Linters/AutoFixingASTLinter.php +++ b/src/Linters/AutoFixingASTLinter.php @@ -11,14 +11,60 @@ namespace Facebook\HHAST\Linters; use type Facebook\HHAST\EditableNode; +use namespace Facebook\HHAST\__Private\LSP; use namespace Facebook\HHAST; +use namespace HH\Lib\{C, Str}; abstract class AutoFixingASTLinter extends BaseASTLinter> -implements AutoFixingLinter> { +implements LSPAutoFixingLinter> { abstract public function getFixedNode(Tnode $node): ?EditableNode; + final public function getCodeActionForError( + FixableASTLintError $error, + ): ?LSP\CodeAction { + $node = $error->getBlameNode(); + $fixed = $this->getFixedNode($node); + if ($fixed === null) { + return null; + } + + $offset = HHAST\find_offset_of_leading($this->getAST(), $node); + $start = $this->getAST()->getCode() + |> Str\slice($$, 0, $offset) + |> Str\split($$, "\n") + |> tuple(C\count($$), Str\length(C\lastx($$))); + + $code = $node->getCode(); + $lines = Str\split($code, "\n"); + $count = C\count($lines); + if ($count === 1) { + $end = tuple($start[0], $start[1] + Str\length($code)); + } else { + $end = tuple($start[0] + $count - 1, Str\length(C\lastx($lines))); + } + return shape( + 'title' => \get_class($this) + |> Str\split($$, "\\") + |> C\lastx($$) + |> Str\strip_suffix($$, "Linter") + |> 'Fix '.$$, + 'edit' => shape( + 'changes' => dict[ + 'file://'.\realpath($this->getFile()) => vec[shape( + 'range' => shape( + 'start' => + shape('line' => $start[0] - 1, 'character' => $start[1]), + 'end' => shape('line' => $end[0] - 1, 'character' => $end[1]), + ), + 'newText' => $fixed->getCode(), + )], + ], + ), + ); + } + final public function fixLintErrors( Traversable> $errors, ): void { diff --git a/src/Linters/LSPAutoFixingLinter.php b/src/Linters/LSPAutoFixingLinter.php new file mode 100644 index 00000000..e0d2da1a --- /dev/null +++ b/src/Linters/LSPAutoFixingLinter.php @@ -0,0 +1,20 @@ + extends AutoFixingLinter { + public function getCodeActionForError( + Terror $err, + ): ?\Facebook\HHAST\__Private\LSP\CodeAction; +} diff --git a/src/__Private/LSPImpl/CodeActionCommand.php b/src/__Private/LSPImpl/CodeActionCommand.php new file mode 100644 index 00000000..1f933711 --- /dev/null +++ b/src/__Private/LSPImpl/CodeActionCommand.php @@ -0,0 +1,44 @@ +; + + public function __construct( + private LSPLib\Client $client, + private ?LintRunConfig $config, + ) { + } + + <<__Override>> + public async function executeAsync( + self::TParams $p, + ): Awaitable { + $uri = $p['textDocument']['uri']; + + $handler = new LintRunLSPCodeActionEventHandler( + $this->client, + $p['context']['diagnostics'], + ); + + await relint_uri_async($handler, $this->config, $uri); + + return self::success($handler->getCodeActions()); + } +} diff --git a/src/__Private/LSPImpl/DidChangeWatchedFilesNotification.php b/src/__Private/LSPImpl/DidChangeWatchedFilesNotification.php index 80b42f0e..69b9a646 100644 --- a/src/__Private/LSPImpl/DidChangeWatchedFilesNotification.php +++ b/src/__Private/LSPImpl/DidChangeWatchedFilesNotification.php @@ -13,7 +13,7 @@ use type Facebook\HHAST\__Private\{ LintRun, LintRunConfig, - LintRunLSPErrorHandler, + LintRunLSPPublishDiagnosticsEventHandler, }; use type Facebook\CLILib\ITerminal; use namespace Facebook\HHAST\__Private\{LSP, LSPLib}; @@ -63,6 +63,10 @@ public function __construct( ); } - await relint_uris_async($this->client, $this->config, $to_relint); + await relint_uris_async( + new LintRunLSPPublishDiagnosticsEventHandler($this->client), + $this->config, + $to_relint, + ); } } diff --git a/src/__Private/LSPImpl/DidOpenTextDocumentNotification.php b/src/__Private/LSPImpl/DidOpenTextDocumentNotification.php index 5a5634ff..86c0e9bf 100644 --- a/src/__Private/LSPImpl/DidOpenTextDocumentNotification.php +++ b/src/__Private/LSPImpl/DidOpenTextDocumentNotification.php @@ -13,7 +13,7 @@ use type Facebook\HHAST\__Private\{ LintRun, LintRunConfig, - LintRunLSPErrorHandler, + LintRunLSPPublishDiagnosticsEventHandler, }; use type Facebook\CLILib\ITerminal; use namespace Facebook\HHAST\__Private\{LSP, LSPLib}; @@ -38,6 +38,10 @@ public function __construct( $this->state->openFiles[] = $uri; - await relint_uri_async($this->client, $this->config, $uri); + await relint_uri_async( + new LintRunLSPPublishDiagnosticsEventHandler($this->client), + $this->config, + $uri, + ); } } diff --git a/src/__Private/LSPImpl/DidSaveTextDocumentNotification.php b/src/__Private/LSPImpl/DidSaveTextDocumentNotification.php index 15c8480f..79cc4e89 100644 --- a/src/__Private/LSPImpl/DidSaveTextDocumentNotification.php +++ b/src/__Private/LSPImpl/DidSaveTextDocumentNotification.php @@ -13,7 +13,7 @@ use type Facebook\HHAST\__Private\{ LintRun, LintRunConfig, - LintRunLSPErrorHandler, + LintRunLSPPublishDiagnosticsEventHandler, }; use type Facebook\CLILib\ITerminal; use namespace Facebook\HHAST\__Private\{LSP, LSPLib}; @@ -35,6 +35,10 @@ public function __construct( return; } - await relint_uri_async($this->client, $this->config, $uri); + await relint_uri_async( + new LintRunLSPPublishDiagnosticsEventHandler($this->client), + $this->config, + $uri, + ); } } diff --git a/src/__Private/LSPImpl/InitializeCommand.php b/src/__Private/LSPImpl/InitializeCommand.php index 42288fc9..ba2e6d5a 100644 --- a/src/__Private/LSPImpl/InitializeCommand.php +++ b/src/__Private/LSPImpl/InitializeCommand.php @@ -26,8 +26,9 @@ final class InitializeCommand 'includeText' => false, ), 'openClose' => true, - ) - ); + ), + 'codeActionProvider' => true, + ); <<__Override>> public async function executeAsync( diff --git a/src/__Private/LSPImpl/Server.php b/src/__Private/LSPImpl/Server.php index 22b4ab21..18453f15 100644 --- a/src/__Private/LSPImpl/Server.php +++ b/src/__Private/LSPImpl/Server.php @@ -34,6 +34,7 @@ protected function getSupportedServerCommands(): vec { return vec[ new LSPImpl\InitializeCommand($this->state), new LSPLib\ShutdownCommand($this->state), + new LSPImpl\CodeActionCommand($this->client, $this->config), ]; } diff --git a/src/__Private/LSPImpl/relint_uri_async.php b/src/__Private/LSPImpl/relint_uri_async.php index b817730e..2d52d39c 100644 --- a/src/__Private/LSPImpl/relint_uri_async.php +++ b/src/__Private/LSPImpl/relint_uri_async.php @@ -13,19 +13,18 @@ use type Facebook\HHAST\__Private\{ LintRun, LintRunConfig, + LintRunEventHandler, LintRunLSPEventHandler, }; -use namespace Facebook\HHAST\__Private\{LSP, LSPLib}; use namespace HH\Lib\Str; async function relint_uri_async( - LSPLib\Client $client, + LintRunEventHandler $handler, ?LintRunConfig $config, string $uri, ): Awaitable { $path = Str\strip_prefix($uri, 'file://'); $config = $config ?? LintRunConfig::getForPath($path); - $handler = (new LintRunLSPEventHandler($client)); await (new LintRun($config, $handler, vec[$path]))->runAsync(); } diff --git a/src/__Private/LSPImpl/relint_uris_async.php b/src/__Private/LSPImpl/relint_uris_async.php index 49f3af11..ecec4c51 100644 --- a/src/__Private/LSPImpl/relint_uris_async.php +++ b/src/__Private/LSPImpl/relint_uris_async.php @@ -12,19 +12,16 @@ use type Facebook\HHAST\__Private\{ LintRun, - LintRunConfig, - LintRunLSPErrorHandler, -}; -use namespace Facebook\HHAST\__Private\{LSP, LSPLib}; + LintRunConfig, LintRunEventHandler}; use namespace HH\Lib\{Str, Vec}; async function relint_uris_async( - LSPLib\Client $client, + LintRunEventHandler $handler, ?LintRunConfig $config, vec $uris, ): Awaitable { await Vec\map_async( $uris, - async $uri ==> await relint_uri_async($client, $config, $uri), + async $uri ==> await relint_uri_async($handler, $config, $uri), ); } diff --git a/src/__Private/LSPLib/CodeActionCommand.php b/src/__Private/LSPLib/CodeActionCommand.php new file mode 100644 index 00000000..d5170db8 --- /dev/null +++ b/src/__Private/LSPLib/CodeActionCommand.php @@ -0,0 +1,20 @@ + $codeActions = vec[]; + + public function __construct( + private LSPLib\Client $client, + private vec $diagnostics, + ) { + } + + public function linterRaisedErrors( + Linters\BaseLinter $linter, + LintRunConfig::TFileConfig $_config, + Traversable $errors, + ): LintAutoFixResult { + if (!$linter instanceof Linters\LSPAutoFixingLinter) { + return LintAutoFixResult::SOME_UNFIXED; + } + + $linter_class = \get_class($linter); + foreach ($errors as $error) { + $d = $this->findDiagnostic($linter, $error); + if ($d === null) { + continue; + } + + $action = $linter->getCodeActionForError($error); + if ($action === null) { + continue; + } + $action['diagnostics'] = vec[$d]; + $this->codeActions[] = $action; + } + + return LintAutoFixResult::SOME_UNFIXED; + } + + private function findDiagnostic( + Linters\BaseLinter $linter, + Linters\LintError $error, + ): ?LSP\Diagnostic { + $linter = \get_class($linter) + |> Str\split($$, "\\") + |> C\lastx($$) + |> Str\strip_suffix($$, "Linter"); + $pos = $error->getPosition() ?? tuple(0, 0); + $start = shape( + 'line' => $pos[0] - 1, + 'character' => $pos[1], + ); + foreach ($this->diagnostics as $d) { + if (($d['code'] ?? '') !== $linter) { + continue; + } + if ($d['range']['start'] !== $start) { + continue; + } + return $d; + } + return null; + } + + public function finishedFile(string $_path, LintRunResult $_result): void { + } + + public function finishedRun(LintRunResult $_): void { + } + + public function getCodeActions(): vec { + return $this->codeActions; + } +} diff --git a/src/__Private/LintRunLSPEventHandler.php b/src/__Private/LintRunLSPPublishDiagnosticsEventHandler.php similarity index 80% rename from src/__Private/LintRunLSPEventHandler.php rename to src/__Private/LintRunLSPPublishDiagnosticsEventHandler.php index 64c7fcb1..9ff24b30 100644 --- a/src/__Private/LintRunLSPEventHandler.php +++ b/src/__Private/LintRunLSPPublishDiagnosticsEventHandler.php @@ -14,7 +14,8 @@ use namespace Facebook\HHAST\__Private\LSPLib; use namespace HH\Lib\{C, Dict, Str, Vec}; -final class LintRunLSPEventHandler implements LintRunEventHandler { +final class LintRunLSPPublishDiagnosticsEventHandler + implements LintRunEventHandler { public function __construct(private LSPLib\Client $client) { } @@ -67,23 +68,34 @@ private function publishDiagnostics( string $file, vec $diagnostics, ): void { - $message = (new LSPLib\PublishDiagnosticsNotification(shape( - 'uri' => 'file://'.$file, - 'diagnostics' => $diagnostics, - )))->asMessage(); + $message = ( + new LSPLib\PublishDiagnosticsNotification(shape( + 'uri' => 'file://'.$file, + 'diagnostics' => $diagnostics, + )) + )->asMessage(); $this->client->sendNotificationMessage($message); } public function finishedFile(string $path, LintRunResult $result): void { $path = \realpath($path); - invariant($this->file === null || $this->file === $path, "Unexpected file change"); + invariant( + $this->file === null || $this->file === $path, + "Unexpected file change", + ); $errors = $this->errors; if ($result === LintRunResult::NO_ERRORS) { - invariant(C\is_empty($errors), "Linter reports no errors, but we have errors"); + invariant( + C\is_empty($errors), + "Linter reports no errors, but we have errors", + ); } else { - invariant(!C\is_empty($errors), "Linter reports errors, but we have none"); + invariant( + !C\is_empty($errors), + "Linter reports errors, but we have none", + ); } $this->publishDiagnostics($path, $errors);