clang-tidy-diff.py 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. #!/usr/bin/env python
  2. #
  3. # This file is based on
  4. # https://github.com/llvm-mirror/clang-tools-extra/blob/5c40544fa40bfb85ec888b6a03421b3905e4a4e7/clang-tidy/tool/clang-tidy-diff.py
  5. #
  6. # ===- clang-tidy-diff.py - ClangTidy Diff Checker ----------*- python -*--===#
  7. #
  8. # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
  9. # See https://llvm.org/LICENSE.txt for license information.
  10. # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
  11. #
  12. # ===----------------------------------------------------------------------===#
  13. r"""
  14. ClangTidy Diff Checker
  15. ======================
  16. This script reads input from a unified diff, runs clang-tidy on all changed
  17. files and outputs clang-tidy warnings in changed lines only. This is useful to
  18. detect clang-tidy regressions in the lines touched by a specific patch.
  19. 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
  23. """
  24. import argparse
  25. import glob
  26. import json
  27. import multiprocessing
  28. import os
  29. import re
  30. import shutil
  31. import subprocess
  32. import sys
  33. import tempfile
  34. import threading
  35. import traceback
  36. try:
  37. import yaml
  38. except ImportError:
  39. yaml = None
  40. is_py2 = sys.version[0] == "2"
  41. if is_py2:
  42. import Queue as queue
  43. else:
  44. import queue as queue
  45. def run_tidy(task_queue, lock, timeout):
  46. watchdog = None
  47. while True:
  48. command = task_queue.get()
  49. try:
  50. proc = subprocess.Popen(
  51. command, stdout=subprocess.PIPE, stderr=subprocess.PIPE
  52. )
  53. if timeout is not None:
  54. watchdog = threading.Timer(timeout, proc.kill)
  55. watchdog.start()
  56. stdout, stderr = proc.communicate()
  57. with lock:
  58. sys.stdout.write(stdout.decode("utf-8") + "\n")
  59. sys.stdout.flush()
  60. if stderr:
  61. sys.stderr.write(stderr.decode("utf-8") + "\n")
  62. sys.stderr.flush()
  63. except Exception as e:
  64. with lock:
  65. sys.stderr.write("Failed: " + str(e) + ": ".join(command) + "\n")
  66. finally:
  67. with lock:
  68. if timeout is not None and watchdog is not None:
  69. if not watchdog.is_alive():
  70. sys.stderr.write(
  71. "Terminated by timeout: " + " ".join(command) + "\n"
  72. )
  73. watchdog.cancel()
  74. task_queue.task_done()
  75. def start_workers(max_tasks, tidy_caller, task_queue, lock, timeout):
  76. for _ in range(max_tasks):
  77. t = threading.Thread(target=tidy_caller, args=(task_queue, lock, timeout))
  78. t.daemon = True
  79. t.start()
  80. def merge_replacement_files(tmpdir, mergefile):
  81. """Merge all replacement files in a directory into a single file"""
  82. # The fixes suggested by clang-tidy >= 4.0.0 are given under
  83. # the top level key 'Diagnostics' in the output yaml files
  84. mergekey = "Diagnostics"
  85. merged = []
  86. for replacefile in glob.iglob(os.path.join(tmpdir, "*.yaml")):
  87. content = yaml.safe_load(open(replacefile, "r"))
  88. if not content:
  89. continue # Skip empty files.
  90. merged.extend(content.get(mergekey, []))
  91. if merged:
  92. # MainSourceFile: The key is required by the definition inside
  93. # include/clang/Tooling/ReplacementsYaml.h, but the value
  94. # is actually never used inside clang-apply-replacements,
  95. # so we set it to '' here.
  96. output = {"MainSourceFile": "", mergekey: merged}
  97. with open(mergefile, "w") as out:
  98. yaml.safe_dump(output, out)
  99. else:
  100. # Empty the file:
  101. open(mergefile, "w").close()
  102. def main():
  103. parser = argparse.ArgumentParser(
  104. description="Run clang-tidy against changed files, and "
  105. "output diagnostics only for modified "
  106. "lines."
  107. )
  108. parser.add_argument(
  109. "-clang-tidy-binary",
  110. metavar="PATH",
  111. default="clang-tidy",
  112. help="path to clang-tidy binary",
  113. )
  114. parser.add_argument(
  115. "-p",
  116. metavar="NUM",
  117. default=0,
  118. help="strip the smallest prefix containing P slashes",
  119. )
  120. parser.add_argument(
  121. "-regex",
  122. metavar="PATTERN",
  123. default=None,
  124. help="custom pattern selecting file paths to check "
  125. "(case sensitive, overrides -iregex)",
  126. )
  127. parser.add_argument(
  128. "-iregex",
  129. metavar="PATTERN",
  130. default=r".*\.(cpp|cc|c\+\+|cxx|c|cl|h|hpp|m|mm|inc)",
  131. help="custom pattern selecting file paths to check "
  132. "(case insensitive, overridden by -regex)",
  133. )
  134. parser.add_argument(
  135. "-j",
  136. type=int,
  137. default=1,
  138. help="number of tidy instances to be run in parallel.",
  139. )
  140. parser.add_argument(
  141. "-timeout", type=int, default=None, help="timeout per each file in seconds."
  142. )
  143. parser.add_argument(
  144. "-fix", action="store_true", default=False, help="apply suggested fixes"
  145. )
  146. parser.add_argument(
  147. "-checks",
  148. help="checks filter, when not specified, use clang-tidy " "default",
  149. default="",
  150. )
  151. parser.add_argument(
  152. "-path", dest="build_path", help="Path used to read a compile command database."
  153. )
  154. if yaml:
  155. parser.add_argument(
  156. "-export-fixes",
  157. metavar="FILE",
  158. dest="export_fixes",
  159. help="Create a yaml file to store suggested fixes in, "
  160. "which can be applied with clang-apply-replacements.",
  161. )
  162. parser.add_argument(
  163. "-extra-arg",
  164. dest="extra_arg",
  165. action="append",
  166. default=[],
  167. help="Additional argument to append to the compiler " "command line.",
  168. )
  169. parser.add_argument(
  170. "-extra-arg-before",
  171. dest="extra_arg_before",
  172. action="append",
  173. default=[],
  174. help="Additional argument to prepend to the compiler " "command line.",
  175. )
  176. parser.add_argument(
  177. "-quiet",
  178. action="store_true",
  179. default=False,
  180. help="Run clang-tidy in quiet mode",
  181. )
  182. clang_tidy_args = []
  183. argv = sys.argv[1:]
  184. if "--" in argv:
  185. clang_tidy_args.extend(argv[argv.index("--") :])
  186. argv = argv[: argv.index("--")]
  187. args = parser.parse_args(argv)
  188. # Extract changed lines for each file.
  189. filename = None
  190. lines_by_file = {}
  191. for line in sys.stdin:
  192. match = re.search('^\+\+\+\ "?(.*?/){%s}([^ \t\n"]*)' % args.p, line)
  193. if match:
  194. filename = match.group(2)
  195. if filename is None:
  196. continue
  197. if args.regex is not None:
  198. if not re.match("^%s$" % args.regex, filename):
  199. continue
  200. else:
  201. if not re.match("^%s$" % args.iregex, filename, re.IGNORECASE):
  202. continue
  203. match = re.search("^@@.*\+(\d+)(,(\d+))?", line)
  204. if match:
  205. start_line = int(match.group(1))
  206. line_count = 1
  207. if match.group(3):
  208. line_count = int(match.group(3))
  209. if line_count == 0:
  210. continue
  211. end_line = start_line + line_count - 1
  212. lines_by_file.setdefault(filename, []).append([start_line, end_line])
  213. if not any(lines_by_file):
  214. print("No relevant changes found.")
  215. sys.exit(0)
  216. max_task_count = args.j
  217. if max_task_count == 0:
  218. max_task_count = multiprocessing.cpu_count()
  219. max_task_count = min(len(lines_by_file), max_task_count)
  220. tmpdir = None
  221. if yaml and args.export_fixes:
  222. tmpdir = tempfile.mkdtemp()
  223. # Tasks for clang-tidy.
  224. task_queue = queue.Queue(max_task_count)
  225. # A lock for console output.
  226. lock = threading.Lock()
  227. # Run a pool of clang-tidy workers.
  228. start_workers(max_task_count, run_tidy, task_queue, lock, args.timeout)
  229. # Form the common args list.
  230. common_clang_tidy_args = []
  231. if args.fix:
  232. common_clang_tidy_args.append("-fix")
  233. if args.checks != "":
  234. common_clang_tidy_args.append("-checks=" + args.checks)
  235. if args.quiet:
  236. common_clang_tidy_args.append("-quiet")
  237. if args.build_path is not None:
  238. common_clang_tidy_args.append("-p=%s" % args.build_path)
  239. for arg in args.extra_arg:
  240. common_clang_tidy_args.append("-extra-arg=%s" % arg)
  241. for arg in args.extra_arg_before:
  242. common_clang_tidy_args.append("-extra-arg-before=%s" % arg)
  243. for name in lines_by_file:
  244. line_filter_json = json.dumps(
  245. [{"name": name, "lines": lines_by_file[name]}], separators=(",", ":")
  246. )
  247. # Run clang-tidy on files containing changes.
  248. command = [args.clang_tidy_binary]
  249. command.append("-line-filter=" + line_filter_json)
  250. if yaml and args.export_fixes:
  251. # Get a temporary file. We immediately close the handle so
  252. # clang-tidy can overwrite it.
  253. (handle, tmp_name) = tempfile.mkstemp(suffix=".yaml", dir=tmpdir)
  254. os.close(handle)
  255. command.append("-export-fixes=" + tmp_name)
  256. command.extend(common_clang_tidy_args)
  257. command.append(name)
  258. command.extend(clang_tidy_args)
  259. task_queue.put(command)
  260. # Wait for all threads to be done.
  261. task_queue.join()
  262. if yaml and args.export_fixes:
  263. print("Writing fixes to " + args.export_fixes + " ...")
  264. try:
  265. merge_replacement_files(tmpdir, args.export_fixes)
  266. except Exception:
  267. sys.stderr.write("Error exporting fixes.\n")
  268. traceback.print_exc()
  269. if tmpdir:
  270. shutil.rmtree(tmpdir)
  271. if __name__ == "__main__":
  272. main()