diff --git a/.clang-format-ignore b/.clang-format-ignore index e11ac133ced5..de4a04571508 100644 --- a/.clang-format-ignore +++ b/.clang-format-ignore @@ -59,6 +59,9 @@ tools/clang-tidy/test/.* # We are testing the incorrect formatting. tools/lint/test/files/file-whitespace/ +# Test reformatting +tools/lint/test/files/clang-format/ + # Contains an XML definition and formatting would break the layout widget/gtk/MPRISInterfaceDescription.h diff --git a/docs/code-quality/lint/linters/clang-format.rst b/docs/code-quality/lint/linters/clang-format.rst new file mode 100644 index 000000000000..2913c7440e0d --- /dev/null +++ b/docs/code-quality/lint/linters/clang-format.rst @@ -0,0 +1,35 @@ +clang-format +============ + +`clang-format `__ is a tool to reformat C/C++ to the right coding style. + +Run Locally +----------- + +The mozlint integration of clang-format can be run using mach: + +.. parsed-literal:: + + $ mach lint --linter clang-format + + +Configuration +------------- + +To enable clang-format on new directory, add the path to the include +section in the `clang-format.yml `_ file. + +While excludes: will work, this linter will read the ignore list from `.clang-format-ignore file `_ +at the root directory. This because it is also used by the ./mach clang-format -p command. + +Autofix +------- + +clang-format can reformat the code with the option `--fix` (based on the upstream option `-i`). +To highlight the results, we are using the ``--dry-run`` option (from clang-format 10). + +Sources +------- + +* `Configuration (YAML) `_ +* `Source `_ diff --git a/tools/lint/clang-format.yml b/tools/lint/clang-format.yml new file mode 100644 index 000000000000..a386ce5b5c54 --- /dev/null +++ b/tools/lint/clang-format.yml @@ -0,0 +1,11 @@ +--- +clang-format: + description: Reformat C/C++ + include: + - '.' + extensions: ['cpp', 'c', 'cc', 'h', 'm', 'mm'] + support-files: + - 'tools/lint/clang-format/**' + type: external + payload: clang-format:lint + code_review_warnings: true diff --git a/tools/lint/clang-format/__init__.py b/tools/lint/clang-format/__init__.py new file mode 100644 index 000000000000..ec48dde2ff95 --- /dev/null +++ b/tools/lint/clang-format/__init__.py @@ -0,0 +1,166 @@ +# 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 signal +import re + +from buildconfig import substs +from mozboot.util import get_state_dir +from mozlint import result +from mozlint.pathutils import expand_exclusions +from mozprocess import ProcessHandler + +CLANG_FORMAT_NOT_FOUND = """ +Could not find clang-format! Install clang-format with: + + $ ./mach bootstrap + +And make sure that it is in the PATH +""".strip() + + +def parse_issues(config, output, paths, log): + + diff_line = re.compile("^(.*):(.*):(.*): warning: .*;(.*);(.*)") + results = [] + for line in output: + match = diff_line.match(line) + file, line_no, col, diff, diff2 = match.groups() + log.debug("file={} line={} col={} diff={} diff2={}".format( + file, line_no, col, diff, diff2)) + d = diff + "\n" + diff2 + res = { + "path": file, + "diff": d, + "level": "warning", + "lineno": line_no, + "column": col, + } + results.append(result.from_config(config, **res)) + + return results + + +class ClangFormatProcess(ProcessHandler): + def __init__(self, config, *args, **kwargs): + self.config = config + kwargs["stream"] = False + kwargs["universal_newlines"] = True + ProcessHandler.__init__(self, *args, **kwargs) + + def run(self, *args, **kwargs): + orig = signal.signal(signal.SIGINT, signal.SIG_IGN) + ProcessHandler.run(self, *args, **kwargs) + signal.signal(signal.SIGINT, orig) + + +def run_process(config, cmd): + proc = ClangFormatProcess(config, cmd) + proc.run() + try: + proc.wait() + except KeyboardInterrupt: + proc.kill() + + return proc.output + + +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 + + clang_tools_path = os.path.join(get_state_dir(), "clang-tools") + bin_path = os.path.join(clang_tools_path, "clang-tidy", "bin") + return os.path.join(bin_path, "clang-format" + substs.get('BIN_SUFFIX', '')) + + +def is_ignored_path(ignored_dir_re, topsrcdir, f): + # Remove up to 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 remove_ignored_path(paths, topsrcdir, log): + path_to_third_party = os.path.join(topsrcdir, '.clang-format-ignore') + + ignored_dir = [] + with open(path_to_third_party, 'r') as fh: + for line in fh: + # In case it starts with a space + line = line.strip() + # Remove comments and empty lines + if line.startswith('#') or len(line) == 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) + + path_list = [] + for f in paths: + if is_ignored_path(ignored_dir_re, topsrcdir, f): + # Early exit if we have provided an ignored directory + log.debug("Ignored third party code '{0}'".format(f)) + continue + path_list.append(f) + + return path_list + + +def lint(paths, config, fix=None, **lintargs): + log = lintargs['log'] + paths = list(expand_exclusions(paths, config, lintargs['root'])) + + # We ignored some specific files for a bunch of reasons. + # Not using excluding to avoid duplication + if lintargs.get('use_filters', True): + paths = remove_ignored_path(paths, lintargs['root'], log) + + # An empty path array can occur when the user passes in `-n`. If we don't + # return early in this case, rustfmt will attempt to read stdin and hang. + if not paths: + return [] + + binary = get_clang_format_binary() + + if not binary: + print(CLANG_FORMAT_NOT_FOUND) + if "MOZ_AUTOMATION" in os.environ: + return 1 + return [] + + cmd_args = [binary] + if fix: + cmd_args.append("-i") + else: + cmd_args.append("--dry-run") + base_command = cmd_args + paths + log.debug("Command: {}".format(' '.join(cmd_args))) + output = run_process(config, base_command) + output_list = [] + + if len(output) % 3 != 0: + raise Exception("clang-format output should be a multiple of 3. Output: %s" % output) + + for i in range(0, len(output), 3): + # Merge the element 3 by 3 (clang-format output) + line = output[i] + line += ";" + output[i+1] + line += ";" + output[i+2] + output_list.append(line) + + if fix: + # clang-format is able to fix all issues so don't bother parsing the output. + return [] + return parse_issues(config, output_list, paths, log) diff --git a/tools/lint/test/files/clang-format/bad/bad.cpp b/tools/lint/test/files/clang-format/bad/bad.cpp new file mode 100644 index 000000000000..f08a83f795d0 --- /dev/null +++ b/tools/lint/test/files/clang-format/bad/bad.cpp @@ -0,0 +1,6 @@ +int main ( ) { + +return 0; + + +} diff --git a/tools/lint/test/files/clang-format/bad/bad2.c b/tools/lint/test/files/clang-format/bad/bad2.c new file mode 100644 index 000000000000..9792e8507174 --- /dev/null +++ b/tools/lint/test/files/clang-format/bad/bad2.c @@ -0,0 +1,8 @@ +#include "bad2.h" + + +int bad2() { + int a =2; + return a; + +} diff --git a/tools/lint/test/files/clang-format/bad/bad2.h b/tools/lint/test/files/clang-format/bad/bad2.h new file mode 100644 index 000000000000..a35d49d7e7af --- /dev/null +++ b/tools/lint/test/files/clang-format/bad/bad2.h @@ -0,0 +1 @@ +int bad2(void ); diff --git a/tools/lint/test/files/clang-format/bad/good.cpp b/tools/lint/test/files/clang-format/bad/good.cpp new file mode 100644 index 000000000000..76e8197013aa --- /dev/null +++ b/tools/lint/test/files/clang-format/bad/good.cpp @@ -0,0 +1 @@ +int main() { return 0; } diff --git a/tools/lint/test/files/clang-format/good/foo.cpp b/tools/lint/test/files/clang-format/good/foo.cpp new file mode 100644 index 000000000000..76e8197013aa --- /dev/null +++ b/tools/lint/test/files/clang-format/good/foo.cpp @@ -0,0 +1 @@ +int main() { return 0; } diff --git a/tools/lint/test/python.ini b/tools/lint/test/python.ini index b3697cd15cce..82b9583a596c 100644 --- a/tools/lint/test/python.ini +++ b/tools/lint/test/python.ini @@ -22,3 +22,5 @@ skip-if = os == "win" || os == "mac" # fails with No module named 'pkg_resource skip-if = os == "win" || os == "mac" # only installed on Linux [test_rustfmt.py] skip-if = os == "win" || os == "mac" # only installed on Linux +[test_clang_format.py] +skip-if = os == "win" || os == "mac" # only installed on Linux diff --git a/tools/lint/test/test_clang_format.py b/tools/lint/test/test_clang_format.py new file mode 100644 index 000000000000..7e94db8c668d --- /dev/null +++ b/tools/lint/test/test_clang_format.py @@ -0,0 +1,48 @@ +import mozunit + +from conftest import build + +LINTER = 'clang-format' + + +def test_good(lint, config, paths): + results = lint(paths("good/"), root=build.topsrcdir, use_filters=False) + print(results) + assert len(results) == 0 + + +def test_basic(lint, config, paths): + results = lint(paths("bad/bad.cpp"), root=build.topsrcdir, use_filters=False) + print(results) + assert len(results) >= 1 + + assert "Reformat C/C++" in results[0].message + assert results[0].level == "warning" + assert results[0].lineno == 1 + assert results[0].column == 4 + assert "bad.cpp" in results[0].path + assert 'int main ( ) {' in results[0].diff + + +def test_dir(lint, config, paths): + results = lint(paths("bad/"), root=build.topsrcdir, use_filters=False) + print(results) + assert len(results) >= 4 + + assert "Reformat C/C++" in results[0].message + assert results[0].level == "warning" + assert results[0].lineno == 1 + assert results[0].column == 4 + assert "bad.cpp" in results[0].path + assert 'int main ( ) {' in results[0].diff + + assert "Reformat C/C++" in results[5].message + assert results[5].level == "warning" + assert results[5].lineno == 1 + assert results[5].column == 18 + assert "bad2.c" in results[5].path + assert "#include" in results[5].diff + + +if __name__ == '__main__': + mozunit.main()