Page MenuHomePhabricator

D12198.id39086.diff
No OneTemporary

D12198.id39086.diff

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 @@
+<?php
+
+final class ArcanistESLintLinter extends ArcanistExternalLinter {
+
+ protected $eslintenv;
+ protected $eslintconfig;
+ protected $eslintignore;
+
+ public function __construct() {
+ // This sets the min supported version of this
+ // linter. If you have set it in your .arcconfig or
+ // .arclint file it will overwrite this. However if
+ // the value is set lower it will cause the linter
+ // to break.
+ $this->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<version>\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 @@
+<?php
+
+final class ArcanistESLintLinterTestCase
+ extends ArcanistExternalLinterTestCase {
+
+ public function testLinter() {
+ $linter = new ArcanistESLintLinter();
+ $linter->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

File Metadata

Mime Type
text/plain
Expires
Wed, Nov 13, 7:34 AM (4 d, 10 h ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
6737630
Default Alt Text
D12198.id39086.diff (11 KB)

Event Timeline