diff --git a/cmake/CodeFormat.cmake b/cmake/CodeFormat.cmake index 2a38222..b3ba5fa 100644 --- a/cmake/CodeFormat.cmake +++ b/cmake/CodeFormat.cmake @@ -1,11 +1,23 @@ +FIND_PACKAGE(Python COMPONENTS Interpreter REQUIRED) + # To exclude directories from the format check, add corresponding clang-format config files into those directories. +ADD_CUSTOM_TARGET(clang-format-check + USES_TERMINAL + COMMAND ${Python_EXECUTABLE} ${CMAKE_SOURCE_DIR}/scripts/run-clang-format.py -warnings-as-errors +) + +ADD_CUSTOM_TARGET(clang-format-check-fix + USES_TERMINAL + COMMAND ${Python_EXECUTABLE} ${CMAKE_SOURCE_DIR}/scripts/run-clang-format.py -fix +) -ADD_CUSTOM_TARGET(format-check +ADD_CUSTOM_TARGET(clang-tidy-check USES_TERMINAL - COMMAND ${CMAKE_SOURCE_DIR}/scripts/run-clang-format.py -warnings-as-errors - ) + COMMAND ${Python_EXECUTABLE} ${CMAKE_SOURCE_DIR}/scripts/run-clang-tidy.py +) -ADD_CUSTOM_TARGET(format-check-fix +ADD_CUSTOM_TARGET(clang-tidy-diff-check USES_TERMINAL - COMMAND ${CMAKE_SOURCE_DIR}/scripts/run-clang-format.py -fix - ) + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + COMMAND git diff -U0 HEAD --no-prefix | ${Python_EXECUTABLE} ${CMAKE_SOURCE_DIR}/scripts/run-clang-tidy-diff.py -path ${CMAKE_BINARY_DIR} +) diff --git a/scripts/run-clang-format.py b/scripts/run-clang-format.py new file mode 100644 index 0000000..8ed4eab --- /dev/null +++ b/scripts/run-clang-format.py @@ -0,0 +1,136 @@ +""" +This script will call clang-format. + +Call example: run-clang-format.py - Parallel clang-format runner + +Based on run-clang-tidy.py, which is part of the LLVM Project, under the +Apache License v2.0 with LLVM Exceptions. +See https://llvm.org/LICENSE.txt for license information. +SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +""" + +import argparse +import json +import multiprocessing +import os +import subprocess +import sys +import threading +import queue as queue + + +def find_compilation_database(path): + """Adjust the directory until a compilation database is found.""" + result = './' + while not os.path.isfile(os.path.join(result, path)): + if os.path.realpath(result) == '/': + print('Error: could not find compilation database.') + sys.exit(1) + result += '../' + return os.path.realpath(result) + + +def make_absolute(f, directory): + """Create a absolute path from given parameters.""" + if os.path.isabs(f): + return f + return os.path.normpath(os.path.join(directory, f)) + + +def get_format_invocation(f, clang_format_binary, fix, warnings_as_errors, quiet): + """Get a command line for clang-format.""" + start = [clang_format_binary] + if fix: + start.append('-i') + else: + start.append('--dry-run') + if warnings_as_errors: + start.append('--Werror') + start.append(f) + return start + + +def run_format(args, _, queue, lock, failed_files): + """Take filenames out of queue and runs clang-format on them.""" + while True: + name = queue.get() + invocation = get_format_invocation(name, args.clang_format_binary, args.fix, args.warnings_as_errors, + args.quiet) + + proc = subprocess.Popen(invocation, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + output, err = proc.communicate() + if proc.returncode != 0: + failed_files.append(name) + with lock: + sys.stdout.write(' '.join(invocation) + '\n' + output.decode('utf-8')) + if len(err) > 0: + sys.stdout.flush() + sys.stderr.write(err.decode('utf-8')) + queue.task_done() + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Runs clang-format over all files ' + 'in a compilation database. Requires ' + 'clang-format in $PATH.') + parser.add_argument('-clang-format-binary', metavar='PATH', default='clang-format', + help='path to clang-format binary') + parser.add_argument('-p', dest='build_path', help='Path used to read a compile command database.') + parser.add_argument('-j', type=int, default=0, help='number of tidy instances to be run in parallel.') + parser.add_argument('-fix', action='store_true', help='reformat files') + parser.add_argument('-warnings-as-errors', action='store_true', + help='Let the clang-tidy process return != 0 if a check failed.') + parser.add_argument('-quiet', action='store_true', help='Run clang-format in quiet mode') + args = parser.parse_args() + + db_path = 'compile_commands.json' + + if args.build_path is not None: + build_path = args.build_path + else: + # Find our database + build_path = find_compilation_database(db_path) + + try: + with open(os.devnull, 'w') as dev_null: + subprocess.check_call([args.clang_format_binary, '--dump-config'], stdout=dev_null) + except Exception as ex: + print(f'Unable to run clang-format. {ex} - {sys.stderr}') + sys.exit(1) + + # Load the database and extract all files. + database = json.load(open(os.path.join(build_path, db_path))) + files = [make_absolute(entry['file'], entry['directory']) for entry in database] + + max_task = args.j + if max_task == 0: + max_task = multiprocessing.cpu_count() + + return_code = 0 + try: + # Spin up a bunch of format-launching threads. + task_queue = queue.Queue(max_task) + # List of files with a non-zero return code. + failed_files = [] + lock = threading.Lock() + for _ in range(max_task): + t = threading.Thread(target=run_format, args=(args, build_path, task_queue, lock, failed_files)) + t.daemon = True + t.start() + + # Fill the queue with files. + for name in files: + task_queue.put(name) + + # Wait for all threads to be done. + task_queue.join() + if len(failed_files): + return_code = 1 + + except KeyboardInterrupt: + # This is a sad hack. Unfortunately subprocess goes + # bonkers with ctrl-c and we start forking merrily. + print('\nCtrl-C detected, goodbye.') + os.kill(0, 9) + + sys.exit(return_code) diff --git a/scripts/run-clang-tidy-diff.py b/scripts/run-clang-tidy-diff.py new file mode 100644 index 0000000..2517599 --- /dev/null +++ b/scripts/run-clang-tidy-diff.py @@ -0,0 +1,337 @@ +#!/usr/bin/env python3 +# +# ===- clang-tidy-diff.py - ClangTidy Diff Checker -----------*- python -*--===# +# +# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +# See https://llvm.org/LICENSE.txt for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# ===-----------------------------------------------------------------------===# + +r""" +ClangTidy Diff Checker +====================== + +This script reads input from a unified diff, runs clang-tidy on all changed +files and outputs clang-tidy warnings in changed lines only. This is useful to +detect clang-tidy regressions in the lines touched by a specific patch. +Example usage for git/svn users: + + git diff -U0 HEAD^ | clang-tidy-diff.py -p1 + svn diff --diff-cmd=diff -x-U0 | \ + clang-tidy-diff.py -fix -checks=-*,modernize-use-override + +""" + +import argparse +import glob +import json +import multiprocessing +import os +import re +import shutil +import subprocess +import sys +import tempfile +import threading +import traceback + +try: + import yaml +except ImportError: + yaml = None + +import queue as queue + + +def run_tidy(task_queue, lock, timeout, failed_files): + watchdog = None + while True: + command = task_queue.get() + try: + proc = subprocess.Popen( + command, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + + if timeout is not None: + watchdog = threading.Timer(timeout, proc.kill) + watchdog.start() + + stdout, stderr = proc.communicate() + if proc.returncode != 0: + if proc.returncode < 0: + msg = 'Terminated by signal %d : %s\n' % ( + -proc.returncode, + ' '.join(command), + ) + stderr += msg.encode('utf-8') + failed_files.append(command) + + with lock: + sys.stdout.write(stdout.decode('utf-8') + '\n') + sys.stdout.flush() + if stderr: + sys.stderr.write(stderr.decode('utf-8') + '\n') + sys.stderr.flush() + except Exception as e: + with lock: + sys.stderr.write('Failed: ' + str(e) + ': '.join(command) + '\n') + finally: + with lock: + if not (timeout is None or watchdog is None): + if not watchdog.is_alive(): + sys.stderr.write( + 'Terminated by timeout: ' + ' '.join(command) + '\n' + ) + watchdog.cancel() + task_queue.task_done() + + +def start_workers(max_tasks, tidy_caller, arguments): + for _ in range(max_tasks): + t = threading.Thread(target=tidy_caller, args=arguments) + t.daemon = True + t.start() + + +def merge_replacement_files(tmpdir, mergefile): + """Merge all replacement files in a directory into a single file""" + # The fixes suggested by clang-tidy >= 4.0.0 are given under + # the top level key 'Diagnostics' in the output yaml files + mergekey = 'Diagnostics' + merged = [] + for replacefile in glob.iglob(os.path.join(tmpdir, '*.yaml')): + content = yaml.safe_load(open(replacefile)) + if not content: + continue # Skip empty files. + merged.extend(content.get(mergekey, [])) + + if merged: + # MainSourceFile: The key is required by the definition inside + # include/clang/Tooling/ReplacementsYaml.h, but the value + # is actually never used inside clang-apply-replacements, + # so we set it to '' here. + output = {'MainSourceFile': '', mergekey: merged} + with open(mergefile, 'w') as out: + yaml.safe_dump(output, out) + else: + # Empty the file: + open(mergefile, 'w').close() + + +def main(): + parser = argparse.ArgumentParser( + description='Run clang-tidy against changed files, and ' + 'output diagnostics only for modified ' + 'lines.' + ) + parser.add_argument( + '-clang-tidy-binary', + metavar='PATH', + default='clang-tidy', + help='path to clang-tidy binary', + ) + parser.add_argument( + '-p', + metavar='NUM', + default=0, + help='strip the smallest prefix containing P slashes', + ) + parser.add_argument( + '-regex', + metavar='PATTERN', + default=None, + help='custom pattern selecting file paths to check ' + '(case sensitive, overrides -iregex)', + ) + parser.add_argument( + '-iregex', + metavar='PATTERN', + default=r'.*\.(cpp|cc|c\+\+|cxx|c|cl|h|hpp|m|mm|inc)', + help='custom pattern selecting file paths to check ' + '(case insensitive, overridden by -regex)', + ) + parser.add_argument( + '-j', + type=int, + default=1, + help='number of tidy instances to be run in parallel.', + ) + parser.add_argument( + '-timeout', type=int, default=None, help='timeout per each file in seconds.' + ) + parser.add_argument( + '-fix', action='store_true', default=False, help='apply suggested fixes' + ) + parser.add_argument( + '-checks', + help='checks filter, when not specified, use clang-tidy ' 'default', + default='', + ) + parser.add_argument('-use-color', action='store_true', help='Use colors in output') + parser.add_argument( + '-path', dest='build_path', help='Path used to read a compile command database.' + ) + if yaml: + parser.add_argument( + '-export-fixes', + metavar='FILE', + dest='export_fixes', + help='Create a yaml file to store suggested fixes in, ' + 'which can be applied with clang-apply-replacements.', + ) + parser.add_argument( + '-extra-arg', + dest='extra_arg', + action='append', + default=[], + help='Additional argument to append to the compiler ' 'command line.', + ) + parser.add_argument( + '-extra-arg-before', + dest='extra_arg_before', + action='append', + default=[], + help='Additional argument to prepend to the compiler ' 'command line.', + ) + parser.add_argument( + '-quiet', + action='store_true', + default=False, + help='Run clang-tidy in quiet mode', + ) + parser.add_argument( + '-load', + dest='plugins', + action='append', + default=[], + help='Load the specified plugin in clang-tidy.', + ) + + clang_tidy_args = [] + argv = sys.argv[1:] + if '--' in argv: + clang_tidy_args.extend(argv[argv.index('--'):]) + argv = argv[: argv.index('--')] + + args = parser.parse_args(argv) + + # Extract changed lines for each file. + filename = None + lines_by_file = {} + for line in sys.stdin: + match = re.search('^\\+\\+\\+\\ "?(.*?/){%s}([^ \t\n"]*)' % args.p, line) + if match: + filename = match.group(2) + if filename is None: + continue + + if args.regex is not None: + if not re.match('^%s$' % args.regex, filename): + continue + else: + if not re.match('^%s$' % args.iregex, filename, re.IGNORECASE): + continue + + match = re.search(r'^@@.*\+(\d+)(,(\d+))?', line) + if match: + start_line = int(match.group(1)) + line_count = 1 + if match.group(3): + line_count = int(match.group(3)) + if line_count == 0: + continue + end_line = start_line + line_count - 1 + lines_by_file.setdefault(filename, []).append([start_line, end_line]) + + if not any(lines_by_file): + print('No relevant changes found.') + sys.exit(0) + + max_task_count = args.j + if max_task_count == 0: + max_task_count = multiprocessing.cpu_count() + max_task_count = min(len(lines_by_file), max_task_count) + + tmpdir = None + if yaml and args.export_fixes: + tmpdir = tempfile.mkdtemp() + + # Tasks for clang-tidy. + task_queue = queue.Queue(max_task_count) + # A lock for console output. + lock = threading.Lock() + + # List of files with a non-zero return code. + failed_files = [] + + # Run a pool of clang-tidy workers. + start_workers( + max_task_count, run_tidy, (task_queue, lock, args.timeout, failed_files) + ) + + # Form the common args list. + common_clang_tidy_args = [] + if args.fix: + common_clang_tidy_args.append('-fix') + if args.checks != '': + common_clang_tidy_args.append('-checks=' + args.checks) + if args.quiet: + common_clang_tidy_args.append('-quiet') + if args.build_path is not None: + common_clang_tidy_args.append('-p=%s' % args.build_path) + if args.use_color: + common_clang_tidy_args.append('--use-color') + for arg in args.extra_arg: + common_clang_tidy_args.append('-extra-arg=%s' % arg) + for arg in args.extra_arg_before: + common_clang_tidy_args.append('-extra-arg-before=%s' % arg) + for plugin in args.plugins: + common_clang_tidy_args.append('-load=%s' % plugin) + + for name in lines_by_file: + line_filter_json = json.dumps( + [{'name': name, 'lines': lines_by_file[name]}], separators=(',', ':') + ) + + # Run clang-tidy on files containing changes. + command = [args.clang_tidy_binary] + command.append('-line-filter=' + line_filter_json) + if yaml and args.export_fixes: + # Get a temporary file. We immediately close the handle so clang-tidy can + # overwrite it. + (handle, tmp_name) = tempfile.mkstemp(suffix='.yaml', dir=tmpdir) + os.close(handle) + command.append('-export-fixes=' + tmp_name) + command.extend(common_clang_tidy_args) + command.append(name) + command.extend(clang_tidy_args) + + task_queue.put(command) + + # Application return code + return_code = 0 + + # Wait for all threads to be done. + task_queue.join() + # Application return code + return_code = 0 + if failed_files: + return_code = 1 + + if yaml and args.export_fixes: + print('Writing fixes to ' + args.export_fixes + ' ...') + try: + merge_replacement_files(tmpdir, args.export_fixes) + except: + sys.stderr.write('Error exporting fixes.\n') + traceback.print_exc() + return_code = 1 + + if tmpdir: + shutil.rmtree(tmpdir) + sys.exit(return_code) + + +if __name__ == '__main__': + main() diff --git a/scripts/run-clang-tidy.py b/scripts/run-clang-tidy.py new file mode 100644 index 0000000..a65c241 --- /dev/null +++ b/scripts/run-clang-tidy.py @@ -0,0 +1,503 @@ +#!/usr/bin/env python3 +# +# ===- run-clang-tidy.py - Parallel clang-tidy runner --------*- python -*--===# +# +# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +# See https://llvm.org/LICENSE.txt for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# ===-----------------------------------------------------------------------===# + + +""" +Parallel clang-tidy runner +========================== + +Runs clang-tidy over all files in a compilation database. Requires clang-tidy +and clang-apply-replacements in $PATH. + +Example invocations. +- Run clang-tidy on all files in the current working directory with a default + set of checks and show warnings in the cpp files and all project headers. + run-clang-tidy.py $PWD + +- Fix all header guards. + run-clang-tidy.py -fix -checks=-*,llvm-header-guard + +- Fix all header guards included from clang-tidy and header guards + for clang-tidy headers. + run-clang-tidy.py -fix -checks=-*,llvm-header-guard extra/clang-tidy \ + -header-filter=extra/clang-tidy + +Compilation database setup: +http://clang.llvm.org/docs/HowToSetupToolingForLLVM.html +""" + + +import argparse +import glob +import json +import multiprocessing +import os +import queue +import re +import shutil +import subprocess +import sys +import tempfile +import threading +import traceback + +try: + import yaml +except ImportError: + yaml = None + + +def strtobool(val): + """Convert a string representation of truth to a bool following LLVM's CLI argument parsing.""" + + val = val.lower() + if val in ['', 'true', '1']: + return True + elif val in ['false', '0']: + return False + + # Return ArgumentTypeError so that argparse does not substitute its own error message + raise argparse.ArgumentTypeError( + f"'{val}' is invalid value for boolean argument! Try 0 or 1." + ) + + +def find_compilation_database(path): + """Adjusts the directory until a compilation database is found.""" + result = os.path.realpath('./') + while not os.path.isfile(os.path.join(result, path)): + parent = os.path.dirname(result) + if result == parent: + print('Error: could not find compilation database.') + sys.exit(1) + result = parent + return result + + +def make_absolute(f, directory): + if os.path.isabs(f): + return f + return os.path.normpath(os.path.join(directory, f)) + + +def get_tidy_invocation( + f, + clang_tidy_binary, + checks, + tmpdir, + build_path, + header_filter, + allow_enabling_alpha_checkers, + extra_arg, + extra_arg_before, + quiet, + config_file_path, + config, + line_filter, + use_color, + plugins, + warnings_as_errors, +): + """Gets a command line for clang-tidy.""" + start = [clang_tidy_binary] + if allow_enabling_alpha_checkers: + start.append('-allow-enabling-analyzer-alpha-checkers') + if header_filter is not None: + start.append('-header-filter=' + header_filter) + if line_filter is not None: + start.append('-line-filter=' + line_filter) + if use_color is not None: + if use_color: + start.append('--use-color') + else: + start.append('--use-color=false') + if checks: + start.append('-checks=' + checks) + if tmpdir is not None: + start.append('-export-fixes') + # Get a temporary file. We immediately close the handle so clang-tidy can + # overwrite it. + (handle, name) = tempfile.mkstemp(suffix='.yaml', dir=tmpdir) + os.close(handle) + start.append(name) + for arg in extra_arg: + start.append('-extra-arg=%s' % arg) + for arg in extra_arg_before: + start.append('-extra-arg-before=%s' % arg) + start.append('-p=' + build_path) + if quiet: + start.append('-quiet') + if config_file_path: + start.append('--config-file=' + config_file_path) + elif config: + start.append('-config=' + config) + for plugin in plugins: + start.append('-load=' + plugin) + if warnings_as_errors: + start.append('--warnings-as-errors=' + warnings_as_errors) + start.append(f) + return start + + +def merge_replacement_files(tmpdir, mergefile): + """Merge all replacement files in a directory into a single file""" + # The fixes suggested by clang-tidy >= 4.0.0 are given under + # the top level key 'Diagnostics' in the output yaml files + mergekey = 'Diagnostics' + merged = [] + for replacefile in glob.iglob(os.path.join(tmpdir, '*.yaml')): + content = yaml.safe_load(open(replacefile)) + if not content: + continue # Skip empty files. + merged.extend(content.get(mergekey, [])) + + if merged: + # MainSourceFile: The key is required by the definition inside + # include/clang/Tooling/ReplacementsYaml.h, but the value + # is actually never used inside clang-apply-replacements, + # so we set it to '' here. + output = {'MainSourceFile': '', mergekey: merged} + with open(mergefile, 'w') as out: + yaml.safe_dump(output, out) + else: + # Empty the file: + open(mergefile, 'w').close() + + +def find_binary(arg, name, build_path): + """Get the path for a binary or exit""" + if arg: + if shutil.which(arg): + return arg + else: + raise SystemExit( + "error: passed binary '{}' was not found or is not executable".format( + arg + ) + ) + + built_path = os.path.join(build_path, 'bin', name) + binary = shutil.which(name) or shutil.which(built_path) + if binary: + return binary + else: + raise SystemExit( + f'error: failed to find {name} in $PATH or at {built_path}' + ) + + +def apply_fixes(args, clang_apply_replacements_binary, tmpdir): + """Calls clang-apply-fixes on a given directory.""" + invocation = [clang_apply_replacements_binary] + invocation.append('-ignore-insert-conflict') + if args.format: + invocation.append('-format') + if args.style: + invocation.append('-style=' + args.style) + invocation.append(tmpdir) + subprocess.call(invocation) + + +def run_tidy(args, clang_tidy_binary, tmpdir, build_path, queue, lock, failed_files): + """Takes filenames out of queue and runs clang-tidy on them.""" + while True: + name = queue.get() + invocation = get_tidy_invocation( + name, + clang_tidy_binary, + args.checks, + tmpdir, + build_path, + args.header_filter, + args.allow_enabling_alpha_checkers, + args.extra_arg, + args.extra_arg_before, + args.quiet, + args.config_file, + args.config, + args.line_filter, + args.use_color, + args.plugins, + args.warnings_as_errors, + ) + + proc = subprocess.Popen( + invocation, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + output, err = proc.communicate() + if proc.returncode != 0: + if proc.returncode < 0: + msg = '%s: terminated by signal %d\n' % (name, -proc.returncode) + err += msg.encode('utf-8') + failed_files.append(name) + with lock: + sys.stdout.write(' '.join(invocation) + '\n' + output.decode('utf-8')) + if len(err) > 0: + sys.stdout.flush() + sys.stderr.write(err.decode('utf-8')) + queue.task_done() + + +def main(): + parser = argparse.ArgumentParser( + description='Runs clang-tidy over all files ' + 'in a compilation database. Requires ' + 'clang-tidy and clang-apply-replacements in ' + '$PATH or in your build directory.' + ) + parser.add_argument( + '-allow-enabling-alpha-checkers', + action='store_true', + help='allow alpha checkers from ' 'clang-analyzer.', + ) + parser.add_argument( + '-clang-tidy-binary', metavar='PATH', help='path to clang-tidy binary' + ) + parser.add_argument( + '-clang-apply-replacements-binary', + metavar='PATH', + help='path to clang-apply-replacements binary', + ) + parser.add_argument( + '-checks', + default=None, + help='checks filter, when not specified, use clang-tidy ' 'default', + ) + config_group = parser.add_mutually_exclusive_group() + config_group.add_argument( + '-config', + default=None, + help='Specifies a configuration in YAML/JSON format: ' + " -config=\"{Checks: '*', " + ' CheckOptions: {x: y}}" ' + 'When the value is empty, clang-tidy will ' + 'attempt to find a file named .clang-tidy for ' + 'each source file in its parent directories.', + ) + config_group.add_argument( + '-config-file', + default=None, + help='Specify the path of .clang-tidy or custom config ' + 'file: e.g. -config-file=/some/path/myTidyConfigFile. ' + 'This option internally works exactly the same way as ' + '-config option after reading specified config file. ' + 'Use either -config-file or -config, not both.', + ) + parser.add_argument( + '-header-filter', + default=None, + help='regular expression matching the names of the ' + 'headers to output diagnostics from. Diagnostics from ' + 'the main file of each translation unit are always ' + 'displayed.', + ) + parser.add_argument( + '-line-filter', + default=None, + help='List of files with line ranges to filter the' 'warnings.', + ) + if yaml: + parser.add_argument( + '-export-fixes', + metavar='filename', + dest='export_fixes', + help='Create a yaml file to store suggested fixes in, ' + 'which can be applied with clang-apply-replacements.', + ) + parser.add_argument( + '-j', + type=int, + default=0, + help='number of tidy instances to be run in parallel.', + ) + parser.add_argument( + 'files', nargs='*', default=['.*'], help='files to be processed (regex on path)' + ) + parser.add_argument('-fix', action='store_true', help='apply fix-its') + parser.add_argument( + '-format', action='store_true', help='Reformat code ' 'after applying fixes' + ) + parser.add_argument( + '-style', + default='file', + help='The style of reformat ' 'code after applying fixes', + ) + parser.add_argument( + '-use-color', + type=strtobool, + nargs='?', + const=True, + help="Use colors in diagnostics, overriding clang-tidy's" + " default behavior. This option overrides the 'UseColor" + "' option in .clang-tidy file, if any.", + ) + parser.add_argument( + '-p', dest='build_path', help='Path used to read a compile command database.' + ) + parser.add_argument( + '-extra-arg', + dest='extra_arg', + action='append', + default=[], + help='Additional argument to append to the compiler ' 'command line.', + ) + parser.add_argument( + '-extra-arg-before', + dest='extra_arg_before', + action='append', + default=[], + help='Additional argument to prepend to the compiler ' 'command line.', + ) + parser.add_argument( + '-quiet', action='store_true', help='Run clang-tidy in quiet mode' + ) + parser.add_argument( + '-load', + dest='plugins', + action='append', + default=[], + help='Load the specified plugin in clang-tidy.', + ) + parser.add_argument( + '-warnings-as-errors', + default=None, + help='Upgrades warnings to errors. Same format as ' "'-checks'", + ) + args = parser.parse_args() + + db_path = 'compile_commands.json' + + if args.build_path is not None: + build_path = args.build_path + else: + # Find our database + build_path = find_compilation_database(db_path) + + clang_tidy_binary = find_binary(args.clang_tidy_binary, 'clang-tidy', build_path) + + tmpdir = None + if args.fix: + clang_apply_replacements_binary = find_binary( + args.clang_apply_replacements_binary, 'clang-apply-replacements', build_path + ) + + if args.fix or (yaml and args.export_fixes): + tmpdir = tempfile.mkdtemp() + + try: + invocation = get_tidy_invocation( + '', + clang_tidy_binary, + args.checks, + None, + build_path, + args.header_filter, + args.allow_enabling_alpha_checkers, + args.extra_arg, + args.extra_arg_before, + args.quiet, + args.config_file, + args.config, + args.line_filter, + args.use_color, + args.plugins, + args.warnings_as_errors, + ) + invocation.append('-list-checks') + invocation.append('-') + if args.quiet: + # Even with -quiet we still want to check if we can call clang-tidy. + with open(os.devnull, 'w') as dev_null: + subprocess.check_call(invocation, stdout=dev_null) + else: + subprocess.check_call(invocation) + except: + print('Unable to run clang-tidy.', file=sys.stderr) + sys.exit(1) + + # Load the database and extract all files. + database = json.load(open(os.path.join(build_path, db_path))) + files = { + make_absolute(entry['file'], entry['directory']) for entry in database + } + + max_task = args.j + if max_task == 0: + max_task = multiprocessing.cpu_count() + + # Build up a big regexy filter from all command line arguments. + file_name_re = re.compile('|'.join(args.files)) + + return_code = 0 + try: + # Spin up a bunch of tidy-launching threads. + task_queue = queue.Queue(max_task) + # List of files with a non-zero return code. + failed_files = [] + lock = threading.Lock() + for _ in range(max_task): + t = threading.Thread( + target=run_tidy, + args=( + args, + clang_tidy_binary, + tmpdir, + build_path, + task_queue, + lock, + failed_files, + ), + ) + t.daemon = True + t.start() + + # Fill the queue with files. + for name in files: + if file_name_re.search(name): + task_queue.put(name) + + # Wait for all threads to be done. + task_queue.join() + if len(failed_files): + return_code = 1 + + except KeyboardInterrupt: + # This is a sad hack. Unfortunately subprocess goes + # bonkers with ctrl-c and we start forking merrily. + print('\nCtrl-C detected, goodbye.') + if tmpdir: + shutil.rmtree(tmpdir) + os.kill(0, 9) + + if yaml and args.export_fixes: + print('Writing fixes to ' + args.export_fixes + ' ...') + try: + merge_replacement_files(tmpdir, args.export_fixes) + except: + print('Error exporting fixes.\n', file=sys.stderr) + traceback.print_exc() + return_code = 1 + + if args.fix: + print('Applying fixes ...') + try: + apply_fixes(args, clang_apply_replacements_binary, tmpdir) + except: + print('Error applying fixes.\n', file=sys.stderr) + traceback.print_exc() + return_code = 1 + + if tmpdir: + shutil.rmtree(tmpdir) + sys.exit(return_code) + + +if __name__ == '__main__': + main()