brintos

brintos / llvm-project-archived public Read only

0
0
Text · 14.3 KiB · b173ecf Raw
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