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