316 lines
11 KiB
Python
Executable file
316 lines
11 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
#
|
|
# ===- check_clang_tidy.py - ClangTidy Test Helper ------------*- python -*--===#
|
|
#
|
|
# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
|
|
# See https://llvm.org/LICENSE.txt for license information.
|
|
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
|
#
|
|
# ===------------------------------------------------------------------------===#
|
|
|
|
r"""
|
|
ClangTidy Test Helper
|
|
=====================
|
|
|
|
This script runs clang-tidy in fix mode and verify fixes, messages or both.
|
|
|
|
Usage:
|
|
check_clang_tidy.py [-resource-dir=<resource-dir>] \
|
|
[-assume-filename=<file-with-source-extension>] \
|
|
[-check-suffix=<comma-separated-file-check-suffixes>] \
|
|
[-check-suffixes=<comma-separated-file-check-suffixes>] \
|
|
[-std=c++(98|11|14|17|20)[-or-later]] \
|
|
<source-file> <check-name> <temp-file> \
|
|
-- [optional clang-tidy arguments]
|
|
|
|
Example:
|
|
// RUN: %check_clang_tidy %s llvm-include-order %t -- -- -isystem %S/Inputs
|
|
|
|
Notes:
|
|
-std=c++(98|11|14|17|20)-or-later:
|
|
This flag will cause multiple runs within the same check_clang_tidy
|
|
execution. Make sure you don't have shared state across these runs.
|
|
"""
|
|
|
|
import argparse
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
|
|
|
|
def write_file(file_name, text):
|
|
with open(file_name, "w", encoding="utf-8") as f:
|
|
f.write(text)
|
|
f.truncate()
|
|
|
|
|
|
def try_run(args, raise_error=True):
|
|
try:
|
|
process_output = subprocess.check_output(args, stderr=subprocess.STDOUT).decode(
|
|
errors="ignore"
|
|
)
|
|
except subprocess.CalledProcessError as e:
|
|
process_output = e.output.decode(errors="ignore")
|
|
print("%s failed:\n%s" % (" ".join(args), process_output))
|
|
if raise_error:
|
|
raise
|
|
return process_output
|
|
|
|
|
|
# This class represents the appearance of a message prefix in a file.
|
|
class MessagePrefix:
|
|
def __init__(self, label):
|
|
self.has_message = False
|
|
self.prefixes = []
|
|
self.label = label
|
|
|
|
def check(self, file_check_suffix, input_text):
|
|
self.prefix = self.label + file_check_suffix
|
|
self.has_message = self.prefix in input_text
|
|
if self.has_message:
|
|
self.prefixes.append(self.prefix)
|
|
return self.has_message
|
|
|
|
|
|
class CheckRunner:
|
|
def __init__(self, args, extra_args):
|
|
self.resource_dir = args.resource_dir
|
|
self.assume_file_name = args.assume_filename
|
|
self.input_file_name = args.input_file_name
|
|
self.check_name = args.check_name
|
|
self.temp_file_name = args.temp_file_name
|
|
self.original_file_name = self.temp_file_name + ".orig"
|
|
self.expect_clang_tidy_error = args.expect_clang_tidy_error
|
|
self.std = args.std
|
|
self.check_suffix = args.check_suffix
|
|
self.input_text = ""
|
|
self.has_check_fixes = False
|
|
self.has_check_messages = False
|
|
self.has_check_notes = False
|
|
self.fixes = MessagePrefix("CHECK-FIXES")
|
|
self.messages = MessagePrefix("CHECK-MESSAGES")
|
|
self.notes = MessagePrefix("CHECK-NOTES")
|
|
|
|
file_name_with_extension = self.assume_file_name or self.input_file_name
|
|
_, extension = os.path.splitext(file_name_with_extension)
|
|
if extension not in [".c", ".hpp", ".m", ".mm"]:
|
|
extension = ".cpp"
|
|
self.temp_file_name = self.temp_file_name + extension
|
|
|
|
self.clang_extra_args = []
|
|
self.clang_tidy_extra_args = extra_args
|
|
if "--" in extra_args:
|
|
i = self.clang_tidy_extra_args.index("--")
|
|
self.clang_extra_args = self.clang_tidy_extra_args[i + 1 :]
|
|
self.clang_tidy_extra_args = self.clang_tidy_extra_args[:i]
|
|
|
|
# If the test does not specify a config style, force an empty one; otherwise
|
|
# auto-detection logic can discover a ".clang-tidy" file that is not related to
|
|
# the test.
|
|
if not any(
|
|
[re.match("^-?-config(-file)?=", arg) for arg in self.clang_tidy_extra_args]
|
|
):
|
|
self.clang_tidy_extra_args.append("--config={}")
|
|
|
|
if extension in [".m", ".mm"]:
|
|
self.clang_extra_args = [
|
|
"-fobjc-abi-version=2",
|
|
"-fobjc-arc",
|
|
"-fblocks",
|
|
] + self.clang_extra_args
|
|
|
|
if extension in [".cpp", ".hpp", ".mm"]:
|
|
self.clang_extra_args.append("-std=" + self.std)
|
|
|
|
# Tests should not rely on STL being available, and instead provide mock
|
|
# implementations of relevant APIs.
|
|
self.clang_extra_args.append("-nostdinc++")
|
|
|
|
if self.resource_dir is not None:
|
|
self.clang_extra_args.append("-resource-dir=%s" % self.resource_dir)
|
|
|
|
def read_input(self):
|
|
with open(self.input_file_name, "r", encoding="utf-8") as input_file:
|
|
self.input_text = input_file.read()
|
|
|
|
def get_prefixes(self):
|
|
for suffix in self.check_suffix:
|
|
if suffix and not re.match("^[A-Z0-9\\-]+$", suffix):
|
|
sys.exit(
|
|
'Only A..Z, 0..9 and "-" are allowed in check suffixes list,'
|
|
+ ' but "%s" was given' % suffix
|
|
)
|
|
|
|
file_check_suffix = ("-" + suffix) if suffix else ""
|
|
|
|
has_check_fix = self.fixes.check(file_check_suffix, self.input_text)
|
|
self.has_check_fixes = self.has_check_fixes or has_check_fix
|
|
|
|
has_check_message = self.messages.check(file_check_suffix, self.input_text)
|
|
self.has_check_messages = self.has_check_messages or has_check_message
|
|
|
|
has_check_note = self.notes.check(file_check_suffix, self.input_text)
|
|
self.has_check_notes = self.has_check_notes or has_check_note
|
|
|
|
if has_check_note and has_check_message:
|
|
sys.exit(
|
|
"Please use either %s or %s but not both"
|
|
% (self.notes.prefix, self.messages.prefix)
|
|
)
|
|
|
|
if not has_check_fix and not has_check_message and not has_check_note:
|
|
sys.exit(
|
|
"%s, %s or %s not found in the input"
|
|
% (self.fixes.prefix, self.messages.prefix, self.notes.prefix)
|
|
)
|
|
|
|
assert self.has_check_fixes or self.has_check_messages or self.has_check_notes
|
|
|
|
def prepare_test_inputs(self):
|
|
# Remove the contents of the CHECK lines to avoid CHECKs matching on
|
|
# themselves. We need to keep the comments to preserve line numbers while
|
|
# avoiding empty lines which could potentially trigger formatting-related
|
|
# checks.
|
|
cleaned_test = re.sub("// *CHECK-[A-Z0-9\\-]*:[^\r\n]*", "//", self.input_text)
|
|
write_file(self.temp_file_name, cleaned_test)
|
|
write_file(self.original_file_name, cleaned_test)
|
|
|
|
def run_clang_tidy(self):
|
|
args = (
|
|
[
|
|
"clang-tidy",
|
|
self.temp_file_name,
|
|
"-fix",
|
|
"--checks=-*," + self.check_name,
|
|
]
|
|
+ self.clang_tidy_extra_args
|
|
+ ["--"]
|
|
+ self.clang_extra_args
|
|
)
|
|
if self.expect_clang_tidy_error:
|
|
args.insert(0, "not")
|
|
print("Running " + repr(args) + "...")
|
|
clang_tidy_output = try_run(args)
|
|
print("------------------------ clang-tidy output -----------------------")
|
|
print(
|
|
clang_tidy_output.encode(sys.stdout.encoding, errors="replace").decode(
|
|
sys.stdout.encoding
|
|
)
|
|
)
|
|
print("------------------------------------------------------------------")
|
|
|
|
diff_output = try_run(
|
|
["diff", "-u", self.original_file_name, self.temp_file_name], False
|
|
)
|
|
print("------------------------------ Fixes -----------------------------")
|
|
print(diff_output)
|
|
print("------------------------------------------------------------------")
|
|
return clang_tidy_output
|
|
|
|
def check_fixes(self):
|
|
if self.has_check_fixes:
|
|
try_run(
|
|
[
|
|
"FileCheck",
|
|
"-input-file=" + self.temp_file_name,
|
|
self.input_file_name,
|
|
"-check-prefixes=" + ",".join(self.fixes.prefixes),
|
|
"-strict-whitespace",
|
|
]
|
|
)
|
|
|
|
def check_messages(self, clang_tidy_output):
|
|
if self.has_check_messages:
|
|
messages_file = self.temp_file_name + ".msg"
|
|
write_file(messages_file, clang_tidy_output)
|
|
try_run(
|
|
[
|
|
"FileCheck",
|
|
"-input-file=" + messages_file,
|
|
self.input_file_name,
|
|
"-check-prefixes=" + ",".join(self.messages.prefixes),
|
|
"-implicit-check-not={{warning|error}}:",
|
|
]
|
|
)
|
|
|
|
def check_notes(self, clang_tidy_output):
|
|
if self.has_check_notes:
|
|
notes_file = self.temp_file_name + ".notes"
|
|
filtered_output = [
|
|
line
|
|
for line in clang_tidy_output.splitlines()
|
|
if not ("note: FIX-IT applied" in line)
|
|
]
|
|
write_file(notes_file, "\n".join(filtered_output))
|
|
try_run(
|
|
[
|
|
"FileCheck",
|
|
"-input-file=" + notes_file,
|
|
self.input_file_name,
|
|
"-check-prefixes=" + ",".join(self.notes.prefixes),
|
|
"-implicit-check-not={{note|warning|error}}:",
|
|
]
|
|
)
|
|
|
|
def run(self):
|
|
self.read_input()
|
|
self.get_prefixes()
|
|
self.prepare_test_inputs()
|
|
clang_tidy_output = self.run_clang_tidy()
|
|
self.check_fixes()
|
|
self.check_messages(clang_tidy_output)
|
|
self.check_notes(clang_tidy_output)
|
|
|
|
|
|
def expand_std(std):
|
|
if std == "c++98-or-later":
|
|
return ["c++98", "c++11", "c++14", "c++17", "c++20", "c++23", "c++2c"]
|
|
if std == "c++11-or-later":
|
|
return ["c++11", "c++14", "c++17", "c++20", "c++23", "c++2c"]
|
|
if std == "c++14-or-later":
|
|
return ["c++14", "c++17", "c++20", "c++23", "c++2c"]
|
|
if std == "c++17-or-later":
|
|
return ["c++17", "c++20", "c++23", "c++2c"]
|
|
if std == "c++20-or-later":
|
|
return ["c++20", "c++23", "c++2c"]
|
|
if std == "c++23-or-later":
|
|
return ["c++23", "c++2c"]
|
|
return [std]
|
|
|
|
|
|
def csv(string):
|
|
return string.split(",")
|
|
|
|
|
|
def parse_arguments():
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("-expect-clang-tidy-error", action="store_true")
|
|
parser.add_argument("-resource-dir")
|
|
parser.add_argument("-assume-filename")
|
|
parser.add_argument("input_file_name")
|
|
parser.add_argument("check_name")
|
|
parser.add_argument("temp_file_name")
|
|
parser.add_argument(
|
|
"-check-suffix",
|
|
"-check-suffixes",
|
|
default=[""],
|
|
type=csv,
|
|
help="comma-separated list of FileCheck suffixes",
|
|
)
|
|
parser.add_argument("-std", type=csv, default=["c++11-or-later"])
|
|
return parser.parse_known_args()
|
|
|
|
|
|
def main():
|
|
args, extra_args = parse_arguments()
|
|
|
|
abbreviated_stds = args.std
|
|
for abbreviated_std in abbreviated_stds:
|
|
for std in expand_std(abbreviated_std):
|
|
args.std = std
|
|
CheckRunner(args, extra_args).run()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|