Skip to content
This repository has been archived by the owner on Dec 1, 2024. It is now read-only.

Commit

Permalink
Add HHClientLinter
Browse files Browse the repository at this point in the history
  • Loading branch information
Atry authored Nov 4, 2021
1 parent 6f5dc3b commit ba3fc24
Show file tree
Hide file tree
Showing 5 changed files with 189 additions and 0 deletions.
55 changes: 55 additions & 0 deletions src/Linters/HHClientLintError.hack
Original file line number Diff line number Diff line change
@@ -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']),
);
}

}
52 changes: 52 additions & 0 deletions src/Linters/HHClientLinter.hack
Original file line number Diff line number Diff line change
@@ -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<HHClientLintError::TJSONError>,
'version' => string,
);

<<__Override>>
public async function getLintErrorsAsync(
): Awaitable<vec<HHClientLintError>> {
$lines = await __Private\execute_async(
'hh_client',
'--lint',
$this->getFile()->getPath(),
'--json',
'--from',
'hhast',
);
$hh_client_lint_result = TypeAssert\matches<this::TJSONResult>(
\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),
);
}
}
58 changes: 58 additions & 0 deletions tests/HHClientLinterTest.hack
Original file line number Diff line number Diff line change
@@ -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("<?hh\nclass Foo {}"),
];
}

protected function getLinter(string $file): BaseLinter {
$ext = Str\strip_suffix($file, '.in')
|> 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<void> {
$mode = 0777;
$recursive = true;
\mkdir(self::TMP_DIR, $mode, $recursive);
}

<<__Override>>
public static async function afterLastTestAsync(): Awaitable<void> {
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);
}

}
7 changes: 7 additions & 0 deletions tests/examples/HHClientLinter/invalid_null_check.hack.expect
Original file line number Diff line number Diff line change
@@ -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."
}
]
17 changes: 17 additions & 0 deletions tests/examples/HHClientLinter/invalid_null_check.hack.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?hh // strict

/**
* Copyright (c) 2016, 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.
*
*/

function invalid_null_check(): void {
$cannot_be_null = 42;
if ($cannot_be_null is null) {
throw new Exception();
}
}

0 comments on commit ba3fc24

Please sign in to comment.