Index: src/__phutil_library_map__.php =================================================================== --- src/__phutil_library_map__.php +++ src/__phutil_library_map__.php @@ -126,6 +126,8 @@ 'ArcanistDuplicateSwitchCaseXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistDuplicateSwitchCaseXHPASTLinterRuleTestCase.php', 'ArcanistDynamicDefineXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistDynamicDefineXHPASTLinterRule.php', 'ArcanistDynamicDefineXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistDynamicDefineXHPASTLinterRuleTestCase.php', + 'ArcanistESLintLinter' => 'lint/linter/ArcanistESLintLinter.php', + 'ArcanistESLintLinterTestCase' => 'lint/linter/__tests__/ArcanistESLintLinterTestCase.php', 'ArcanistElseIfUsageXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistElseIfUsageXHPASTLinterRule.php', 'ArcanistElseIfUsageXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistElseIfUsageXHPASTLinterRuleTestCase.php', 'ArcanistEmptyFileXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistEmptyFileXHPASTLinterRule.php', @@ -540,6 +542,8 @@ 'ArcanistDuplicateSwitchCaseXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistDynamicDefineXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistDynamicDefineXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', + 'ArcanistESLintLinter' => 'ArcanistExternalLinter', + 'ArcanistESLintLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistElseIfUsageXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistElseIfUsageXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistEmptyFileXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', Index: src/lint/linter/ArcanistESLintLinter.php =================================================================== --- /dev/null +++ src/lint/linter/ArcanistESLintLinter.php @@ -0,0 +1,149 @@ +getExecutableCommand()); + + $matches = array(); + $regex = '/^v(?P\d+\.\d+\.\d+)$/'; + if (preg_match($regex, $stdout, $matches)) { + return $matches['version']; + } else { + return false; + } + } + + public function getInstallInstructions() { + return pht('Install ESLint using `%s`.', 'npm install -g eslint'); + } + + protected function getMandatoryFlags() { + $options = array(); + + $options[] = '--format=json'; + + if ($this->eslintrc) { + $options[] = '--config='.$this->eslintrc; + } + + if ($this->eslintignore) { + $options[] = '--ignore-path='.$this->eslintignore; + } + + return $options; + } + + public function getLinterConfigurationOptions() { + $options = array( + 'eslint.eslintignore' => array( + 'type' => 'optional string', + 'help' => pht('Pass in a custom eslintignore file path.'), + ), + 'eslint.eslintrc' => array( + 'type' => 'optional string', + 'help' => pht('Custom configuration file.'), + ), + ); + + return $options + parent::getLinterConfigurationOptions(); + } + + public function setLinterConfigurationValue($key, $value) { + switch ($key) { + case 'eslint.eslintignore': + $this->eslintignore = $value; + return; + + case 'eslint.eslintrc': + $this->eslintrc = $value; + return; + } + + return parent::setLinterConfigurationValue($key, $value); + } + + protected function parseLinterOutput($path, $err, $stdout, $stderr) { + $errors = null; + try { + $errors = phutil_json_decode($stdout); + } catch (PhutilJSONParserException $ex) { + // Something went wrong and we can't decode the output. Exit abnormally. + throw new PhutilProxyException( + pht('ESLint returned unparseable output.'), + $ex); + } + + $messages = array(); + foreach (idx($errors[0], 'messages') as $err) { + $message = new ArcanistLintMessage(); + $message->setPath($path); + $message->setLine(idx($err, 'line')); + $message->setChar(idx($err, 'column')); + $message->setDescription(idx($err, 'message')); + $message->setSeverity( + $this->getLintMessageSeverity(idx($err, 'severity'))); + + // In case of a parsing error, eslint does not specify what rule failed + // Instead it sets fatal to true + if (idx($err, 'fatal')) { + $message->setCode('fatal'); + $message->setName('ParsingError'); + } else { + $message->setCode(idx($err, 'ruleId')); + $message->setName(idx($err, 'nodeType')); + } + + $messages[] = $message; + } + + return $messages; + } + + protected function getLintCodeFromLinterConfigurationKey($code) { + return $code; + } + +} Index: src/lint/linter/__tests__/.eslintrc =================================================================== --- /dev/null +++ src/lint/linter/__tests__/.eslintrc @@ -0,0 +1,3 @@ +{ + "extends": "eslint:recommended" +} Index: src/lint/linter/__tests__/ArcanistESLintLinterTestCase.php =================================================================== --- /dev/null +++ src/lint/linter/__tests__/ArcanistESLintLinterTestCase.php @@ -0,0 +1,18 @@ +setLinterConfigurationValue( + 'eslint.eslintrc', + dirname(realpath(__FILE__)).'/.eslintrc'); + return $linter; + } + + public function testLinter() { + $this->executeTestsInDirectory(dirname(__FILE__).'/eslint/'); + } + +} Index: src/lint/linter/__tests__/eslint/eslint.lint-test =================================================================== --- /dev/null +++ src/lint/linter/__tests__/eslint/eslint.lint-test @@ -0,0 +1,11 @@ +function f() { + for (ii = 0; ii < 3; ii++) { + g() + } +} +~~~~~~~~~~ +error:1:10 +error:2:8 +error:2:16 +error:2:24 +error:3:5 Index: src/lint/linter/__tests__/eslint/expected-conditional.lint-test =================================================================== --- /dev/null +++ src/lint/linter/__tests__/eslint/expected-conditional.lint-test @@ -0,0 +1,7 @@ +var foo; +if (foo = 'bar') { + foo += 'baz'; +} +~~~~~~~~~~ +error:2:1 +error:2:5 Index: src/lint/linter/__tests__/eslint/missing-semicolon.lint-test =================================================================== --- /dev/null +++ src/lint/linter/__tests__/eslint/missing-semicolon.lint-test @@ -0,0 +1,4 @@ +console.log('foobar') +~~~~~~~~~~ +error:1:1 +error:1:1 Index: src/lint/linter/__tests__/eslint/parse-failure.lint-test =================================================================== --- /dev/null +++ src/lint/linter/__tests__/eslint/parse-failure.lint-test @@ -0,0 +1,4 @@ +function main() { + +~~~~~~~~~~ +error:3:2 Index: src/lint/linter/__tests__/eslint/unnecessary-semicolon.lint-test =================================================================== --- /dev/null +++ src/lint/linter/__tests__/eslint/unnecessary-semicolon.lint-test @@ -0,0 +1,6 @@ +function main() { + return 'Hello, World!'; +}; +~~~~~~~~~~ +error:1:10 +error:3:2