Bug 1369792 - Add a rustfmt linter draft
authorAndrew Halberstadt <ahalberstadt@mozilla.com>
Tue, 06 Jun 2017 09:11:17 -0400
changeset 589725 e29df8fa1da58debab1f3013eb292014e07ba4f8
parent 589724 ddede19687e9ac17315284954dba054e43d194fe
child 631984 f226d106ec3ab17c9c9f2c44577ed8bbb1754e0c
push id62482
push userahalberstadt@mozilla.com
push dateTue, 06 Jun 2017 17:52:36 +0000
bugs1369792
milestone55.0a1
Bug 1369792 - Add a rustfmt linter MozReview-Commit-ID: 4i55UofYMaa
python/mozlint/mozlint/formatters/stylish.py
python/mozlint/mozlint/formatters/treeherder.py
python/mozlint/mozlint/pathutils.py
tools/lint/rustfmt.yml
tools/lint/rustfmt/__init__.py
--- 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