3 # ===- clang-tidy-diff.py - ClangTidy Diff Checker -----------*- 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 # ===-----------------------------------------------------------------------===#
12 ClangTidy Diff Checker
13 ======================
15 This script reads input from a unified diff, runs clang-tidy on all changed
16 files and outputs clang-tidy warnings in changed lines only. This is useful to
17 detect clang-tidy regressions in the lines touched by a specific patch.
18 Example usage for git/svn users:
20 git diff -U0 HEAD^ | clang-tidy-diff.py -p1
21 svn diff --diff-cmd=diff -x-U0 | \
22 clang-tidy-diff.py -fix -checks=-*,modernize-use-override
29 import multiprocessing
44 is_py2
= sys
.version
[0] == "2"
52 def run_tidy(task_queue
, lock
, timeout
, failed_files
):
55 command
= task_queue
.get()
57 proc
= subprocess
.Popen(
58 command
, stdout
=subprocess
.PIPE
, stderr
=subprocess
.PIPE
61 if timeout
is not None:
62 watchdog
= threading
.Timer(timeout
, proc
.kill
)
65 stdout
, stderr
= proc
.communicate()
66 if proc
.returncode
!= 0:
67 if proc
.returncode
< 0:
68 msg
= "Terminated by signal %d : %s\n" % (
72 stderr
+= msg
.encode("utf-8")
73 failed_files
.append(command
)
76 sys
.stdout
.write(stdout
.decode("utf-8") + "\n")
79 sys
.stderr
.write(stderr
.decode("utf-8") + "\n")
81 except Exception as e
:
83 sys
.stderr
.write("Failed: " + str(e
) + ": ".join(command
) + "\n")
86 if not (timeout
is None or watchdog
is None):
87 if not watchdog
.is_alive():
89 "Terminated by timeout: " + " ".join(command
) + "\n"
92 task_queue
.task_done()
95 def start_workers(max_tasks
, tidy_caller
, arguments
):
96 for _
in range(max_tasks
):
97 t
= threading
.Thread(target
=tidy_caller
, args
=arguments
)
102 def merge_replacement_files(tmpdir
, mergefile
):
103 """Merge all replacement files in a directory into a single file"""
104 # The fixes suggested by clang-tidy >= 4.0.0 are given under
105 # the top level key 'Diagnostics' in the output yaml files
106 mergekey
= "Diagnostics"
108 for replacefile
in glob
.iglob(os
.path
.join(tmpdir
, "*.yaml")):
109 content
= yaml
.safe_load(open(replacefile
, "r"))
111 continue # Skip empty files.
112 merged
.extend(content
.get(mergekey
, []))
115 # MainSourceFile: The key is required by the definition inside
116 # include/clang/Tooling/ReplacementsYaml.h, but the value
117 # is actually never used inside clang-apply-replacements,
118 # so we set it to '' here.
119 output
= {"MainSourceFile": "", mergekey
: merged
}
120 with
open(mergefile
, "w") as out
:
121 yaml
.safe_dump(output
, out
)
124 open(mergefile
, "w").close()
128 parser
= argparse
.ArgumentParser(
129 description
="Run clang-tidy against changed files, and "
130 "output diagnostics only for modified "
134 "-clang-tidy-binary",
136 default
="clang-tidy",
137 help="path to clang-tidy binary",
143 help="strip the smallest prefix containing P slashes",
149 help="custom pattern selecting file paths to check "
150 "(case sensitive, overrides -iregex)",
155 default
=r
".*\.(cpp|cc|c\+\+|cxx|c|cl|h|hpp|m|mm|inc)",
156 help="custom pattern selecting file paths to check "
157 "(case insensitive, overridden by -regex)",
163 help="number of tidy instances to be run in parallel.",
166 "-timeout", type=int, default
=None, help="timeout per each file in seconds."
169 "-fix", action
="store_true", default
=False, help="apply suggested fixes"
173 help="checks filter, when not specified, use clang-tidy " "default",
179 help="Specify the path of .clang-tidy or custom config file",
182 parser
.add_argument("-use-color", action
="store_true", help="Use colors in output")
184 "-path", dest
="build_path", help="Path used to read a compile command database."
189 metavar
="FILE_OR_DIRECTORY",
191 help="A directory or a yaml file to store suggested fixes in, "
192 "which can be applied with clang-apply-replacements. If the "
193 "parameter is a directory, the fixes of each compilation unit are "
194 "stored in individual yaml files in the directory.",
201 help="A directory to store suggested fixes in, which can be applied "
202 "with clang-apply-replacements. The fixes of each compilation unit are "
203 "stored in individual yaml files in the directory.",
210 help="Additional argument to append to the compiler " "command line.",
214 dest
="extra_arg_before",
217 help="Additional argument to prepend to the compiler " "command line.",
223 help="Run clang-tidy in quiet mode",
230 help="Load the specified plugin in clang-tidy.",
235 help="Allow empty enabled checks.",
241 clang_tidy_args
.extend(argv
[argv
.index("--") :])
242 argv
= argv
[: argv
.index("--")]
244 args
= parser
.parse_args(argv
)
246 # Extract changed lines for each file.
249 for line
in sys
.stdin
:
250 match
= re
.search('^\\+\\+\\+\\ "?(.*?/){%s}([^ \t\n"]*)' % args
.p
, line
)
252 filename
= match
.group(2)
256 if args
.regex
is not None:
257 if not re
.match("^%s$" % args
.regex
, filename
):
260 if not re
.match("^%s$" % args
.iregex
, filename
, re
.IGNORECASE
):
263 match
= re
.search(r
"^@@.*\+(\d+)(,(\d+))?", line
)
265 start_line
= int(match
.group(1))
268 line_count
= int(match
.group(3))
271 end_line
= start_line
+ line_count
- 1
272 lines_by_file
.setdefault(filename
, []).append([start_line
, end_line
])
274 if not any(lines_by_file
):
275 print("No relevant changes found.")
278 max_task_count
= args
.j
279 if max_task_count
== 0:
280 max_task_count
= multiprocessing
.cpu_count()
281 max_task_count
= min(len(lines_by_file
), max_task_count
)
283 combine_fixes
= False
284 export_fixes_dir
= None
285 delete_fixes_dir
= False
286 if args
.export_fixes
is not None:
287 # if a directory is given, create it if it does not exist
288 if args
.export_fixes
.endswith(os
.path
.sep
) and not os
.path
.isdir(
291 os
.makedirs(args
.export_fixes
)
293 if not os
.path
.isdir(args
.export_fixes
):
296 "Cannot combine fixes in one yaml file. Either install PyYAML or specify an output directory."
301 if os
.path
.isdir(args
.export_fixes
):
302 export_fixes_dir
= args
.export_fixes
305 export_fixes_dir
= tempfile
.mkdtemp()
306 delete_fixes_dir
= True
308 # Tasks for clang-tidy.
309 task_queue
= queue
.Queue(max_task_count
)
310 # A lock for console output.
311 lock
= threading
.Lock()
313 # List of files with a non-zero return code.
316 # Run a pool of clang-tidy workers.
318 max_task_count
, run_tidy
, (task_queue
, lock
, args
.timeout
, failed_files
)
321 # Form the common args list.
322 common_clang_tidy_args
= []
324 common_clang_tidy_args
.append("-fix")
325 if args
.checks
!= "":
326 common_clang_tidy_args
.append("-checks=" + args
.checks
)
327 if args
.config_file
!= "":
328 common_clang_tidy_args
.append("-config-file=" + args
.config_file
)
330 common_clang_tidy_args
.append("-quiet")
331 if args
.build_path
is not None:
332 common_clang_tidy_args
.append("-p=%s" % args
.build_path
)
334 common_clang_tidy_args
.append("--use-color")
335 if args
.allow_no_checks
:
336 common_clang_tidy_args
.append("--allow-no-checks")
337 for arg
in args
.extra_arg
:
338 common_clang_tidy_args
.append("-extra-arg=%s" % arg
)
339 for arg
in args
.extra_arg_before
:
340 common_clang_tidy_args
.append("-extra-arg-before=%s" % arg
)
341 for plugin
in args
.plugins
:
342 common_clang_tidy_args
.append("-load=%s" % plugin
)
344 for name
in lines_by_file
:
345 line_filter_json
= json
.dumps(
346 [{"name": name
, "lines": lines_by_file
[name
]}], separators
=(",", ":")
349 # Run clang-tidy on files containing changes.
350 command
= [args
.clang_tidy_binary
]
351 command
.append("-line-filter=" + line_filter_json
)
352 if args
.export_fixes
is not None:
353 # Get a temporary file. We immediately close the handle so clang-tidy can
355 (handle
, tmp_name
) = tempfile
.mkstemp(suffix
=".yaml", dir=export_fixes_dir
)
357 command
.append("-export-fixes=" + tmp_name
)
358 command
.extend(common_clang_tidy_args
)
360 command
.extend(clang_tidy_args
)
362 task_queue
.put(command
)
364 # Application return code
367 # Wait for all threads to be done.
369 # Application return code
375 print("Writing fixes to " + args
.export_fixes
+ " ...")
377 merge_replacement_files(export_fixes_dir
, args
.export_fixes
)
379 sys
.stderr
.write("Error exporting fixes.\n")
380 traceback
.print_exc()
384 shutil
.rmtree(export_fixes_dir
)
385 sys
.exit(return_code
)
388 if __name__
== "__main__":