Page MenuHomePhabricator

D12198.id35057.diff
No OneTemporary

D12198.id35057.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,141 @@
+<?php
+
+final class ArcanistESLintLinter extends ArcanistExternalLinter {
+
+ protected $eslintenv;
+ protected $eslintconfig;
+
+ 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 getInfoDescription() {
+ return pht('ESLint is a linter for JavaScript source files.');
+ }
+
+ public function getVersion() {
+ $output = exec('eslint --version');
+
+ if (strpos($output, 'command not found') !== false) {
+ return false;
+ }
+
+ return $output;
+ }
+
+ 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=stylish';
+
+ if ($this->eslintenv) {
+ $options[] = '--env='.$this->eslintenv;
+ }
+
+ if ($this->eslintconfig) {
+ $options[] = '--config='.$this->eslintconfig;
+ }
+
+ 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.'),
+ ),
+ );
+
+ return $options + parent::getLinterConfigurationOptions();
+ }
+
+ public function setLinterConfigurationValue($key, $value) {
+
+ switch ($key) {
+ case 'eslint.eslintenv':
+ $this->eslintenv = $value;
+ return;
+ case 'eslint.eslintconfig':
+ $this->eslintconfig = $value;
+ return;
+ }
+
+ return parent::setLinterConfigurationValue($key, $value);
+ }
+
+ protected function canCustomizeLintSeverities() {
+ return true;
+ }
+
+ protected function parseLinterOutput($path, $err, $stdout, $stderr) {
+ $lines = phutil_split_lines($stdout, false);
+
+ $messages = array();
+ foreach ($lines as $line) {
+ // Clean up nasty ESLint output
+ $clean_line = $output = preg_replace('!\s+!', ' ', $line);
+ $parts = explode(' ', ltrim($clean_line));
+
+ if (isset($parts[1]) &&
+ ($parts[1] === 'error' || $parts[1] === 'warning')) {
+ $severity = $parts[1] === 'error' ?
+ ArcanistLintSeverity::SEVERITY_ERROR :
+ ArcanistLintSeverity::SEVERITY_WARNING;
+
+ list($line, $char) = explode(':', $parts[0]);
+
+ $message = new ArcanistLintMessage();
+ $message->setPath($path);
+ $message->setLine($line);
+ $message->setChar($char);
+ $message->setCode($this->getLinterName());
+ $message->setName($this->getLinterName());
+ $message->setDescription(implode(' ', $parts));
+ $message->setSeverity($severity);
+
+ $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
Mon, Mar 10, 3:58 PM (16 h, 44 m)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
7455198
Default Alt Text
D12198.id35057.diff (9 KB)

Event Timeline