clang-tools  14.0.0git
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('-use-color', action='store_true',
146  help='Use colors in output')
147  parser.add_argument('-path', dest='build_path',
148  help='Path used to read a compile command database.')
149  if yaml:
150  parser.add_argument('-export-fixes', metavar='FILE', dest='export_fixes',
151  help='Create a yaml file to store suggested fixes in, '
152  'which can be applied with clang-apply-replacements.')
153  parser.add_argument('-extra-arg', dest='extra_arg',
154  action='append', default=[],
155  help='Additional argument to append to the compiler '
156  'command line.')
157  parser.add_argument('-extra-arg-before', dest='extra_arg_before',
158  action='append', default=[],
159  help='Additional argument to prepend to the compiler '
160  'command line.')
161  parser.add_argument('-quiet', action='store_true', default=False,
162  help='Run clang-tidy in quiet mode')
163  clang_tidy_args = []
164  argv = sys.argv[1:]
165  if '--' in argv:
166  clang_tidy_args.extend(argv[argv.index('--'):])
167  argv = argv[:argv.index('--')]
168 
169  args = parser.parse_args(argv)
170 
171  # Extract changed lines for each file.
172  filename = None
173  lines_by_file = {}
174  for line in sys.stdin:
175  match = re.search('^\+\+\+\ \"?(.*?/){%s}([^ \t\n\"]*)' % args.p, line)
176  if match:
177  filename = match.group(2)
178  if filename is None:
179  continue
180 
181  if args.regex is not None:
182  if not re.match('^%s$' % args.regex, filename):
183  continue
184  else:
185  if not re.match('^%s$' % args.iregex, filename, re.IGNORECASE):
186  continue
187 
188  match = re.search('^@@.*\+(\d+)(,(\d+))?', line)
189  if match:
190  start_line = int(match.group(1))
191  line_count = 1
192  if match.group(3):
193  line_count = int(match.group(3))
194  if line_count == 0:
195  continue
196  end_line = start_line + line_count - 1
197  lines_by_file.setdefault(filename, []).append([start_line, end_line])
198 
199  if not any(lines_by_file):
200  print("No relevant changes found.")
201  sys.exit(0)
202 
203  max_task_count = args.j
204  if max_task_count == 0:
205  max_task_count = multiprocessing.cpu_count()
206  max_task_count = min(len(lines_by_file), max_task_count)
207 
208  tmpdir = None
209  if yaml and args.export_fixes:
210  tmpdir = tempfile.mkdtemp()
211 
212  # Tasks for clang-tidy.
213  task_queue = queue.Queue(max_task_count)
214  # A lock for console output.
215  lock = threading.Lock()
216 
217  # Run a pool of clang-tidy workers.
218  start_workers(max_task_count, run_tidy, task_queue, lock, args.timeout)
219 
220  # Form the common args list.
221  common_clang_tidy_args = []
222  if args.fix:
223  common_clang_tidy_args.append('-fix')
224  if args.checks != '':
225  common_clang_tidy_args.append('-checks=' + args.checks)
226  if args.quiet:
227  common_clang_tidy_args.append('-quiet')
228  if args.build_path is not None:
229  common_clang_tidy_args.append('-p=%s' % args.build_path)
230  if args.use_color:
231  common_clang_tidy_args.append('--use-color')
232  for arg in args.extra_arg:
233  common_clang_tidy_args.append('-extra-arg=%s' % arg)
234  for arg in args.extra_arg_before:
235  common_clang_tidy_args.append('-extra-arg-before=%s' % arg)
236 
237  for name in lines_by_file:
238  line_filter_json = json.dumps(
239  [{"name": name, "lines": lines_by_file[name]}],
240  separators=(',', ':'))
241 
242  # Run clang-tidy on files containing changes.
243  command = [args.clang_tidy_binary]
244  command.append('-line-filter=' + line_filter_json)
245  if yaml and args.export_fixes:
246  # Get a temporary file. We immediately close the handle so clang-tidy can
247  # overwrite it.
248  (handle, tmp_name) = tempfile.mkstemp(suffix='.yaml', dir=tmpdir)
249  os.close(handle)
250  command.append('-export-fixes=' + tmp_name)
251  command.extend(common_clang_tidy_args)
252  command.append(name)
253  command.extend(clang_tidy_args)
254 
255  task_queue.put(command)
256 
257  # Wait for all threads to be done.
258  task_queue.join()
259 
260  if yaml and args.export_fixes:
261  print('Writing fixes to ' + args.export_fixes + ' ...')
262  try:
263  merge_replacement_files(tmpdir, args.export_fixes)
264  except:
265  sys.stderr.write('Error exporting fixes.\n')
266  traceback.print_exc()
267 
268  if tmpdir:
269  shutil.rmtree(tmpdir)
270 
271 
272 if __name__ == '__main__':
273  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:78
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