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