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