xref: /llvm-project/clang-tools-extra/test/clang-tidy/check_clang_tidy.py (revision 154d00d5d0416c42388d6e82cac96fbc091101d2)
1#!/usr/bin/env python3
2#
3# ===- check_clang_tidy.py - ClangTidy Test Helper ------------*- python -*--===#
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-exception
8#
9# ===------------------------------------------------------------------------===#
10
11"""
12ClangTidy Test Helper
13=====================
14
15This script is used to simplify writing, running, and debugging tests compatible
16with llvm-lit. By default it runs clang-tidy in fix mode and uses FileCheck to
17verify messages and/or fixes.
18
19For debugging, with --export-fixes, the tool simply exports fixes to a provided
20file and does not run FileCheck.
21
22Extra arguments, those after the first -- if any, are passed to either
23clang-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
28
29Examples
30--------
31
32  // RUN: %check_clang_tidy %s llvm-include-order %t -- -- -isystem %S/Inputs
33
34or
35
36  // RUN: %check_clang_tidy %s llvm-include-order --export-fixes=fixes.yaml %t -std=c++20
37
38Notes
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"""
44
45import argparse
46import os
47import pathlib
48import re
49import subprocess
50import sys
51
52
53def write_file(file_name, text):
54    with open(file_name, "w", encoding="utf-8") as f:
55        f.write(text)
56        f.truncate()
57
58
59def try_run(args, raise_error=True):
60    try:
61        process_output = subprocess.check_output(args, stderr=subprocess.STDOUT).decode(
62            errors="ignore"
63        )
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
70
71
72# This class represents the appearance of a message prefix in a file.
73class MessagePrefix:
74    def __init__(self, label):
75        self.has_message = False
76        self.prefixes = []
77        self.label = label
78
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
85
86
87class 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")
107
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
113
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]
120
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]
126        ):
127            self.clang_tidy_extra_args.append("--config={}")
128
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
135
136        if extension in [".cpp", ".hpp", ".mm"]:
137            self.clang_extra_args.append("-std=" + self.std)
138
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++")
142
143        if self.resource_dir is not None:
144            self.clang_extra_args.append("-resource-dir=%s" % self.resource_dir)
145
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()
149
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
156                )
157
158            file_check_suffix = ("-" + suffix) if suffix else ""
159
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
162
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
165
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
168
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)
173                )
174
175            if not has_check_fix and not has_check_message and not has_check_note:
176                self.expect_no_diagnosis = True
177
178        expect_diagnosis = (
179            self.has_check_fixes or self.has_check_messages or self.has_check_notes
180        )
181        if self.expect_no_diagnosis and expect_diagnosis:
182            sys.exit(
183                "%s, %s or %s not found in the input"
184                % (
185                    self.fixes.prefix,
186                    self.messages.prefix,
187                    self.notes.prefix,
188                )
189            )
190        assert expect_diagnosis or self.expect_no_diagnosis
191
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)
200
201    def run_clang_tidy(self):
202        args = (
203            [
204                "clang-tidy",
205                self.temp_file_name,
206            ]
207            + [
208                (
209                    "-fix"
210                    if self.export_fixes is None
211                    else "--export-fixes=" + self.export_fixes
212                )
213            ]
214            + [
215                "--checks=-*," + self.check_name,
216            ]
217            + self.clang_tidy_extra_args
218            + ["--"]
219            + self.clang_extra_args
220        )
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
229            )
230        )
231        print("------------------------------------------------------------------")
232
233        diff_output = try_run(
234            ["diff", "-u", self.original_file_name, self.temp_file_name], False
235        )
236        print("------------------------------ Fixes -----------------------------")
237        print(diff_output)
238        print("------------------------------------------------------------------")
239        return clang_tidy_output
240
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")
244
245    def check_fixes(self):
246        if self.has_check_fixes:
247            try_run(
248                [
249                    "FileCheck",
250                    "-input-file=" + self.temp_file_name,
251                    self.input_file_name,
252                    "-check-prefixes=" + ",".join(self.fixes.prefixes),
253                    "-strict-whitespace",
254                ]
255            )
256
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(
262                [
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}}:",
268                ]
269            )
270
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)
278            ]
279            write_file(notes_file, "\n".join(filtered_output))
280            try_run(
281                [
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}}:",
287                ]
288            )
289
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)
302
303
304CPP_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"),
312]
313C_STANDARDS = ["c99", ("c11", "c1x"), "c17", ("c23", "c2x"), "c2y"]
314
315
316def expand_std(std):
317    split_std, or_later, _ = std.partition("-or-later")
318
319    if not or_later:
320        return [split_std]
321
322    for standard_list in (CPP_STANDARDS, C_STANDARDS):
323        item = next(
324            (
325                i
326                for i, v in enumerate(standard_list)
327                if (split_std in v if isinstance(v, (list, tuple)) else split_std == v)
328            ),
329            None,
330        )
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 :]
334            ]
335    return [std]
336
337
338def csv(string):
339    return string.split(",")
340
341
342def parse_arguments():
343    parser = argparse.ArgumentParser(
344        prog=pathlib.Path(__file__).stem,
345        description=__doc__,
346        formatter_class=argparse.RawDescriptionHelpFormatter,
347    )
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",
360    )
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.",
367    )
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.",
373    )
374    return parser.parse_known_args()
375
376
377def main():
378    args, extra_args = parse_arguments()
379
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()
385
386
387if __name__ == "__main__":
388    main()
389