Bug 1258341 - Modify 'eslint' implementation to work with the mozlint framework, r?smacleod draft
authorAndrew Halberstadt <ahalberstadt@mozilla.com>
Tue, 30 Aug 2016 14:28:56 -0400
changeset 407662 5968b4a93d00aba2369a65331e3c0f97955b9e94
parent 407661 f34811f5e3f54302644f9a0a93d447bface9c34b
child 407663 d1e29d638931a3758937787a49fbf1a0998d5a1e
push id28003
push userahalberstadt@mozilla.com
push dateTue, 30 Aug 2016 18:31:07 +0000
reviewerssmacleod
bugs1258341
milestone51.0a1
Bug 1258341 - Modify 'eslint' implementation to work with the mozlint framework, r?smacleod This integrates 'eslint' with the mozlint framework. The old |mach eslint| command is kept around for backwards compatibility and will simply dispatch to |mach lint|. But |mach lint| should be preferred as the old command may eventually be removed. The old |mach eslint| command should be mostly backwards compatible with a few exceptions: 1. Can no longer define --extensions on the command line, this is instead hardcoded into eslint.lint. 2. No longer using eslint formatters. However, the default mozlint formatter should be identical to the default eslint formatter, so developers should not notice a change. This does mean that non-default eslint formatters can no longer be used unless a mozlint copy of them is created. 3. Installs dependencies automatically without prompting the user. This was necessary due to python multiprocessing limitations, but is actually also a better UX. Because the npm dependencies aren't global anymore, there isn't really any reason *not* to install them automatically. Apart from that, any difference from the old |mach eslint| I'd consider a bug. The main eslint implementation now lives in tools/lint/eslint.lint instead of tools/lint/mach_commands.py. MozReview-Commit-ID: KYhC6SEySC3
tools/lint/eslint.lint
tools/lint/mach_commands.py
new file mode 100644
--- /dev/null
+++ b/tools/lint/eslint.lint
@@ -0,0 +1,349 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import json
+import os
+import platform
+import re
+import signal
+import subprocess
+import sys
+from distutils.version import LooseVersion
+
+import which
+from mozprocess import ProcessHandler
+
+from mozlint import result
+
+
+ESLINT_NOT_FOUND_MESSAGE = """
+Could not find eslint!  We looked at the --binary option, at the ESLINT
+environment variable, and then at your local node_modules path. Please Install
+eslint and needed plugins with:
+
+mach eslint --setup
+
+and try again.
+""".strip()
+
+NODE_NOT_FOUND_MESSAGE = """
+nodejs v4.2.3 is either not installed or is installed to a non-standard path.
+Please install nodejs from https://nodejs.org and try again.
+
+Valid installation paths:
+""".strip()
+
+NPM_NOT_FOUND_MESSAGE = """
+Node Package Manager (npm) is either not installed or installed to a
+non-standard path. Please install npm from https://nodejs.org (it comes as an
+option in the node installation) and try again.
+
+Valid installation paths:
+""".strip()
+
+
+VERSION_RE = re.compile(r"^\d+\.\d+\.\d+$")
+CARET_VERSION_RANGE_RE = re.compile(r"^\^((\d+)\.\d+\.\d+)$")
+
+EXTENSIONS = ['.js', '.jsm', '.jsx', '.xml', '.html', '.xhtml']
+
+project_root = None
+
+
+def eslint_setup():
+    """Ensure eslint is optimally configured.
+
+    This command will inspect your eslint configuration and
+    guide you through an interactive wizard helping you configure
+    eslint for optimal use on Mozilla projects.
+    """
+    orig_cwd = os.getcwd()
+    sys.path.append(os.path.dirname(__file__))
+
+    module_path = get_eslint_module_path()
+
+    # npm sometimes fails to respect cwd when it is run using check_call so
+    # we manually switch folders here instead.
+    os.chdir(module_path)
+
+    npm_path = get_node_or_npm_path("npm")
+    if not npm_path:
+        return 1
+
+    # Install ESLint and external plugins
+    cmd = [npm_path, "install"]
+    print("Installing eslint for mach using \"%s\"..." % (" ".join(cmd)))
+    if not call_process("eslint", cmd):
+        return 1
+
+    # Install in-tree ESLint plugin
+    cmd = [npm_path, "install",
+           os.path.join(module_path, "eslint-plugin-mozilla")]
+    print("Installing eslint-plugin-mozilla using \"%s\"..." % (" ".join(cmd)))
+    if not call_process("eslint-plugin-mozilla", cmd):
+        return 1
+
+    eslint_path = os.path.join(module_path, "node_modules", ".bin", "eslint")
+
+    print("\nESLint and approved plugins installed successfully!")
+    print("\nNOTE: Your local eslint binary is at %s\n" % eslint_path)
+
+    os.chdir(orig_cwd)
+
+
+def call_process(name, cmd, cwd=None):
+    try:
+        with open(os.devnull, "w") as fnull:
+            subprocess.check_call(cmd, cwd=cwd, stdout=fnull)
+    except subprocess.CalledProcessError:
+        if cwd:
+            print("\nError installing %s in the %s folder, aborting." % (name, cwd))
+        else:
+            print("\nError installing %s, aborting." % name)
+
+        return False
+
+    return True
+
+
+def expected_eslint_modules():
+    # Read the expected version of ESLint and external modules
+    expected_modules_path = os.path.join(get_eslint_module_path(), "package.json")
+    with open(expected_modules_path, "r") as f:
+        expected_modules = json.load(f)["dependencies"]
+
+    # Also read the in-tree ESLint plugin version
+    mozilla_json_path = os.path.join(get_eslint_module_path(),
+                                     "eslint-plugin-mozilla", "package.json")
+    with open(mozilla_json_path, "r") as f:
+        expected_modules["eslint-plugin-mozilla"] = json.load(f)["version"]
+
+    return expected_modules
+
+
+def eslint_module_has_issues():
+    has_issues = False
+    node_modules_path = os.path.join(get_eslint_module_path(), "node_modules")
+
+    for name, version_range in expected_eslint_modules().iteritems():
+        path = os.path.join(node_modules_path, name, "package.json")
+
+        if not os.path.exists(path):
+            print("%s v%s needs to be installed locally." % (name, version_range))
+            has_issues = True
+            continue
+
+        data = json.load(open(path))
+
+        if not version_in_range(data["version"], version_range):
+            print("%s v%s should be v%s." % (name, data["version"], version_range))
+            has_issues = True
+
+    return has_issues
+
+
+def version_in_range(version, version_range):
+    """
+    Check if a module version is inside a version range.  Only supports explicit versions and
+    caret ranges for the moment, since that's all we've used so far.
+    """
+    if version == version_range:
+        return True
+
+    version_match = VERSION_RE.match(version)
+    if not version_match:
+        raise RuntimeError("mach eslint doesn't understand module version %s" % version)
+    version = LooseVersion(version)
+
+    # Caret ranges as specified by npm allow changes that do not modify the left-most non-zero
+    # digit in the [major, minor, patch] tuple.  The code below assumes the major digit is
+    # non-zero.
+    range_match = CARET_VERSION_RANGE_RE.match(version_range)
+    if range_match:
+        range_version = range_match.group(1)
+        range_major = int(range_match.group(2))
+
+        range_min = LooseVersion(range_version)
+        range_max = LooseVersion("%d.0.0" % (range_major + 1))
+
+        return range_min <= version < range_max
+
+    return False
+
+
+def get_possible_node_paths_win():
+    """
+    Return possible nodejs paths on Windows.
+    """
+    if platform.system() != "Windows":
+        return []
+
+    return list({
+        "%s\\nodejs" % os.environ.get("SystemDrive"),
+        os.path.join(os.environ.get("ProgramFiles"), "nodejs"),
+        os.path.join(os.environ.get("PROGRAMW6432"), "nodejs"),
+        os.path.join(os.environ.get("PROGRAMFILES"), "nodejs")
+    })
+
+
+def get_node_or_npm_path(filename, minversion=None):
+    """
+    Return the nodejs or npm path.
+    """
+    if platform.system() == "Windows":
+        for ext in [".cmd", ".exe", ""]:
+            try:
+                node_or_npm_path = which.which(filename + ext,
+                                               path=get_possible_node_paths_win())
+                if is_valid(node_or_npm_path, minversion):
+                    return node_or_npm_path
+            except which.WhichError:
+                pass
+    else:
+        try:
+            node_or_npm_path = which.which(filename)
+            if is_valid(node_or_npm_path, minversion):
+                return node_or_npm_path
+        except which.WhichError:
+            pass
+
+    if filename == "node":
+        print(NODE_NOT_FOUND_MESSAGE)
+    elif filename == "npm":
+        print(NPM_NOT_FOUND_MESSAGE)
+
+    if platform.system() == "Windows":
+        app_paths = get_possible_node_paths_win()
+
+        for p in app_paths:
+            print("  - %s" % p)
+    elif platform.system() == "Darwin":
+        print("  - /usr/local/bin/node")
+    elif platform.system() == "Linux":
+        print("  - /usr/bin/nodejs")
+
+    return None
+
+
+def is_valid(path, minversion=None):
+    try:
+        version_str = subprocess.check_output([path, "--version"],
+                                              stderr=subprocess.STDOUT)
+        if minversion:
+            # nodejs prefixes its version strings with "v"
+            version = LooseVersion(version_str.lstrip('v'))
+            return version >= minversion
+        return True
+    except (subprocess.CalledProcessError, OSError):
+        return False
+
+
+def get_project_root():
+    global project_root
+    return project_root
+
+
+def get_eslint_module_path():
+    return os.path.join(get_project_root(), "tools", "lint", "eslint")
+
+
+def lint(paths, binary=None, fix=None, setup=None, **lintargs):
+    """Run eslint."""
+    global project_root
+    project_root = lintargs['root']
+
+    module_path = get_eslint_module_path()
+
+    # eslint requires at least node 4.2.3
+    node_path = get_node_or_npm_path("node", LooseVersion("4.2.3"))
+    if not node_path:
+        return 1
+
+    if setup:
+        return eslint_setup()
+
+    npm_path = get_node_or_npm_path("npm")
+    if not npm_path:
+        return 1
+
+    if eslint_module_has_issues():
+        eslint_setup()
+
+    # Valid binaries are:
+    #  - Any provided by the binary argument.
+    #  - Any pointed at by the ESLINT environmental variable.
+    #  - Those provided by mach eslint --setup.
+    #
+    #  eslint --setup installs some mozilla specific plugins and installs
+    #  all node modules locally. This is the preferred method of
+    #  installation.
+
+    if not binary:
+        binary = os.environ.get('ESLINT', None)
+
+        if not binary:
+            binary = os.path.join(module_path, "node_modules", ".bin", "eslint")
+            if not os.path.isfile(binary):
+                binary = None
+
+    if not binary:
+        print(ESLINT_NOT_FOUND_MESSAGE)
+        return 1
+
+    extra_args = lintargs.get('extra_args') or []
+    cmd_args = [binary,
+                # Enable the HTML plugin.
+                # We can't currently enable this in the global config file
+                # because it has bad interactions with the SublimeText
+                # ESLint plugin (bug 1229874).
+                '--plugin', 'html',
+                # This keeps ext as a single argument.
+                '--ext', '[{}]'.format(','.join(EXTENSIONS)),
+                '--format', 'json',
+                ] + extra_args + paths
+
+    # eslint requires that --fix be set before the --ext argument.
+    if fix:
+        cmd_args.insert(1, '--fix')
+
+    orig = signal.signal(signal.SIGINT, signal.SIG_IGN)
+    proc = ProcessHandler(cmd_args, env=os.environ, stream=None)
+    proc.run()
+    signal.signal(signal.SIGINT, orig)
+
+    try:
+        proc.wait()
+    except KeyboardInterrupt:
+        proc.kill()
+        return []
+
+    results = []
+    for obj in json.loads(proc.output[0] or '[]'):
+        errors = obj['messages']
+
+        for err in errors:
+            err.update({
+                'hint': err.get('fix'),
+                'level': 'error' if err['severity'] == 2 else 'warning',
+                'lineno': err.get('line'),
+                'path': obj['filePath'],
+                'rule': err.get('ruleId'),
+            })
+            results.append(result.from_linter(LINTER, **err))
+
+    return results
+
+
+LINTER = {
+    'name': "eslint",
+    'description': "JavaScript linter",
+    # ESLint infra handles its own path filtering, so just include cwd
+    'include': ['.'],
+    'exclude': [],
+    'extensions': EXTENSIONS,
+    'type': 'external',
+    'payload': lint,
+}
--- a/tools/lint/mach_commands.py
+++ b/tools/lint/mach_commands.py
@@ -1,69 +1,32 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from __future__ import absolute_import, print_function, unicode_literals
 
 import argparse
-import json
-import logging
 import os
-import platform
-import re
-import subprocess
-import sys
-import which
-from distutils.version import LooseVersion
 
 from mozbuild.base import (
     MachCommandBase,
 )
 
 
 from mach.decorators import (
     CommandArgument,
     CommandProvider,
     Command,
 )
 
 
 here = os.path.abspath(os.path.dirname(__file__))
 
 
-ESLINT_NOT_FOUND_MESSAGE = '''
-Could not find eslint!  We looked at the --binary option, at the ESLINT
-environment variable, and then at your local node_modules path. Please Install
-eslint and needed plugins with:
-
-mach eslint --setup
-
-and try again.
-'''.strip()
-
-NODE_NOT_FOUND_MESSAGE = '''
-nodejs v4.2.3 is either not installed or is installed to a non-standard path.
-Please install nodejs from https://nodejs.org and try again.
-
-Valid installation paths:
-'''.strip()
-
-NPM_NOT_FOUND_MESSAGE = '''
-Node Package Manager (npm) is either not installed or installed to a
-non-standard path. Please install npm from https://nodejs.org (it comes as an
-option in the node installation) and try again.
-
-Valid installation paths:
-'''.strip()
-
-VERSION_RE = re.compile(r"^\d+\.\d+\.\d+$")
-CARET_VERSION_RANGE_RE = re.compile(r"^\^((\d+)\.\d+\.\d+)$")
-
-
 def setup_argument_parser():
     from mozlint import cli
     return cli.MozlintParser()
 
 
 @CommandProvider
 class MachCommands(MachCommandBase):
 
@@ -75,300 +38,23 @@ class MachCommands(MachCommandBase):
         """Run linters."""
         from mozlint import cli
         lintargs['exclude'] = ['obj*']
         cli.SEARCH_PATHS.append(here)
         return cli.run(*runargs, **lintargs)
 
     @Command('eslint', category='devenv',
              description='Run eslint or help configure eslint for optimal development.')
+    @CommandArgument('paths', default=None, nargs='*',
+                     help="Paths to file or directories to lint, like "
+                          "'browser/components/loop' Defaults to the "
+                          "current directory if not given.")
     @CommandArgument('-s', '--setup', default=False, action='store_true',
                      help='Configure eslint for optimal development.')
-    @CommandArgument('-e', '--ext', default='[.js,.jsm,.jsx,.xml,.html,.xhtml]',
-                     help='Filename extensions to lint, default: ' +
-                     '"[.js,.jsm,.jsx,.xml,.html,.xhtml]".')
     @CommandArgument('-b', '--binary', default=None,
                      help='Path to eslint binary.')
     @CommandArgument('--fix', default=False, action='store_true',
                      help='Request that eslint automatically fix errors, where possible.')
-    @CommandArgument('args', nargs=argparse.REMAINDER)  # Passed through to eslint.
-    def eslint(self, setup, ext=None, binary=None, fix=False, args=None):
-        '''Run eslint.'''
-
-        module_path = self.get_eslint_module_path()
-
-        # eslint requires at least node 4.2.3
-        nodePath = self.get_node_or_npm_path("node", LooseVersion("4.2.3"))
-        if not nodePath:
-            return 1
-
-        if setup:
-            return self.eslint_setup()
-
-        npm_path = self.get_node_or_npm_path("npm")
-        if not npm_path:
-            return 1
-
-        if self.eslint_module_has_issues():
-            install = self._prompt_yn("\nContinuing will automatically fix "
-                                      "these issues. Would you like to "
-                                      "continue")
-            if install:
-                self.eslint_setup()
-            else:
-                return 1
-
-        # Valid binaries are:
-        #  - Any provided by the binary argument.
-        #  - Any pointed at by the ESLINT environmental variable.
-        #  - Those provided by mach eslint --setup.
-        #
-        #  eslint --setup installs some mozilla specific plugins and installs
-        #  all node modules locally. This is the preferred method of
-        #  installation.
-
-        if not binary:
-            binary = os.environ.get('ESLINT', None)
-
-            if not binary:
-                binary = os.path.join(module_path, "node_modules", ".bin", "eslint")
-                if not os.path.isfile(binary):
-                    binary = None
-
-        if not binary:
-            print(ESLINT_NOT_FOUND_MESSAGE)
-            return 1
-
-        self.log(logging.INFO, 'eslint', {'binary': binary, 'args': args},
-                 'Running {binary}')
-
-        args = args or ['.']
-
-        cmd_args = [binary,
-                    # Enable the HTML plugin.
-                    # We can't currently enable this in the global config file
-                    # because it has bad interactions with the SublimeText
-                    # ESLint plugin (bug 1229874).
-                    '--plugin', 'html',
-                    '--ext', ext,  # This keeps ext as a single argument.
-                    ] + args
-
-        # eslint requires that --fix be set before the --ext argument.
-        if fix:
-            cmd_args.insert(1, '--fix')
-
-        success = self.run_process(
-            cmd_args,
-            pass_thru=True,  # Allow user to run eslint interactively.
-            ensure_exit_code=False,  # Don't throw on non-zero exit code.
-            require_unix_environment=True  # eslint is not a valid Win32 binary.
-        )
-
-        self.log(logging.INFO, 'eslint', {'msg': ('No errors' if success == 0 else 'Errors')},
-                 'Finished eslint. {msg} encountered.')
-        return success
-
-    def eslint_setup(self):
-        """Ensure eslint is optimally configured.
-
-        This command will inspect your eslint configuration and
-        guide you through an interactive wizard helping you configure
-        eslint for optimal use on Mozilla projects.
-        """
-        orig_cwd = os.getcwd()
-        sys.path.append(os.path.dirname(__file__))
-
-        module_path = self.get_eslint_module_path()
-
-        # npm sometimes fails to respect cwd when it is run using check_call so
-        # we manually switch folders here instead.
-        os.chdir(module_path)
-
-        npm_path = self.get_node_or_npm_path("npm")
-        if not npm_path:
-            return 1
-
-        # Install ESLint and external plugins
-        cmd = [npm_path, "install"]
-        print("Installing eslint for mach using \"%s\"..." % (" ".join(cmd)))
-        if not self.call_process("eslint", cmd):
-            return 1
-
-        # Install in-tree ESLint plugin
-        cmd = [npm_path, "install",
-               os.path.join(module_path, "eslint-plugin-mozilla")]
-        print("Installing eslint-plugin-mozilla using \"%s\"..." % (" ".join(cmd)))
-        if not self.call_process("eslint-plugin-mozilla", cmd):
-            return 1
-
-        eslint_path = os.path.join(module_path, "node_modules", ".bin", "eslint")
-
-        print("\nESLint and approved plugins installed successfully!")
-        print("\nNOTE: Your local eslint binary is at %s\n" % eslint_path)
-
-        os.chdir(orig_cwd)
-
-    def call_process(self, name, cmd, cwd=None):
-        try:
-            with open(os.devnull, "w") as fnull:
-                subprocess.check_call(cmd, cwd=cwd, stdout=fnull)
-        except subprocess.CalledProcessError:
-            if cwd:
-                print("\nError installing %s in the %s folder, aborting." % (name, cwd))
-            else:
-                print("\nError installing %s, aborting." % name)
-
-            return False
-
-        return True
-
-    def expected_eslint_modules(self):
-        # Read the expected version of ESLint and external modules
-        expected_modules_path = os.path.join(self.get_eslint_module_path(), "package.json")
-        with open(expected_modules_path, "r") as f:
-            expected_modules = json.load(f)["dependencies"]
-
-        # Also read the in-tree ESLint plugin version
-        mozilla_json_path = os.path.join(self.get_eslint_module_path(),
-                                         "eslint-plugin-mozilla", "package.json")
-        with open(mozilla_json_path, "r") as f:
-            expected_modules["eslint-plugin-mozilla"] = json.load(f)["version"]
-
-        return expected_modules
-
-    def eslint_module_has_issues(self):
-        has_issues = False
-        node_modules_path = os.path.join(self.get_eslint_module_path(), "node_modules")
-
-        for name, version_range in self.expected_eslint_modules().iteritems():
-            path = os.path.join(node_modules_path, name, "package.json")
-
-            if not os.path.exists(path):
-                print("%s v%s needs to be installed locally." % (name, version_range))
-                has_issues = True
-                continue
-
-            data = json.load(open(path))
-
-            if not self.version_in_range(data["version"], version_range):
-                print("%s v%s should be v%s." % (name, data["version"], version_range))
-                has_issues = True
-
-        return has_issues
-
-    def version_in_range(self, version, version_range):
-        """
-        Check if a module version is inside a version range.  Only supports explicit versions and
-        caret ranges for the moment, since that's all we've used so far.
-        """
-        if version == version_range:
-            return True
-
-        version_match = VERSION_RE.match(version)
-        if not version_match:
-            raise RuntimeError("mach eslint doesn't understand module version %s" % version)
-        version = LooseVersion(version)
-
-        # Caret ranges as specified by npm allow changes that do not modify the left-most non-zero
-        # digit in the [major, minor, patch] tuple.  The code below assumes the major digit is
-        # non-zero.
-        range_match = CARET_VERSION_RANGE_RE.match(version_range)
-        if range_match:
-            range_version = range_match.group(1)
-            range_major = int(range_match.group(2))
-
-            range_min = LooseVersion(range_version)
-            range_max = LooseVersion("%d.0.0" % (range_major + 1))
-
-            return range_min <= version < range_max
-
-        return False
-
-    def get_possible_node_paths_win(self):
-        """
-        Return possible nodejs paths on Windows.
-        """
-        if platform.system() != "Windows":
-            return []
-
-        return list({
-            "%s\\nodejs" % os.environ.get("SystemDrive"),
-            os.path.join(os.environ.get("ProgramFiles"), "nodejs"),
-            os.path.join(os.environ.get("PROGRAMW6432"), "nodejs"),
-            os.path.join(os.environ.get("PROGRAMFILES"), "nodejs")
-        })
-
-    def get_node_or_npm_path(self, filename, minversion=None):
-        """
-        Return the nodejs or npm path.
-        """
-        if platform.system() == "Windows":
-            for ext in [".cmd", ".exe", ""]:
-                try:
-                    node_or_npm_path = which.which(filename + ext,
-                                                   path=self.get_possible_node_paths_win())
-                    if self.is_valid(node_or_npm_path, minversion):
-                        return node_or_npm_path
-                except which.WhichError:
-                    pass
-        else:
-            try:
-                node_or_npm_path = which.which(filename)
-                if self.is_valid(node_or_npm_path, minversion):
-                    return node_or_npm_path
-            except which.WhichError:
-                pass
-
-        if filename == "node":
-            print(NODE_NOT_FOUND_MESSAGE)
-        elif filename == "npm":
-            print(NPM_NOT_FOUND_MESSAGE)
-
-        if platform.system() == "Windows":
-            appPaths = self.get_possible_node_paths_win()
-
-            for p in appPaths:
-                print("  - %s" % p)
-        elif platform.system() == "Darwin":
-            print("  - /usr/local/bin/node")
-        elif platform.system() == "Linux":
-            print("  - /usr/bin/nodejs")
-
-        return None
-
-    def is_valid(self, path, minversion=None):
-        try:
-            version_str = subprocess.check_output([path, "--version"],
-                                                  stderr=subprocess.STDOUT)
-            if minversion:
-                # nodejs prefixes its version strings with "v"
-                version = LooseVersion(version_str.lstrip('v'))
-                return version >= minversion
-            return True
-        except (subprocess.CalledProcessError, OSError):
-            return False
-
-    def get_project_root(self):
-        fullpath = os.path.abspath(sys.modules['__main__'].__file__)
-        return os.path.dirname(fullpath)
-
-    def get_eslint_module_path(self):
-        return os.path.join(self.get_project_root(), "tools", "lint", "eslint")
-
-    def _prompt_yn(self, msg):
-        if not sys.stdin.isatty():
-            return False
-
-        print('%s? [Y/n]' % msg)
-
-        while True:
-            choice = raw_input().lower().strip()
-
-            if not choice:
-                return True
-
-            if choice in ('y', 'yes'):
-                return True
-
-            if choice in ('n', 'no'):
-                return False
-
-            print('Must reply with one of {yes, no, y, n}.')
+    @CommandArgument('extra_args', nargs=argparse.REMAINDER,
+                     help='Extra args that will be forwarded to eslint.')
+    def eslint(self, paths, **kwargs):
+        self._mach_context.commands.dispatch('lint', self._mach_context,
+                                             linters=['eslint'], paths=paths, **kwargs)