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 runs clang-tidy in fix mode and verify fixes, messages or both.
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 [-std=c++(98|11|14|17|20)[-or-later]] \
23 <source-file> <check-name> <temp-file> \
24 -- [optional clang-tidy arguments]
27 // RUN: %check_clang_tidy %s llvm-include-order %t -- -- -isystem %S/Inputs
30 -std=c++(98|11|14|17|20)-or-later:
31 This flag will cause multiple runs within the same check_clang_tidy
32 execution. Make sure you don't have shared state across these runs.
42 def write_file(file_name
, text
):
43 with
open(file_name
, "w", encoding
="utf-8") as f
:
48 def try_run(args
, raise_error
=True):
50 process_output
= subprocess
.check_output(args
, stderr
=subprocess
.STDOUT
).decode(
53 except subprocess
.CalledProcessError
as e
:
54 process_output
= e
.output
.decode(errors
="ignore")
55 print("%s failed:\n%s" % (" ".join(args
), process_output
))
61 # This class represents the appearance of a message prefix in a file.
63 def __init__(self
, label
):
64 self
.has_message
= False
68 def check(self
, file_check_suffix
, input_text
):
69 self
.prefix
= self
.label
+ file_check_suffix
70 self
.has_message
= self
.prefix
in input_text
72 self
.prefixes
.append(self
.prefix
)
73 return self
.has_message
77 def __init__(self
, args
, extra_args
):
78 self
.resource_dir
= args
.resource_dir
79 self
.assume_file_name
= args
.assume_filename
80 self
.input_file_name
= args
.input_file_name
81 self
.check_name
= args
.check_name
82 self
.temp_file_name
= args
.temp_file_name
83 self
.original_file_name
= self
.temp_file_name
+ ".orig"
84 self
.expect_clang_tidy_error
= args
.expect_clang_tidy_error
86 self
.check_suffix
= args
.check_suffix
88 self
.has_check_fixes
= False
89 self
.has_check_messages
= False
90 self
.has_check_notes
= False
91 self
.fixes
= MessagePrefix("CHECK-FIXES")
92 self
.messages
= MessagePrefix("CHECK-MESSAGES")
93 self
.notes
= MessagePrefix("CHECK-NOTES")
95 file_name_with_extension
= self
.assume_file_name
or self
.input_file_name
96 _
, extension
= os
.path
.splitext(file_name_with_extension
)
97 if extension
not in [".c", ".hpp", ".m", ".mm"]:
99 self
.temp_file_name
= self
.temp_file_name
+ extension
101 self
.clang_extra_args
= []
102 self
.clang_tidy_extra_args
= extra_args
103 if "--" in extra_args
:
104 i
= self
.clang_tidy_extra_args
.index("--")
105 self
.clang_extra_args
= self
.clang_tidy_extra_args
[i
+ 1 :]
106 self
.clang_tidy_extra_args
= self
.clang_tidy_extra_args
[:i
]
108 # If the test does not specify a config style, force an empty one; otherwise
109 # auto-detection logic can discover a ".clang-tidy" file that is not related to
112 [re
.match("^-?-config(-file)?=", arg
) for arg
in self
.clang_tidy_extra_args
]
114 self
.clang_tidy_extra_args
.append("--config={}")
116 if extension
in [".m", ".mm"]:
117 self
.clang_extra_args
= [
118 "-fobjc-abi-version=2",
121 ] + self
.clang_extra_args
123 if extension
in [".cpp", ".hpp", ".mm"]:
124 self
.clang_extra_args
.append("-std=" + self
.std
)
126 # Tests should not rely on STL being available, and instead provide mock
127 # implementations of relevant APIs.
128 self
.clang_extra_args
.append("-nostdinc++")
130 if self
.resource_dir
is not None:
131 self
.clang_extra_args
.append("-resource-dir=%s" % self
.resource_dir
)
133 def read_input(self
):
134 with
open(self
.input_file_name
, "r", encoding
="utf-8") as input_file
:
135 self
.input_text
= input_file
.read()
137 def get_prefixes(self
):
138 for suffix
in self
.check_suffix
:
139 if suffix
and not re
.match("^[A-Z0-9\\-]+$", suffix
):
141 'Only A..Z, 0..9 and "-" are allowed in check suffixes list,'
142 + ' but "%s" was given' % suffix
145 file_check_suffix
= ("-" + suffix
) if suffix
else ""
147 has_check_fix
= self
.fixes
.check(file_check_suffix
, self
.input_text
)
148 self
.has_check_fixes
= self
.has_check_fixes
or has_check_fix
150 has_check_message
= self
.messages
.check(file_check_suffix
, self
.input_text
)
151 self
.has_check_messages
= self
.has_check_messages
or has_check_message
153 has_check_note
= self
.notes
.check(file_check_suffix
, self
.input_text
)
154 self
.has_check_notes
= self
.has_check_notes
or has_check_note
156 if has_check_note
and has_check_message
:
158 "Please use either %s or %s but not both"
159 % (self
.notes
.prefix
, self
.messages
.prefix
)
162 if not has_check_fix
and not has_check_message
and not has_check_note
:
164 "%s, %s or %s not found in the input"
165 % (self
.fixes
.prefix
, self
.messages
.prefix
, self
.notes
.prefix
)
168 assert self
.has_check_fixes
or self
.has_check_messages
or self
.has_check_notes
170 def prepare_test_inputs(self
):
171 # Remove the contents of the CHECK lines to avoid CHECKs matching on
172 # themselves. We need to keep the comments to preserve line numbers while
173 # avoiding empty lines which could potentially trigger formatting-related
175 cleaned_test
= re
.sub("// *CHECK-[A-Z0-9\\-]*:[^\r\n]*", "//", self
.input_text
)
176 write_file(self
.temp_file_name
, cleaned_test
)
177 write_file(self
.original_file_name
, cleaned_test
)
179 def run_clang_tidy(self
):
185 "--checks=-*," + self
.check_name
,
187 + self
.clang_tidy_extra_args
189 + self
.clang_extra_args
191 if self
.expect_clang_tidy_error
:
192 args
.insert(0, "not")
193 print("Running " + repr(args
) + "...")
194 clang_tidy_output
= try_run(args
)
195 print("------------------------ clang-tidy output -----------------------")
197 clang_tidy_output
.encode(sys
.stdout
.encoding
, errors
="replace").decode(
201 print("------------------------------------------------------------------")
203 diff_output
= try_run(
204 ["diff", "-u", self
.original_file_name
, self
.temp_file_name
], False
206 print("------------------------------ Fixes -----------------------------")
208 print("------------------------------------------------------------------")
209 return clang_tidy_output
211 def check_fixes(self
):
212 if self
.has_check_fixes
:
216 "-input-file=" + self
.temp_file_name
,
217 self
.input_file_name
,
218 "-check-prefixes=" + ",".join(self
.fixes
.prefixes
),
219 "-strict-whitespace",
223 def check_messages(self
, clang_tidy_output
):
224 if self
.has_check_messages
:
225 messages_file
= self
.temp_file_name
+ ".msg"
226 write_file(messages_file
, clang_tidy_output
)
230 "-input-file=" + messages_file
,
231 self
.input_file_name
,
232 "-check-prefixes=" + ",".join(self
.messages
.prefixes
),
233 "-implicit-check-not={{warning|error}}:",
237 def check_notes(self
, clang_tidy_output
):
238 if self
.has_check_notes
:
239 notes_file
= self
.temp_file_name
+ ".notes"
242 for line
in clang_tidy_output
.splitlines()
243 if not ("note: FIX-IT applied" in line
)
245 write_file(notes_file
, "\n".join(filtered_output
))
249 "-input-file=" + notes_file
,
250 self
.input_file_name
,
251 "-check-prefixes=" + ",".join(self
.notes
.prefixes
),
252 "-implicit-check-not={{note|warning|error}}:",
259 self
.prepare_test_inputs()
260 clang_tidy_output
= self
.run_clang_tidy()
262 self
.check_messages(clang_tidy_output
)
263 self
.check_notes(clang_tidy_output
)
267 if std
== "c++98-or-later":
268 return ["c++98", "c++11", "c++14", "c++17", "c++20", "c++23", "c++2c"]
269 if std
== "c++11-or-later":
270 return ["c++11", "c++14", "c++17", "c++20", "c++23", "c++2c"]
271 if std
== "c++14-or-later":
272 return ["c++14", "c++17", "c++20", "c++23", "c++2c"]
273 if std
== "c++17-or-later":
274 return ["c++17", "c++20", "c++23", "c++2c"]
275 if std
== "c++20-or-later":
276 return ["c++20", "c++23", "c++2c"]
277 if std
== "c++23-or-later":
278 return ["c++23", "c++2c"]
283 return string
.split(",")
286 def parse_arguments():
287 parser
= argparse
.ArgumentParser()
288 parser
.add_argument("-expect-clang-tidy-error", action
="store_true")
289 parser
.add_argument("-resource-dir")
290 parser
.add_argument("-assume-filename")
291 parser
.add_argument("input_file_name")
292 parser
.add_argument("check_name")
293 parser
.add_argument("temp_file_name")
299 help="comma-separated list of FileCheck suffixes",
301 parser
.add_argument("-std", type=csv
, default
=["c++11-or-later"])
302 return parser
.parse_known_args()
306 args
, extra_args
= parse_arguments()
308 abbreviated_stds
= args
.std
309 for abbreviated_std
in abbreviated_stds
:
310 for std
in expand_std(abbreviated_std
):
312 CheckRunner(args
, extra_args
).run()
315 if __name__
== "__main__":