1#!/usr/bin/env python
2#
3#===- check_clang_tidy.py - ClangTidy Test Helper ------------*- 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 Test Helper
13=====================
14
15This script runs clang-tidy in fix mode and verify fixes, messages or both.
16
17Usage:
18  check_clang_tidy.py [-resource-dir=<resource-dir>] \
19    [-assume-filename=<file-with-source-extension>] \
20    [-check-suffix=<comma-separated-file-check-suffixes>] \
21    [-check-suffixes=<comma-separated-file-check-suffixes>] \
22    <source-file> <check-name> <temp-file> \
23    -- [optional clang-tidy arguments]
24
25Example:
26  // RUN: %check_clang_tidy %s llvm-include-order %t -- -- -isystem %S/Inputs
27"""
28
29import argparse
30import os
31import re
32import subprocess
33import sys
34
35
36def write_file(file_name, text):
37  with open(file_name, 'w') as f:
38    f.write(text)
39    f.truncate()
40
41
42def run_test_once(args, extra_args):
43  resource_dir = args.resource_dir
44  assume_file_name = args.assume_filename
45  input_file_name = args.input_file_name
46  check_name = args.check_name
47  temp_file_name = args.temp_file_name
48  expect_clang_tidy_error = args.expect_clang_tidy_error
49  std = args.std
50
51  file_name_with_extension = assume_file_name or input_file_name
52  _, extension = os.path.splitext(file_name_with_extension)
53  if extension not in ['.c', '.hpp', '.m', '.mm']:
54    extension = '.cpp'
55  temp_file_name = temp_file_name + extension
56
57  clang_tidy_extra_args = extra_args
58  clang_extra_args = []
59  if '--' in extra_args:
60    i = clang_tidy_extra_args.index('--')
61    clang_extra_args = clang_tidy_extra_args[i + 1:]
62    clang_tidy_extra_args = clang_tidy_extra_args[:i]
63
64  # If the test does not specify a config style, force an empty one; otherwise
65  # autodetection logic can discover a ".clang-tidy" file that is not related to
66  # the test.
67  if not any(
68      [arg.startswith('-config=') for arg in clang_tidy_extra_args]):
69    clang_tidy_extra_args.append('-config={}')
70
71  if extension in ['.m', '.mm']:
72    clang_extra_args = ['-fobjc-abi-version=2', '-fobjc-arc', '-fblocks'] + \
73        clang_extra_args
74
75  if extension in ['.cpp', '.hpp', '.mm']:
76    clang_extra_args.append('-std=' + std)
77
78  # Tests should not rely on STL being available, and instead provide mock
79  # implementations of relevant APIs.
80  clang_extra_args.append('-nostdinc++')
81
82  if resource_dir is not None:
83    clang_extra_args.append('-resource-dir=%s' % resource_dir)
84
85  with open(input_file_name, 'r') as input_file:
86    input_text = input_file.read()
87
88  check_fixes_prefixes = []
89  check_messages_prefixes = []
90  check_notes_prefixes = []
91
92  has_check_fixes = False
93  has_check_messages = False
94  has_check_notes = False
95
96  for check in args.check_suffix:
97    if check and not re.match('^[A-Z0-9\-]+$', check):
98      sys.exit('Only A..Z, 0..9 and "-" are ' +
99        'allowed in check suffixes list, but "%s" was given' % (check))
100
101    file_check_suffix = ('-' + check) if check else ''
102    check_fixes_prefix = 'CHECK-FIXES' + file_check_suffix
103    check_messages_prefix = 'CHECK-MESSAGES' + file_check_suffix
104    check_notes_prefix = 'CHECK-NOTES' + file_check_suffix
105
106    has_check_fix = check_fixes_prefix in input_text
107    has_check_message = check_messages_prefix in input_text
108    has_check_note = check_notes_prefix in input_text
109
110    if has_check_note and has_check_message:
111      sys.exit('Please use either %s or %s but not both' %
112        (check_notes_prefix, check_messages_prefix))
113
114    if not has_check_fix and not has_check_message and not has_check_note:
115      sys.exit('%s, %s or %s not found in the input' %
116        (check_fixes_prefix, check_messages_prefix, check_notes_prefix))
117
118    has_check_fixes = has_check_fixes or has_check_fix
119    has_check_messages = has_check_messages or has_check_message
120    has_check_notes = has_check_notes or has_check_note
121
122    if has_check_fix:
123      check_fixes_prefixes.append(check_fixes_prefix)
124    if has_check_message:
125      check_messages_prefixes.append(check_messages_prefix)
126    if has_check_note:
127      check_notes_prefixes.append(check_notes_prefix)
128
129  assert has_check_fixes or has_check_messages or has_check_notes
130  # Remove the contents of the CHECK lines to avoid CHECKs matching on
131  # themselves.  We need to keep the comments to preserve line numbers while
132  # avoiding empty lines which could potentially trigger formatting-related
133  # checks.
134  cleaned_test = re.sub('// *CHECK-[A-Z0-9\-]*:[^\r\n]*', '//', input_text)
135
136  write_file(temp_file_name, cleaned_test)
137
138  original_file_name = temp_file_name + ".orig"
139  write_file(original_file_name, cleaned_test)
140
141  args = ['clang-tidy', temp_file_name, '-fix', '--checks=-*,' + check_name] + \
142      clang_tidy_extra_args + ['--'] + clang_extra_args
143  if expect_clang_tidy_error:
144    args.insert(0, 'not')
145  print('Running ' + repr(args) + '...')
146  try:
147    clang_tidy_output = \
148        subprocess.check_output(args, stderr=subprocess.STDOUT).decode()
149  except subprocess.CalledProcessError as e:
150    print('clang-tidy failed:\n' + e.output.decode())
151    raise
152
153  print('------------------------ clang-tidy output -----------------------\n' +
154        clang_tidy_output +
155        '\n------------------------------------------------------------------')
156
157  try:
158    diff_output = subprocess.check_output(
159        ['diff', '-u', original_file_name, temp_file_name],
160        stderr=subprocess.STDOUT)
161  except subprocess.CalledProcessError as e:
162    diff_output = e.output
163
164  print('------------------------------ Fixes -----------------------------\n' +
165        diff_output.decode(errors='ignore') +
166        '\n------------------------------------------------------------------')
167
168  if has_check_fixes:
169    try:
170      subprocess.check_output(
171          ['FileCheck', '-input-file=' + temp_file_name, input_file_name,
172           '-check-prefixes=' + ','.join(check_fixes_prefixes),
173           '-strict-whitespace'],
174          stderr=subprocess.STDOUT)
175    except subprocess.CalledProcessError as e:
176      print('FileCheck failed:\n' + e.output.decode())
177      raise
178
179  if has_check_messages:
180    messages_file = temp_file_name + '.msg'
181    write_file(messages_file, clang_tidy_output)
182    try:
183      subprocess.check_output(
184          ['FileCheck', '-input-file=' + messages_file, input_file_name,
185           '-check-prefixes=' + ','.join(check_messages_prefixes),
186           '-implicit-check-not={{warning|error}}:'],
187          stderr=subprocess.STDOUT)
188    except subprocess.CalledProcessError as e:
189      print('FileCheck failed:\n' + e.output.decode())
190      raise
191
192  if has_check_notes:
193    notes_file = temp_file_name + '.notes'
194    filtered_output = [line for line in clang_tidy_output.splitlines()
195                       if not "note: FIX-IT applied" in line]
196    write_file(notes_file, '\n'.join(filtered_output))
197    try:
198      subprocess.check_output(
199          ['FileCheck', '-input-file=' + notes_file, input_file_name,
200           '-check-prefixes=' + ','.join(check_notes_prefixes),
201           '-implicit-check-not={{note|warning|error}}:'],
202          stderr=subprocess.STDOUT)
203    except subprocess.CalledProcessError as e:
204      print('FileCheck failed:\n' + e.output.decode())
205      raise
206
207
208def expand_std(std):
209  if std == 'c++98-or-later':
210    return ['c++98', 'c++11', 'c++14', 'c++17', 'c++20']
211  if std == 'c++11-or-later':
212    return ['c++11', 'c++14', 'c++17', 'c++20']
213  if std == 'c++14-or-later':
214    return ['c++14', 'c++17', 'c++20']
215  if std == 'c++17-or-later':
216    return ['c++17', 'c++20']
217  if std == 'c++20-or-later':
218    return ['c++20']
219  return [std]
220
221
222def csv(string):
223  return string.split(',')
224
225
226def main():
227  parser = argparse.ArgumentParser()
228  parser.add_argument('-expect-clang-tidy-error', action='store_true')
229  parser.add_argument('-resource-dir')
230  parser.add_argument('-assume-filename')
231  parser.add_argument('input_file_name')
232  parser.add_argument('check_name')
233  parser.add_argument('temp_file_name')
234  parser.add_argument(
235      '-check-suffix',
236      '-check-suffixes',
237      default=[''],
238      type=csv,
239      help='comma-separated list of FileCheck suffixes')
240  parser.add_argument('-std', type=csv, default=['c++11-or-later'])
241
242  args, extra_args = parser.parse_known_args()
243
244  abbreviated_stds = args.std
245  for abbreviated_std in abbreviated_stds:
246    for std in expand_std(abbreviated_std):
247      args.std = std
248      run_test_once(args, extra_args)
249
250
251if __name__ == '__main__':
252  main()
253