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 10, 2021
1 parent 2dc62d1 commit af9e9b7
Show file tree
Hide file tree
Showing 9 changed files with 235 additions and 11 deletions.
73 changes: 73 additions & 0 deletions src/Linters/HHClientLintError.hack
Original file line number Diff line number Diff line change
@@ -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']);
}

}
26 changes: 26 additions & 0 deletions src/Linters/HHClientLintRule.hack
Original file line number Diff line number Diff line change
@@ -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 (string)$this->code;
}

public function getErrorCode(): string {
return (string)$this->code;
}

public function __construct(private int $code) {}

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

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->file, $error),
);
}
}
2 changes: 1 addition & 1 deletion src/Linters/LintError.hack
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ namespace Facebook\HHAST;
*/
<<__Sealed(
SingleRuleLintError::class,
// HHClientProblem::class
HHClientLintError::class
)>>
interface LintError {
public function getFile(): File;
Expand Down
5 changes: 1 addition & 4 deletions src/Linters/LintRule.hack
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 1 addition & 6 deletions src/Linters/Linter.hack
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
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): HHClientLinter {
$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 af9e9b7

Please sign in to comment.