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 @@ -89,6 +89,8 @@ 'ArcanistDuplicateKeysInArrayXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistDuplicateKeysInArrayXHPASTLinterRule.php', 'ArcanistDuplicateSwitchCaseXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistDuplicateSwitchCaseXHPASTLinterRule.php', 'ArcanistDynamicDefineXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistDynamicDefineXHPASTLinterRule.php', + 'ArcanistESLintLinter' => 'lint/linter/ArcanistESLintLinter.php', + 'ArcanistESLintLinterTestCase' => 'lint/linter/__tests__/ArcanistESLintLinterTestCase.php', 'ArcanistElseIfUsageXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistElseIfUsageXHPASTLinterRule.php', 'ArcanistEmptyFileXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistEmptyFileXHPASTLinterRule.php', 'ArcanistEmptyStatementXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistEmptyStatementXHPASTLinterRule.php', @@ -378,6 +380,8 @@ 'ArcanistDuplicateKeysInArrayXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistDuplicateSwitchCaseXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistDynamicDefineXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', + 'ArcanistESLintLinter' => 'ArcanistExternalLinter', + 'ArcanistESLintLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistElseIfUsageXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistEmptyFileXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistEmptyStatementXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', diff --git a/src/lint/linter/ArcanistESLintLinter.php b/src/lint/linter/ArcanistESLintLinter.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/ArcanistESLintLinter.php @@ -0,0 +1,195 @@ +setVersionRequirement('>=v1.0.0'); + } + + public function getInfoName() { + return 'ESLint'; + } + + public function getInfoURI() { + return 'https://www.eslint.org'; + } + + public function getRuleDocumentationURI($rule_id) { + return $this->getInfoURI().'/docs/rules/'.$rule_id; + } + + public function getInfoDescription() { + return pht('ESLint is a linter for JavaScript source files.'); + } + + public function getVersion() { + list($stdout, $stderr) = execx( + '%C --version', $this->getExecutableCommand()); + + $matches = array(); + $regex = '/^v(?P\d+\.\d+\.\d+)$/'; + if (preg_match($regex, $stdout, $matches)) { + return $matches['version']; + } else { + return false; + } + } + + public function getLinterName() { + return 'ESLINT'; + } + + public function getLinterConfigurationName() { + return 'eslint'; + } + + public function getDefaultBinary() { + return 'eslint'; + } + + public function getInstallInstructions() { + return pht('Install ESLint using `%s`.', 'npm install -g eslint'); + } + + public function getUpgradeInstructions() { + return pht('Upgrade ESLint using `%s`.', 'npm update -g eslint'); + } + + protected function getMandatoryFlags() { + $options = array(); + $options[] = '--format=json'; + $options[] = '--no-color'; + + if ($this->eslintenv) { + $options[] = '--env='.$this->eslintenv; + } + + if ($this->eslintconfig) { + $options[] = '--config='.$this->eslintconfig; + } + + if ($this->eslintignore) { + $options[] = '--ignore-path='.$this->eslintignore; + } + return $options; + } + + public function getLinterConfigurationOptions() { + $options = array( + 'eslint.eslintenv' => array( + 'type' => 'optional string', + 'help' => pht('enables specific environments.'), + ), + 'eslint.eslintconfig' => array( + 'type' => 'optional string', + 'help' => pht('config file to use the default is .eslint.'), + ), + 'eslint.eslintignore' => array( + 'type' => 'optional string', + 'help' => pht( + 'ignore file to use. the default is .eslintignore.'), + ), + ); + + return $options + parent::getLinterConfigurationOptions(); + } + + public function getLintSeverityMap() { + return array( + 2 => ArcanistLintSeverity::SEVERITY_ERROR, + 1 => ArcanistLintSeverity::SEVERITY_WARNING, + ); + } + + public function setLinterConfigurationValue($key, $value) { + + switch ($key) { + case 'eslint.eslintenv': + $this->eslintenv = $value; + return; + case 'eslint.eslintconfig': + $this->eslintconfig = $value; + return; + case 'eslint.eslintignore': + $this->eslintignore = $value; + return; + } + + return parent::setLinterConfigurationValue($key, $value); + } + + protected function canCustomizeLintSeverities() { + return true; + } + + 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. + if (empty($stdout)) { + throw new PhutilProxyException( + pht('ESLint threw an error: %s', $stderr), + $ex + ); + } else { + throw new PhutilProxyException( + pht('ESLint returned unparseable output: %s', $stdout), + $ex + ); + } + } + + $messages = array(); + foreach (idx($errors[0], 'messages') as $result) { + $message = new ArcanistLintMessage(); + $message->setPath($path); + $message->setLine(idx($result, 'line')); + $message->setChar(idx($result, 'column')); + $message->setDescription(idx($result, 'message')); + $message->setSeverity( + $this->getLintMessageSeverity(idx($result, 'severity'))); + + // In case of a parsing error, eslint does not specify what rule + // failed + // Instead it sets fatal to true + if (idx($result, 'fatal')) { + $message->setCode('fatal'); + $message->setName('ParsingError'); + } else { + $rule_id = idx($result, 'ruleId'); + + $message->setCode($rule_id); + $message->setName(idx($result, 'nodeType')); + $message->setDescription( + pht( + "%s\r\nSee documentation at %s", + idx($result, 'message'), + $this->getRuleDocumentationURI($rule_id))); + } + + // Log files that ignored by ESLint + if (!$message->getName()) { + echo pht("Couldn't lint path: %s\r\n", $path); + continue; + } + + $messages[] = $message; + } + + return $messages; + } + +} diff --git a/src/lint/linter/ArcanistExternalLinter.php b/src/lint/linter/ArcanistExternalLinter.php --- a/src/lint/linter/ArcanistExternalLinter.php +++ b/src/lint/linter/ArcanistExternalLinter.php @@ -63,7 +63,7 @@ * this method and return true so execution continues when it exits with * a nonzero status. * - * @param bool Return true to continue on nonzero error code. + * @return bool Return true to continue on nonzero error code. * @task bin */ public function shouldExpectCommandErrors() { diff --git a/src/lint/linter/__tests__/ArcanistESLintLinterTestCase.php b/src/lint/linter/__tests__/ArcanistESLintLinterTestCase.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/__tests__/ArcanistESLintLinterTestCase.php @@ -0,0 +1,14 @@ +setLinterConfigurationValue( + 'eslint.eslintconfig', + dirname(__FILE__).'/eslint/.eslintrc'); + $this->executeTestsInDirectory(dirname(__FILE__).'/eslint/', $linter); + } + +} diff --git a/src/lint/linter/__tests__/eslint/.eslintrc b/src/lint/linter/__tests__/eslint/.eslintrc new file mode 100644 --- /dev/null +++ b/src/lint/linter/__tests__/eslint/.eslintrc @@ -0,0 +1,24 @@ +{ + "rules": { + "indent": [ + 1, + "tab" + ], + "quotes": [ + 1, + "double" + ], + "linebreak-style": [ + 2, + "unix" + ], + "semi": [ + 2, + "always" + ] + }, + "env": { + "es6": true, + "browser": true + }, +} diff --git a/src/lint/linter/__tests__/eslint/eslint.lint-test b/src/lint/linter/__tests__/eslint/eslint.lint-test new file mode 100644 --- /dev/null +++ b/src/lint/linter/__tests__/eslint/eslint.lint-test @@ -0,0 +1,93 @@ +var express = require('express'); +var path = require('path'); +var logger = require('morgan'); +var cookieParser = require('cookie-parser'); +var bodyParser = require('body-parser'); +var routes = require('./routes/index'); +var users = require('./routes/users'); + +var app = express(); +var app = express(); + +// view engine setup +app.set('views', path.join(__dirname, 'views')); +app.set('view engine', 'jade'); + +// uncomment after placing your favicon in /public +//app.use(favicon(__dirname + '/public/favicon.ico')); +app.use(logger('dev')); +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ extended: false })); +app.use(cookieParser()); +app.use(express.static(path.join(__dirname, 'public'))); + +app.use('/', routes); +app.use('/users', users); + +// catch 404 and forward to error handler +app.use(function(req, res, next) { + var err = new Error('Not Found') + err.status = 404; +}); + +// error handlers + +// development error handler +// will print stacktrace +if (app.get('env') === 'development') { + app.use(function(err, req, res) { + res.status(err.status || 500) + res.render('error', { + message: err.message, + error: err + }); + }); +} + +// production error handler +// no stacktraces leaked to user +app.use(function(err, req, res) { + res.status(err.status || 500); + res.render('error', { + message: err.message, + error: {} + }); +}); + + +module.exports = app; + +~~~~~~~~~~ +warning:1:23 +warning:2:20 +warning:3:22 +warning:4:28 +warning:5:26 +warning:6:22 +warning:7:21 +warning:13:9 +warning:13:39 +warning:14:9 +warning:14:24 +warning:18:16 +warning:22:45 +warning:24:9 +warning:25:9 +warning:29:3 +warning:29:23 +error:29:35 +warning:30:3 +warning:37:13 +warning:37:24 +warning:38:3 +warning:39:5 +error:39:34 +warning:40:5 +warning:40:16 +warning:41:7 +warning:42:7 +warning:50:3 +warning:51:3 +warning:51:14 +warning:52:5 +warning:53:5