Bug 1177933 - Add |mach eslint| command. r=mcomella,dmose
authorNick Alexander <nalexander@mozilla.com>
Thu, 02 Jul 2015 12:18:52 -0700
changeset 270164 0105f77365edd07b16f13a3286ffc92b49701218
parent 270163 d57875a9877ea2dd66edb06811c514a3a450da96
child 270165 98101796b275e229828430c6c622085e6c21fece
push id5067
push userraliiev@mozilla.com
push dateMon, 21 Sep 2015 14:04:52 +0000
treeherdermozilla-esr52@14221ffe5b2f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmcomella, dmose
bugs1177933
milestone42.0a1
Bug 1177933 - Add |mach eslint| command. r=mcomella,dmose DONTBUILD NPOTB
python/mach_commands.py
--- a/python/mach_commands.py
+++ b/python/mach_commands.py
@@ -3,42 +3,54 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from __future__ import absolute_import, print_function, unicode_literals
 
 import argparse
 import logging
 import mozpack.path as mozpath
 import os
+import which
 
 from mozbuild.base import (
     MachCommandBase,
 )
 
 from mach.decorators import (
     CommandArgument,
     CommandProvider,
     Command,
 )
 
 
+ESLINT_NOT_FOUND_MESSAGE = '''
+Could not find eslint!  We looked at the --binary option, at the ESLINT
+environment variable, and then at your path.  Install eslint and needed plugins
+with
+
+npm install -g eslint eslint-plugin-react
+
+and try again.
+'''.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
         self.log_manager.terminal_handler.setLevel(logging.CRITICAL)
 
         self._activate_virtualenv()
 
         return self.run_process([self.virtualenv_manager.python_path] + args,
-            pass_thru=True, # Allow user to run Python interactively.
-            ensure_exit_code=False, # Don't throw on non-zero exit code.
+            pass_thru=True,  # Allow user to run Python interactively.
+            ensure_exit_code=False,  # Don't throw on non-zero exit code.
             # Note: subprocess requires native strings in os.environ on Windows
             append_env={b'PYTHONDONTWRITEBYTECODE': str('1')})
 
     @Command('python-test', category='testing',
         description='Run Python unit tests.')
     @CommandArgument('--verbose',
         default=False,
         action='store_true',
@@ -84,24 +96,25 @@ class MachCommands(MachCommandBase):
                 elif d == last_search_dir:
                     self.log(logging.WARN, 'python-test',
                              {'test': t},
                              'TEST-UNEXPECTED-FAIL | Invalid test: {test}')
                     if stop:
                         return 1
 
         for f in files:
-            file_displayed_test = [] # Used as a boolean.
+            file_displayed_test = []  # Used as a boolean.
+
             def _line_handler(line):
                 if not file_displayed_test and line.startswith('TEST-'):
                     file_displayed_test.append(True)
 
             inner_return_code = self.run_process(
                 [self.virtualenv_manager.python_path, f],
-                ensure_exit_code=False, # Don't throw on non-zero exit code.
+                ensure_exit_code=False,  # Don't throw on non-zero exit code.
                 log_name='python-test',
                 # subprocess requires native strings in os.environ on Windows
                 append_env={b'PYTHONDONTWRITEBYTECODE': str('1')},
                 line_handler=_line_handler)
             return_code += inner_return_code
 
             if not file_displayed_test:
                 self.log(logging.WARN, 'python-test', {'file': f},
@@ -113,8 +126,59 @@ class MachCommands(MachCommandBase):
                              'Test failed: {file}')
                 else:
                     self.log(logging.INFO, 'python-test', {'file': f},
                              'Test passed: {file}')
             if stop and return_code > 0:
                 return 1
 
         return 0 if return_code == 0 else 1
+
+    @Command('eslint', category='devenv')
+    @CommandArgument('path', nargs='?', default='.',
+        help='Path to files to lint, like "browser/components/loop" '
+            'or "mobile/android". '
+            'Defaults to the current directory if not given.')
+    @CommandArgument('-e', '--ext', default='[.js,.jsm,.jsx]',
+        help='Filename extensions to lint, default: "[.js,.jsm,.jsx]".')
+    @CommandArgument('-b', '--binary', default=None,
+        help='Path to eslint binary.')
+    @CommandArgument('args', nargs=argparse.REMAINDER)  # Passed through to eslint.
+    def eslint(self, path, ext=None, binary=None, args=[]):
+        '''Run eslint.'''
+
+        if not binary:
+            binary = os.environ.get('ESLINT', None)
+            if not binary:
+                try:
+                    binary = which.which('eslint')
+                except which.WhichError:
+                    pass
+
+        if not binary:
+            print(ESLINT_NOT_FOUND_MESSAGE)
+            return 1
+
+        # The cwd below is unfortunate.  eslint --config=PATH/TO/.eslintrc works,
+        # but --ignore-path=PATH/TO/.eslintignore treats paths as relative to
+        # the current directory, rather than as relative to the location of
+        # .eslintignore (see https://github.com/eslint/eslint/issues/1382).
+        # mach commands always execute in the topsrcdir, so we could make all
+        # paths in .eslint relative to the topsrcdir, but it's not clear if
+        # that's a good choice for future eslint and IDE integrations.
+        # Unfortunately, running after chdir does not print the full path to
+        # files (convenient for opening with copy-and-paste).  In the meantime,
+        # we just print the active path.
+
+        self.log(logging.INFO, 'eslint', {'binary': binary, 'path': path},
+            'Running {binary} in {path}')
+
+        cmd_args = [binary,
+            '--ext', ext,  # This keeps ext as a single argument.
+        ] + args
+        # Path must come after arguments.  Path is '.' due to cwd below.
+        cmd_args += ['.']
+
+        return self.run_process(cmd_args,
+            cwd=path,
+            pass_thru=True,  # Allow user to run eslint interactively.
+            ensure_exit_code=False,  # Don't throw on non-zero exit code.
+        )