Bug 1369792 - Add a rustfmt linter
MozReview-Commit-ID: 4i55UofYMaa
--- a/python/mozlint/mozlint/formatters/stylish.py
+++ b/python/mozlint/mozlint/formatters/stylish.py
@@ -33,16 +33,17 @@ class StylishFormatter(object):
"""Formatter based on the eslint default."""
# Colors later on in the list are fallbacks in case the terminal
# doesn't support colors earlier in the list.
# See http://www.calmar.ws/vim/256-xterm-24bit-rgb-color-chart.html
_colors = {
'grey': [247, 8, 7],
'red': [1],
+ 'green': [2],
'yellow': [3],
'brightred': [9, 1],
'brightyellow': [11, 3],
}
fmt = " {c1}{lineno}{column} {c2}{level}{normal} {message} {c1}{rule}({linter}){normal}"
fmt_summary = "{t.bold}{c}\u2716 {problem} ({error}, {warning}{failure}){t.normal}"
def __init__(self, disable_colors=None):
@@ -65,23 +66,37 @@ class StylishFormatter(object):
self.max_message = 0
def _update_max(self, err):
"""Calculates the longest length of each token for spacing."""
self.max_lineno = max(self.max_lineno, len(str(err.lineno)))
if err.column:
self.max_column = max(self.max_column, len(str(err.column)))
self.max_level = max(self.max_level, len(str(err.level)))
- self.max_message = max(self.max_message, len(err.message))
+ if '\n' not in err.message:
+ self.max_message = max(self.max_message, len(err.message))
def _pluralize(self, s, num):
if num != 1:
s += 's'
return str(num) + ' ' + s
+ def _format_source(self, source):
+ lines = source.splitlines()
+ source = []
+ for line in lines:
+ if line.startswith('-'):
+ line = self.color('red') + line
+ elif line.startswith('+'):
+ line = self.color('green') + line
+ else:
+ line = self.term.normal + line
+ source.append(' ' + line)
+ return '\n'.join(source)
+
def __call__(self, result, failed=None, **kwargs):
message = []
failed = failed or []
num_errors = 0
num_warnings = 0
for path, errors in sorted(result.iteritems()):
self._reset_max()
@@ -104,16 +119,20 @@ class StylishFormatter(object):
lineno=str(err.lineno).rjust(self.max_lineno),
column=(":" + str(err.column).ljust(self.max_column)) if err.column else "",
level=err.level.ljust(self.max_level),
message=err.message.ljust(self.max_message),
rule='{} '.format(err.rule) if err.rule else '',
linter=err.linter.lower(),
))
+ if err.source:
+ source = self._format_source(err.source)
+ message.append(source)
+
message.append('') # newline
# If there were failures, make it clear which linters failed
for fail in failed:
message.append("{c}A failure occured in the {name} linter.".format(
c=self.color('brightred'),
name=fail,
))
--- a/python/mozlint/mozlint/formatters/treeherder.py
+++ b/python/mozlint/mozlint/formatters/treeherder.py
@@ -23,9 +23,12 @@ class TreeherderFormatter(object):
assert isinstance(err, ResultContainer)
d = {s: getattr(err, s) for s in err.__slots__}
d["column"] = ":%s" % d["column"] if d["column"] else ""
d['level'] = d['level'].upper()
d['rule'] = d['rule'] or d['linter']
message.append(self.fmt.format(**d))
+ if d.get('source'):
+ message.append(d['source'])
+
return "\n".join(message)
--- a/python/mozlint/mozlint/pathutils.py
+++ b/python/mozlint/mozlint/pathutils.py
@@ -7,17 +7,17 @@ from __future__ import unicode_literals,
import os
from mozpack import path as mozpath
from mozpack.files import FileFinder
class FilterPath(object):
"""Helper class to make comparing and matching file paths easier."""
- def __init__(self, path, exclude=None):
+ def __init__(self, path, exclude=()):
self.path = os.path.normpath(path)
self._finder = None
self.exclude = exclude
@property
def finder(self):
if self._finder:
return self._finder
@@ -37,17 +37,17 @@ class FilterPath(object):
def isfile(self):
return os.path.isfile(self.path)
@property
def isdir(self):
return os.path.isdir(self.path)
def join(self, *args):
- return FilterPath(os.path.join(self, *args))
+ return FilterPath(os.path.join(self.path, *args))
def match(self, patterns):
return any(mozpath.match(self.path, pattern.path) for pattern in patterns)
def contains(self, other):
"""Return True if other is a subdirectory of self or equals self."""
if isinstance(other, FilterPath):
other = other.path
new file mode 100644
--- /dev/null
+++ b/tools/lint/rustfmt.yml
@@ -0,0 +1,8 @@
+rustfmt:
+ description: Rust linter
+ include:
+ - testing/geckodriver
+ exclude: []
+ extensions: ['.rs']
+ type: external
+ payload: rustfmt:lint
new file mode 100644
--- /dev/null
+++ b/tools/lint/rustfmt/__init__.py
@@ -0,0 +1,103 @@
+# 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 unicode_literals
+
+import os
+from xml.etree import ElementTree
+
+from mozprocess import ProcessHandler
+
+from mozlint.pathutils import FilterPath
+from mozlint.result import from_config
+
+
+ERROR_PREFIXES = ("Rustfmt failed at", "Diff in")
+
+
+def find_files(paths):
+ files = []
+ for p in paths:
+ if os.path.isfile(p):
+ files.append(p)
+ continue
+
+ p = FilterPath(p)
+ for name, f in p.finder.find('**/*.rs'):
+ files.append(p.join(name).path)
+
+ return files
+
+
+def parse_diff(lines):
+ header = lines[0]
+ diff = '\n'.join(lines[1:]).rstrip()
+
+ header = header[len(ERROR_PREFIXES[1]):]
+ path, lineno = header.strip(" :").split("at line")
+ return {
+ 'path': path,
+ 'lineno': lineno,
+ 'level': 'error',
+ 'message': 'bad formatting in ' + os.path.basename(path),
+ 'source': diff,
+ }
+
+
+def parse_failure(lines):
+ line = lines[0]
+ line = line[len(ERROR_PREFIXES[0]):]
+ tokens = line.split(':')
+ return {
+ 'path': tokens[0],
+ 'lineno': tokens[1],
+ 'level': 'error',
+ 'message': tokens[2],
+ }
+
+
+def find_errors(lines):
+ buf = []
+ for line in lines:
+ line = line.decode('utf-8')
+ if buf and line.startswith(ERROR_PREFIXES):
+ yield buf
+ buf = []
+
+ if line.startswith(ERROR_PREFIXES[0]):
+ yield [line]
+ continue
+
+ buf.append(line)
+
+ if buf:
+ yield buf
+
+
+def lint(paths, config, **lintargs):
+ # rustfmt can't handle directories, so if we have any directories
+ # first find all .rs files under them.
+ files = find_files(paths)
+
+ extra_args = lintargs.get('extra_args') or []
+ cmd = [
+ 'rustfmt',
+ '--write-mode=diff',
+ ] + extra_args + files
+
+ proc = ProcessHandler(cmd, stream=None)
+ proc.run()
+ proc.wait()
+
+ results = []
+ for error in find_errors(proc.output):
+ if len(error) == 1 and error[0]:
+ errobj = parse_failure(error)
+ elif len(error) > 1:
+ errobj = parse_diff(error)
+ else:
+ continue
+
+ results.append(from_config(config, **errobj))
+ return results