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