Bug 1466070 - WIP - Add clang-format as target of ./mach lint - WIP draft
authorSylvestre Ledru <sledru@mozilla.com>
Tue, 22 May 2018 08:30:25 -0700 (2018-05-22)
changeset 802614 a2d0e53b917ebf29dc584a6d9edfdf268cd4c55a
parent 798298 45efd00707fce5cd4ed1616c1cf44742b76cd0d3
push id111931
push usersledru@mozilla.com
push dateFri, 01 Jun 2018 09:38:36 +0000 (2018-06-01)
bugs1466070
milestone62.0a1
Bug 1466070 - WIP - Add clang-format as target of ./mach lint - WIP MozReview-Commit-ID: 7snowgEgnf6
python/mozbuild/mozbuild/clang_format/__init__.py
python/mozbuild/mozbuild/clang_format/commands.py
python/mozbuild/mozbuild/clang_format/files.py
python/mozbuild/mozbuild/mach_commands.py
tools/lint/clang-format.yml
tools/lint/clang-format/__init__.py
new file mode 100644
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/clang_format/commands.py
@@ -0,0 +1,141 @@
+# 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 files
+
+def get_clang_format_diff_command(repository):
+    if repository.name == 'hg':
+        args = ["hg", "diff", "-U0", "-r" ".^"]
+        for dot_extension in self._format_include_extensions:
+            args += ['--include', 'glob:**{0}'.format(dot_extension)]
+        args += ['--exclude', 'listfile:{0}'.format(self._format_ignore_file)]
+    else:
+        args = ["git", "diff", "--no-color", "-U0", "HEAD", "--"]
+        for dot_extension in self._format_include_extensions:
+            args += ['*{0}'.format(dot_extension)]
+        # git-diff doesn't support an 'exclude-from-files' param, but
+        # allow to add individual exclude pattern since v1.9, see
+        # https://git-scm.com/docs/gitglossary#gitglossary-aiddefpathspecapathspec
+        with open(self._format_ignore_file, 'rb') as exclude_pattern_file:
+            for pattern in exclude_pattern_file.readlines():
+                pattern = pattern.rstrip()
+                pattern = pattern.replace('.*', '**')
+                if not pattern or pattern.startswith('#'):
+                    continue  # empty or comment
+                magics = ['exclude']
+                if pattern.startswith('^'):
+                    magics += ['top']
+                    pattern = pattern[1:]
+                args += [':({0}){1}'.format(','.join(magics), pattern)]
+    return args
+
+def _run_clang_format_diff(self, clang_format_diff, clang_format, show):
+    # Run clang-format on the diff
+    # Note that this will potentially miss a lot things
+    from subprocess import Popen, PIPE, check_output, CalledProcessError
+
+    diff_process = Popen(self._get_clang_format_diff_command(), stdout=PIPE)
+    args = [sys.executable, clang_format_diff, "-p1", "-binary=%s" % clang_format]
+
+    if not show:
+        args.append("-i")
+    try:
+        output = check_output(args, stdin=diff_process.stdout)
+        if show:
+            # We want to print the diffs
+            print(output)
+        return 0
+    except CalledProcessError as e:
+        # Something wrong happend
+        print("clang-format: An error occured while running clang-format-diff.")
+        return e.returncode
+
+
+def run_clang_format_path(repository, clang_format, show, paths):
+    # Run clang-format on files or directories directly
+    from subprocess import check_output, CalledProcessError
+
+    args = [clang_format, "-i"]
+
+    path_list = files.generate_path_list("/home/sylvestre/dev/mozilla/mozilla-central.hg", paths)
+
+    if path_list == []:
+        return
+
+    print("Processing %d file(s)..." % len(path_list))
+
+    batchsize = 200
+
+    for i in range(0, len(path_list), batchsize):
+        l = path_list[i: (i + batchsize)]
+        # Run clang-format on the list
+        try:
+            check_output(args + l)
+        except CalledProcessError as e:
+            # Something wrong happend
+            print("clang-format: An error occured while running clang-format.")
+            return e.returncode
+
+    if show:
+        # show the diff
+        if repository.name == 'hg':
+            diff_command = ["hg", "diff"] + paths
+        else:
+            assert repository.name == 'git'
+            diff_command = ["git", "diff"] + paths
+        try:
+            output = check_output(diff_command)
+            print(output)
+        except CalledProcessError as e:
+            # Something wrong happend
+            print("clang-format: Unable to run the diff command.")
+            return e.returncode
+    return 0
+
+def get_clang_format_diff_command(repository):
+    if repository.name == 'hg':
+        args = ["hg", "diff", "-U0", "-r" ".^"]
+        for dot_extension in self._format_include_extensions:
+            args += ['--include', 'glob:**{0}'.format(dot_extension)]
+        args += ['--exclude', 'listfile:{0}'.format(self._format_ignore_file)]
+    else:
+        args = ["git", "diff", "--no-color", "-U0", "HEAD", "--"]
+        for dot_extension in self._format_include_extensions:
+            args += ['*{0}'.format(dot_extension)]
+        # git-diff doesn't support an 'exclude-from-files' param, but
+        # allow to add individual exclude pattern since v1.9, see
+        # https://git-scm.com/docs/gitglossary#gitglossary-aiddefpathspecapathspec
+        with open(self._format_ignore_file, 'rb') as exclude_pattern_file:
+            for pattern in exclude_pattern_file.readlines():
+                pattern = pattern.rstrip()
+                pattern = pattern.replace('.*', '**')
+                if not pattern or pattern.startswith('#'):
+                    continue  # empty or comment
+                magics = ['exclude']
+                if pattern.startswith('^'):
+                    magics += ['top']
+                    pattern = pattern[1:]
+                args += [':({0}){1}'.format(','.join(magics), pattern)]
+    return args
+
+def _run_clang_format_diff(self, clang_format_diff, clang_format, show):
+    # Run clang-format on the diff
+    # Note that this will potentially miss a lot things
+    from subprocess import Popen, PIPE, check_output, CalledProcessError
+
+    diff_process = Popen(self._get_clang_format_diff_command(), stdout=PIPE)
+    args = [sys.executable, clang_format_diff, "-p1", "-binary=%s" % clang_format]
+
+    if not show:
+        args.append("-i")
+    try:
+        output = check_output(args, stdin=diff_process.stdout)
+        if show:
+            # We want to print the diffs
+            print(output)
+        return 0
+    except CalledProcessError as e:
+        # Something wrong happend
+        print("clang-format: An error occured while running clang-format-diff.")
+        return e.returncode
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/clang_format/files.py
@@ -0,0 +1,57 @@
+# 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 re
+
+# List of file extension to consider (should start with dot)
+_format_include_extensions = ('.cpp', '.c', '.h')
+# File contaning all paths to exclude from formatting
+_format_ignore_file = '.clang-format-ignore'
+
+def is_ignored_path(topsrcdir, ignored_dir_re, f):
+    # Remove upto topsrcdir in pathname and match
+    if f.startswith(topsrcdir + '/'):
+        match_f = f[len(topsrcdir + '/'):]
+    else:
+        match_f = f
+    return re.match(ignored_dir_re, match_f)
+
+def generate_path_list(topsrcdir, paths):
+    path_to_third_party = os.path.join(topsrcdir, _format_ignore_file)
+    ignored_dir = []
+    with open(path_to_third_party, 'r') as fh:
+        for line in fh:
+            # Remove comments and empty lines
+            if line.startswith('#') or len(line.strip()) == 0:
+                continue
+            # The regexp is to make sure we are managing relative paths
+            ignored_dir.append(r"^[\./]*" + line.rstrip())
+
+    # Generates the list of regexp
+    ignored_dir_re = '(%s)' % '|'.join(ignored_dir)
+    extensions = _format_include_extensions
+
+    path_list = []
+    for f in paths:
+        if is_ignored_path(topsrcdir, ignored_dir_re, f):
+            # Early exit if we have provided an ignored directory
+            print("clang-format: Ignored third party code '{0}'".format(f))
+            continue
+
+        if os.path.isdir(f):
+            # Processing a directory, generate the file list
+            for folder, subs, files in os.walk(f):
+                subs.sort()
+                for filename in sorted(files):
+                    f_in_dir = os.path.join(folder, filename)
+                    if (f_in_dir.endswith(extensions)
+                        and not is_ignored_path(topsrcdir, ignored_dir_re, f_in_dir)):
+                        # Supported extension and accepted path
+                        path_list.append(f_in_dir)
+        else:
+            if f.endswith(extensions):
+                path_list.append(f)
+
+    return path_list
--- a/python/mozbuild/mozbuild/mach_commands.py
+++ b/python/mozbuild/mozbuild/mach_commands.py
@@ -1,14 +1,19 @@
 # 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
 
+from mozbuild.clang_format import (
+    files,
+    commands
+)
+
 import argparse
 import hashlib
 import itertools
 import json
 import logging
 import operator
 import os
 import re
@@ -1595,21 +1600,16 @@ class StaticAnalysisMonitor(object):
             return (warning, False)
         return (warning, True)
 
 
 @CommandProvider
 class StaticAnalysis(MachCommandBase):
     """Utilities for running C++ static analysis checks and format."""
 
-    # List of file extension to consider (should start with dot)
-    _format_include_extensions = ('.cpp', '.c', '.h')
-    # File contaning all paths to exclude from formatting
-    _format_ignore_file = '.clang-format-ignore'
-
     @Command('static-analysis', category='testing',
              description='Run C++ static analysis checks')
     def static_analysis(self):
         # If not arguments are provided, just print a help message.
         mach = Mach(os.getcwd())
         mach.run(['static-analysis', '--help'])
 
     @StaticAnalysisSubCommand('static-analysis', 'check',
@@ -1854,20 +1854,20 @@ class StaticAnalysis(MachCommandBase):
 
         os.chdir(self.topsrcdir)
 
         rc = self._get_clang_tools(verbose=verbose)
         if rc != 0:
             return rc
 
         if path is None:
-            return self._run_clang_format_diff(self._clang_format_diff,
+            return commands.run_clang_format_diff(self._clang_format_diff,
                                                self._clang_format_path, show)
         else:
-            return self._run_clang_format_path(self._clang_format_path, show, path)
+            return commands.run_clang_format_path(self._clang_format_path, show, path)
 
     def _verify_checker(self, item):
         check = item['name']
         test_file_path = mozpath.join(self._clang_tidy_base_path, "test", check)
         test_file_path_cpp = test_file_path + '.cpp'
         test_file_path_json = test_file_path + '.json'
 
         self.log(logging.INFO, 'static-analysis', {},"RUNNING: clang-tidy checker {}.".format(check))
@@ -2125,149 +2125,16 @@ class StaticAnalysis(MachCommandBase):
                             'the expected output')
 
         assert os.path.exists(self._clang_tidy_path)
         assert os.path.exists(self._clang_format_path)
         assert os.path.exists(self._clang_apply_replacements)
         assert os.path.exists(self._run_clang_tidy_path)
         return 0
 
-    def _get_clang_format_diff_command(self):
-        if self.repository.name == 'hg':
-            args = ["hg", "diff", "-U0", "-r" ".^"]
-            for dot_extension in self._format_include_extensions:
-                args += ['--include', 'glob:**{0}'.format(dot_extension)]
-            args += ['--exclude', 'listfile:{0}'.format(self._format_ignore_file)]
-        else:
-            args = ["git", "diff", "--no-color", "-U0", "HEAD", "--"]
-            for dot_extension in self._format_include_extensions:
-                args += ['*{0}'.format(dot_extension)]
-            # git-diff doesn't support an 'exclude-from-files' param, but
-            # allow to add individual exclude pattern since v1.9, see
-            # https://git-scm.com/docs/gitglossary#gitglossary-aiddefpathspecapathspec
-            with open(self._format_ignore_file, 'rb') as exclude_pattern_file:
-                for pattern in exclude_pattern_file.readlines():
-                    pattern = pattern.rstrip()
-                    pattern = pattern.replace('.*', '**')
-                    if not pattern or pattern.startswith('#'):
-                        continue  # empty or comment
-                    magics = ['exclude']
-                    if pattern.startswith('^'):
-                        magics += ['top']
-                        pattern = pattern[1:]
-                    args += [':({0}){1}'.format(','.join(magics), pattern)]
-        return args
-
-    def _run_clang_format_diff(self, clang_format_diff, clang_format, show):
-        # Run clang-format on the diff
-        # Note that this will potentially miss a lot things
-        from subprocess import Popen, PIPE, check_output, CalledProcessError
-
-        diff_process = Popen(self._get_clang_format_diff_command(), stdout=PIPE)
-        args = [sys.executable, clang_format_diff, "-p1", "-binary=%s" % clang_format]
-
-        if not show:
-            args.append("-i")
-        try:
-            output = check_output(args, stdin=diff_process.stdout)
-            if show:
-                # We want to print the diffs
-                print(output)
-            return 0
-        except CalledProcessError as e:
-            # Something wrong happend
-            print("clang-format: An error occured while running clang-format-diff.")
-            return e.returncode
-
-    def _is_ignored_path(self, ignored_dir_re, f):
-        # Remove upto topsrcdir in pathname and match
-        if f.startswith(self.topsrcdir + '/'):
-            match_f = f[len(self.topsrcdir + '/'):]
-        else:
-            match_f = f
-        return re.match(ignored_dir_re, match_f)
-
-    def _generate_path_list(self, paths):
-        path_to_third_party = os.path.join(self.topsrcdir, self._format_ignore_file)
-        ignored_dir = []
-        with open(path_to_third_party, 'r') as fh:
-            for line in fh:
-                # Remove comments and empty lines
-                if line.startswith('#') or len(line.strip()) == 0:
-                    continue
-                # The regexp is to make sure we are managing relative paths
-                ignored_dir.append(r"^[\./]*" + line.rstrip())
-
-        # Generates the list of regexp
-        ignored_dir_re = '(%s)' % '|'.join(ignored_dir)
-        extensions = self._format_include_extensions
-
-        path_list = []
-        for f in paths:
-            if self._is_ignored_path(ignored_dir_re, f):
-                # Early exit if we have provided an ignored directory
-                print("clang-format: Ignored third party code '{0}'".format(f))
-                continue
-
-            if os.path.isdir(f):
-                # Processing a directory, generate the file list
-                for folder, subs, files in os.walk(f):
-                    subs.sort()
-                    for filename in sorted(files):
-                        f_in_dir = os.path.join(folder, filename)
-                        if (f_in_dir.endswith(extensions)
-                            and not self._is_ignored_path(ignored_dir_re, f_in_dir)):
-                            # Supported extension and accepted path
-                            path_list.append(f_in_dir)
-            else:
-                if f.endswith(extensions):
-                    path_list.append(f)
-
-        return path_list
-
-    def _run_clang_format_path(self, clang_format, show, paths):
-        # Run clang-format on files or directories directly
-        from subprocess import check_output, CalledProcessError
-
-        args = [clang_format, "-i"]
-
-        path_list = self._generate_path_list(paths)
-
-        if path_list == []:
-            return
-
-        print("Processing %d file(s)..." % len(path_list))
-
-        batchsize = 200
-
-        for i in range(0, len(path_list), batchsize):
-            l = path_list[i: (i + batchsize)]
-            # Run clang-format on the list
-            try:
-                check_output(args + l)
-            except CalledProcessError as e:
-                # Something wrong happend
-                print("clang-format: An error occured while running clang-format.")
-                return e.returncode
-
-        if show:
-            # show the diff
-            if self.repository.name == 'hg':
-                diff_command = ["hg", "diff"] + paths
-            else:
-                assert self.repository.name == 'git'
-                diff_command = ["git", "diff"] + paths
-            try:
-                output = check_output(diff_command)
-                print(output)
-            except CalledProcessError as e:
-                # Something wrong happend
-                print("clang-format: Unable to run the diff command.")
-                return e.returncode
-        return 0
 
 @CommandProvider
 class Vendor(MachCommandBase):
     """Vendor third-party dependencies into the source repository."""
 
     @Command('vendor', category='misc',
              description='Vendor third-party dependencies into the source repository.')
     def vendor(self):
new file mode 100644
--- /dev/null
+++ b/tools/lint/clang-format.yml
@@ -0,0 +1,14 @@
+---
+clang-format:
+    description: C/C++ coding style
+    include:
+        - dom/presentation/
+    exclude:
+        - third_party
+    extensions:
+        - cpp
+        - c
+    support-files:
+        - 'tools/lint/clang-format/**'
+    type: external
+    payload: clang-format:lint
new file mode 100644
--- /dev/null
+++ b/tools/lint/clang-format/__init__.py
@@ -0,0 +1,145 @@
+# 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
+from mozversioncontrol import get_repository_object
+
+import os
+import signal
+import which
+import re
+
+from mozbuild.clang_format import (
+    commands,
+    files
+)
+
+
+# Py3/Py2 compatibility.
+try:
+    from json.decoder import JSONDecodeError
+except ImportError:
+    JSONDecodeError = ValueError
+
+from mozlint import result
+from mozlint.util import pip
+from mozprocess import ProcessHandlerMixin
+
+here = os.path.abspath(os.path.dirname(__file__))
+CLANG_FORMAT_PATH="/home/sylvestre/.mozbuild/clang-tidy/clang/bin/clang-format"
+# CLANG_FORMAT_NOT_FOUND = """
+# Could not find clang-format! Install clang-format and try again.
+
+#     $ pip install -U --require-hashes -r {}
+# """.strip().format(CLANG_FORMAT_REQUIREMENTS_PATH)
+
+
+CLANG_FORMAT_INSTALL_ERROR = """
+Unable to install correct version of clang-format
+{}
+
+""".strip().format(CLANG_FORMAT_PATH)
+
+results = []
+
+CLANG_FORMAT_FORMAT_REGEX = re.compile(r'(.*):(.*): (.*) ==> (.*)$')
+
+
+class ClangFormatProcess(ProcessHandlerMixin):
+    def __init__(self, config, *args, **kwargs):
+        self.config = config
+        kwargs['processOutputLine'] = [self.process_line]
+        ProcessHandlerMixin.__init__(self, *args, **kwargs)
+#        ProcessHandlerMixin.__init__(self, *args, **kwargs)
+
+
+    def process_line(self, line):
+        try:
+            match = CLANG_FORMAT_FORMAT_REGEX.match(line)
+            abspath, line, typo, correct = match.groups()
+        except AttributeError:
+#            print('Unable to match regex against output: {}'.format(line))
+            return
+
+        # Ignore false positive like aParent (which would be fixed to apparent)
+        # See https://github.com/lucasdemarchi/codespell/issues/314
+        m = re.match(r'^[a-z][A-Z][a-z]*', typo)
+        if m:
+            return
+        res = {'path': os.path.relpath(abspath, self.config['root']),
+               'message': typo + " ==> " + correct,
+               'level': "warning",
+               'lineno': line,
+               }
+        results.append(result.from_config(self.config, **res))
+
+    def run(self, *args, **kwargs):
+        print("---------------")
+        print(args)
+        print(*args)
+        print(*kwargs)
+        print(**kwargs)
+#        commands.run_clang_format_path(kwargs['base_command'])
+        orig = signal.signal(signal.SIGINT, signal.SIG_IGN)
+        ProcessHandlerMixin.run(self, *args, **kwargs)
+        signal.signal(signal.SIGINT, orig)
+
+
+def run_process(config, binary, paths):
+    print(config)
+    repository = get_repository_object(config["root"])
+    commands.run_clang_format_path(repository, binary, True, config["include"])
+
+
+
+def get_clang_format_binary():
+    """
+    Returns the path of the first clang-format binary available
+    if not found returns None
+    """
+    binary = os.environ.get('CLANG_FORMAT')
+    if binary:
+        return binary
+
+    try:
+        return which.which('clang-format')
+    except which.WhichError:
+        return None
+
+
+def lint(paths, config, fix=None, **lintargs):
+
+    if not os.path.isfile(CLANG_FORMAT_PATH):
+        print(CLANG_FORMAT_INSTALL_ERROR)
+        return 1
+
+    binary = get_clang_format_binary()
+
+    if not binary:
+        print(CLANG_FORMAT_NOT_FOUND)
+        if 'MOZ_AUTOMATION' in os.environ:
+            return 1
+        return []
+
+    config['root'] = lintargs['root']
+    exclude_list = os.path.join(here, 'exclude-list.txt')
+    cmd_args = [binary,
+                '--disable-colors',
+                # Silence some warnings:
+                # 1: disable warnings about wrong encoding
+                # 2: disable warnings about binary file
+                # 4: shut down warnings about automatic fixes
+                #    that were disabled in dictionary.
+                '--quiet-level=7',
+                '--ignore-words=' + exclude_list,
+                # Ignore dictonnaries
+                '--skip=*.dic',
+                ]
+
+    if fix:
+        cmd_args.append('--write-changes')
+    cmd_args = [binary]
+    paths = files.generate_path_list("/home/sylvestre/dev/mozilla/mozilla-central.hg", paths)
+    run_process(config, binary, paths)
+    return results