From ba3fc24dd1160e738251f5a6be430954ae71b179 Mon Sep 17 00:00:00 2001 From: "Yang, Bo" Date: Thu, 4 Nov 2021 10:04:45 -0700 Subject: [PATCH] Add HHClientLinter --- src/Linters/HHClientLintError.hack | 55 ++++++++++++++++++ src/Linters/HHClientLinter.hack | 52 +++++++++++++++++ tests/HHClientLinterTest.hack | 58 +++++++++++++++++++ .../invalid_null_check.hack.expect | 7 +++ .../HHClientLinter/invalid_null_check.hack.in | 17 ++++++ 5 files changed, 189 insertions(+) create mode 100644 src/Linters/HHClientLintError.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..0bb75d57d --- /dev/null +++ b/src/Linters/HHClientLintError.hack @@ -0,0 +1,55 @@ +/* + * 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 extends 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( + HHClientLinter $linter, + private this::TJSONError $error, + ) { + parent::__construct($linter, $error['descr']); + } + + <<__Override>> + public function getPosition(): (int, int) { + return tuple($this->error['line'], $this->error['end']); + } + + <<__Override>> + public function getRange(): ((int, int), (int, int)) { + return tuple( + tuple($this->error['line'], $this->error['start']), + tuple($this->error['line'], $this->error['end']), + ); + } + +} diff --git a/src/Linters/HHClientLinter.hack b/src/Linters/HHClientLinter.hack new file mode 100644 index 000000000..12d3ce44d --- /dev/null +++ b/src/Linters/HHClientLinter.hack @@ -0,0 +1,52 @@ +/* + * 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 type Facebook\HHAST\{BaseLinter}; +use namespace Facebook\TypeAssert; +use namespace HH\Lib\{C, Vec}; + +/** + * A linter as a proxy invoking `hh_client --lint`. + */ +final class HHClientLinter extends BaseLinter { + + const type TConfig = shape(); + + const type TJSONResult = shape( + 'errors' => vec, + 'version' => string, + ); + + <<__Override>> + 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, $error), + ); + } +} diff --git a/tests/HHClientLinterTest.hack b/tests/HHClientLinterTest.hack new file mode 100644 index 000000000..75f596b0e --- /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 @@ +