Skip to content

Commit

Permalink
Support auto-fixing AST-based linters
Browse files Browse the repository at this point in the history
fixes hhvm#79
  • Loading branch information
fredemmott committed Jul 9, 2018
1 parent dd72a26 commit fb12b4b
Show file tree
Hide file tree
Showing 14 changed files with 265 additions and 27 deletions.
1 change: 0 additions & 1 deletion src/Linters/ASTLintError.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ final public function getRange(): ((int, int), (int, int)) {
);
}

<<__Override>>
final public function getBlameCode(): string {
return $this->node->getCode();
}
Expand Down
48 changes: 47 additions & 1 deletion src/Linters/AutoFixingASTLinter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<Tnode as EditableNode>
extends BaseASTLinter<Tnode, FixableASTLintError<Tnode>>
implements AutoFixingLinter<FixableASTLintError<Tnode>> {
implements LSPAutoFixingLinter<FixableASTLintError<Tnode>> {

abstract public function getFixedNode(Tnode $node): ?EditableNode;

final public function getCodeActionForError(
FixableASTLintError<Tnode> $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<FixableASTLintError<Tnode>> $errors,
): void {
Expand Down
20 changes: 20 additions & 0 deletions src/Linters/LSPAutoFixingLinter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?hh // strict
/*
* Copyright (c) 2017-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

namespace Facebook\HHAST\Linters;

use type Facebook\HHAST\EditableNode;
use namespace Facebook\HHAST;

interface LSPAutoFixingLinter<Terror as FixableLintError> extends AutoFixingLinter<Terror> {
public function getCodeActionForError(
Terror $err,
): ?\Facebook\HHAST\__Private\LSP\CodeAction;
}
44 changes: 44 additions & 0 deletions src/__Private/LSPImpl/CodeActionCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?hh // strict
/*
* Copyright (c) 2017-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

namespace Facebook\HHAST\__Private\LSPImpl;

use type Facebook\HHAST\__Private\{
LintRunConfig,
LintRunLSPCodeActionEventHandler,
};
use namespace Facebook\HHAST\__Private\{LSP, LSPLib};
use namespace HH\Lib\Str;

final class CodeActionCommand extends LSPLib\CodeActionCommand {
const type TResponse = vec<LSP\CodeAction>;

public function __construct(
private LSPLib\Client $client,
private ?LintRunConfig $config,
) {
}

<<__Override>>
public async function executeAsync(
self::TParams $p,
): Awaitable<this::TExecuteResult> {
$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());
}
}
8 changes: 6 additions & 2 deletions src/__Private/LSPImpl/DidChangeWatchedFilesNotification.php
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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,
);
}
}
8 changes: 6 additions & 2 deletions src/__Private/LSPImpl/DidOpenTextDocumentNotification.php
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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,
);
}
}
8 changes: 6 additions & 2 deletions src/__Private/LSPImpl/DidSaveTextDocumentNotification.php
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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,
);
}
}
5 changes: 3 additions & 2 deletions src/__Private/LSPImpl/InitializeCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ final class InitializeCommand
'includeText' => false,
),
'openClose' => true,
)
);
),
'codeActionProvider' => true,
);

<<__Override>>
public async function executeAsync(
Expand Down
1 change: 1 addition & 0 deletions src/__Private/LSPImpl/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ protected function getSupportedServerCommands(): vec<LSPLib\ServerCommand> {
return vec[
new LSPImpl\InitializeCommand($this->state),
new LSPLib\ShutdownCommand($this->state),
new LSPImpl\CodeActionCommand($this->client, $this->config),
];
}

Expand Down
5 changes: 2 additions & 3 deletions src/__Private/LSPImpl/relint_uri_async.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
$path = Str\strip_prefix($uri, 'file://');
$config = $config ?? LintRunConfig::getForPath($path);

$handler = (new LintRunLSPEventHandler($client));
await (new LintRun($config, $handler, vec[$path]))->runAsync();
}
9 changes: 3 additions & 6 deletions src/__Private/LSPImpl/relint_uris_async.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> $uris,
): Awaitable<void> {
await Vec\map_async(
$uris,
async $uri ==> await relint_uri_async($client, $config, $uri),
async $uri ==> await relint_uri_async($handler, $config, $uri),
);
}
20 changes: 20 additions & 0 deletions src/__Private/LSPLib/CodeActionCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?hh // strict
/*
* Copyright (c) 2017-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

namespace Facebook\HHAST\__Private\LSPLib;

use namespace Facebook\HHAST\__Private\LSP;

abstract class CodeActionCommand extends ServerCommand {
const string METHOD = 'textDocument/codeAction';
const type TParams = LSP\CodeActionParams;
const type TErrorCode = int;
const type TErrorData = mixed;
}
87 changes: 87 additions & 0 deletions src/__Private/LintRunLSPCodeActionEventHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?hh // strict
/*
* Copyright (c) 2017-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

namespace Facebook\HHAST\__Private;

use namespace Facebook\HHAST\Linters;
use namespace Facebook\HHAST\__Private\{LSP, LSPLib};
use namespace HH\Lib\{C, Dict, Str, Vec};

final class LintRunLSPCodeActionEventHandler implements LintRunEventHandler {
private vec<LSP\CodeAction> $codeActions = vec[];

public function __construct(
private LSPLib\Client $client,
private vec<LSP\Diagnostic> $diagnostics,
) {
}

public function linterRaisedErrors(
Linters\BaseLinter $linter,
LintRunConfig::TFileConfig $_config,
Traversable<Linters\LintError> $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<LSP\CodeAction> {
return $this->codeActions;
}
}
Loading

0 comments on commit fb12b4b

Please sign in to comment.