[flang][OpenMP] Parse METADIRECTIVE in specification part (#123397)
[llvm-project.git] / clang-tools-extra / test / clang-tidy / check_clang_tidy.py
blob5e39c05f76d866aa7b975aac69c28c42aac9dcdb
1 #!/usr/bin/env python3
3 # ===- check_clang_tidy.py - ClangTidy Test Helper ------------*- python -*--===#
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
9 # ===------------------------------------------------------------------------===#
11 """
12 ClangTidy Test Helper
13 =====================
15 This script is used to simplify writing, running, and debugging tests compatible
16 with llvm-lit. By default it runs clang-tidy in fix mode and uses FileCheck to
17 verify messages and/or fixes.
19 For debugging, with --export-fixes, the tool simply exports fixes to a provided
20 file and does not run FileCheck.
22 Extra arguments, those after the first -- if any, are passed to either
23 clang-tidy or clang:
24 * Arguments between the first -- and second -- are clang-tidy arguments.
25 * May be only whitespace if there are no clang-tidy arguments.
26 * clang-tidy's --config would go here.
27 * Arguments after the second -- are clang arguments
29 Examples
30 --------
32 // RUN: %check_clang_tidy %s llvm-include-order %t -- -- -isystem %S/Inputs
36 // RUN: %check_clang_tidy %s llvm-include-order --export-fixes=fixes.yaml %t -std=c++20
38 Notes
39 -----
40 -std=c++(98|11|14|17|20)-or-later:
41 This flag will cause multiple runs within the same check_clang_tidy
42 execution. Make sure you don't have shared state across these runs.
43 """
45 import argparse
46 import os
47 import pathlib
48 import re
49 import subprocess
50 import sys
53 def write_file(file_name, text):
54 with open(file_name, "w", encoding="utf-8") as f:
55 f.write(text)
56 f.truncate()
59 def try_run(args, raise_error=True):
60 try:
61 process_output = subprocess.check_output(args, stderr=subprocess.STDOUT).decode(
62 errors="ignore"
64 except subprocess.CalledProcessError as e:
65 process_output = e.output.decode(errors="ignore")
66 print("%s failed:\n%s" % (" ".join(args), process_output))
67 if raise_error:
68 raise
69 return process_output
72 # This class represents the appearance of a message prefix in a file.
73 class MessagePrefix:
74 def __init__(self, label):
75 self.has_message = False
76 self.prefixes = []
77 self.label = label
79 def check(self, file_check_suffix, input_text):
80 self.prefix = self.label + file_check_suffix
81 self.has_message = self.prefix in input_text
82 if self.has_message:
83 self.prefixes.append(self.prefix)
84 return self.has_message
87 class CheckRunner:
88 def __init__(self, args, extra_args):
89 self.resource_dir = args.resource_dir
90 self.assume_file_name = args.assume_filename
91 self.input_file_name = args.input_file_name
92 self.check_name = args.check_name
93 self.temp_file_name = args.temp_file_name
94 self.original_file_name = self.temp_file_name + ".orig"
95 self.expect_clang_tidy_error = args.expect_clang_tidy_error
96 self.std = args.std
97 self.check_suffix = args.check_suffix
98 self.input_text = ""
99 self.has_check_fixes = False
100 self.has_check_messages = False
101 self.has_check_notes = False
102 self.expect_no_diagnosis = False
103 self.export_fixes = args.export_fixes
104 self.fixes = MessagePrefix("CHECK-FIXES")
105 self.messages = MessagePrefix("CHECK-MESSAGES")
106 self.notes = MessagePrefix("CHECK-NOTES")
108 file_name_with_extension = self.assume_file_name or self.input_file_name
109 _, extension = os.path.splitext(file_name_with_extension)
110 if extension not in [".c", ".hpp", ".m", ".mm"]:
111 extension = ".cpp"
112 self.temp_file_name = self.temp_file_name + extension
114 self.clang_extra_args = []
115 self.clang_tidy_extra_args = extra_args
116 if "--" in extra_args:
117 i = self.clang_tidy_extra_args.index("--")
118 self.clang_extra_args = self.clang_tidy_extra_args[i + 1 :]
119 self.clang_tidy_extra_args = self.clang_tidy_extra_args[:i]
121 # If the test does not specify a config style, force an empty one; otherwise
122 # auto-detection logic can discover a ".clang-tidy" file that is not related to
123 # the test.
124 if not any(
125 [re.match("^-?-config(-file)?=", arg) for arg in self.clang_tidy_extra_args]
127 self.clang_tidy_extra_args.append("--config={}")
129 if extension in [".m", ".mm"]:
130 self.clang_extra_args = [
131 "-fobjc-abi-version=2",
132 "-fobjc-arc",
133 "-fblocks",
134 ] + self.clang_extra_args
136 if extension in [".cpp", ".hpp", ".mm"]:
137 self.clang_extra_args.append("-std=" + self.std)
139 # Tests should not rely on STL being available, and instead provide mock
140 # implementations of relevant APIs.
141 self.clang_extra_args.append("-nostdinc++")
143 if self.resource_dir is not None:
144 self.clang_extra_args.append("-resource-dir=%s" % self.resource_dir)
146 def read_input(self):
147 with open(self.input_file_name, "r", encoding="utf-8") as input_file:
148 self.input_text = input_file.read()
150 def get_prefixes(self):
151 for suffix in self.check_suffix:
152 if suffix and not re.match("^[A-Z0-9\\-]+$", suffix):
153 sys.exit(
154 'Only A..Z, 0..9 and "-" are allowed in check suffixes list,'
155 + ' but "%s" was given' % suffix
158 file_check_suffix = ("-" + suffix) if suffix else ""
160 has_check_fix = self.fixes.check(file_check_suffix, self.input_text)
161 self.has_check_fixes = self.has_check_fixes or has_check_fix
163 has_check_message = self.messages.check(file_check_suffix, self.input_text)
164 self.has_check_messages = self.has_check_messages or has_check_message
166 has_check_note = self.notes.check(file_check_suffix, self.input_text)
167 self.has_check_notes = self.has_check_notes or has_check_note
169 if has_check_note and has_check_message:
170 sys.exit(
171 "Please use either %s or %s but not both"
172 % (self.notes.prefix, self.messages.prefix)
175 if not has_check_fix and not has_check_message and not has_check_note:
176 self.expect_no_diagnosis = True
178 expect_diagnosis = (
179 self.has_check_fixes or self.has_check_messages or self.has_check_notes
181 if self.expect_no_diagnosis and expect_diagnosis:
182 sys.exit(
183 "%s, %s or %s not found in the input"
185 self.fixes.prefix,
186 self.messages.prefix,
187 self.notes.prefix,
190 assert expect_diagnosis or self.expect_no_diagnosis
192 def prepare_test_inputs(self):
193 # Remove the contents of the CHECK lines to avoid CHECKs matching on
194 # themselves. We need to keep the comments to preserve line numbers while
195 # avoiding empty lines which could potentially trigger formatting-related
196 # checks.
197 cleaned_test = re.sub("// *CHECK-[A-Z0-9\\-]*:[^\r\n]*", "//", self.input_text)
198 write_file(self.temp_file_name, cleaned_test)
199 write_file(self.original_file_name, cleaned_test)
201 def run_clang_tidy(self):
202 args = (
204 "clang-tidy",
205 self.temp_file_name,
209 "-fix"
210 if self.export_fixes is None
211 else "--export-fixes=" + self.export_fixes
215 "--checks=-*," + self.check_name,
217 + self.clang_tidy_extra_args
218 + ["--"]
219 + self.clang_extra_args
221 if self.expect_clang_tidy_error:
222 args.insert(0, "not")
223 print("Running " + repr(args) + "...")
224 clang_tidy_output = try_run(args)
225 print("------------------------ clang-tidy output -----------------------")
226 print(
227 clang_tidy_output.encode(sys.stdout.encoding, errors="replace").decode(
228 sys.stdout.encoding
231 print("------------------------------------------------------------------")
233 diff_output = try_run(
234 ["diff", "-u", self.original_file_name, self.temp_file_name], False
236 print("------------------------------ Fixes -----------------------------")
237 print(diff_output)
238 print("------------------------------------------------------------------")
239 return clang_tidy_output
241 def check_no_diagnosis(self, clang_tidy_output):
242 if clang_tidy_output != "":
243 sys.exit("No diagnostics were expected, but found the ones above")
245 def check_fixes(self):
246 if self.has_check_fixes:
247 try_run(
249 "FileCheck",
250 "-input-file=" + self.temp_file_name,
251 self.input_file_name,
252 "-check-prefixes=" + ",".join(self.fixes.prefixes),
253 "-strict-whitespace",
257 def check_messages(self, clang_tidy_output):
258 if self.has_check_messages:
259 messages_file = self.temp_file_name + ".msg"
260 write_file(messages_file, clang_tidy_output)
261 try_run(
263 "FileCheck",
264 "-input-file=" + messages_file,
265 self.input_file_name,
266 "-check-prefixes=" + ",".join(self.messages.prefixes),
267 "-implicit-check-not={{warning|error}}:",
271 def check_notes(self, clang_tidy_output):
272 if self.has_check_notes:
273 notes_file = self.temp_file_name + ".notes"
274 filtered_output = [
275 line
276 for line in clang_tidy_output.splitlines()
277 if not ("note: FIX-IT applied" in line)
279 write_file(notes_file, "\n".join(filtered_output))
280 try_run(
282 "FileCheck",
283 "-input-file=" + notes_file,
284 self.input_file_name,
285 "-check-prefixes=" + ",".join(self.notes.prefixes),
286 "-implicit-check-not={{note|warning|error}}:",
290 def run(self):
291 self.read_input()
292 if self.export_fixes is None:
293 self.get_prefixes()
294 self.prepare_test_inputs()
295 clang_tidy_output = self.run_clang_tidy()
296 if self.expect_no_diagnosis:
297 self.check_no_diagnosis(clang_tidy_output)
298 elif self.export_fixes is None:
299 self.check_fixes()
300 self.check_messages(clang_tidy_output)
301 self.check_notes(clang_tidy_output)
304 CPP_STANDARDS = [
305 "c++98",
306 "c++11",
307 ("c++14", "c++1y"),
308 ("c++17", "c++1z"),
309 ("c++20", "c++2a"),
310 ("c++23", "c++2b"),
311 ("c++26", "c++2c"),
313 C_STANDARDS = ["c99", ("c11", "c1x"), "c17", ("c23", "c2x"), "c2y"]
316 def expand_std(std):
317 split_std, or_later, _ = std.partition("-or-later")
319 if not or_later:
320 return [split_std]
322 for standard_list in (CPP_STANDARDS, C_STANDARDS):
323 item = next(
326 for i, v in enumerate(standard_list)
327 if (split_std in v if isinstance(v, (list, tuple)) else split_std == v)
329 None,
331 if item is not None:
332 return [split_std] + [
333 x if isinstance(x, str) else x[0] for x in standard_list[item + 1 :]
335 return [std]
338 def csv(string):
339 return string.split(",")
342 def parse_arguments():
343 parser = argparse.ArgumentParser(
344 prog=pathlib.Path(__file__).stem,
345 description=__doc__,
346 formatter_class=argparse.RawDescriptionHelpFormatter,
348 parser.add_argument("-expect-clang-tidy-error", action="store_true")
349 parser.add_argument("-resource-dir")
350 parser.add_argument("-assume-filename")
351 parser.add_argument("input_file_name")
352 parser.add_argument("check_name")
353 parser.add_argument("temp_file_name")
354 parser.add_argument(
355 "-check-suffix",
356 "-check-suffixes",
357 default=[""],
358 type=csv,
359 help="comma-separated list of FileCheck suffixes",
361 parser.add_argument(
362 "-export-fixes",
363 default=None,
364 type=str,
365 metavar="file",
366 help="A file to export fixes into instead of fixing.",
368 parser.add_argument(
369 "-std",
370 type=csv,
371 default=["c++11-or-later"],
372 help="Passed to clang. Special -or-later values are expanded.",
374 return parser.parse_known_args()
377 def main():
378 args, extra_args = parse_arguments()
380 abbreviated_stds = args.std
381 for abbreviated_std in abbreviated_stds:
382 for std in expand_std(abbreviated_std):
383 args.std = std
384 CheckRunner(args, extra_args).run()
387 if __name__ == "__main__":
388 main()