Bug 1258341 - Move all eslint related infrastructure to tools/lint, r?mratcliffe draft
authorAndrew Halberstadt <ahalberstadt@mozilla.com>
Thu, 02 Jun 2016 15:38:36 -0400
changeset 374697 25cfeb5089451297a1333ca75650092c5f40e7f4
parent 374456 92e0c73391e71a400e2c6674bca5ca70804ab081
child 522678 751571714f52f3e7998363d08899e3ad69686601
push id20070
push userahalberstadt@mozilla.com
push dateThu, 02 Jun 2016 20:15:49 +0000
reviewersmratcliffe
bugs1258341
milestone49.0a1
Bug 1258341 - Move all eslint related infrastructure to tools/lint, r?mratcliffe This commit simply moves 'testing/eslint' to 'tools/lint/eslint' and the eslint related mach command from 'python/mach_commands.py' to 'tools/lint/mach_commands.py'. It shouldn't have any functional change on running eslint, either through mach or taskcluster. This is in preparation for bug 1258341, to make the diffs there a little easier to read. MozReview-Commit-ID: K03sn9lv9Lv
.eslintignore
.gitignore
.hgignore
python/mach_commands.py
testing/eslint/eslint-plugin-mozilla/LICENSE
testing/eslint/eslint-plugin-mozilla/docs/balanced-listeners.rst
testing/eslint/eslint-plugin-mozilla/docs/import-browserjs-globals.rst
testing/eslint/eslint-plugin-mozilla/docs/import-globals.rst
testing/eslint/eslint-plugin-mozilla/docs/import-headjs-globals.rst
testing/eslint/eslint-plugin-mozilla/docs/index.rst
testing/eslint/eslint-plugin-mozilla/docs/mark-test-function-used.rst
testing/eslint/eslint-plugin-mozilla/docs/no-aArgs.rst
testing/eslint/eslint-plugin-mozilla/docs/no-cpows-in-tests.rst
testing/eslint/eslint-plugin-mozilla/docs/reject-importGlobalProperties.rst
testing/eslint/eslint-plugin-mozilla/docs/var-only-at-top-level.rst
testing/eslint/eslint-plugin-mozilla/lib/globals.js
testing/eslint/eslint-plugin-mozilla/lib/helpers.js
testing/eslint/eslint-plugin-mozilla/lib/index.js
testing/eslint/eslint-plugin-mozilla/lib/processors/xbl-bindings.js
testing/eslint/eslint-plugin-mozilla/lib/rules/.eslintrc
testing/eslint/eslint-plugin-mozilla/lib/rules/balanced-listeners.js
testing/eslint/eslint-plugin-mozilla/lib/rules/import-browserjs-globals.js
testing/eslint/eslint-plugin-mozilla/lib/rules/import-globals.js
testing/eslint/eslint-plugin-mozilla/lib/rules/import-headjs-globals.js
testing/eslint/eslint-plugin-mozilla/lib/rules/mark-test-function-used.js
testing/eslint/eslint-plugin-mozilla/lib/rules/no-aArgs.js
testing/eslint/eslint-plugin-mozilla/lib/rules/no-cpows-in-tests.js
testing/eslint/eslint-plugin-mozilla/lib/rules/reject-importGlobalProperties.js
testing/eslint/eslint-plugin-mozilla/lib/rules/var-only-at-top-level.js
testing/eslint/eslint-plugin-mozilla/moz.build
testing/eslint/eslint-plugin-mozilla/package.json
testing/eslint/manifest.tt
testing/eslint/npm-shrinkwrap.json
testing/eslint/package.json
testing/eslint/update
testing/taskcluster/tasks/branches/base_jobs.yml
testing/taskcluster/tasks/tests/eslint-gecko.yml
tools/lint/eslint/eslint-plugin-mozilla/LICENSE
tools/lint/eslint/eslint-plugin-mozilla/docs/balanced-listeners.rst
tools/lint/eslint/eslint-plugin-mozilla/docs/import-browserjs-globals.rst
tools/lint/eslint/eslint-plugin-mozilla/docs/import-globals.rst
tools/lint/eslint/eslint-plugin-mozilla/docs/import-headjs-globals.rst
tools/lint/eslint/eslint-plugin-mozilla/docs/index.rst
tools/lint/eslint/eslint-plugin-mozilla/docs/mark-test-function-used.rst
tools/lint/eslint/eslint-plugin-mozilla/docs/no-aArgs.rst
tools/lint/eslint/eslint-plugin-mozilla/docs/no-cpows-in-tests.rst
tools/lint/eslint/eslint-plugin-mozilla/docs/reject-importGlobalProperties.rst
tools/lint/eslint/eslint-plugin-mozilla/docs/var-only-at-top-level.rst
tools/lint/eslint/eslint-plugin-mozilla/lib/globals.js
tools/lint/eslint/eslint-plugin-mozilla/lib/helpers.js
tools/lint/eslint/eslint-plugin-mozilla/lib/index.js
tools/lint/eslint/eslint-plugin-mozilla/lib/processors/xbl-bindings.js
tools/lint/eslint/eslint-plugin-mozilla/lib/rules/.eslintrc
tools/lint/eslint/eslint-plugin-mozilla/lib/rules/balanced-listeners.js
tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-browserjs-globals.js
tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-globals.js
tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-headjs-globals.js
tools/lint/eslint/eslint-plugin-mozilla/lib/rules/mark-test-function-used.js
tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-aArgs.js
tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-cpows-in-tests.js
tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-importGlobalProperties.js
tools/lint/eslint/eslint-plugin-mozilla/lib/rules/var-only-at-top-level.js
tools/lint/eslint/eslint-plugin-mozilla/moz.build
tools/lint/eslint/eslint-plugin-mozilla/package.json
tools/lint/eslint/manifest.tt
tools/lint/eslint/npm-shrinkwrap.json
tools/lint/eslint/package.json
tools/lint/eslint/update
tools/lint/mach_commands.py
--- a/.eslintignore
+++ b/.eslintignore
@@ -35,18 +35,18 @@ netwerk/**
 nsprpub/**
 other-licenses/**
 parser/**
 probes/**
 python/**
 rdf/**
 startupcache/**
 testing/**
-!testing/eslint-plugin-mozilla/
-testing/eslint-plugin-mozilla/node_modules/**
+!tools/lint/eslint/eslint-plugin-mozilla/
+tools/lint/eslint/eslint-plugin-mozilla/node_modules/**
 tools/**
 uriloader/**
 view/**
 widget/**
 xpcom/**
 xpfe/**
 xulrunner/**
 
--- a/.gitignore
+++ b/.gitignore
@@ -96,17 +96,17 @@ embedding/ios/GeckoEmbed/GeckoEmbed.xcod
 # Ignore mozharness execution files
 testing/mozharness/.tox/
 testing/mozharness/build/
 testing/mozharness/logs/
 testing/mozharness/.coverage
 testing/mozharness/nosetests.xml
 
 # Ignore node_modules
-testing/eslint/node_modules/
+tools/lint/eslint/node_modules/
 
 # Ignore talos virtualenv and tp5n files.
 # The tp5n set is supposed to be decompressed at
 # testing/talos/talos/page_load_test/tp5n in order to run tests like tps
 # locally. Similarly, running talos requires a Python package virtual
 # environment. Both the virtual environment and tp5n files end up littering
 # the status command, so we ignore them.
 testing/talos/.Python
--- a/.hgignore
+++ b/.hgignore
@@ -113,17 +113,17 @@ GPATH
 ^testing/mozharness/logs/
 ^testing/mozharness/.coverage
 ^testing/mozharness/nosetests.xml
 
 # Ignore tox generated dir
 .tox/
 
 # Ignore node_modules
-^testing/eslint/node_modules/
+^tools/lint/eslint/node_modules/
 
 # Ignore talos virtualenv and tp5n files.
 # The tp5n set is supposed to be decompressed at
 # testing/talos/talos/page_load_test/tp5n in order to run tests like tps
 # locally. Similarly, running talos requires a Python package virtual
 # environment. Both the virtual environment and tp5n files end up littering
 # the status command, so we ignore them.
 ^testing/talos/.Python
--- a/python/mach_commands.py
+++ b/python/mach_commands.py
@@ -1,68 +1,30 @@
 # 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 __main__
 import argparse
-import json
 import logging
 import mozpack.path as mozpath
 import os
-import platform
-import subprocess
-import sys
-import which
-from distutils.version import LooseVersion
 
 from mozbuild.base import (
     MachCommandBase,
 )
 
 from mach.decorators import (
     CommandArgument,
     CommandProvider,
     Command,
 )
 
-ESLINT_PACKAGES = [
-    "eslint@2.9.0",
-    "eslint-plugin-html@1.4.0",
-    "eslint-plugin-mozilla@0.0.3",
-    "eslint-plugin-react@4.2.3"
-]
-
-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()
-
 
 @CommandProvider
 class MachCommands(MachCommandBase):
     @Command('python', category='devenv',
         description='Run Python.')
     @CommandArgument('args', nargs=argparse.REMAINDER)
     def python(self, args):
         # Avoid logging the command
@@ -194,277 +156,8 @@ class MachCommands(MachCommandBase):
             message = 'TEST-UNEXPECTED-FAIL | No tests collected'
             if not path_only:
                  message += ' (Not in PYTHON_UNIT_TESTS? Try --path-only?)'
             self.log(logging.WARN, 'python-test', {}, message)
             return 1
 
         return 0 if return_code == 0 else 1
 
-    @Command('eslint', category='devenv',
-        description='Run eslint or help configure eslint for optimal development.')
-    @CommandArgument('-s', '--setup', default=False, action='store_true',
-        help='configure eslint for optimal development.')
-    @CommandArgument('-e', '--ext', default='[.js,.jsm,.jsx,.xml,.html]',
-        help='Filename extensions to lint, default: "[.js,.jsm,.jsx,.xml,.html]".')
-    @CommandArgument('-b', '--binary', default=None,
-        help='Path to eslint binary.')
-    @CommandArgument('args', nargs=argparse.REMAINDER)  # Passed through to eslint.
-    def eslint(self, setup, ext=None, binary=None, args=None):
-        '''Run eslint.'''
-
-        module_path = self.get_eslint_module_path()
-
-        # eslint requires at least node 4.2.3
-        nodePath = self.getNodeOrNpmPath("node", LooseVersion("4.2.3"))
-        if not nodePath:
-            return 1
-
-        if setup:
-            return self.eslint_setup()
-
-        npmPath = self.getNodeOrNpmPath("npm")
-        if not npmPath:
-            return 1
-
-        if self.eslintModuleHasIssues():
-            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
-
-        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, update_only=False):
-        """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)
-
-        npmPath = self.getNodeOrNpmPath("npm")
-        if not npmPath:
-            return 1
-
-        # Install eslint and necessary plugins.
-        for pkg in ESLINT_PACKAGES:
-            name, version = pkg.split("@")
-            success = False
-
-            if self.node_package_installed(pkg, cwd=module_path):
-                success = True
-            else:
-                if pkg.startswith("eslint-plugin-mozilla"):
-                    cmd = [npmPath, "install",
-                           os.path.join(module_path, "eslint-plugin-mozilla")]
-                else:
-                    cmd = [npmPath, "install", pkg]
-
-                print("Installing %s v%s using \"%s\"..."
-                      % (name, version, " ".join(cmd)))
-                success = self.callProcess(pkg, cmd)
-
-            if not success:
-                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 callProcess(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 eslintModuleHasIssues(self):
-        has_issues = False
-        node_module_path = os.path.join(self.get_eslint_module_path(), "node_modules")
-
-        for pkg in ESLINT_PACKAGES:
-            name, req_version = pkg.split("@")
-            path = os.path.join(node_module_path, name, "package.json")
-
-            if not os.path.exists(path):
-                print("%s v%s needs to be installed locally." % (name, req_version))
-                has_issues = True
-                continue
-
-            data = json.load(open(path))
-
-            if data["version"] != req_version:
-                print("%s v%s should be v%s." % (name, version, req_version))
-                has_issues = True
-
-        return has_issues
-
-    def node_package_installed(self, package_name="", globalInstall=False, cwd=None):
-        try:
-            npmPath = self.getNodeOrNpmPath("npm")
-
-            cmd = [npmPath, "ls", "--parseable", package_name]
-
-            if globalInstall:
-                cmd.append("-g")
-
-            with open(os.devnull, "w") as fnull:
-                subprocess.check_call(cmd, stdout=fnull, stderr=fnull, cwd=cwd)
-
-            return True
-        except subprocess.CalledProcessError:
-            return False
-
-    def getPossibleNodePathsWin(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 getNodeOrNpmPath(self, filename, minversion=None):
-        """
-        Return the nodejs or npm path.
-        """
-        if platform.system() == "Windows":
-            for ext in [".cmd", ".exe", ""]:
-                try:
-                    nodeOrNpmPath = which.which(filename + ext,
-                                                path=self.getPossibleNodePathsWin())
-                    if self.is_valid(nodeOrNpmPath, minversion):
-                        return nodeOrNpmPath
-                except which.WhichError:
-                    pass
-        else:
-            try:
-                nodeOrNpmPath = which.which(filename)
-                if self.is_valid(nodeOrNpmPath, minversion):
-                    return nodeOrNpmPath
-            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.getPossibleNodePathsWin()
-
-            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(), "testing", "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}.')
--- a/testing/taskcluster/tasks/branches/base_jobs.yml
+++ b/testing/taskcluster/tasks/branches/base_jobs.yml
@@ -517,17 +517,17 @@ tasks:
         - '**/*.jsm'
         - '**/*.jsx'
         - '**/*.html'
         - '**/*.xml'
         # Run when eslint policies change.
         - '**/.eslintignore'
         - '**/*eslintrc*'
         # The plugin implementing custom checks.
-        - 'testing/eslint/eslint-plugin-mozilla/**'
+        - 'tools/lint/eslint/eslint-plugin-mozilla/**'
         # Other misc lint related files.
         - 'tools/lint/**'
         - 'testing/docker/lint/**'
   android-api-15-gradle-dependencies:
     task: tasks/builds/android_api_15_gradle_dependencies.yml
     root: true
     when:
       file_patterns:
--- a/testing/taskcluster/tasks/tests/eslint-gecko.yml
+++ b/testing/taskcluster/tasks/tests/eslint-gecko.yml
@@ -17,22 +17,22 @@ task:
       path: 'public/image.tar'
       taskId: '{{#task_id_for_image}}lint{{/task_id_for_image}}'
 
     command:
       - bash
       - -cx
       - >
           tc-vcs checkout ./gecko {{base_repository}} {{head_repository}} {{head_rev}} {{head_ref}} &&
-          cd gecko/testing/eslint &&
+          cd gecko/tools/lint/eslint &&
           /build/tooltool.py fetch -m manifest.tt &&
           tar xvfz eslint.tar.gz &&
           rm eslint.tar.gz &&
-          cd ../.. &&
-          testing/eslint/node_modules/.bin/eslint --quiet --plugin html --ext [.js,.jsm,.jsx,.xml,.html] -f tools/lint/eslint-formatter .
+          cd ../../.. &&
+          tools/lint/eslint/node_modules/.bin/eslint --quiet --plugin html --ext [.js,.jsm,.jsx,.xml,.html] -f tools/lint/eslint-formatter .
 
   extra:
     locations:
         build: null
         tests: null
     treeherder:
         machine:
             platform: lint
rename from testing/eslint/eslint-plugin-mozilla/LICENSE
rename to tools/lint/eslint/eslint-plugin-mozilla/LICENSE
rename from testing/eslint/eslint-plugin-mozilla/docs/balanced-listeners.rst
rename to tools/lint/eslint/eslint-plugin-mozilla/docs/balanced-listeners.rst
rename from testing/eslint/eslint-plugin-mozilla/docs/import-browserjs-globals.rst
rename to tools/lint/eslint/eslint-plugin-mozilla/docs/import-browserjs-globals.rst
rename from testing/eslint/eslint-plugin-mozilla/docs/import-globals.rst
rename to tools/lint/eslint/eslint-plugin-mozilla/docs/import-globals.rst
rename from testing/eslint/eslint-plugin-mozilla/docs/import-headjs-globals.rst
rename to tools/lint/eslint/eslint-plugin-mozilla/docs/import-headjs-globals.rst
rename from testing/eslint/eslint-plugin-mozilla/docs/index.rst
rename to tools/lint/eslint/eslint-plugin-mozilla/docs/index.rst
rename from testing/eslint/eslint-plugin-mozilla/docs/mark-test-function-used.rst
rename to tools/lint/eslint/eslint-plugin-mozilla/docs/mark-test-function-used.rst
rename from testing/eslint/eslint-plugin-mozilla/docs/no-aArgs.rst
rename to tools/lint/eslint/eslint-plugin-mozilla/docs/no-aArgs.rst
rename from testing/eslint/eslint-plugin-mozilla/docs/no-cpows-in-tests.rst
rename to tools/lint/eslint/eslint-plugin-mozilla/docs/no-cpows-in-tests.rst
rename from testing/eslint/eslint-plugin-mozilla/docs/reject-importGlobalProperties.rst
rename to tools/lint/eslint/eslint-plugin-mozilla/docs/reject-importGlobalProperties.rst
rename from testing/eslint/eslint-plugin-mozilla/docs/var-only-at-top-level.rst
rename to tools/lint/eslint/eslint-plugin-mozilla/docs/var-only-at-top-level.rst
rename from testing/eslint/eslint-plugin-mozilla/lib/globals.js
rename to tools/lint/eslint/eslint-plugin-mozilla/lib/globals.js
rename from testing/eslint/eslint-plugin-mozilla/lib/helpers.js
rename to tools/lint/eslint/eslint-plugin-mozilla/lib/helpers.js
rename from testing/eslint/eslint-plugin-mozilla/lib/index.js
rename to tools/lint/eslint/eslint-plugin-mozilla/lib/index.js
rename from testing/eslint/eslint-plugin-mozilla/lib/processors/xbl-bindings.js
rename to tools/lint/eslint/eslint-plugin-mozilla/lib/processors/xbl-bindings.js
rename from testing/eslint/eslint-plugin-mozilla/lib/rules/.eslintrc
rename to tools/lint/eslint/eslint-plugin-mozilla/lib/rules/.eslintrc
rename from testing/eslint/eslint-plugin-mozilla/lib/rules/balanced-listeners.js
rename to tools/lint/eslint/eslint-plugin-mozilla/lib/rules/balanced-listeners.js
rename from testing/eslint/eslint-plugin-mozilla/lib/rules/import-browserjs-globals.js
rename to tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-browserjs-globals.js
rename from testing/eslint/eslint-plugin-mozilla/lib/rules/import-globals.js
rename to tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-globals.js
rename from testing/eslint/eslint-plugin-mozilla/lib/rules/import-headjs-globals.js
rename to tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-headjs-globals.js
rename from testing/eslint/eslint-plugin-mozilla/lib/rules/mark-test-function-used.js
rename to tools/lint/eslint/eslint-plugin-mozilla/lib/rules/mark-test-function-used.js
rename from testing/eslint/eslint-plugin-mozilla/lib/rules/no-aArgs.js
rename to tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-aArgs.js
rename from testing/eslint/eslint-plugin-mozilla/lib/rules/no-cpows-in-tests.js
rename to tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-cpows-in-tests.js
rename from testing/eslint/eslint-plugin-mozilla/lib/rules/reject-importGlobalProperties.js
rename to tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-importGlobalProperties.js
rename from testing/eslint/eslint-plugin-mozilla/lib/rules/var-only-at-top-level.js
rename to tools/lint/eslint/eslint-plugin-mozilla/lib/rules/var-only-at-top-level.js
rename from testing/eslint/eslint-plugin-mozilla/moz.build
rename to tools/lint/eslint/eslint-plugin-mozilla/moz.build
rename from testing/eslint/eslint-plugin-mozilla/package.json
rename to tools/lint/eslint/eslint-plugin-mozilla/package.json
rename from testing/eslint/manifest.tt
rename to tools/lint/eslint/manifest.tt
rename from testing/eslint/npm-shrinkwrap.json
rename to tools/lint/eslint/npm-shrinkwrap.json
rename from testing/eslint/package.json
rename to tools/lint/eslint/package.json
rename from testing/eslint/update
rename to tools/lint/eslint/update
--- a/testing/eslint/update
+++ b/tools/lint/eslint/update
@@ -1,10 +1,10 @@
 #!/bin/sh
-# Force the scripts working directory to be projdir/testing/eslint.
+# Force the scripts working directory to be projdir/tools/lint/eslint.
 DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
 cd $DIR
 
 echo "To complete this script you will need the following tokens from https://api.pub.build.mozilla.org/tokenauth/"
 echo " - tooltool.upload.public"
 echo " - tooltool.download.public"
 echo ""
 read -p "Are these tokens visible at the above URL (y/n)?" choice
@@ -50,16 +50,16 @@ echo "Downloading tooltool..."
 wget https://raw.githubusercontent.com/mozilla/build-tooltool/master/tooltool.py
 chmod +x tooltool.py
 
 echo "Adding eslint.tar.gz to tooltool..."
 rm manifest.tt
 ./tooltool.py add --visibility public eslint.tar.gz
 
 echo "Uploading eslint.tar.gz to tooltool..."
-./tooltool.py upload --authentication-file=~/.tooltool-token --message "node_modules folder update for testing/eslint"
+./tooltool.py upload --authentication-file=~/.tooltool-token --message "node_modules folder update for tools/lint/eslint"
 
 echo "Cleaning up..."
 rm eslint.tar.gz
 rm tooltool.py
 
 echo ""
 echo "Update complete, please commit and check in your changes."
--- a/tools/lint/mach_commands.py
+++ b/tools/lint/mach_commands.py
@@ -1,15 +1,23 @@
 # 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 subprocess
+import sys
+import which
+from distutils.version import LooseVersion
 
 from mozbuild.base import (
     MachCommandBase,
 )
 
 
 from mach.decorators import (
     CommandArgument,
@@ -17,16 +25,49 @@ from mach.decorators import (
     Command,
     SubCommand,
 )
 
 
 here = os.path.abspath(os.path.dirname(__file__))
 
 
+ESLINT_PACKAGES = [
+    "eslint@2.9.0",
+    "eslint-plugin-html@1.4.0",
+    "eslint-plugin-mozilla@0.0.3",
+    "eslint-plugin-react@4.2.3"
+]
+
+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()
+
+
 @CommandProvider
 class MachCommands(MachCommandBase):
 
     @Command(
         'lint', category='devenv',
         description='Run linters.')
     @CommandArgument(
         'paths', nargs='*', default=None,
@@ -83,8 +124,279 @@ class MachCommands(MachCommandBase):
             if ext != '.lint':
                 continue
 
             if linters and name not in linters:
                 continue
 
             lints.append(os.path.join(here, f))
         return lints
+
+    @Command('eslint', category='devenv',
+             description='Run eslint or help configure eslint for optimal development.')
+    @CommandArgument('-s', '--setup', default=False, action='store_true',
+                     help='configure eslint for optimal development.')
+    @CommandArgument('-e', '--ext', default='[.js,.jsm,.jsx,.xml,.html]',
+                     help='Filename extensions to lint, default: "[.js,.jsm,.jsx,.xml,.html]".')
+    @CommandArgument('-b', '--binary', default=None,
+                     help='Path to eslint binary.')
+    @CommandArgument('args', nargs=argparse.REMAINDER)  # Passed through to eslint.
+    def eslint(self, setup, ext=None, binary=None, args=None):
+        '''Run eslint.'''
+
+        module_path = self.get_eslint_module_path()
+
+        # eslint requires at least node 4.2.3
+        nodePath = self.getNodeOrNpmPath("node", LooseVersion("4.2.3"))
+        if not nodePath:
+            return 1
+
+        if setup:
+            return self.eslint_setup()
+
+        npmPath = self.getNodeOrNpmPath("npm")
+        if not npmPath:
+            return 1
+
+        if self.eslintModuleHasIssues():
+            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
+
+        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, update_only=False):
+        """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)
+
+        npmPath = self.getNodeOrNpmPath("npm")
+        if not npmPath:
+            return 1
+
+        # Install eslint and necessary plugins.
+        for pkg in ESLINT_PACKAGES:
+            name, version = pkg.split("@")
+            success = False
+
+            if self.node_package_installed(pkg, cwd=module_path):
+                success = True
+            else:
+                if pkg.startswith("eslint-plugin-mozilla"):
+                    cmd = [npmPath, "install",
+                           os.path.join(module_path, "eslint-plugin-mozilla")]
+                else:
+                    cmd = [npmPath, "install", pkg]
+
+                print("Installing %s v%s using \"%s\"..."
+                      % (name, version, " ".join(cmd)))
+                success = self.callProcess(pkg, cmd)
+
+            if not success:
+                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 callProcess(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 eslintModuleHasIssues(self):
+        has_issues = False
+        node_module_path = os.path.join(self.get_eslint_module_path(), "node_modules")
+
+        for pkg in ESLINT_PACKAGES:
+            name, req_version = pkg.split("@")
+            path = os.path.join(node_module_path, name, "package.json")
+
+            if not os.path.exists(path):
+                print("%s v%s needs to be installed locally." % (name, req_version))
+                has_issues = True
+                continue
+
+            data = json.load(open(path))
+
+            if data["version"] != req_version:
+                print("%s v%s should be v%s." % (name, data["version"], req_version))
+                has_issues = True
+
+        return has_issues
+
+    def node_package_installed(self, package_name="", globalInstall=False, cwd=None):
+        try:
+            npmPath = self.getNodeOrNpmPath("npm")
+
+            cmd = [npmPath, "ls", "--parseable", package_name]
+
+            if globalInstall:
+                cmd.append("-g")
+
+            with open(os.devnull, "w") as fnull:
+                subprocess.check_call(cmd, stdout=fnull, stderr=fnull, cwd=cwd)
+
+            return True
+        except subprocess.CalledProcessError:
+            return False
+
+    def getPossibleNodePathsWin(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 getNodeOrNpmPath(self, filename, minversion=None):
+        """
+        Return the nodejs or npm path.
+        """
+        if platform.system() == "Windows":
+            for ext in [".cmd", ".exe", ""]:
+                try:
+                    nodeOrNpmPath = which.which(filename + ext,
+                                                path=self.getPossibleNodePathsWin())
+                    if self.is_valid(nodeOrNpmPath, minversion):
+                        return nodeOrNpmPath
+                except which.WhichError:
+                    pass
+        else:
+            try:
+                nodeOrNpmPath = which.which(filename)
+                if self.is_valid(nodeOrNpmPath, minversion):
+                    return nodeOrNpmPath
+            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.getPossibleNodePathsWin()
+
+            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(), "testing", "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}.')