Skip to content

Commit 5e07a7b

Browse files
committed
Add clang_tidy_test
This is a new test rule that executes clang-tidy as a test via shell script instead of an aspect. The biggest implementation issue with this is that the paths you get from CcInfo do not match the directory structure when running clang-tidy in a test. This requires a bit of fixing up. This attempts to share as much logic as possible with the aspect. I think a bit more could be shared in the future. Based on #65 Closes: #65 Fixes: #15
1 parent 0b86881 commit 5e07a7b

File tree

5 files changed

+191
-48
lines changed

5 files changed

+191
-48
lines changed

.bazelrc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
build:clang-tidy --aspects @bazel_clang_tidy//clang_tidy:clang_tidy.bzl%clang_tidy_aspect
2-
build:clang-tidy --output_groups=report
2+
build:clang-tidy --output_groups=report
3+
4+
build --test_output=errors

clang_tidy/clang_tidy.bzl

Lines changed: 58 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ def _run_tidy(
88
additional_deps,
99
config,
1010
flags,
11-
compilation_contexts,
1211
infile,
1312
discriminator,
13+
additional_files,
1414
additional_inputs):
1515
cc_toolchain = find_cpp_toolchain(ctx)
1616
direct_inputs = (
@@ -23,9 +23,7 @@ def _run_tidy(
2323

2424
inputs = depset(
2525
direct = direct_inputs,
26-
transitive =
27-
[compilation_context.headers for compilation_context in compilation_contexts] +
28-
[cc_toolchain.all_files],
26+
transitive = [additional_files, cc_toolchain.all_files],
2927
)
3028

3129
args = ctx.actions.args()
@@ -53,42 +51,18 @@ def _run_tidy(
5351
# start args passed to the compiler
5452
args.add("--")
5553

56-
# add args specified by the toolchain, on the command line and rule copts
57-
args.add_all(flags)
58-
59-
for compilation_context in compilation_contexts:
60-
# add defines
61-
for define in compilation_context.defines.to_list():
62-
args.add("-D" + define)
63-
64-
for define in compilation_context.local_defines.to_list():
65-
args.add("-D" + define)
66-
67-
# add includes
68-
for i in compilation_context.framework_includes.to_list():
69-
args.add("-F" + i)
70-
71-
for i in compilation_context.includes.to_list():
72-
args.add("-I" + i)
73-
74-
args.add_all(compilation_context.quote_includes.to_list(), before_each = "-iquote")
75-
76-
args.add_all(compilation_context.system_includes.to_list(), before_each = "-isystem")
77-
78-
args.add_all(compilation_context.external_includes.to_list(), before_each = "-isystem")
79-
8054
ctx.actions.run(
8155
inputs = inputs,
8256
outputs = [outfile],
8357
executable = wrapper,
84-
arguments = [args],
58+
arguments = [args] + flags,
8559
mnemonic = "ClangTidy",
8660
use_default_shell_env = True,
8761
progress_message = "Run clang-tidy on {}".format(infile.short_path),
8862
)
8963
return outfile
9064

91-
def _rule_sources(ctx, include_headers):
65+
def rule_sources(attr, include_headers):
9266
header_extensions = (
9367
".h",
9468
".hh",
@@ -117,18 +91,18 @@ def _rule_sources(ctx, include_headers):
11791
return False
11892

11993
srcs = []
120-
if hasattr(ctx.rule.attr, "srcs"):
121-
for src in ctx.rule.attr.srcs:
94+
if hasattr(attr, "srcs"):
95+
for src in attr.srcs:
12296
srcs += [src for src in src.files.to_list() if src.is_source and check_valid_file_type(src)]
123-
if hasattr(ctx.rule.attr, "hdrs"):
124-
for hdr in ctx.rule.attr.hdrs:
97+
if hasattr(attr, "hdrs"):
98+
for hdr in attr.hdrs:
12599
srcs += [hdr for hdr in hdr.files.to_list() if hdr.is_source and check_valid_file_type(hdr)]
126100
if include_headers:
127101
return srcs
128102
else:
129103
return [src for src in srcs if not src.basename.endswith(header_extensions)]
130104

131-
def _toolchain_flags(ctx, action_name = ACTION_NAMES.cpp_compile):
105+
def toolchain_flags(ctx, action_name = ACTION_NAMES.cpp_compile):
132106
cc_toolchain = find_cpp_toolchain(ctx)
133107
feature_configuration = cc_common.configure_features(
134108
ctx = ctx,
@@ -151,7 +125,47 @@ def _toolchain_flags(ctx, action_name = ACTION_NAMES.cpp_compile):
151125
)
152126
return flags
153127

154-
def _safe_flags(flags):
128+
def deps_flags(ctx, deps, *, escape):
129+
compilation_contexts = [dep[CcInfo].compilation_context for dep in deps]
130+
additional_files = depset(transitive = [
131+
compilation_context.headers
132+
for compilation_context in compilation_contexts
133+
])
134+
135+
flags = []
136+
for compilation_context in compilation_contexts:
137+
# add defines
138+
for define in compilation_context.defines.to_list():
139+
if escape:
140+
flags.append("-D'" + define + "'")
141+
else:
142+
flags.append("-D" + define)
143+
144+
for define in compilation_context.local_defines.to_list():
145+
if escape:
146+
flags.append("-D'" + define + "'")
147+
else:
148+
flags.append("-D" + define)
149+
150+
# add includes
151+
for i in compilation_context.framework_includes.to_list():
152+
flags.append("-F" + i)
153+
154+
for i in compilation_context.includes.to_list():
155+
flags.append("-I" + i)
156+
157+
for i in compilation_context.quote_includes.to_list():
158+
flags.extend(["-iquote", i])
159+
160+
for i in compilation_context.system_includes.to_list():
161+
flags.extend(["-isystem", i])
162+
163+
for i in compilation_context.external_includes.to_list():
164+
flags.extend(["-isystem", i])
165+
166+
return flags, additional_files
167+
168+
def safe_flags(flags):
155169
# Some flags might be used by GCC, but not understood by Clang.
156170
# Remove them here, to allow users to run clang-tidy, without having
157171
# a clang toolchain configured (that would produce a good command line with --compiler clang)
@@ -162,7 +176,7 @@ def _safe_flags(flags):
162176

163177
return [flag for flag in flags if flag not in unsupported_flags]
164178

165-
def _is_c_translation_unit(src, tags):
179+
def is_c_translation_unit(src, tags):
166180
"""Judge if a source file is for C.
167181
168182
Args:
@@ -201,24 +215,21 @@ def _clang_tidy_aspect_impl(target, ctx):
201215
additional_deps = ctx.attr._clang_tidy_additional_deps
202216
config = ctx.attr._clang_tidy_config.files.to_list()[0]
203217

204-
compilation_contexts = [target[CcInfo].compilation_context]
205-
if hasattr(ctx.rule.attr, "implementation_deps"):
206-
compilation_contexts.extend([implementation_dep[CcInfo].compilation_context for implementation_dep in ctx.rule.attr.implementation_deps])
207-
218+
deps = [target] + getattr(ctx.rule.attr, "implementation_deps", [])
219+
rule_flags, additional_files = deps_flags(ctx, deps, escape = False)
208220
copts = ctx.rule.attr.copts if hasattr(ctx.rule.attr, "copts") else []
209-
rule_flags = []
210221
for copt in copts:
211222
rule_flags.append(ctx.expand_make_variables(
212223
"copts",
213224
copt,
214225
{},
215226
))
216227

217-
c_flags = _safe_flags(_toolchain_flags(ctx, ACTION_NAMES.c_compile) + rule_flags) + ["-xc"]
218-
cxx_flags = _safe_flags(_toolchain_flags(ctx, ACTION_NAMES.cpp_compile) + rule_flags) + ["-xc++"]
228+
c_flags = safe_flags(toolchain_flags(ctx, ACTION_NAMES.c_compile) + rule_flags) + ["-xc"]
229+
cxx_flags = safe_flags(toolchain_flags(ctx, ACTION_NAMES.cpp_compile) + rule_flags) + ["-xc++"]
219230

220231
include_headers = "no-clang-tidy-headers" not in ctx.rule.attr.tags
221-
srcs = _rule_sources(ctx, include_headers)
232+
srcs = rule_sources(ctx.rule.attr, include_headers)
222233

223234
outputs = [
224235
_run_tidy(
@@ -227,10 +238,10 @@ def _clang_tidy_aspect_impl(target, ctx):
227238
exe,
228239
additional_deps,
229240
config,
230-
c_flags if _is_c_translation_unit(src, ctx.rule.attr.tags) else cxx_flags,
231-
compilation_contexts,
241+
c_flags if is_c_translation_unit(src, ctx.rule.attr.tags) else cxx_flags,
232242
src,
233243
target.label.name,
244+
additional_files,
234245
additional_inputs = getattr(ctx.rule.attr, "additional_compiler_inputs", []),
235246
)
236247
for src in srcs

clang_tidy/clang_tidy_test.bzl

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
"""A test rule to run clang-tidy"""
2+
3+
load("@bazel_tools//tools/build_defs/cc:action_names.bzl", "ACTION_NAMES")
4+
load("@bazel_tools//tools/cpp:toolchain_utils.bzl", "find_cpp_toolchain")
5+
load(":clang_tidy.bzl", "deps_flags", "is_c_translation_unit", "rule_sources", "safe_flags", "toolchain_flags")
6+
7+
# Tests run with a different directory structure than normal compiles. This
8+
# fixes up include paths or any other arguments that are sensitive to this
9+
def _fix_argument_path(ctx, arg):
10+
return arg.replace(ctx.bin_dir.path, ".")
11+
12+
def _get_copts_attr(ctx, copts_attr):
13+
copts = []
14+
for copt in getattr(ctx.attr, copts_attr):
15+
copts.append(ctx.expand_make_variables(
16+
copts_attr,
17+
copt,
18+
{},
19+
))
20+
21+
return copts
22+
23+
def _clang_tidy_rule_impl(ctx):
24+
clang_tidy = ctx.attr.clang_tidy_executable
25+
clang_tidy_executable = clang_tidy[DefaultInfo].files_to_run.executable
26+
27+
ccinfo_copts, additional_files = deps_flags(ctx, ctx.attr.deps, escape = True)
28+
29+
include_headers = "no-clang-tidy-headers" not in ctx.attr.tags
30+
srcs = rule_sources(ctx.attr, include_headers)
31+
32+
rule_copts = _get_copts_attr(ctx, "copts")
33+
rule_conlyopts = _get_copts_attr(ctx, "conlyopts")
34+
rule_cxxopts = _get_copts_attr(ctx, "cxxopts")
35+
36+
c_flags = safe_flags(toolchain_flags(ctx, ACTION_NAMES.c_compile) + rule_copts + rule_conlyopts) + ["-xc"]
37+
cxx_flags = safe_flags(toolchain_flags(ctx, ACTION_NAMES.cpp_compile) + rule_copts + rule_cxxopts) + ["-xc++"]
38+
39+
ctx.actions.write(
40+
output = ctx.outputs.executable,
41+
is_executable = True,
42+
content = """\
43+
#!/usr/bin/env bash
44+
45+
set -euo pipefail
46+
47+
readonly bin="{clang_tidy_bin}"
48+
readonly config="{clang_tidy_config}"
49+
50+
test -e .clang-tidy || ln -s -f \\$config .clang-tidy
51+
if [[ ! -f .clang-tidy ]]; then
52+
echo "error: failed to setup config" >&2
53+
exit 1
54+
fi
55+
56+
ln -s .. external
57+
58+
has_srcs=false
59+
if [[ -n "{c_sources}" ]]; then
60+
"$bin" --quiet {c_sources} -- {c_flags}
61+
has_srcs=true
62+
fi
63+
64+
if [[ -n "{cxx_sources}" ]]; then
65+
"$bin" --quiet {cxx_sources} -- {cxx_flags}
66+
has_srcs=true
67+
fi
68+
69+
if [[ "$has_srcs" == "false" ]]; then
70+
echo "error: no sources to run clang-tidy on" >&2
71+
exit 1
72+
fi
73+
""".format(
74+
clang_tidy_bin = clang_tidy_executable.short_path if clang_tidy_executable else "clang-tidy",
75+
clang_tidy_config = ctx.file.clang_tidy_config.short_path,
76+
output = ctx.outputs.executable.path,
77+
c_sources = " ".join([x.short_path for x in srcs if is_c_translation_unit(x, ctx.attr.tags)]),
78+
cxx_sources = " ".join([x.short_path for x in srcs if not is_c_translation_unit(x, ctx.attr.tags)]),
79+
c_flags = " ".join([_fix_argument_path(ctx, x) for x in ccinfo_copts + c_flags]),
80+
cxx_flags = " ".join([_fix_argument_path(ctx, x) for x in ccinfo_copts + cxx_flags]),
81+
),
82+
)
83+
84+
return [
85+
DefaultInfo(
86+
executable = ctx.outputs.executable,
87+
runfiles = ctx.runfiles(
88+
ctx.files.srcs + ctx.files.hdrs + ctx.files.data,
89+
transitive_files = depset(
90+
[ctx.file.clang_tidy_config],
91+
transitive = [additional_files, find_cpp_toolchain(ctx).all_files, ctx.attr.clang_tidy_additional_deps.files],
92+
),
93+
)
94+
.merge(clang_tidy[DefaultInfo].default_runfiles),
95+
),
96+
]
97+
98+
clang_tidy_test = rule(
99+
implementation = _clang_tidy_rule_impl,
100+
test = True,
101+
fragments = ["cpp"],
102+
attrs = {
103+
"deps": attr.label_list(providers = [CcInfo]),
104+
"clang_tidy_executable": attr.label(default = Label("//:clang_tidy_executable")),
105+
"clang_tidy_additional_deps": attr.label(default = Label("//:clang_tidy_additional_deps")),
106+
"clang_tidy_config": attr.label(
107+
default = Label("//:clang_tidy_config"),
108+
allow_single_file = True,
109+
),
110+
"srcs": attr.label_list(allow_files = True),
111+
"hdrs": attr.label_list(allow_files = True),
112+
"data": attr.label_list(allow_files = True),
113+
"copts": attr.string_list(),
114+
"conlyopts": attr.string_list(),
115+
"cxxopts": attr.string_list(),
116+
},
117+
toolchains = ["@bazel_tools//tools/cpp:toolchain_type"],
118+
)

example/cc_test_example/BUILD

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
load("//clang_tidy:clang_tidy_test.bzl", "clang_tidy_test")
2+
3+
clang_tidy_test(
4+
name = "check_files_test",
5+
srcs = [
6+
"lib.cpp",
7+
],
8+
)

example/cc_test_example/lib.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
int func() {
2+
int result = 5;
3+
return result;
4+
}

0 commit comments

Comments
 (0)