From 1267b3f09727668e7c4ee055c16966118551430f Mon Sep 17 00:00:00 2001 From: "Yang, Bo" Date: Mon, 8 Nov 2021 23:07:08 +0000 Subject: [PATCH] Add HHClientLinter --- src/Linters/HHClientLintError.hack | 73 +++++++++++++++++++ src/Linters/HHClientLintRule.hack | 26 +++++++ src/Linters/HHClientLinter.hack | 51 +++++++++++++ src/Linters/LintError.hack | 2 +- src/Linters/LintRule.hack | 5 +- src/Linters/Linter.hack | 7 +- tests/HHClientLinterTest.hack | 58 +++++++++++++++ .../invalid_null_check.hack.expect | 7 ++ .../HHClientLinter/invalid_null_check.hack.in | 17 +++++ 9 files changed, 235 insertions(+), 11 deletions(-) create mode 100644 src/Linters/HHClientLintError.hack create mode 100644 src/Linters/HHClientLintRule.hack create mode 100644 src/Linters/HHClientLinter.hack create mode 100644 tests/HHClientLinterTest.hack create mode 100644 tests/examples/HHClientLinter/invalid_null_check.hack.expect create mode 100644 tests/examples/HHClientLinter/invalid_null_check.hack.in diff --git a/src/Linters/HHClientLintError.hack b/src/Linters/HHClientLintError.hack new file mode 100644 index 000000000..d5f60bc18 --- /dev/null +++ b/src/Linters/HHClientLintError.hack @@ -0,0 +1,73 @@ +/* + * 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; + +enum ArcanistLintSeverityEnum: string as string { + ADVICE = 'advice'; + AUTOFIX = 'autofix'; + WARNING = 'warning'; + ERROR = 'error'; + DISABLED = 'disabled'; +} + +final class HHClientLintError implements LintError { + + const type TJSONError = shape( + 'descr' => string, + 'severity' => ArcanistLintSeverityEnum, + 'path' => string, + 'line' => int, + 'start' => int, + 'end' => int, + 'code' => int, + 'bypass_changed_lines' => bool, + 'original' => string, + 'replacement' => string, + ); + + public function __construct( + private File $file, + private this::TJSONError $error, + private ?string $blame_code = null + ) { + } + + public function getFile(): File { + return $this->file; + } + + public function getDescription(): string { + return $this->error['descr']; + } + + public function getPosition(): (int, int) { + return tuple($this->error['line'], $this->error['end']); + } + + public function getRange(): ((int, int), (int, int)) { + return tuple( + tuple($this->error['line'], $this->error['start']), + tuple($this->error['line'], $this->error['end']), + ); + } + + public function getBlameCode(): ?string { + return $this->blame_code; + } + + public function getPrettyBlame(): ?string { + return $this->getBlameCode(); + } + + public function getLintRule(): LintRule { + return new HHClientLintRule($this->error['code']); + } + +} diff --git a/src/Linters/HHClientLintRule.hack b/src/Linters/HHClientLintRule.hack new file mode 100644 index 000000000..64e9e985b --- /dev/null +++ b/src/Linters/HHClientLintRule.hack @@ -0,0 +1,26 @@ +/* + * 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; + +/** + * The lint rule of an error code reported by the hh_client + */ +final class HHClientLintRule implements LintRule { + public function getName(): string { + return 'Linter: '.$this->code; + } + + public function getErrorCode(): string { + return (string)$this->code; + } + + public function __construct(private int $code) {} + +} diff --git a/src/Linters/HHClientLinter.hack b/src/Linters/HHClientLinter.hack new file mode 100644 index 000000000..215d02ab7 --- /dev/null +++ b/src/Linters/HHClientLinter.hack @@ -0,0 +1,51 @@ +/* + * 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; + +use namespace Facebook\TypeAssert; +use namespace HH\Lib\{C, Vec}; + +/** + * A linter as a proxy invoking `hh_client --lint`. + */ +final class HHClientLinter implements Linter { + use LinterTrait; + + const type TConfig = shape(); + + const type TJSONResult = shape( + 'errors' => vec, + 'version' => string, + ); + + public async function getLintErrorsAsync( + ): Awaitable> { + $lines = await __Private\execute_async( + 'hh_client', + '--lint', + $this->getFile()->getPath(), + '--json', + '--from', + 'hhast', + ); + $hh_client_lint_result = TypeAssert\matches( + \json_decode( + C\firstx($lines), + /* assoc = */ true, + /* depth = */ 512, + \JSON_FB_HACK_ARRAYS, + ), + ); + return Vec\map( + $hh_client_lint_result['errors'], + $error ==> new HHClientLintError($this->file, $error), + ); + } +} diff --git a/src/Linters/LintError.hack b/src/Linters/LintError.hack index 361a77de8..af34ae0ca 100644 --- a/src/Linters/LintError.hack +++ b/src/Linters/LintError.hack @@ -17,7 +17,7 @@ namespace Facebook\HHAST; */ <<__Sealed( SingleRuleLintError::class, - // HHClientProblem::class + HHClientLintError::class )>> interface LintError { public function getFile(): File; diff --git a/src/Linters/LintRule.hack b/src/Linters/LintRule.hack index 09160c094..0101497f5 100644 --- a/src/Linters/LintRule.hack +++ b/src/Linters/LintRule.hack @@ -13,10 +13,7 @@ namespace Facebook\HHAST; * An abstract lint rule that provides a single error reason, which could be * either a HHAST linter or a lint rule written in OCaml. */ -<<__Sealed( - SingleRuleLinter::class, - // HHClientLintRule::class, -)>> +<<__Sealed(SingleRuleLinter::class, HHClientLintRule::class)>> interface LintRule { /** * The human readable name of the lint rule, which can be used to report diff --git a/src/Linters/Linter.hack b/src/Linters/Linter.hack index 2cfb81a6e..3741210c3 100644 --- a/src/Linters/Linter.hack +++ b/src/Linters/Linter.hack @@ -15,12 +15,7 @@ namespace Facebook\HHAST; * Problems found by a Linter could associated with different LintRules, * especially when the Linter is a proxy calling other linting tools. */ -<< - __Sealed( - SingleRuleLinter::class, - // HHClientLinter::class - ) ->> +<<__Sealed(SingleRuleLinter::class, HHClientLinter::class)>> interface Linter { <<__Reifiable>> abstract const type TConfig; diff --git a/tests/HHClientLinterTest.hack b/tests/HHClientLinterTest.hack new file mode 100644 index 000000000..14e683299 --- /dev/null +++ b/tests/HHClientLinterTest.hack @@ -0,0 +1,58 @@ +/* + * 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; +use namespace HH\Lib\Str; + +final class HHClientLinterTest extends TestCase { + use LinterTestTrait; + + /** + * The temporary directory to include testing source files to lint. + * + * Note that the temporary directory must be under the current directory, + * otherwise hh_client will not work. + */ + const string TMP_DIR = __DIR__.'/../.var/tmp/hhast/HHClientLinterTest'; + + public function getCleanExamples(): vec<(string)> { + return vec[ + tuple(" Str\ends_with($$, '.php') + |> $$ ? 'php' : 'hack'; + $hh_client_tmp_file = + self::TMP_DIR.'/'.\bin2hex(\random_bytes(16)).'.'.$ext; + \copy($file, $hh_client_tmp_file); + return HHClientLinter::fromPath($hh_client_tmp_file); + } + + <<__Override>> + public static async function beforeFirstTestAsync(): Awaitable { + $mode = 0777; + $recursive = true; + \mkdir(self::TMP_DIR, $mode, $recursive); + } + + <<__Override>> + public static async function afterLastTestAsync(): Awaitable { + foreach (\scandir(self::TMP_DIR) as $file) { + $path_file = self::TMP_DIR.'/'.$file; + if (\is_file($path_file)) { + \unlink($path_file); + } + } + \rmdir(self::TMP_DIR); + } + +} diff --git a/tests/examples/HHClientLinter/invalid_null_check.hack.expect b/tests/examples/HHClientLinter/invalid_null_check.hack.expect new file mode 100644 index 000000000..168fabac4 --- /dev/null +++ b/tests/examples/HHClientLinter/invalid_null_check.hack.expect @@ -0,0 +1,7 @@ +[ + { + "blame": null, + "blame_pretty": null, + "description": "Invalid null check: This expression will always return `false`.\nA value of type `int` can never be null." + } +] diff --git a/tests/examples/HHClientLinter/invalid_null_check.hack.in b/tests/examples/HHClientLinter/invalid_null_check.hack.in new file mode 100644 index 000000000..9e87766a3 --- /dev/null +++ b/tests/examples/HHClientLinter/invalid_null_check.hack.in @@ -0,0 +1,17 @@ +