diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -83,6 +83,8 @@ 'ArcanistGitHookPreReceiveWorkflow' => 'workflow/ArcanistGitHookPreReceiveWorkflow.php', 'ArcanistGoLintLinter' => 'lint/linter/ArcanistGoLintLinter.php', 'ArcanistGoLintLinterTestCase' => 'lint/linter/__tests__/ArcanistGoLintLinterTestCase.php', + 'ArcanistHackLinter' => 'lint/linter/ArcanistHackLinter.php', + 'ArcanistHackLinterTestCase' => 'lint/linter/__tests__/ArcanistHackLinterTestCase.php', 'ArcanistHelpWorkflow' => 'workflow/ArcanistHelpWorkflow.php', 'ArcanistHgClientChannel' => 'hgdaemon/ArcanistHgClientChannel.php', 'ArcanistHgProxyClient' => 'hgdaemon/ArcanistHgProxyClient.php', @@ -273,6 +275,8 @@ 'ArcanistGitHookPreReceiveWorkflow' => 'ArcanistWorkflow', 'ArcanistGoLintLinter' => 'ArcanistExternalLinter', 'ArcanistGoLintLinterTestCase' => 'ArcanistArcanistLinterTestCase', + 'ArcanistHackLinter' => 'ArcanistLinter', + 'ArcanistHackLinterTestCase' => 'ArcanistArcanistLinterTestCase', 'ArcanistHelpWorkflow' => 'ArcanistWorkflow', 'ArcanistHgClientChannel' => 'PhutilProtocolChannel', 'ArcanistHgServerChannel' => 'PhutilProtocolChannel', diff --git a/src/lint/linter/ArcanistHackLinter.php b/src/lint/linter/ArcanistHackLinter.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/ArcanistHackLinter.php @@ -0,0 +1,145 @@ +getEngine()->getWorkingCopy()->getProjectRoot()); + + if ($err !== 0 && $err !== 2) { + return false; + } + + $output = last(explode("\n", trim($stderr))); + return idx(phutil_json_decode($output), 'version', false); + } + + public function willLintPaths(array $paths) { + list($err, $stdout, $stderr) = + id(new ExecFuture('hh_client --json check')) + ->setCWD($this->getEngine()->getWorkingCopy()->getProjectRoot()) + ->resolve(); + + $result = $this->parseLinterOutput($err, $stdout, $stderr); + + if ($result === false) { + $this->stopAllLinters(); + return; + } + + } + + public function lintPath($path) { + // already did all the work in `willLintPaths` + return; + } + + private function parseLinterOutput($err, $stdout, $stderr) { + // `hh_client` uses 0 for success, 1 for failure, 2 for type errors + if ($err !== 0 && $err !== 2) { + throw new Exception(pht( + 'You have enabled the Hack type checker, but it failed to '. + 'return normally.')); + + return false; + } + + $json = last(explode("\n", trim($stderr))); + + $results = phutil_json_decode($json); + $passed = idx($results, 'passed', false); + $errors = idx($results, 'errors', array()); + + if ($passed) { + // everything typechecks! + return array(); + } + + $paths = array(); + foreach ($this->paths as $current_path) { + $paths[$this->getEngine()->getFilePathOnDisk($current_path)] = true; + } + + $lint_messages = array(); + foreach ($errors as $current_error) { + // Each Hack error consists of a list of messages; each message + // has its own position info. We report the error at the first + // location that matches a path affect by the current diff. + // + // Location keys are: + // - descr : string + // - path : string + // - line : int + // - start : int + // - end : int + + $message = $current_error['message']; + foreach ($message as $location) { + $location_path = $location['path']; + if (array_key_exists($location_path, $paths)) { + $warning = array( + 'path' => $location_path, + 'line' => $location['line'], + 'char' => $location['start'], + 'name' => pht('Hack type error'), + 'code' => $this->getLinterName(), + 'description' => $this->formatDescription($message), + 'severity' => ArcanistLintSeverity::SEVERITY_ERROR + ); + + $this->addLintMessage( + ArcanistLintMessage::newFromDictionary($warning)); + break; + } + } + } + + return true; + } + + private function formatDescription(array $locations) { + $result = array(); + $result[] = pht('Type error:'); + foreach ($locations as $location) { + $description = pht( + "%s line %d:%d-%d\n %s", + $location['path'], + $location['line'], + $location['start'], + $location['end'], + $location['descr']); + + $result[] = $description; + } + + return implode("\n", $result); + } +} diff --git a/src/lint/linter/__tests__/ArcanistHackLinterTestCase.php b/src/lint/linter/__tests__/ArcanistHackLinterTestCase.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/__tests__/ArcanistHackLinterTestCase.php @@ -0,0 +1,20 @@ +executeTestsInDirectory( + $testdir, + new ArcanistHackLinter()); + } + + protected function didCreateTemporaryDirectory($path) { + $hhconfig = Filesystem::resolvePath('.hhconfig', $path); + Filesystem::writeFile($hhconfig, "\n"); + } + + protected function getFilenameSuffix() { + return '.php'; + } +} diff --git a/src/lint/linter/__tests__/ArcanistLinterTestCase.php b/src/lint/linter/__tests__/ArcanistLinterTestCase.php --- a/src/lint/linter/__tests__/ArcanistLinterTestCase.php +++ b/src/lint/linter/__tests__/ArcanistLinterTestCase.php @@ -22,6 +22,14 @@ pht('Expected to find some .lint-test tests in directory %s!', $root)); } + protected function didCreateTemporaryDirectory($path) { + return; + } + + protected function getFilenameSuffix() { + return ''; + } + private function lintFile($file, ArcanistLinter $linter) { $linter = clone $linter; @@ -59,11 +67,12 @@ $caught_exception = false; try { - $tmp = new TempFile($basename); + $tmp = new TempFile($basename.$this->getFilenameSuffix()); Filesystem::writeFile($tmp, $data); $full_path = (string)$tmp; $dir = dirname($full_path); + $this->didCreateTemporaryDirectory($dir); $path = basename($full_path); $config_file = null; $arcconfig = idx($config, 'arcconfig'); diff --git a/src/lint/linter/__tests__/hack/01_basic_error.lint-test b/src/lint/linter/__tests__/hack/01_basic_error.lint-test new file mode 100644 --- /dev/null +++ b/src/lint/linter/__tests__/hack/01_basic_error.lint-test @@ -0,0 +1,11 @@ +