1+ from concurrent .futures import ThreadPoolExecutor
12import subprocess
23import sys
3- from argparse import ArgumentParser
4+ from argparse import ArgumentParser , ArgumentTypeError
45from pathlib import Path
5- from typing import Optional , Tuple
6+ from typing import List , Optional , Tuple
67
78from cpp_linter_hooks .util import resolve_install , DEFAULT_CLANG_TIDY_VERSION
89
910COMPILE_DB_SEARCH_DIRS = ["build" , "out" , "cmake-build-debug" , "_build" ]
11+ SOURCE_FILE_SUFFIXES = {
12+ ".c" ,
13+ ".cc" ,
14+ ".cp" ,
15+ ".cpp" ,
16+ ".cxx" ,
17+ ".c++" ,
18+ ".cu" ,
19+ ".cuh" ,
20+ ".h" ,
21+ ".hh" ,
22+ ".hpp" ,
23+ ".hxx" ,
24+ ".h++" ,
25+ ".ipp" ,
26+ ".inl" ,
27+ ".ixx" ,
28+ ".tpp" ,
29+ ".txx" ,
30+ }
31+
32+
33+ def _positive_int (value : str ) -> int :
34+ jobs = int (value )
35+ if jobs < 1 :
36+ raise ArgumentTypeError ("--jobs must be greater than 0" )
37+ return jobs
38+
1039
1140parser = ArgumentParser ()
1241parser .add_argument ("--version" , default = DEFAULT_CLANG_TIDY_VERSION )
1342parser .add_argument ("--compile-commands" , default = None , dest = "compile_commands" )
1443parser .add_argument (
1544 "--no-compile-commands" , action = "store_true" , dest = "no_compile_commands"
1645)
46+ parser .add_argument ("-j" , "--jobs" , type = _positive_int , default = 1 )
1747parser .add_argument ("-v" , "--verbose" , action = "store_true" )
1848
1949
@@ -74,6 +104,38 @@ def _exec_clang_tidy(command) -> Tuple[int, str]:
74104 return 1 , str (e )
75105
76106
107+ def _looks_like_source_file (path : str ) -> bool :
108+ return Path (path ).suffix .lower () in SOURCE_FILE_SUFFIXES
109+
110+
111+ def _split_source_files (args : List [str ]) -> Tuple [List [str ], List [str ]]:
112+ split_idx = len (args )
113+ source_files : List [str ] = []
114+ for idx in range (len (args ) - 1 , - 1 , - 1 ):
115+ if not _looks_like_source_file (args [idx ]):
116+ break
117+ source_files .append (args [idx ])
118+ split_idx = idx
119+ return args [:split_idx ], list (reversed (source_files ))
120+
121+
122+ def _combine_outputs (results : List [Tuple [int , str ]]) -> str :
123+ return "\n " .join (output .rstrip ("\n " ) for _ , output in results if output )
124+
125+
126+ def _exec_parallel_clang_tidy (
127+ command_prefix : List [str ], source_files : List [str ], jobs : int
128+ ) -> Tuple [int , str ]:
129+ def run_file (source_file : str ) -> Tuple [int , str ]:
130+ return _exec_clang_tidy (command_prefix + [source_file ])
131+
132+ with ThreadPoolExecutor (max_workers = min (jobs , len (source_files ))) as executor :
133+ results = list (executor .map (run_file , source_files ))
134+
135+ retval = 1 if any (retval != 0 for retval , _ in results ) else 0
136+ return retval , _combine_outputs (results )
137+
138+
77139def run_clang_tidy (args = None ) -> Tuple [int , str ]:
78140 hook_args , other_args = parser .parse_known_args (args )
79141 if hook_args .version :
@@ -90,6 +152,21 @@ def run_clang_tidy(args=None) -> Tuple[int, str]:
90152 )
91153 other_args = ["-p" , compile_db_path ] + other_args
92154
155+ clang_tidy_args , source_files = _split_source_files (other_args )
156+
157+ # Parallel execution is unsafe when arguments include flags that write to a
158+ # shared output path (e.g., --export-fixes fixes.yaml). In that case, force
159+ # serial execution to avoid concurrent writes/overwrites.
160+ unsafe_parallel = any (
161+ arg == "--export-fixes" or arg .startswith ("--export-fixes=" )
162+ for arg in clang_tidy_args
163+ )
164+
165+ if hook_args .jobs > 1 and len (source_files ) > 1 and not unsafe_parallel :
166+ return _exec_parallel_clang_tidy (
167+ ["clang-tidy" ] + clang_tidy_args , source_files , hook_args .jobs
168+ )
169+
93170 return _exec_clang_tidy (["clang-tidy" ] + other_args )
94171
95172
0 commit comments