Mach integration and adding linters draft
authorAndrew Halberstadt <ahalberstadt@mozilla.com>
Wed, 24 Feb 2016 16:55:59 -0500
changeset 334316 4fa24dc1511c45cec271dc2a0e6bf4df75530182
parent 334315 816844df905df1a19c72427326ca310548383915
child 514874 9cfba342776f5769770deeb38a90540143381bb0
push id11504
push userahalberstadt@mozilla.com
push dateWed, 24 Feb 2016 22:00:59 +0000
milestone47.0a1
Mach integration and adding linters MozReview-Commit-ID: HGIX7D5QT3W
build/mach_bootstrap.py
build/virtualenv_packages.txt
tools/lint/eslint.lint
tools/lint/flake8.lint
tools/lint/mach_commands.py
tools/lint/moz-transition.lint
--- a/build/mach_bootstrap.py
+++ b/build/mach_bootstrap.py
@@ -50,16 +50,17 @@ environment.
 MERCURIAL_SETUP_FATAL_INTERVAL = 31 * 24 * 60 * 60
 
 
 # TODO Bug 794506 Integrate with the in-tree virtualenv configuration.
 SEARCH_PATHS = [
     'python/mach',
     'python/mozboot',
     'python/mozbuild',
+    'python/mozlint',
     'python/mozversioncontrol',
     'python/blessings',
     'python/compare-locales',
     'python/configobj',
     'python/jsmin',
     'python/psutil',
     'python/which',
     'python/pystache',
@@ -130,16 +131,17 @@ MACH_MODULES = [
     'testing/marionette/mach_commands.py',
     'testing/mochitest/mach_commands.py',
     'testing/mozharness/mach_commands.py',
     'testing/talos/mach_commands.py',
     'testing/taskcluster/mach_commands.py',
     'testing/web-platform/mach_commands.py',
     'testing/xpcshell/mach_commands.py',
     'tools/docs/mach_commands.py',
+    'tools/lint/mach_commands.py',
     'tools/mercurial/mach_commands.py',
     'tools/mach_commands.py',
     'tools/power/mach_commands.py',
     'mobile/android/mach_commands.py',
 ]
 
 
 CATEGORIES = {
--- a/build/virtualenv_packages.txt
+++ b/build/virtualenv_packages.txt
@@ -2,16 +2,17 @@ marionette_driver.pth:testing/marionette
 browsermobproxy.pth:testing/marionette/client/marionette/runner/mixins/browsermob-proxy-py
 wptserve.pth:testing/web-platform/tests/tools/wptserve
 marionette.pth:testing/marionette/client
 blessings.pth:python/blessings
 configobj.pth:python/configobj
 jsmin.pth:python/jsmin
 mach.pth:python/mach
 mozbuild.pth:python/mozbuild
+mozlint.pth:python/mozlint
 pymake.pth:build/pymake
 optional:setup.py:python/psutil:build_ext:--inplace
 optional:psutil.pth:python/psutil
 which.pth:python/which
 ply.pth:other-licenses/ply/
 mock.pth:python/mock-1.0.0
 mozilla.pth:build
 mozilla.pth:config
new file mode 100644
--- /dev/null
+++ b/tools/lint/eslint.lint
@@ -0,0 +1,81 @@
+# -*- 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 subprocess
+from collections import defaultdict
+
+from mozlint import result
+
+
+ESLINT_NOT_FOUND = '''
+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
+
+mach eslint --setup
+
+and try again.
+'''.strip()
+
+EXTENSIONS = ('.js', '.jsm', '.jsx', '.xml', '.html')
+
+def lint(files):
+    import which
+
+    binary = os.environ.get('ESLINT')
+    if not binary:
+        try:
+            binary = which.which('eslint')
+        except which.WhichError:
+            pass
+
+    if not binary:
+        print(ESLINT_NOT_FOUND)
+        return 1
+
+    cmdargs = [binary,
+        '--plugin', 'html',
+        '--ext', '[{}]'.format(','.join(EXTENSIONS)),  # This keeps ext as a single argument.
+        '--format', 'json',
+    ]
+    # Files must come after arguments.
+    cmdargs += files
+
+    proc = subprocess.Popen(cmdargs, stdout=subprocess.PIPE, env=os.environ)
+    output = proc.communicate()[0] or '[]'
+
+    # Format the json output into a list of ResultContainers
+    # for each file.
+    results = []
+    for obj in json.loads(output):
+        path = obj['filePath']
+        errors = obj['messages']
+
+        for error in errors:
+            args = {
+                'column': error.get('column'),
+                'level': 'error' if error['severity'] == 2 else 'warning',
+                'lineno': error['line'],
+                'linter': LINTER['name'],
+                'hint': error.get('fix'),
+                'message': error['message'].rstrip('.'),
+                'path': path,
+                'rule': error.get('ruleId'),
+                'source': error.get('source'),
+            }
+            results.append(result.from_linter(LINTER, **args))
+    return results
+
+
+LINTER = {
+    'name': "eslint",
+    'description': "JavaScript linter",
+    'include': ['**/*{}'.format(ext) for ext in EXTENSIONS],
+    'type': 'external',
+    'payload': lint,
+}
new file mode 100644
--- /dev/null
+++ b/tools/lint/flake8.lint
@@ -0,0 +1,59 @@
+# -*- 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 subprocess
+from collections import defaultdict
+
+from mozlint import result
+
+
+FLAKE8_NOT_FOUND = """
+Could not find flake8! Install flake8 and try again.
+""".strip()
+
+
+def lint(files):
+    import which
+
+    binary = os.environ.get('FLAKE8')
+    if not binary:
+        try:
+            binary = which.which('flake8')
+        except which.WhichError:
+            pass
+
+    if not binary:
+        print(FLAKE8_NOT_FOUND)
+        return 1
+
+    cmdargs = [
+        binary,
+        '--format',
+        '{"path":"%(path)s","lineno":%(row)s,"column":%(col)s,"rule":"%(code)s","message":"%(text)s"}',
+    ] + files
+
+    proc = subprocess.Popen(cmdargs, stdout=subprocess.PIPE, env=os.environ)
+    output = proc.communicate()[0] or '[]'
+
+    results = []
+    for line in output.splitlines():
+        res = json.loads(line)
+        if 'code' in res and res['code'].startswith('W'):
+            res['level'] = 'warning'
+        results.append(result.from_linter(LINTER, **res))
+
+    return results
+
+
+LINTER = {
+    'name': "flake8",
+    'description': "Python linter",
+    'include': ['**/*.py'],
+    'type': 'external',
+    'payload': lint,
+}
new file mode 100644
--- /dev/null
+++ b/tools/lint/mach_commands.py
@@ -0,0 +1,66 @@
+# 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 os
+
+from mozbuild.base import (
+    MachCommandBase,
+)
+
+from mozpack import path as mozpath
+
+from mach.decorators import (
+    CommandArgument,
+    CommandProvider,
+    Command,
+)
+
+
+here = os.path.abspath(os.path.dirname(__file__))
+
+
+@CommandProvider
+class MachCommands(MachCommandBase):
+
+    @Command('lint', category='devenv',
+        description='Run linters.')
+    @CommandArgument('paths', nargs='*', default=None,
+        help="Paths to file or directories to lint, like "
+             "'browser/components/loop' or 'mobile/android'. "
+             "Defaults to the current directory if not given.")
+    @CommandArgument('-l', '--linter',
+        dest='linters', default=None, action='append',
+        help="Linters to run, e.g 'eslint'. By default all linters are run "
+             "for all the appropriate files.")
+    @CommandArgument('-f', '--format',
+        dest='fmt', default='stylish',
+        help="Formatter to use. Defaults to 'stylish'.")
+    def lint(self, paths, linters, fmt):
+        """Run linters."""
+        from mozlint import LintRoller, formatters
+
+        paths = paths or [os.getcwd()]
+        # TODO absolute paths make eslint ignore .eslintrc files
+        #paths = [os.path.abspath(p) for p in paths]
+
+        lints = []
+        files = os.listdir(here)
+        for f in files:
+            name, ext = os.path.splitext(f)
+            if ext != '.lint':
+                continue
+
+            if linters and name not in linters:
+                continue
+
+            lints.append(os.path.join(here, f))
+
+        lint = LintRoller(exclude=['obj*'])
+        lint.read(lints)
+
+        # run all linters
+        formatter = formatters.get(fmt)
+        print(formatter(lint.roll(paths)))
new file mode 100644
--- /dev/null
+++ b/tools/lint/moz-transition.lint
@@ -0,0 +1,18 @@
+# -*- 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/.
+
+LINTER = {
+    'name': "CSSMozTransitionLint",
+    'description': "Prevent -moz css prefix on transition",
+    'message': "unnecessary -moz prefix in -moz-transition",
+    'hint': "Use 'transition' instead.",
+    'include': [
+        '**/*.css',
+    ],
+    'level': 'warning',
+    'type': 'string',
+    'payload': '-moz-transition',
+}