414 lines · python
1#!/usr/bin/env python32#3# ===-----------------------------------------------------------------------===#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-exception8#9# ===------------------------------------------------------------------------===#10 11"""12ClangTidy Test Helper13=====================14 15This script is used to simplify writing, running, and debugging tests compatible16with llvm-lit. By default it runs clang-tidy in fix mode and uses FileCheck to17verify messages and/or fixes.18 19For debugging, with --export-fixes, the tool simply exports fixes to a provided20file and does not run FileCheck.21 22Extra arguments, those after the first -- if any, are passed to either23clang-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 arguments28 29Examples30--------31 32 // RUN: %check_clang_tidy %s llvm-include-order %t -- -- -isystem %S/Inputs33 34or35 36 // RUN: %check_clang_tidy %s llvm-include-order --export-fixes=fixes.yaml %t -std=c++2037 38Notes39-----40 -std=c++(98|11|14|17|20)-or-later:41 This flag will cause multiple runs within the same check_clang_tidy42 execution. Make sure you don't have shared state across these runs.43"""44 45import argparse46import os47import pathlib48import platform49import re50import subprocess51import sys52from typing import List, Tuple53 54 55def write_file(file_name: str, text: str) -> None:56 with open(file_name, "w", encoding="utf-8") as f:57 f.write(text)58 f.truncate()59 60 61def try_run(args: List[str], raise_error: bool = True) -> str:62 try:63 process_output = subprocess.check_output(args, stderr=subprocess.STDOUT).decode(64 errors="ignore"65 )66 except subprocess.CalledProcessError as e:67 process_output = e.output.decode(errors="ignore")68 print("%s failed:\n%s" % (" ".join(args), process_output))69 if raise_error:70 raise71 return process_output72 73 74# This class represents the appearance of a message prefix in a file.75class MessagePrefix:76 def __init__(self, label: str) -> None:77 self.has_message = False78 self.prefixes: List[str] = []79 self.label = label80 81 def check(self, file_check_suffix: str, input_text: str) -> bool:82 self.prefix = self.label + file_check_suffix83 self.has_message = self.prefix in input_text84 if self.has_message:85 self.prefixes.append(self.prefix)86 return self.has_message87 88 89class CheckRunner:90 def __init__(self, args: argparse.Namespace, extra_args: List[str]) -> None:91 self.resource_dir = args.resource_dir92 self.assume_file_name = args.assume_filename93 self.input_file_name = args.input_file_name94 self.check_name = args.check_name95 self.temp_file_name = args.temp_file_name96 self.original_file_name = self.temp_file_name + ".orig"97 self.expect_clang_tidy_error = args.expect_clang_tidy_error98 self.std = args.std99 self.check_suffix = args.check_suffix100 self.input_text = ""101 self.has_check_fixes = False102 self.has_check_messages = False103 self.has_check_notes = False104 self.expect_no_diagnosis = False105 self.export_fixes = args.export_fixes106 self.fixes = MessagePrefix("CHECK-FIXES")107 self.messages = MessagePrefix("CHECK-MESSAGES")108 self.notes = MessagePrefix("CHECK-NOTES")109 self.match_partial_fixes = args.match_partial_fixes110 111 file_name_with_extension = self.assume_file_name or self.input_file_name112 _, extension = os.path.splitext(file_name_with_extension)113 if extension not in [".c", ".hpp", ".m", ".mm"]:114 extension = ".cpp"115 self.temp_file_name = self.temp_file_name + extension116 117 self.clang_extra_args = []118 self.clang_tidy_extra_args = extra_args119 if "--" in extra_args:120 i = self.clang_tidy_extra_args.index("--")121 self.clang_extra_args = self.clang_tidy_extra_args[i + 1 :]122 self.clang_tidy_extra_args = self.clang_tidy_extra_args[:i]123 124 # If the test does not specify a config style, force an empty one; otherwise125 # auto-detection logic can discover a ".clang-tidy" file that is not related to126 # the test.127 if not any(128 [re.match("^-?-config(-file)?=", arg) for arg in self.clang_tidy_extra_args]129 ):130 self.clang_tidy_extra_args.append("--config={}")131 132 if extension in [".m", ".mm"]:133 self.clang_extra_args = [134 "-fobjc-abi-version=2",135 "-fobjc-arc",136 "-fblocks",137 ] + self.clang_extra_args138 139 self.clang_extra_args.append("-std=" + self.std)140 141 # Tests should not rely on STL being available, and instead provide mock142 # implementations of relevant APIs.143 self.clang_extra_args.append("-nostdinc++")144 145 if self.resource_dir is not None:146 self.clang_extra_args.append("-resource-dir=%s" % self.resource_dir)147 148 def read_input(self) -> None:149 # Use a "\\?\" prefix on Windows to handle long file paths transparently:150 # https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation151 file_name = self.input_file_name152 if platform.system() == "Windows":153 file_name = "\\\\?\\" + os.path.abspath(file_name)154 with open(file_name, "r", encoding="utf-8") as input_file:155 self.input_text = input_file.read()156 157 def get_prefixes(self) -> None:158 for suffix in self.check_suffix:159 if suffix and not re.match("^[A-Z0-9\\-]+$", suffix):160 sys.exit(161 'Only A..Z, 0..9 and "-" are allowed in check suffixes list,'162 + ' but "%s" was given' % suffix163 )164 165 file_check_suffix = ("-" + suffix) if suffix else ""166 167 has_check_fix = self.fixes.check(file_check_suffix, self.input_text)168 self.has_check_fixes = self.has_check_fixes or has_check_fix169 170 has_check_message = self.messages.check(file_check_suffix, self.input_text)171 self.has_check_messages = self.has_check_messages or has_check_message172 173 has_check_note = self.notes.check(file_check_suffix, self.input_text)174 self.has_check_notes = self.has_check_notes or has_check_note175 176 if has_check_note and has_check_message:177 sys.exit(178 "Please use either %s or %s but not both"179 % (self.notes.prefix, self.messages.prefix)180 )181 182 if not has_check_fix and not has_check_message and not has_check_note:183 self.expect_no_diagnosis = True184 185 expect_diagnosis = (186 self.has_check_fixes or self.has_check_messages or self.has_check_notes187 )188 if self.expect_no_diagnosis and expect_diagnosis:189 sys.exit(190 "%s, %s or %s not found in the input"191 % (192 self.fixes.prefix,193 self.messages.prefix,194 self.notes.prefix,195 )196 )197 assert expect_diagnosis or self.expect_no_diagnosis198 199 def prepare_test_inputs(self) -> None:200 # Remove the contents of the CHECK lines to avoid CHECKs matching on201 # themselves. We need to keep the comments to preserve line numbers while202 # avoiding empty lines which could potentially trigger formatting-related203 # checks.204 cleaned_test = re.sub("// *CHECK-[A-Z0-9\\-]*:[^\r\n]*", "//", self.input_text)205 write_file(self.temp_file_name, cleaned_test)206 write_file(self.original_file_name, cleaned_test)207 208 def run_clang_tidy(self) -> str:209 args = (210 [211 "clang-tidy",212 "--experimental-custom-checks",213 self.temp_file_name,214 ]215 + [216 (217 "-fix"218 if self.export_fixes is None219 else "--export-fixes=" + self.export_fixes220 )221 ]222 + [223 "--checks=-*," + self.check_name,224 ]225 + self.clang_tidy_extra_args226 + ["--"]227 + self.clang_extra_args228 )229 if self.expect_clang_tidy_error:230 args.insert(0, "not")231 print("Running " + repr(args) + "...")232 clang_tidy_output = try_run(args)233 print("------------------------ clang-tidy output -----------------------")234 print(235 clang_tidy_output.encode(sys.stdout.encoding, errors="replace").decode(236 sys.stdout.encoding237 )238 )239 print("------------------------------------------------------------------")240 241 diff_output = try_run(242 ["diff", "-u", self.original_file_name, self.temp_file_name], False243 )244 print("------------------------------ Fixes -----------------------------")245 print(diff_output)246 print("------------------------------------------------------------------")247 return clang_tidy_output248 249 def check_no_diagnosis(self, clang_tidy_output: str) -> None:250 if clang_tidy_output != "":251 sys.exit("No diagnostics were expected, but found the ones above")252 253 def check_fixes(self) -> None:254 if self.has_check_fixes:255 try_run(256 [257 "FileCheck",258 "--input-file=" + self.temp_file_name,259 self.input_file_name,260 "--check-prefixes=" + ",".join(self.fixes.prefixes),261 (262 "--match-full-lines"263 if not self.match_partial_fixes264 else "--strict-whitespace" # Keeping past behavior.265 ),266 ]267 )268 269 def check_messages(self, clang_tidy_output: str) -> None:270 if self.has_check_messages:271 messages_file = self.temp_file_name + ".msg"272 write_file(messages_file, clang_tidy_output)273 try_run(274 [275 "FileCheck",276 "-input-file=" + messages_file,277 self.input_file_name,278 "-check-prefixes=" + ",".join(self.messages.prefixes),279 "-implicit-check-not={{warning|error}}:",280 ]281 )282 283 def check_notes(self, clang_tidy_output: str) -> None:284 if self.has_check_notes:285 notes_file = self.temp_file_name + ".notes"286 filtered_output = [287 line288 for line in clang_tidy_output.splitlines()289 if not ("note: FIX-IT applied" in line)290 ]291 write_file(notes_file, "\n".join(filtered_output))292 try_run(293 [294 "FileCheck",295 "-input-file=" + notes_file,296 self.input_file_name,297 "-check-prefixes=" + ",".join(self.notes.prefixes),298 "-implicit-check-not={{note|warning|error}}:",299 ]300 )301 302 def run(self) -> None:303 self.read_input()304 if self.export_fixes is None:305 self.get_prefixes()306 self.prepare_test_inputs()307 clang_tidy_output = self.run_clang_tidy()308 if self.expect_no_diagnosis:309 self.check_no_diagnosis(clang_tidy_output)310 elif self.export_fixes is None:311 self.check_fixes()312 self.check_messages(clang_tidy_output)313 self.check_notes(clang_tidy_output)314 315 316CPP_STANDARDS = [317 "c++98",318 "c++11",319 ("c++14", "c++1y"),320 ("c++17", "c++1z"),321 ("c++20", "c++2a"),322 ("c++23", "c++2b"),323 ("c++26", "c++2c"),324]325C_STANDARDS = ["c99", ("c11", "c1x"), "c17", ("c23", "c2x"), "c2y"]326 327 328def expand_std(std: str) -> List[str]:329 split_std, or_later, _ = std.partition("-or-later")330 331 if not or_later:332 return [split_std]333 334 for standard_list in (CPP_STANDARDS, C_STANDARDS):335 item = next(336 (337 i338 for i, v in enumerate(standard_list)339 if (split_std in v if isinstance(v, (list, tuple)) else split_std == v)340 ),341 None,342 )343 if item is not None:344 return [split_std] + [345 x if isinstance(x, str) else x[0] for x in standard_list[item + 1 :]346 ]347 return [std]348 349 350def csv(string: str) -> List[str]:351 return string.split(",")352 353 354def parse_arguments() -> Tuple[argparse.Namespace, List[str]]:355 parser = argparse.ArgumentParser(356 prog=pathlib.Path(__file__).stem,357 description=__doc__,358 formatter_class=argparse.RawDescriptionHelpFormatter,359 )360 parser.add_argument("-expect-clang-tidy-error", action="store_true")361 parser.add_argument("-resource-dir")362 parser.add_argument("-assume-filename")363 parser.add_argument("input_file_name")364 parser.add_argument("check_name")365 parser.add_argument("temp_file_name")366 parser.add_argument(367 "-check-suffix",368 "-check-suffixes",369 default=[""],370 type=csv,371 help="comma-separated list of FileCheck suffixes",372 )373 parser.add_argument(374 "-export-fixes",375 default=None,376 type=str,377 metavar="file",378 help="A file to export fixes into instead of fixing.",379 )380 parser.add_argument(381 "-std",382 type=csv,383 default=None,384 help="Passed to clang. Special -or-later values are expanded.",385 )386 parser.add_argument(387 "--match-partial-fixes",388 action="store_true",389 help="allow partial line matches for fixes",390 )391 392 args, extra_args = parser.parse_known_args()393 if args.std is None:394 _, extension = os.path.splitext(args.assume_filename or args.input_file_name)395 args.std = ["c99-or-later" if extension in [".c", ".m"] else "c++11-or-later"]396 397 return (args, extra_args)398 399 400def main() -> None:401 sys.stdout.reconfigure(encoding="utf-8")402 sys.stderr.reconfigure(encoding="utf-8")403 args, extra_args = parse_arguments()404 405 abbreviated_stds = args.std406 for abbreviated_std in abbreviated_stds:407 for std in expand_std(abbreviated_std):408 args.std = std409 CheckRunner(args, extra_args).run()410 411 412if __name__ == "__main__":413 main()414