Page MenuHomePhabricator

D12198.id35015.diff
No OneTemporary

D12198.id35015.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,127 @@
+<?php
+
+final class ArcanistESLintLinter extends ArcanistExternalLinter {
+
+ protected $eslintenv;
+ protected $eslintconfig;
+
+ 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');
+ }
+
+ 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->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,10 @@
+<?php
+
+final class ArcanistESLintLinterTestCase
+ extends ArcanistExternalLinterTestCase {
+
+ public function testLinter() {
+ $this->executeTestsInDirectory(dirname(__FILE__).'/eslint/');
+ }
+
+}
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,95 @@
+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;
+
+~~~~~~~~~~
+error:1:14
+error:1:22
+error:2:11
+error:2:19
+error:3:13
+error:3:21
+error:4:19
+error:4:27
+error:5:17
+error:5:25
+error:6:13
+error:6:21
+error:7:12
+error:7:20
+error:10:4
+error:13:8
+error:13:27
+error:13:38
+error:14:8
+error:14:23
+error:18:15
+error:22:33
+error:22:44
+error:24:8
+error:25:8
+error:28:8
+error:28:27
+error:29:22
+error:37:12
+error:37:23
+error:38:10
+error:40:15
+error:49:8
+error:51:13
+error:58:0

File Metadata

Mime Type
text/plain
Expires
Thu, May 9, 6:42 AM (3 w, 4 d ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
6274296
Default Alt Text
D12198.id35015.diff (8 KB)

Event Timeline