Bug 1379151 - Add --fix and --edit to mozlint, r?Standard8
While --fix previously worked with eslint, it is now more official (will show
up in the |mach lint --help|). ESlint is still the only thing that implements
it, but we can implement it for flake8 using the `autopep8` module.
--edit is a new concept that will open an editor for each failing file to let
you fix the errors manually. For now it is very naive (just opens the file),
and is only really useful if you have an editor integration for the linter(s).
But in the future I'd like to have editor-specific implementations for this.
For example, with vim, we can use -q to pass in an error file that will start
the editor pre-populated with a list of all errors that can then be easily
jumped to. Other editors may just open up to the line containing the error.
--fix and --edit can be used in conjunction with one another. Doing that means
only errors that can't be fixed automatically will show up in your editor.
MozReview-Commit-ID: 5JJJhMIrMIB
--- a/python/mozlint/mozlint/cli.py
+++ b/python/mozlint/mozlint/cli.py
@@ -1,18 +1,21 @@
# 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 print_function, unicode_literals
import os
+import subprocess
import sys
from argparse import REMAINDER, ArgumentParser
+from mozlint.formatters import all_formatters
+
SEARCH_PATHS = []
class MozlintParser(ArgumentParser):
arguments = [
[['paths'],
{'nargs': '*',
'default': None,
@@ -31,16 +34,17 @@ class MozlintParser(ArgumentParser):
{'dest': 'list_linters',
'default': False,
'action': 'store_true',
'help': "List all available linters and exit.",
}],
[['-f', '--format'],
{'dest': 'fmt',
'default': 'stylish',
+ 'choices': all_formatters.keys(),
'help': "Formatter to use. Defaults to 'stylish'.",
}],
[['-n', '--no-filter'],
{'dest': 'use_filters',
'default': True,
'action': 'store_false',
'help': "Ignore all filtering. This is useful for quickly "
"testing a directory that otherwise wouldn't be run, "
@@ -58,16 +62,28 @@ class MozlintParser(ArgumentParser):
{'const': 'all',
'nargs': '?',
'choices': ['staged', 'all'],
'help': "Lint files touched by changes in the working directory "
"(i.e haven't been committed yet). On git, --workdir=staged "
"can be used to only consider staged files. Works with "
"mercurial or git.",
}],
+ [['--fix'],
+ {'action': 'store_true',
+ 'default': False,
+ 'help': "Fix lint errors if possible. Any errors that could not be fixed "
+ "will be printed as normal."
+ }],
+ [['--edit'],
+ {'action': 'store_true',
+ 'default': False,
+ 'help': "Each file containing lint errors will be opened in $EDITOR one after "
+ "the other."
+ }],
[['extra_args'],
{'nargs': REMAINDER,
'help': "Extra arguments that will be forwarded to the underlying linter.",
}],
]
def __init__(self, **kwargs):
ArgumentParser.__init__(self, usage=self.__doc__, **kwargs)
@@ -82,18 +98,24 @@ class MozlintParser(ArgumentParser):
i = args[0].index(token)
args[0].pop(i)
args[0][i:i] = [token[:2], '-' + token[2]]
# This is here so the eslint mach command doesn't lose 'extra_args'
# when using mach's dispatch functionality.
args, extra = ArgumentParser.parse_known_args(self, *args, **kwargs)
args.extra_args = extra
+
+ self.validate(args)
return args, extra
+ def validate(self, args):
+ if args.edit and not os.environ.get('EDITOR'):
+ self.error("must set the $EDITOR environment variable to use --edit")
+
def find_linters(linters=None):
lints = []
for search_path in SEARCH_PATHS:
if not os.path.isdir(search_path):
continue
sys.path.insert(0, search_path)
@@ -108,17 +130,17 @@ def find_linters(linters=None):
if linters and name not in linters:
continue
lints.append(os.path.join(search_path, f))
return lints
-def run(paths, linters, fmt, outgoing, workdir, list_linters=None, **lintargs):
+def run(paths, linters, fmt, outgoing, workdir, edit, list_linters=None, **lintargs):
from mozlint import LintRoller, formatters
if list_linters:
lint_paths = find_linters(linters)
print("Available linters: {}".format(
[os.path.splitext(os.path.basename(l))[0] for l in lint_paths]
))
return 0
@@ -128,16 +150,23 @@ def run(paths, linters, fmt, outgoing, w
# Check if the path that is entered is a valid one.
invalid_paths = [path for path in paths if not os.path.exists(path)]
if invalid_paths:
print("Error: The following paths do not exist:\n{}".format("\n".join(invalid_paths)))
return 1
# run all linters
results = lint.roll(paths, outgoing=outgoing, workdir=workdir)
+
+ if edit:
+ editor = os.environ['EDITOR']
+ for path in results:
+ subprocess.call([editor, path])
+ return 1 if lint.failed else 0
+
formatter = formatters.get(fmt)
# Encode output with 'replace' to avoid UnicodeEncodeErrors on
# environments that aren't using utf-8.
out = formatter(results, failed=lint.failed).encode(
sys.stdout.encoding or 'ascii', 'replace')
if out:
print(out)
--- a/python/mozlint/test/linters/external.py
+++ b/python/mozlint/test/linters/external.py
@@ -6,16 +6,20 @@ from mozlint import result
from mozlint.errors import LintException
def badreturncode(files, config, **lintargs):
return 1
def external(files, config, **lintargs):
+ if lintargs.get('fix'):
+ # mimics no results because they got fixed
+ return []
+
results = []
for path in files:
with open(path, 'r') as fh:
for i, line in enumerate(fh.readlines()):
if 'foobar' in line:
results.append(result.from_config(
config, path=path, lineno=i+1, column=1, rule="no-foobar"))
return results
--- a/python/mozlint/test/python.ini
+++ b/python/mozlint/test/python.ini
@@ -1,11 +1,12 @@
[DEFAULT]
subsuite = mozlint, os == "linux"
+[test_cli.py]
[test_formatters.py]
[test_parser.py]
[test_roller.py]
[test_types.py]
[test_vcs.py]
# these tests run in the build images on non-linux, which have old
# versions of mercurial and git installed
skip-if = os != "linux"
new file mode 100644
--- /dev/null
+++ b/python/mozlint/test/test_cli.py
@@ -0,0 +1,55 @@
+# 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 os
+import sys
+
+import pytest
+
+from mozlint import cli
+
+here = os.path.abspath(os.path.dirname(__file__))
+
+
+@pytest.fixture
+def parser():
+ return cli.MozlintParser()
+
+
+@pytest.fixture
+def run(parser, lintdir, files):
+ if lintdir not in cli.SEARCH_PATHS:
+ cli.SEARCH_PATHS.append(lintdir)
+
+ def inner(args=None):
+ args = args or []
+ args.extend(files)
+ lintargs = vars(parser.parse_args(args))
+ lintargs['root'] = here
+ return cli.run(**lintargs)
+ return inner
+
+
+def test_cli_run_with_fix(run, capfd):
+ ret = run(['-f', 'json', '--fix', '--linter', 'external'])
+ out, err = capfd.readouterr()
+ assert ret == 0
+ assert out.endswith('{}\n')
+
+
+def test_cli_run_with_edit(run, parser, capfd):
+ os.environ['EDITOR'] = 'echo'
+
+ ret = run(['-f', 'json', '--edit', '--linter', 'external'])
+ out = capfd.readouterr()[0].strip()
+ assert ret == 0
+ assert os.path.basename(out) == 'foobar.js'
+
+ del os.environ['EDITOR']
+ with pytest.raises(SystemExit):
+ parser.parse_args(['--edit'])
+
+
+if __name__ == '__main__':
+ sys.exit(pytest.main(['--verbose', __file__]))