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 # ===------------------------------------------------------------------------===#
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
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
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
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.
53 def write_file(file_name
, text
):
54 with
open(file_name
, "w", encoding
="utf-8") as f
:
59 def try_run(args
, raise_error
=True):
61 process_output
= subprocess
.check_output(args
, stderr
=subprocess
.STDOUT
).decode(
64 except subprocess
.CalledProcessError
as e
:
65 process_output
= e
.output
.decode(errors
="ignore")
66 print("%s failed:\n%s" % (" ".join(args
), process_output
))
72 # This class represents the appearance of a message prefix in a file.
74 def __init__(self
, label
):
75 self
.has_message
= False
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
83 self
.prefixes
.append(self
.prefix
)
84 return self
.has_message
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
97 self
.check_suffix
= args
.check_suffix
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"]:
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
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",
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
):
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
:
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
179 self
.has_check_fixes
or self
.has_check_messages
or self
.has_check_notes
181 if self
.expect_no_diagnosis
and expect_diagnosis
:
183 "%s, %s or %s not found in the input"
186 self
.messages
.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
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
):
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
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 -----------------------")
227 clang_tidy_output
.encode(sys
.stdout
.encoding
, errors
="replace").decode(
231 print("------------------------------------------------------------------")
233 diff_output
= try_run(
234 ["diff", "-u", self
.original_file_name
, self
.temp_file_name
], False
236 print("------------------------------ Fixes -----------------------------")
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
:
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
)
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"
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
))
283 "-input-file=" + notes_file
,
284 self
.input_file_name
,
285 "-check-prefixes=" + ",".join(self
.notes
.prefixes
),
286 "-implicit-check-not={{note|warning|error}}:",
292 if self
.export_fixes
is None:
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:
300 self
.check_messages(clang_tidy_output
)
301 self
.check_notes(clang_tidy_output
)
313 C_STANDARDS
= ["c99", ("c11", "c1x"), "c17", ("c23", "c2x"), "c2y"]
317 split_std
, or_later
, _
= std
.partition("-or-later")
322 for standard_list
in (CPP_STANDARDS
, C_STANDARDS
):
326 for i
, v
in enumerate(standard_list
)
327 if (split_std
in v
if isinstance(v
, (list, tuple)) else split_std
== v
)
332 return [split_std
] + [
333 x
if isinstance(x
, str) else x
[0] for x
in standard_list
[item
+ 1 :]
339 return string
.split(",")
342 def parse_arguments():
343 parser
= argparse
.ArgumentParser(
344 prog
=pathlib
.Path(__file__
).stem
,
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")
359 help="comma-separated list of FileCheck suffixes",
366 help="A file to export fixes into instead of fixing.",
371 default
=["c++11-or-later"],
372 help="Passed to clang. Special -or-later values are expanded.",
374 return parser
.parse_known_args()
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
):
384 CheckRunner(args
, extra_args
).run()
387 if __name__
== "__main__":