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' => 'ArcanistExternalLinter', + '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,144 @@ +getExecutableCommand(), + $this->getEngine()->getWorkingCopy()->getProjectRoot()); + + if ($err === 1) { + return false; + } + + $output = last(explode("\n", trim($stderr))); + return idx(phutil_json_decode($output), 'version', false); + } + + public function getInstallInstructions() { + return pht('Follow the instructions at http://hacklang.org/install/'); + } + + public function shouldExpectCommandErrors() { + return true; + } + + public function supportsReadDataFromStdin() { + return false; + } + + protected function getMandatoryFlags() { + $arguments = array('--json', 'check'); + $arguments[] = $this->getEngine()->getWorkingCopy()->getProjectRoot(); + return $arguments; + } + + protected function getPathArgumentForLinterFuture($path) { + return ''; + } + + protected function parseLinterOutput($path, $err, $stdout, $stderr) { + // `hh_client` uses 0 for success, 1 for failure, 2 for type errors + if ($err === 1) { + 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($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' => $current_path, + 'line' => $location['line'], + 'char' => $location['start'], + 'name' => 'Hack type error', + 'code' => $this->getLinterName(), + 'description' => $this->formatDescription($message), + 'severity' => ArcanistLintSeverity::SEVERITY_ERROR + ); + + $lint_messages[] = ArcanistLintMessage::newFromDictionary($warning); + break; + } + } + } + + return $lint_messages; + } + + private function formatDescription($error) { + $result = array(); + $result[] = 'Type error(s):'; + foreach ($error as $location) { + $description = sprintf( + "File %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 fixupTempDir($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 fixupTempDir($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->fixupTempDir($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 @@ +