--- 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',
+}