#!/usr/bin/env python3 # SPDX-FileCopyrightText: 2026 Blender Authors # # SPDX-License-Identifier: GPL-2.0-or-later """ Tool to inspect and automatically fix clang-tidy warnings and hints. Run complete checks: ./tools/utils_maintenance/clang_tidy.py check -- source Perform safe auto fixes: ./tools/utils_maintenance/clang_tidy.py fix --config=safe -- source Run checks without auto fixing: ./tools/utils_maintenance/clang_tidy.py check --config=safe -- source """ __all__ = ( "main", ) import argparse import contextlib import multiprocessing import pathlib import sys import subprocess from collections.abc import ( Sequence, ) from concurrent.futures import ( ProcessPoolExecutor, ) base_dir = pathlib.Path(__file__).parent.parent.parent.resolve() extensions = ( ".c", ".cc", ".cpp", ".cxx", ".h", ".hh", ".hpp", ".hxx", ".m", ".mm", ) # A subset of checks that are safe and useful to fix. config_safe = """ Checks: > -*, modernize-min-max-use-initializer-list, modernize-redundant-void-arg, modernize-use-bool-literals, modernize-use-nullptr, modernize-use-override, modernize-use-starts-ends-with, readability-braces-around-statements, readability-container-contains, readability-duplicate-include, readability-qualified-auto, readability-redundant-inline-specifier """ # These sometimes seem to make mistakes, so need manual verification. config_unsafe = """ Checks: > -*, modernize-deprecated-headers, modernize-use-equals-default, modernize-use-ranges, modernize-use-using, readability-redundant-casting, readability-use-std-min-max """ # Config to fix automatically add const. This requires some manual fixes due to wrong # changes with functions pointers. And to avoid changing the existing const qualifier # order it can be used as follows: # # echo "QualifierAlignment: Left" >> .clang-format # make format # git commit source # # ./tools/utils_maintenance/clang_tidy.py fix --config=const -- source # make format # git commit source # # git checkout .clang-format # git revert HEAD~1 # config_const = """ Checks: > -*, misc-const-correctness, readability-non-const-parameter, CheckOptions: - key: misc-const-correctness.WarnPointersAsPointers value: 1 - key: misc-const-correctness.TransformPointersAsPointers value: 1 """ # Config to remove unused includes. # This can easily break with different platforms and build options, and also # requires LLVM version 23 or newer to support disabling MissingIncludes. config_includes = """ Checks: > -*, misc-include-cleaner CheckOptions: - key: misc-include-cleaner.MissingIncludes value: 0 - key: misc-include-cleaner.UnusedIncludes value: 1 """ configs = { "complete": None, "safe": config_safe, "unsafe": config_unsafe, "const": config_const, "includes": config_includes, } def source_files_from_git(paths: Sequence[str]) -> list[str]: cmd = ("git", "ls-tree", "-r", "HEAD", *paths, "--name-only", "-z") try: files_bytes = subprocess.check_output(cmd, cwd=base_dir).split(b"\0") except subprocess.CalledProcessError: return [] files = [f.decode("utf-8") for f in files_bytes if f] return [f for f in files if f.endswith(extensions)] def process_file( file_path: str, tidy_config: str | None, fix: bool, compile_commands_dir: str, done: multiprocessing.managers.ValueProxy[int], lock: contextlib.AbstractContextManager[bool], total: int, ) -> None: # Progress display. with lock: done.value += 1 progress_text = f"[{done.value}/{total}] {file_path}" if sys.stdout.isatty(): sys.stdout.write(f"\r{progress_text}\033[K") else: sys.stdout.write(f"{progress_text}\n") sys.stdout.flush() cmd = ["clang-tidy", "-p", compile_commands_dir] if tidy_config: cmd.append(f"--config={tidy_config}") if fix: cmd.append("--fix") cmd.append("--quiet") cmd.append(file_path) # Ignore errors as clang-tidy returns non-zero for irrelevant errors. if fix: subprocess.run( cmd, check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, cwd=base_dir, ) else: subprocess.run(cmd, check=False, cwd=base_dir) def main() -> None: parser = argparse.ArgumentParser( description="Run clang-tidy on files.", formatter_class=argparse.RawDescriptionHelpFormatter, ) # Show full help instead of short usage on argument errors. parser.print_usage = parser.print_help # type: ignore[method-assign] parser.add_argument( "command", choices=["check", "fix"], help="Check or auto-fix", ) parser.add_argument( "--config", choices=configs.keys(), default="complete", help="Config to use, see the description for details", ) parser.add_argument( "--compile-commands-dir", default=str(base_dir), help="Directory containing compile_commands.json (default: project root)", ) parser.add_argument( "paths", nargs="+", help="Files or directories to process.", ) args = parser.parse_args() compile_commands = pathlib.Path(args.compile_commands_dir) / "compile_commands.json" if not compile_commands.exists(): sys.stderr.write(f"Error: {compile_commands} not found\n") sys.stderr.write("Enable CMAKE_EXPORT_COMPILE_COMMANDS or use --compile-commands-dir=/build/dir/\n") sys.exit(1) if args.command == "fix" and args.config == "complete": parser.error("The 'complete' configuration cannot be used with the 'fix' command.") files = source_files_from_git(args.paths) if not files: print("No files found to process.") return jobs = multiprocessing.cpu_count() total_files = len(files) with multiprocessing.Manager() as mpm: done = mpm.Value("i", 0) lock = mpm.Lock() with ProcessPoolExecutor(max_workers=jobs) as executor: futures = [ executor.submit( process_file, f, configs[args.config], args.command == "fix", args.compile_commands_dir, done, lock, total_files, ) for f in files ] for future in futures: future.result() print("\nDone.") if __name__ == "__main__": main()