diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5245b83..b209620 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: # Standard hooks - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.3.0 + rev: v4.6.0 hooks: - id: check-added-large-files - id: check-ast @@ -34,9 +34,15 @@ repos: # Python hooks - repo: https://github.com/PyCQA/flake8 - rev: 6.0.0 + rev: 7.1.1 hooks: - - id: flake8 + - id: flake8 + + - repo: https://github.com/psf/black + rev: 24.8.0 + hooks: + - id: black + language_version: python3 # CPP hooks - repo: local diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..55ec8d7 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,2 @@ +[tool.black] +line-length = 120 diff --git a/scripts/check-message-compatibility.py b/scripts/check-message-compatibility.py index 5af4c6e..f648150 100755 --- a/scripts/check-message-compatibility.py +++ b/scripts/check-message-compatibility.py @@ -9,8 +9,8 @@ from typing import Optional -TOPIC_LIST_FILE = 'px4_ros2_cpp/include/px4_ros2/components/message_compatibility_check.hpp' -MESSAGES_DEFINE = 'ALL_PX4_ROS2_MESSAGES' +TOPIC_LIST_FILE = "px4_ros2_cpp/include/px4_ros2/components/message_compatibility_check.hpp" +MESSAGES_DEFINE = "ALL_PX4_ROS2_MESSAGES" def message_fields_str_for_message_hash(topic_type: str, msgs_dir: str) -> str: @@ -20,7 +20,7 @@ def message_fields_str_for_message_hash(topic_type: str, msgs_dir: str) -> str: """ filename = f"{msgs_dir}/msg/{topic_type}.msg" try: - with open(filename, 'r') as file: + with open(filename, "r") as file: text = file.read() except IOError: print(f"Failed to open {filename}") @@ -29,15 +29,25 @@ def message_fields_str_for_message_hash(topic_type: str, msgs_dir: str) -> str: fields_str = "" # Regular expression to match field types from .msg definitions - msg_field_type_regex = re.compile( - r"(?:^|\n)\s*([a-zA-Z0-9_/]+)(\[[^\]]*\])?\s+(\w+)[ \t]*(=)?" - ) + msg_field_type_regex = re.compile(r"(?:^|\n)\s*([a-zA-Z0-9_/]+)(\[[^\]]*\])?\s+(\w+)[ \t]*(=)?") # Set of basic types basic_types = { - "bool", "byte", "char", "float32", "float64", - "int8", "uint8", "int16", "uint16", "int32", - "uint32", "int64", "uint64", "string", "wstring" + "bool", + "byte", + "char", + "float32", + "float64", + "int8", + "uint8", + "int16", + "uint16", + "int32", + "uint32", + "int64", + "uint64", + "string", + "wstring", } # Iterate over all matches in the text @@ -50,7 +60,7 @@ def message_fields_str_for_message_hash(topic_type: str, msgs_dir: str) -> str: fields_str += f"{type_}{array} {field_name}\n" if type_ not in basic_types: - if '/' not in type_: + if "/" not in type_: # Recursive call to handle nested types fields_str += message_fields_str_for_message_hash(type_, msgs_dir) else: @@ -61,7 +71,7 @@ def message_fields_str_for_message_hash(topic_type: str, msgs_dir: str) -> str: def hash32_fnv1a_const(s: str) -> int: """Computes the 32-bit FNV-1a hash of a given string""" - kVal32Const = 0x811c9dc5 + kVal32Const = 0x811C9DC5 kPrime32Const = 0x1000193 hash_value = kVal32Const for c in s: @@ -82,8 +92,9 @@ def snake_to_pascal(name: str) -> str: return f'{name.replace("_", " ").title().replace(" ", "")}' -def extract_message_type_from_file(filename: str, extract_start_after: Optional[str] = None, - extract_end_before: Optional[str] = None) -> list[str]: +def extract_message_type_from_file( + filename: str, extract_start_after: Optional[str] = None, extract_end_before: Optional[str] = None +) -> list[str]: """Extract message type names from a given file""" with open(filename) as file: if extract_start_after is not None: @@ -109,12 +120,12 @@ def extract_message_type_from_file(filename: str, extract_start_after: Optional[ def compare_files(file1: str, file2: str): - """Compare two files and print their differences. """ - with open(file1, 'r') as f1, open(file2, 'r') as f2: + """Compare two files and print their differences.""" + with open(file1, "r") as f1, open(file2, "r") as f2: diff = list(difflib.unified_diff(f1.readlines(), f2.readlines(), fromfile=file1, tofile=file2)) if diff: print(f"Mismatch found between {file1} and {file2}:") - print(''.join(diff), end='\n\n') + print("".join(diff), end="\n\n") return False return True @@ -125,14 +136,14 @@ def main(repo1: str, repo2: str, verbose: bool = False): sys.exit(1) # Retrieve list of message types to check - messages_types = sorted(extract_message_type_from_file( - os.path.join(os.path.dirname(__file__), '..', TOPIC_LIST_FILE), - MESSAGES_DEFINE, - r'^\s*$') + messages_types = sorted( + extract_message_type_from_file( + os.path.join(os.path.dirname(__file__), "..", TOPIC_LIST_FILE), MESSAGES_DEFINE, r"^\s*$" + ) ) if verbose: - print("Checking the following message files:", end='\n\n') + print("Checking the following message files:", end="\n\n") for msg_type in messages_types: print(f" - {msg_type}.msg") print() @@ -150,11 +161,14 @@ def main(repo1: str, repo2: str, verbose: bool = False): else: if verbose: for msg_type in incompatible_types: - file1 = os.path.join(repo1, 'msg', f'{msg_type}.msg') - file2 = os.path.join(repo2, 'msg', f'{msg_type}.msg') + file1 = os.path.join(repo1, "msg", f"{msg_type}.msg") + file2 = os.path.join(repo2, "msg", f"{msg_type}.msg") compare_files(file1, file2) - print("Note: The printed diff includes all content differences. " - "The computed check is less sensitive to formatting and comments.", end='\n\n') + print( + "Note: The printed diff includes all content differences. " + "The computed check is less sensitive to formatting and comments.", + end="\n\n", + ) print("FAILED! Some files differ:") for msg_type in incompatible_types: print(f" - {msg_type}.msg") @@ -162,13 +176,21 @@ def main(repo1: str, repo2: str, verbose: bool = False): if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Check message compatibility between two repositories \ - using the set of checked messages ALL_PX4_ROS2_MESSAGES.") - parser.add_argument('repo1', help="path to the first repo containing a msg/ directory \ - (e.g /path/to/px4_msgs/)") - parser.add_argument('repo2', help="path to the second repo containing a msg/ directory \ - (e.g /path/to/PX4-Autopilot/)") - parser.add_argument('-v', '--verbose', dest='verbose', action='store_true', help='verbose output') + parser = argparse.ArgumentParser( + description="Check message compatibility between two repositories \ + using the set of checked messages ALL_PX4_ROS2_MESSAGES." + ) + parser.add_argument( + "repo1", + help="path to the first repo containing a msg/ directory \ + (e.g /path/to/px4_msgs/)", + ) + parser.add_argument( + "repo2", + help="path to the second repo containing a msg/ directory \ + (e.g /path/to/PX4-Autopilot/)", + ) + parser.add_argument("-v", "--verbose", dest="verbose", action="store_true", help="verbose output") args = parser.parse_args() main(args.repo1, args.repo2, args.verbose) diff --git a/scripts/check-used-topics.py b/scripts/check-used-topics.py index 72f8ad2..ccb6e46 100755 --- a/scripts/check-used-topics.py +++ b/scripts/check-used-topics.py @@ -7,20 +7,23 @@ from typing import Optional -ignored_topics = ['message_format_request', 'message_format_response'] +ignored_topics = ["message_format_request", "message_format_response"] configs = [ # Tuples of (topics_list_file, topic define, source_dir list) - ('px4_ros2_cpp/include/px4_ros2/components/message_compatibility_check.hpp', 'ALL_PX4_ROS2_MESSAGES', [ - 'px4_ros2_cpp/src' - ]), + ( + "px4_ros2_cpp/include/px4_ros2/components/message_compatibility_check.hpp", + "ALL_PX4_ROS2_MESSAGES", + ["px4_ros2_cpp/src"], + ), ] -project_root_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '..') +project_root_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "..") -def extract_topics_from_file(filename: str, extract_start_after: Optional[str] = None, - extract_end_before: Optional[str] = None) -> list[str]: +def extract_topics_from_file( + filename: str, extract_start_after: Optional[str] = None, extract_end_before: Optional[str] = None +) -> list[str]: with open(filename) as file: if extract_start_after is not None: for line in file: @@ -43,9 +46,9 @@ def check(verbose: bool): for topics_list_file, define, source_list in configs: topics_list_file = os.path.join(project_root_dir, topics_list_file) - checked_topics = extract_topics_from_file(topics_list_file, define, r'^\s*$') + checked_topics = extract_topics_from_file(topics_list_file, define, r"^\s*$") if verbose: - print(f'checked topics: {checked_topics}') + print(f"checked topics: {checked_topics}") assert len(checked_topics) > 0 all_used_topics = [] @@ -54,47 +57,50 @@ def check(verbose: bool): # Iterate recursively for subdir, dirs, files in os.walk(source_dir): for file in files: - if file.endswith('.hpp') or file.endswith('.cpp'): + if file.endswith(".hpp") or file.endswith(".cpp"): file_name = os.path.join(subdir, file) if os.path.normpath(file_name) == os.path.normpath(topics_list_file): continue if verbose: - print(f'extracting from file: {file_name}') + print(f"extracting from file: {file_name}") all_used_topics.extend(extract_topics_from_file(file_name)) all_used_topics = set(all_used_topics) # remove duplicates if verbose: - print(f'used topics: {all_used_topics}') + print(f"used topics: {all_used_topics}") not_found_topics = [] for used_topic in all_used_topics: if used_topic not in checked_topics and used_topic not in ignored_topics: not_found_topics.append(used_topic) if len(not_found_topics) > 0: - raise RuntimeError(f'Topic(s) {not_found_topics} are not in the list of checked topics ({define}).\n' - f'Add the topic to the file {topics_list_file}') + raise RuntimeError( + f"Topic(s) {not_found_topics} are not in the list of checked topics ({define}).\n" + f"Add the topic to the file {topics_list_file}" + ) not_used_topics = [] for checked_topic in checked_topics: if checked_topic not in all_used_topics and checked_topic not in ignored_topics: not_used_topics.append(checked_topic) if len(not_used_topics) > 0: - raise RuntimeError(f'Topic(s) {not_used_topics} are in the list of checked topics ({define}), but not ' - f'used in code.\n' - f'Remove the topic from the file {topics_list_file}') + raise RuntimeError( + f"Topic(s) {not_used_topics} are in the list of checked topics ({define}), but not " + f"used in code.\n" + f"Remove the topic from the file {topics_list_file}" + ) def main(): - parser = argparse.ArgumentParser(description='Check used PX4 topics in source') + parser = argparse.ArgumentParser(description="Check used PX4 topics in source") - parser.add_argument('-v', '--verbose', dest='verbose', action='store_true', - help='Verbose Output') + parser.add_argument("-v", "--verbose", dest="verbose", action="store_true", help="Verbose Output") args = parser.parse_args() check(args.verbose) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/scripts/run-clang-tidy.py b/scripts/run-clang-tidy.py index 0dfab5a..fed0ea0 100755 --- a/scripts/run-clang-tidy.py +++ b/scripts/run-clang-tidy.py @@ -1,12 +1,12 @@ #!/usr/bin/env python3 # -#===- run-clang-tidy.py - Parallel clang-tidy runner --------*- python -*--===# +# ===- 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 # -#===-----------------------------------------------------------------------===# +# ===-----------------------------------------------------------------------===# # FIXME: Integrate with clang-tidy-diff.py # flake8: noqa @@ -54,372 +54,412 @@ import traceback try: - import yaml + import yaml except ImportError: - yaml = None + yaml = None def strtobool(val): - """Convert a string representation of truth to a bool following LLVM's CLI argument parsing.""" + """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 + 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("'{}' is invalid value for boolean argument! Try 0 or 1.".format(val)) - # Return ArgumentTypeError so that argparse does not substitute its own error message - raise argparse.ArgumentTypeError( - "'{}' is invalid value for boolean argument! Try 0 or 1.".format(val) - ) def filter_files(ignore_config, files): - """Filter out all files specified via globs in DEFAULT_CLANG_TIDY_IGNORE. - """ - globs = list() - with open(ignore_config, 'r') as tidy_ignore: - for l in tidy_ignore: - line = l.strip() - # Tolerate comments and empty lines - if not line or line.startswith('#'): - continue - globs.append(line) - - exclude = set() - for g in globs: - for entry in files: - if fnmatch.fnmatch(entry, g): - exclude.add(entry) - - return list(set(files) - exclude), exclude + """Filter out all files specified via globs in DEFAULT_CLANG_TIDY_IGNORE.""" + globs = list() + with open(ignore_config, "r") as tidy_ignore: + for l in tidy_ignore: + line = l.strip() + # Tolerate comments and empty lines + if not line or line.startswith("#"): + continue + globs.append(line) + + exclude = set() + for g in globs: + for entry in files: + if fnmatch.fnmatch(entry, g): + exclude.add(entry) + + return list(set(files) - exclude), exclude + 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 + """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): - """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) - start.append(f) - return start + 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, +): + """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) + 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, 'r')) - 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() + """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, "r")) + 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 + """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( - "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( - "error: failed to find {} in $PATH or at {}" - .format(name, built_path)) + raise SystemExit("error: failed to find {} in $PATH or at {}".format(name, 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) - - 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() + """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, + ) + + 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('-ignore', default='', - help='File path to clang-tidy-ignore') - 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.') - 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) - 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) - 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) + 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("-ignore", default="", help="File path to clang-tidy-ignore") + 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." + ) + args = parser.parse_args() + + db_path = "compile_commands.json" + + if args.build_path is not None: + build_path = args.build_path 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 = set([make_absolute(entry['file'], entry['directory']) - for entry in database]) - if args.ignore: - files, excluded = filter_files(args.ignore, files) - if excluded: - print("Excluding the following files:\n" + "\n".join(excluded) + "\n") - - 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) + # 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 + ) + tmpdir = tempfile.mkdtemp() - if yaml and args.export_fixes: - print('Writing fixes to ' + args.export_fixes + ' ...') try: - merge_replacement_files(tmpdir, args.export_fixes) + 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, + ) + 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('Error exporting fixes.\n', file=sys.stderr) - traceback.print_exc() - return_code=1 + 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 = set([make_absolute(entry["file"], entry["directory"]) for entry in database]) + if args.ignore: + files, excluded = filter_files(args.ignore, files) + if excluded: + print("Excluding the following files:\n" + "\n".join(excluded) + "\n") + + max_task = args.j + if max_task == 0: + max_task = multiprocessing.cpu_count() - if args.fix: - print('Applying fixes ...') + # Build up a big regexy filter from all command line arguments. + file_name_re = re.compile("|".join(args.files)) + + return_code = 0 try: - apply_fixes(args, clang_apply_replacements_binary, tmpdir) - except: - print('Error applying fixes.\n', file=sys.stderr) - traceback.print_exc() - return_code = 1 + # 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 tmpdir: + shutil.rmtree(tmpdir) + sys.exit(return_code) -if __name__ == '__main__': - main() +if __name__ == "__main__": + main()