unplugged-system/build/bazel/rules/cc/clang_tidy_test.bzl

771 lines
24 KiB
Python

# Copyright (C) 2022 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
load("@bazel_skylib//lib:new_sets.bzl", "sets")
load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts")
load(
"//build/bazel/rules/test_common:args.bzl",
"get_all_args_with_prefix",
"get_single_arg_with_prefix",
)
load("//build/bazel/rules/test_common:rules.bzl", "expect_failure_test")
load(":cc_library_static.bzl", "cc_library_static")
load(":clang_tidy.bzl", "generate_clang_tidy_actions")
_PACKAGE_HEADER_FILTER = "^build/bazel/rules/cc/"
_DEFAULT_GLOBAL_CHECKS = [
"android-*",
"bugprone-*",
"cert-*",
"clang-diagnostic-unused-command-line-argument",
"google-build-explicit-make-pair",
"google-build-namespaces",
"google-runtime-operator",
"google-upgrade-*",
"misc-*",
"performance-*",
"portability-*",
"-bugprone-assignment-in-if-condition",
"-bugprone-easily-swappable-parameters",
"-bugprone-narrowing-conversions",
"-misc-const-correctness",
"-misc-no-recursion",
"-misc-non-private-member-variables-in-classes",
"-misc-unused-parameters",
"-performance-no-int-to-ptr",
"-clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling",
]
_DEFAULT_CHECKS = [
"-misc-no-recursion",
"-readability-function-cognitive-complexity",
"-bugprone-unchecked-optional-access",
"-bugprone-reserved-identifier*",
"-cert-dcl51-cpp",
"-cert-dcl37-c",
"-readability-qualified-auto",
"-bugprone-implicit-widening-of-multiplication-result",
"-bugprone-easily-swappable-parameters",
"-cert-err33-c",
"-bugprone-unchecked-optional-access",
"-misc-use-anonymous-namespace",
]
_DEFAULT_CHECKS_AS_ERRORS = [
"-bugprone-assignment-in-if-condition",
"-bugprone-branch-clone",
"-bugprone-signed-char-misuse",
"-misc-const-correctness",
]
_EXTRA_ARGS_BEFORE = [
"-D__clang_analyzer__",
"-Xclang",
"-analyzer-config",
"-Xclang",
"c++-temp-dtor-inlining=false",
]
def _clang_tidy_impl(ctx):
tidy_outs = generate_clang_tidy_actions(
ctx,
ctx.attr.copts,
ctx.attr.deps,
ctx.files.srcs,
ctx.files.hdrs,
ctx.attr.language,
ctx.attr.tidy_flags,
ctx.attr.tidy_checks,
ctx.attr.tidy_checks_as_errors,
ctx.attr.tidy_timeout_srcs,
)
return [
DefaultInfo(files = depset(tidy_outs)),
]
_clang_tidy = rule(
implementation = _clang_tidy_impl,
attrs = {
"srcs": attr.label_list(allow_files = True),
"deps": attr.label_list(),
"copts": attr.string_list(),
"hdrs": attr.label_list(allow_files = True),
"language": attr.string(values = ["c++", "c"], default = "c++"),
"tidy_checks": attr.string_list(),
"tidy_checks_as_errors": attr.string_list(),
"tidy_flags": attr.string_list(),
"tidy_timeout_srcs": attr.label_list(allow_files = True),
"_clang_tidy_sh": attr.label(
default = Label("@//prebuilts/clang/host/linux-x86:clang-tidy.sh"),
allow_single_file = True,
executable = True,
cfg = "exec",
doc = "The clang tidy shell wrapper",
),
"_clang_tidy": attr.label(
default = Label("@//prebuilts/clang/host/linux-x86:clang-tidy"),
allow_single_file = True,
executable = True,
cfg = "exec",
doc = "The clang tidy executable",
),
"_clang_tidy_real": attr.label(
default = Label("@//prebuilts/clang/host/linux-x86:clang-tidy.real"),
allow_single_file = True,
executable = True,
cfg = "exec",
),
"_with_tidy": attr.label(
default = "//build/bazel/flags/cc/tidy:with_tidy",
),
"_allow_local_tidy_true": attr.label(
default = "//build/bazel/flags/cc/tidy:allow_local_tidy_true",
),
"_with_tidy_flags": attr.label(
default = "//build/bazel/flags/cc/tidy:with_tidy_flags",
),
"_default_tidy_header_dirs": attr.label(
default = "//build/bazel/flags/cc/tidy:default_tidy_header_dirs",
),
"_tidy_timeout": attr.label(
default = "//build/bazel/flags/cc/tidy:tidy_timeout",
),
"_product_variables": attr.label(
default = "//build/bazel/product_config:product_vars",
),
},
toolchains = ["@bazel_tools//tools/cpp:toolchain_type"],
fragments = ["cpp"],
)
def _get_all_arg(env, actions, argname):
args = get_all_args_with_prefix(actions[0].argv, argname)
asserts.false(env, args == [], "could not arguments that start with `{}`".format(argname))
return args
def _get_single_arg(actions, argname):
return get_single_arg_with_prefix(actions[0].argv, argname)
def _checks_test_impl(ctx):
env = analysistest.begin(ctx)
actions = analysistest.target_actions(env)
checks = _get_single_arg(actions, "-checks=").split(",")
asserts.set_equals(env, sets.make(ctx.attr.expected_checks), sets.make(checks))
if len(ctx.attr.unexpected_checks) > 0:
for c in ctx.attr.unexpected_checks:
asserts.false(env, c in checks, "found unexpected check in -checks flag: %s" % c)
checks_as_errors = _get_single_arg(actions, "-warnings-as-errors=").split(",")
asserts.set_equals(env, sets.make(ctx.attr.expected_checks_as_errors), sets.make(checks_as_errors))
return analysistest.end(env)
_checks_test = analysistest.make(
_checks_test_impl,
attrs = {
"expected_checks": attr.string_list(mandatory = True),
"expected_checks_as_errors": attr.string_list(mandatory = True),
"unexpected_checks": attr.string_list(),
},
)
def _copts_test_impl(ctx):
env = analysistest.begin(ctx)
actions = analysistest.target_actions(env)
args = actions[0].argv
clang_flags = []
for i, a in enumerate(args):
if a == "--" and len(args) > i + 1:
clang_flags = args[i + 1:]
break
asserts.true(
env,
len(clang_flags) > 0,
"no flags passed to clang; all arguments: %s" % args,
)
for expected_arg in ctx.attr.expected_copts:
asserts.true(
env,
expected_arg in clang_flags,
"expected `%s` not present in clang flags" % expected_arg,
)
return analysistest.end(env)
_copts_test = analysistest.make(
_copts_test_impl,
attrs = {
"expected_copts": attr.string_list(mandatory = True),
},
)
def _tidy_flags_test_impl(ctx):
env = analysistest.begin(ctx)
actions = analysistest.target_actions(env)
args = actions[0].argv
tidy_flags = []
for i, a in enumerate(args):
if a == "--" and len(args) > i + 1:
tidy_flags = args[:i]
asserts.true(
env,
len(tidy_flags) > 0,
"no tidy flags passed to clang-tidy; all arguments: %s" % args,
)
for expected_arg in ctx.attr.expected_tidy_flags:
asserts.true(
env,
expected_arg in tidy_flags,
"expected `%s` not present in flags to clang-tidy" % expected_arg,
)
header_filter = _get_single_arg(actions, "-header-filter=")
asserts.true(
env,
header_filter == ctx.attr.expected_header_filter,
(
"expected header-filter to have value `%s`; got `%s`" %
(ctx.attr.expected_header_filter, header_filter)
),
)
extra_arg_before = _get_all_arg(env, actions, "-extra-arg-before=")
for expected_arg in ctx.attr.expected_extra_arg_before:
asserts.true(
env,
expected_arg in extra_arg_before,
"did not find expected flag `%s` in args to clang-tidy" % expected_arg,
)
return analysistest.end(env)
_tidy_flags_test = analysistest.make(
_tidy_flags_test_impl,
attrs = {
"expected_tidy_flags": attr.string_list(),
"expected_header_filter": attr.string(mandatory = True),
"expected_extra_arg_before": attr.string_list(),
},
)
def _test_clang_tidy():
name = "checks"
test_name = name + "_test"
checks_test_name = test_name + "_checks"
copts_test_name = test_name + "_copts"
tidy_flags_test_name = test_name + "_tidy_flags"
_clang_tidy(
name = name,
# clang-tidy operates differently on generated and non-generated files
# use test_srcs so that the tidy rule doesn't think these are genearted
# files
srcs = ["//build/bazel/rules/cc/testing:test_srcs"],
copts = ["-asdf1", "-asdf2"],
tidy_flags = ["-tidy-flag1", "-tidy-flag2"],
tags = ["manual"],
)
_checks_test(
name = checks_test_name,
target_under_test = name,
expected_checks = _DEFAULT_CHECKS + _DEFAULT_GLOBAL_CHECKS,
expected_checks_as_errors = _DEFAULT_CHECKS_AS_ERRORS,
)
_copts_test(
name = copts_test_name,
target_under_test = name,
expected_copts = ["-asdf1", "-asdf2"],
)
_tidy_flags_test(
name = tidy_flags_test_name,
target_under_test = name,
expected_tidy_flags = ["-tidy-flag1", "-tidy-flag2"],
expected_header_filter = _PACKAGE_HEADER_FILTER,
expected_extra_arg_before = _EXTRA_ARGS_BEFORE,
)
return [
checks_test_name,
copts_test_name,
tidy_flags_test_name,
]
def _test_custom_header_dir():
name = "custom_header_dir"
test_name = name + "_test"
_clang_tidy(
name = name,
srcs = ["a.cpp"],
tidy_flags = ["-header-filter=dir1/"],
tags = ["manual"],
)
_tidy_flags_test(
name = test_name,
target_under_test = name,
expected_header_filter = "dir1/",
)
return [
test_name,
]
def _test_disabled_checks_are_removed():
name = "disabled_checks_are_removed"
test_name = name + "_test"
_clang_tidy(
name = name,
# clang-tidy operates differently on generated and non-generated files.
# use test_srcs so that the tidy rule doesn't think these are genearted
# files
srcs = ["//build/bazel/rules/cc/testing:test_srcs"],
tidy_checks = ["misc-no-recursion", "readability-function-cognitive-complexity"],
tags = ["manual"],
)
_checks_test(
name = test_name,
target_under_test = name,
expected_checks = _DEFAULT_CHECKS + _DEFAULT_GLOBAL_CHECKS,
expected_checks_as_errors = _DEFAULT_CHECKS_AS_ERRORS,
unexpected_checks = ["misc-no-recursion", "readability-function-cognitive-complexity"],
)
return [
test_name,
]
def _create_bad_tidy_checks_test(name, tidy_checks, failure_message):
name = "bad_tidy_checks_fail_" + name
test_name = name + "_test"
_clang_tidy(
name = name,
srcs = ["a.cpp"],
tidy_checks = tidy_checks,
tags = ["manual"],
)
expect_failure_test(
name = test_name,
target_under_test = name,
failure_message = failure_message,
)
return [
test_name,
]
def _test_bad_tidy_checks_fail():
return (
_create_bad_tidy_checks_test(
name = "with_spaces",
tidy_checks = ["check with spaces"],
failure_message = "Check `check with spaces` invalid, cannot contain spaces",
) +
_create_bad_tidy_checks_test(
name = "with_commas",
tidy_checks = ["check,with,commas"],
failure_message = "Check `check,with,commas` invalid, cannot contain commas. Split each entry into its own string instead",
)
)
def _create_bad_tidy_flags_test(name, tidy_flags, failure_message):
name = "bad_tidy_flags_fail_" + name
test_name = name + "_test"
_clang_tidy(
name = name,
srcs = ["a.cpp"],
tidy_flags = tidy_flags,
tags = ["manual"],
)
expect_failure_test(
name = test_name,
target_under_test = name,
failure_message = failure_message,
)
return [
test_name,
]
def _test_bad_tidy_flags_fail():
return (
_create_bad_tidy_flags_test(
name = "without_leading_dash",
tidy_flags = ["flag1"],
failure_message = "Flag `flag1` must start with `-`",
) +
_create_bad_tidy_flags_test(
name = "fix_flags",
tidy_flags = ["-fix"],
failure_message = "Flag `%s` is not allowed, since it could cause multiple writes to the same source file",
) +
_create_bad_tidy_flags_test(
name = "checks_in_flags",
tidy_flags = ["-checks=asdf"],
failure_message = "Flag `-checks=asdf` is not allowed, use `tidy_checks` property instead",
) +
_create_bad_tidy_flags_test(
name = "warnings_as_errors_in_flags",
tidy_flags = ["-warnings-as-errors=asdf"],
failure_message = "Flag `-warnings-as-errors=asdf` is not allowed, use `tidy_checks_as_errors` property instead",
) +
_create_bad_tidy_flags_test(
name = "space_in_flags",
tidy_flags = ["-flag with spaces"],
failure_message = "Bad flag: `-flag with spaces` is not an allowed multi-word flag. Should it be split into multiple flags",
)
)
def _test_disable_global_checks():
name = "disable_global_checks"
test_name = name + "_test"
_clang_tidy(
name = name,
srcs = ["a.cpp"],
tidy_checks = ["-*"],
tags = ["manual"],
)
_checks_test(
name = test_name,
target_under_test = name,
expected_checks = ["-*"] + _DEFAULT_CHECKS,
expected_checks_as_errors = _DEFAULT_CHECKS_AS_ERRORS,
)
return [
test_name,
]
def _cc_library_static_generates_clang_tidy_actions_for_srcs_test_impl(ctx):
env = analysistest.begin(ctx)
actions = analysistest.target_actions(env)
clang_tidy_actions = [a for a in actions if a.mnemonic == "ClangTidy"]
asserts.equals(
env,
ctx.attr.expected_num_actions,
len(clang_tidy_actions),
"expected to have %s clang-tidy actions, but got %s; actions: %s" % (
ctx.attr.expected_num_actions,
len(clang_tidy_actions),
clang_tidy_actions,
),
)
for a in clang_tidy_actions:
for input in a.inputs.to_list():
input_is_expected_header = input.short_path in [f.short_path for f in ctx.files.expected_headers]
if input in ctx.files._clang_tidy_tools or input_is_expected_header:
continue
asserts.true(
env,
input in ctx.files.srcs,
"clang-tidy operated on a file not in srcs: %s; all inputs: %s" % (input, a.inputs.to_list()),
)
asserts.true(
env,
input not in ctx.files.disabled_srcs,
"clang-tidy operated on a file in disabled_srcs: %s; all inputs: %s" % (input, a.inputs.to_list()),
)
return analysistest.end(env)
_cc_library_static_generates_clang_tidy_actions_for_srcs_test = analysistest.make(
impl = _cc_library_static_generates_clang_tidy_actions_for_srcs_test_impl,
attrs = {
"expected_num_actions": attr.int(mandatory = True),
"srcs": attr.label_list(allow_files = True),
"disabled_srcs": attr.label_list(allow_files = True),
"expected_headers": attr.label_list(allow_files = True),
"_clang_tidy_tools": attr.label_list(
default = [
"@//prebuilts/clang/host/linux-x86:clang-tidy",
"@//prebuilts/clang/host/linux-x86:clang-tidy.real",
"@//prebuilts/clang/host/linux-x86:clang-tidy.sh",
],
allow_files = True,
),
},
config_settings = {
"@//build/bazel/flags/cc/tidy:allow_local_tidy_true": True,
},
)
def _create_cc_library_static_generates_clang_tidy_actions_for_srcs(
name,
srcs,
expected_num_actions,
disabled_srcs = None,
expected_headers = []):
name = "cc_library_static_generates_clang_tidy_actions_for_srcs_" + name
test_name = name + "_test"
cc_library_static(
name = name,
srcs = srcs,
tidy_disabled_srcs = disabled_srcs,
tidy = "local",
tags = ["manual"],
)
_cc_library_static_generates_clang_tidy_actions_for_srcs_test(
name = test_name,
target_under_test = name,
expected_num_actions = expected_num_actions,
srcs = srcs,
disabled_srcs = disabled_srcs,
expected_headers = expected_headers + select({
"//build/bazel/platforms/os:android": ["@//bionic/libc:generated_android_ids"],
"//conditions:default": [],
}),
)
return test_name
def _test_cc_library_static_generates_clang_tidy_actions_for_srcs():
return [
_create_cc_library_static_generates_clang_tidy_actions_for_srcs(
name = "with_srcs",
srcs = ["a.cpp", "b.cpp"],
expected_num_actions = 2,
),
_create_cc_library_static_generates_clang_tidy_actions_for_srcs(
name = "with_disabled_srcs",
srcs = ["a.cpp", "b.cpp"],
disabled_srcs = ["b.cpp", "c.cpp"],
expected_num_actions = 1,
),
]
def _no_clang_analyzer_on_generated_files_test_impl(ctx):
env = analysistest.begin(ctx)
actions = analysistest.target_actions(env)
clang_tidy_actions = [a for a in actions if a.mnemonic == "ClangTidy"]
for a in clang_tidy_actions:
found_clang_analyzer = False
for arg in a.argv:
if "-clang-analyzer-*" in arg:
found_clang_analyzer = True
asserts.true(env, found_clang_analyzer)
return analysistest.end(env)
_no_clang_analyzer_on_generated_files_test = analysistest.make(
impl = _no_clang_analyzer_on_generated_files_test_impl,
config_settings = {
"@//build/bazel/flags/cc/tidy:allow_local_tidy_true": True,
},
)
def _test_no_clang_analyzer_on_generated_files():
name = "no_clang_analyzer_on_generated_files"
gen_name = name + "_generated_files"
test_name = name + "_test"
native.genrule(
name = gen_name,
outs = ["aout.cpp", "bout.cpp"],
cmd = "touch $(OUTS)",
tags = ["manual"],
)
cc_library_static(
name = name,
srcs = [":" + gen_name],
tidy = "local",
tags = ["manual"],
)
_no_clang_analyzer_on_generated_files_test(
name = test_name,
target_under_test = name,
)
return [
test_name,
]
def _clang_tidy_actions_count_no_tidy_env_test_impl(ctx):
env = analysistest.begin(ctx)
actions = analysistest.target_actions(env)
clang_tidy_actions = [a for a in actions if a.mnemonic == "ClangTidy"]
asserts.equals(
env,
ctx.attr.expected_num_tidy_actions,
len(clang_tidy_actions),
"expected to find %d tidy actions, but found %d" % (
ctx.attr.expected_num_tidy_actions,
len(clang_tidy_actions),
),
)
return analysistest.end(env)
_clang_tidy_actions_count_no_tidy_env_test = analysistest.make(
impl = _clang_tidy_actions_count_no_tidy_env_test_impl,
attrs = {
"expected_num_tidy_actions": attr.int(),
},
)
_clang_tidy_actions_count_with_tidy_true_test = analysistest.make(
impl = _clang_tidy_actions_count_no_tidy_env_test_impl,
attrs = {
"expected_num_tidy_actions": attr.int(),
},
config_settings = {
"@//build/bazel/flags/cc/tidy:with_tidy": True,
},
)
_clang_tidy_actions_count_with_allow_local_tidy_true_test = analysistest.make(
impl = _clang_tidy_actions_count_no_tidy_env_test_impl,
attrs = {
"expected_num_tidy_actions": attr.int(),
},
config_settings = {
"@//build/bazel/flags/cc/tidy:allow_local_tidy_true": True,
},
)
def _test_clang_tidy_runs_if_tidy_true():
name = "clang_tidy_runs_if_tidy_true"
test_name = name + "_test"
with_tidy_test_name = test_name + "_with_tidy_true"
allow_local_tidy_true_test_name = test_name + "_allow_local_tidy_true"
cc_library_static(
name = name,
srcs = ["a.cpp"],
tidy = "local",
tags = ["manual"],
)
_clang_tidy_actions_count_no_tidy_env_test(
name = test_name,
target_under_test = name,
expected_num_tidy_actions = 0,
)
_clang_tidy_actions_count_with_tidy_true_test(
name = with_tidy_test_name,
target_under_test = name,
expected_num_tidy_actions = 1,
)
_clang_tidy_actions_count_with_allow_local_tidy_true_test(
name = allow_local_tidy_true_test_name,
target_under_test = name,
expected_num_tidy_actions = 1,
)
return [
test_name,
with_tidy_test_name,
allow_local_tidy_true_test_name,
]
def _test_clang_tidy_runs_if_attribute_unset():
name = "clang_tidy_runs_if_attribute_unset"
test_name = name + "_test"
with_tidy_test_name = test_name + "_with_tidy_true"
allow_local_tidy_true_test_name = test_name + "_allow_local_tidy_true"
cc_library_static(
name = name,
srcs = ["a.cpp"],
tags = ["manual"],
)
_clang_tidy_actions_count_no_tidy_env_test(
name = test_name,
target_under_test = name,
expected_num_tidy_actions = 0,
)
_clang_tidy_actions_count_with_tidy_true_test(
name = with_tidy_test_name,
target_under_test = name,
expected_num_tidy_actions = 1,
)
_clang_tidy_actions_count_with_allow_local_tidy_true_test(
name = allow_local_tidy_true_test_name,
target_under_test = name,
expected_num_tidy_actions = 0,
)
return [
test_name,
with_tidy_test_name,
allow_local_tidy_true_test_name,
]
def _test_no_clang_tidy_if_tidy_false():
name = "no_clang_tidy_if_tidy_false"
test_name = name + "_test"
with_tidy_test_name = test_name + "_with_tidy_true"
allow_local_tidy_true_test_name = test_name + "_allow_local_tidy_true"
cc_library_static(
name = name,
srcs = ["a.cpp"],
tidy = "never",
tags = ["manual"],
)
_clang_tidy_actions_count_no_tidy_env_test(
name = test_name,
target_under_test = name,
expected_num_tidy_actions = 0,
)
_clang_tidy_actions_count_with_tidy_true_test(
name = with_tidy_test_name,
target_under_test = name,
expected_num_tidy_actions = 0,
)
_clang_tidy_actions_count_with_allow_local_tidy_true_test(
name = allow_local_tidy_true_test_name,
target_under_test = name,
expected_num_tidy_actions = 0,
)
return [
test_name,
with_tidy_test_name,
allow_local_tidy_true_test_name,
]
def clang_tidy_test_suite(name):
native.test_suite(
name = name,
tests =
_test_clang_tidy() +
_test_custom_header_dir() +
_test_disabled_checks_are_removed() +
_test_bad_tidy_checks_fail() +
_test_bad_tidy_flags_fail() +
_test_disable_global_checks() +
_test_cc_library_static_generates_clang_tidy_actions_for_srcs() +
_test_no_clang_analyzer_on_generated_files() +
_test_no_clang_tidy_if_tidy_false() +
_test_clang_tidy_runs_if_tidy_true() +
_test_clang_tidy_runs_if_attribute_unset(),
)