3 #===- run-clang-tidy.py - Parallel clang-tidy runner ---------*- python -*--===#
5 # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
6 # See https://llvm.org/LICENSE.txt for license information.
7 # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
9 #===------------------------------------------------------------------------===#
10 # FIXME: Integrate with clang-tidy-diff.py
13 Parallel clang-tidy runner
14 ==========================
16 Runs clang-tidy over all files in a compilation database. Requires clang-tidy
17 and clang-apply-replacements in $PATH.
20 - Run clang-tidy on all files in the current working directory with a default
21 set of checks and show warnings in the cpp files and all project headers.
22 run-clang-tidy.py $PWD
24 - Fix all header guards.
25 run-clang-tidy.py -fix -checks=-*,llvm-header-guard
27 - Fix all header guards included from clang-tidy and header guards
28 for clang-tidy headers.
29 run-clang-tidy.py -fix -checks=-*,llvm-header-guard extra/clang-tidy \
30 -header-filter=extra/clang-tidy
32 Compilation database setup:
33 http://clang.llvm.org/docs/HowToSetupToolingForLLVM.html
36 from __future__
import print_function
41 import multiprocessing
56 is_py2
= sys
.version
[0] == '2'
63 def find_compilation_database(path
):
64 """Adjusts the directory until a compilation database is found."""
66 while not os
.path
.isfile(os
.path
.join(result
, path
)):
67 if os
.path
.realpath(result
) == '/':
68 print('Error: could not find compilation database.')
71 return os
.path
.realpath(result
)
74 def make_absolute(f
, directory
):
77 return os
.path
.normpath(os
.path
.join(directory
, f
))
80 def get_tidy_invocation(f
, clang_tidy_binary
, checks
, tmpdir
, build_path
,
81 header_filter
, extra_arg
, extra_arg_before
, quiet
,
83 """Gets a command line for clang-tidy."""
84 start
= [clang_tidy_binary
]
85 if header_filter
is not None:
86 start
.append('-header-filter=' + header_filter
)
88 start
.append('-checks=' + checks
)
89 if tmpdir
is not None:
90 start
.append('-export-fixes')
91 # Get a temporary file. We immediately close the handle so clang-tidy can
93 (handle
, name
) = tempfile
.mkstemp(suffix
='.yaml', dir=tmpdir
)
97 start
.append('-extra-arg=%s' % arg
)
98 for arg
in extra_arg_before
:
99 start
.append('-extra-arg-before=%s' % arg
)
100 start
.append('-p=' + build_path
)
102 start
.append('-quiet')
104 start
.append('-config=' + config
)
109 def merge_replacement_files(tmpdir
, mergefile
):
110 """Merge all replacement files in a directory into a single file"""
111 # The fixes suggested by clang-tidy >= 4.0.0 are given under
112 # the top level key 'Diagnostics' in the output yaml files
113 mergekey
="Diagnostics"
115 for replacefile
in glob
.iglob(os
.path
.join(tmpdir
, '*.yaml')):
116 content
= yaml
.safe_load(open(replacefile
, 'r'))
118 continue # Skip empty files.
119 merged
.extend(content
.get(mergekey
, []))
122 # MainSourceFile: The key is required by the definition inside
123 # include/clang/Tooling/ReplacementsYaml.h, but the value
124 # is actually never used inside clang-apply-replacements,
125 # so we set it to '' here.
126 output
= { 'MainSourceFile': '', mergekey
: merged
}
127 with
open(mergefile
, 'w') as out
:
128 yaml
.safe_dump(output
, out
)
131 open(mergefile
, 'w').close()
134 def check_clang_apply_replacements_binary(args
):
135 """Checks if invoking supplied clang-apply-replacements binary works."""
137 subprocess
.check_call([args
.clang_apply_replacements_binary
, '--version'])
139 print('Unable to run clang-apply-replacements. Is clang-apply-replacements '
140 'binary correctly specified?', file=sys
.stderr
)
141 traceback
.print_exc()
145 def apply_fixes(args
, tmpdir
):
146 """Calls clang-apply-fixes on a given directory."""
147 invocation
= [args
.clang_apply_replacements_binary
]
149 invocation
.append('-format')
151 invocation
.append('-style=' + args
.style
)
152 invocation
.append(tmpdir
)
153 subprocess
.call(invocation
)
156 def run_tidy(args
, tmpdir
, build_path
, queue
, lock
, failed_files
):
157 """Takes filenames out of queue and runs clang-tidy on them."""
160 invocation
= get_tidy_invocation(name
, args
.clang_tidy_binary
, args
.checks
,
161 tmpdir
, build_path
, args
.header_filter
,
162 args
.extra_arg
, args
.extra_arg_before
,
163 args
.quiet
, args
.config
)
165 proc
= subprocess
.Popen(invocation
)
167 if proc
.returncode
!= 0:
168 failed_files
.append(name
)
173 parser
= argparse
.ArgumentParser(description
='Runs clang-tidy over all files '
174 'in a compilation database. Requires '
175 'clang-tidy and clang-apply-replacements in '
177 parser
.add_argument('-clang-tidy-binary', metavar
='PATH',
178 default
='clang-tidy',
179 help='path to clang-tidy binary')
180 parser
.add_argument('-clang-apply-replacements-binary', metavar
='PATH',
181 default
='clang-apply-replacements',
182 help='path to clang-apply-replacements binary')
183 parser
.add_argument('-checks', default
=None,
184 help='checks filter, when not specified, use clang-tidy '
186 parser
.add_argument('-config', default
=None,
187 help='Specifies a configuration in YAML/JSON format: '
188 ' -config="{Checks: \'*\', '
189 ' CheckOptions: [{key: x, '
191 'When the value is empty, clang-tidy will '
192 'attempt to find a file named .clang-tidy for '
193 'each source file in its parent directories.')
194 parser
.add_argument('-header-filter', default
=None,
195 help='regular expression matching the names of the '
196 'headers to output diagnostics from. Diagnostics from '
197 'the main file of each translation unit are always '
200 parser
.add_argument('-export-fixes', metavar
='filename', dest
='export_fixes',
201 help='Create a yaml file to store suggested fixes in, '
202 'which can be applied with clang-apply-replacements.')
203 parser
.add_argument('-j', type=int, default
=0,
204 help='number of tidy instances to be run in parallel.')
205 parser
.add_argument('files', nargs
='*', default
=['.*'],
206 help='files to be processed (regex on path)')
207 parser
.add_argument('-fix', action
='store_true', help='apply fix-its')
208 parser
.add_argument('-format', action
='store_true', help='Reformat code '
209 'after applying fixes')
210 parser
.add_argument('-style', default
='file', help='The style of reformat '
211 'code after applying fixes')
212 parser
.add_argument('-p', dest
='build_path',
213 help='Path used to read a compile command database.')
214 parser
.add_argument('-extra-arg', dest
='extra_arg',
215 action
='append', default
=[],
216 help='Additional argument to append to the compiler '
218 parser
.add_argument('-extra-arg-before', dest
='extra_arg_before',
219 action
='append', default
=[],
220 help='Additional argument to prepend to the compiler '
222 parser
.add_argument('-quiet', action
='store_true',
223 help='Run clang-tidy in quiet mode')
224 args
= parser
.parse_args()
226 db_path
= 'compile_commands.json'
228 if args
.build_path
is not None:
229 build_path
= args
.build_path
232 build_path
= find_compilation_database(db_path
)
235 invocation
= [args
.clang_tidy_binary
, '-list-checks']
236 invocation
.append('-p=' + build_path
)
238 invocation
.append('-checks=' + args
.checks
)
239 invocation
.append('-')
241 # Even with -quiet we still want to check if we can call clang-tidy.
242 with
open(os
.devnull
, 'w') as dev_null
:
243 subprocess
.check_call(invocation
, stdout
=dev_null
)
245 subprocess
.check_call(invocation
)
247 print("Unable to run clang-tidy.", file=sys
.stderr
)
250 # Load the database and extract all files.
251 database
= json
.load(open(os
.path
.join(build_path
, db_path
)))
252 files
= [make_absolute(entry
['file'], entry
['directory'])
253 for entry
in database
]
257 max_task
= multiprocessing
.cpu_count()
260 if args
.fix
or (yaml
and args
.export_fixes
):
261 check_clang_apply_replacements_binary(args
)
262 tmpdir
= tempfile
.mkdtemp()
264 # Build up a big regexy filter from all command line arguments.
265 file_name_re
= re
.compile('|'.join(args
.files
))
269 # Spin up a bunch of tidy-launching threads.
270 task_queue
= queue
.Queue(max_task
)
271 # List of files with a non-zero return code.
273 lock
= threading
.Lock()
274 for _
in range(max_task
):
275 t
= threading
.Thread(target
=run_tidy
,
276 args
=(args
, tmpdir
, build_path
, task_queue
, lock
, failed_files
))
280 # Fill the queue with files.
282 if file_name_re
.search(name
):
285 # Wait for all threads to be done.
287 if len(failed_files
):
290 except KeyboardInterrupt:
291 # This is a sad hack. Unfortunately subprocess goes
292 # bonkers with ctrl-c and we start forking merrily.
293 print('\nCtrl-C detected, goodbye.')
295 shutil
.rmtree(tmpdir
)
298 if yaml
and args
.export_fixes
:
299 print('Writing fixes to ' + args
.export_fixes
+ ' ...')
301 merge_replacement_files(tmpdir
, args
.export_fixes
)
303 print('Error exporting fixes.\n', file=sys
.stderr
)
304 traceback
.print_exc()
308 print('Applying fixes ...')
310 apply_fixes(args
, tmpdir
)
312 print('Error applying fixes.\n', file=sys
.stderr
)
313 traceback
.print_exc()
317 shutil
.rmtree(tmpdir
)
318 sys
.exit(return_code
)
320 if __name__
== '__main__':