Skip to content

Commit a645483

Browse files
committed
Add per-target clippy_config attribute to Rust rules
1 parent 02be80c commit a645483

8 files changed

Lines changed: 172 additions & 4 deletions

File tree

docs/rust_clippy.vm

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,20 @@ the upstream implementation of clippy, this file must be named either `.clippy.t
3030
```text
3131
build --@rules_rust//rust/settings:clippy.toml=//:clippy.toml
3232
```
33+
34+
Individual targets may also point at their own configuration file via the
35+
`clippy_config` attribute, which overrides the build setting above for that
36+
target only. This is useful in large workspaces where different subtrees need
37+
different `disallowed-methods`, `disallowed-types`, or other clippy options.
38+
39+
```python
40+
rust_library(
41+
name = "hello_lib",
42+
srcs = ["src/lib.rs"],
43+
clippy_config = "//path/to:clippy.toml",
44+
)
45+
```
46+
47+
Note that `rust_test` targets which use the `crate` attribute to test a library
48+
do not inherit that library's `clippy_config`. Set `clippy_config` explicitly on
49+
the `rust_test` target if it should use the same configuration.

rust/private/clippy.bzl

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,13 +274,17 @@ def _clippy_aspect_impl(target, ctx):
274274
if ctx.attr._clippy_output_diagnostics[ClippyOutputDiagnosticsInfo].output_diagnostics:
275275
clippy_diagnostics = ctx.actions.declare_file(ctx.label.name + ".clippy.diagnostics", sibling = crate_info.output)
276276

277+
# Prefer a per-target clippy.toml when the underlying rule sets one,
278+
# otherwise fall back to the global //rust/settings:clippy.toml flag.
279+
config = getattr(ctx.rule.file, "clippy_config", None) or ctx.file._config
280+
277281
# Run clippy using the extracted function
278282
rust_clippy_action(
279283
ctx = ctx,
280284
clippy_executable = toolchain.clippy_driver,
281285
process_wrapper = ctx.executable._process_wrapper,
282286
crate_info = crate_info,
283-
config = ctx.file._config,
287+
config = config,
284288
output = clippy_out,
285289
cap_at_warnings = clippy_out != None, # If we're capturing output, we want the build to continue.
286290
success_marker = clippy_success_marker,

rust/private/rust.bzl

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -664,6 +664,21 @@ _COMMON_ATTRS = {
664664
"""),
665665
default = False,
666666
),
667+
"clippy_config": attr.label(
668+
doc = dedent("""\
669+
A `clippy.toml` configuration file used when running clippy on this target.
670+
671+
When set, `rust_clippy_aspect` points `CLIPPY_CONF_DIR` at the directory
672+
containing this file instead of the global file resolved through the
673+
`@rules_rust//rust/settings:clippy.toml` build setting. The file must be
674+
named `clippy.toml` or `.clippy.toml`.
675+
676+
Note that `rust_test` targets using the `crate` attribute do not inherit
677+
this setting from the crate under test. Set `clippy_config` on the test
678+
target explicitly if it should use the same configuration.
679+
"""),
680+
allow_single_file = True,
681+
),
667682
"compile_data": attr.label_list(
668683
doc = dedent("""\
669684
List of files used by this rule at compile time.

test/clippy/BUILD.bazel

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
load("@bazel_skylib//rules:write_file.bzl", "write_file")
12
load(
23
"@rules_rust//rust:defs.bzl",
34
"rust_binary",
@@ -50,6 +51,24 @@ rust_proc_macro(
5051
edition = "2018",
5152
)
5253

54+
rust_library(
55+
name = "ok_library_with_clippy_config",
56+
srcs = ["src/lib.rs"],
57+
clippy_config = "//test/clippy/target_config:clippy.toml",
58+
edition = "2018",
59+
)
60+
61+
rust_test(
62+
name = "ok_crate_test_without_clippy_config",
63+
crate = ":ok_library_with_clippy_config",
64+
)
65+
66+
rust_test(
67+
name = "ok_crate_test_with_clippy_config",
68+
clippy_config = "//test/clippy/target_config:clippy.toml",
69+
crate = ":ok_library_with_clippy_config",
70+
)
71+
5372
# Clippy analysis of passing targets.
5473

5574
rust_clippy(
@@ -83,6 +102,11 @@ rust_clippy(
83102
deps = [":ok_proc_macro"],
84103
)
85104

105+
rust_clippy(
106+
name = "ok_library_with_clippy_config_clippy",
107+
deps = [":ok_library_with_clippy_config"],
108+
)
109+
86110
# Declaration of failing targets.
87111

88112
rust_binary(
@@ -127,6 +151,28 @@ rust_proc_macro(
127151
tags = ["noclippy"],
128152
)
129153

154+
rust_library(
155+
name = "bad_library_with_clippy_config",
156+
srcs = ["src/lib.rs"],
157+
clippy_config = "//test/clippy/too_many_args:clippy.toml",
158+
edition = "2018",
159+
tags = ["noclippy"],
160+
)
161+
162+
write_file(
163+
name = "generated_clippy_toml",
164+
out = "gen/clippy.toml",
165+
content = ["too-many-arguments-threshold = 1"],
166+
)
167+
168+
rust_library(
169+
name = "bad_library_with_generated_clippy_config",
170+
srcs = ["src/lib.rs"],
171+
clippy_config = ":generated_clippy_toml",
172+
edition = "2018",
173+
tags = ["noclippy"],
174+
)
175+
130176
# Clippy analysis of failing targets.
131177

132178
rust_clippy(
@@ -166,6 +212,18 @@ rust_clippy(
166212
deps = [":bad_proc_macro"],
167213
)
168214

215+
rust_clippy(
216+
name = "bad_library_with_clippy_config_clippy",
217+
tags = ["manual"],
218+
deps = [":bad_library_with_clippy_config"],
219+
)
220+
221+
rust_clippy(
222+
name = "bad_library_with_generated_clippy_config_clippy",
223+
tags = ["manual"],
224+
deps = [":bad_library_with_generated_clippy_config"],
225+
)
226+
169227
sh_binary(
170228
name = "clippy_failure_tester",
171229
srcs = ["clippy_failure_tester.sh"],

test/clippy/clippy_failure_tester.sh

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ function test_all() {
5757
local -r BUILD_OK=0
5858
local -r BUILD_FAILED=1
5959
local -r CAPTURE_OUTPUT="--@rules_rust//rust/settings:capture_clippy_output=True --@rules_rust//rust/settings:error_format=json"
60-
local -r BAD_CLIPPY_TOML="--@rules_rust//rust/settings:clippy.toml=//too_many_args:clippy.toml"
60+
local -r BAD_CLIPPY_TOML="--@rules_rust//rust/settings:clippy.toml=//test/clippy/too_many_args:clippy.toml"
6161

6262
mkdir -p "${NEW_WORKSPACE}/test/clippy" && \
6363
cp -r test/clippy/* "${NEW_WORKSPACE}/test/clippy/" && \
@@ -103,6 +103,7 @@ EOF
103103
check_build_result $BUILD_OK ok_static_library_clippy
104104
check_build_result $BUILD_OK ok_test_clippy
105105
check_build_result $BUILD_OK ok_proc_macro_clippy
106+
check_build_result $BUILD_OK ok_library_with_clippy_config_clippy
106107
check_build_result $BUILD_FAILED bad_binary_clippy
107108
check_build_result $BUILD_FAILED bad_library_clippy
108109
check_build_result $BUILD_FAILED bad_shared_library_clippy
@@ -123,6 +124,20 @@ EOF
123124
# Test that we can make the ok_library_clippy fail when using an extra config file.
124125
# Proves that the config file is used and overrides default settings.
125126
check_build_result $BUILD_FAILED ok_library_clippy $BAD_CLIPPY_TOML
127+
128+
# Test that a per-target clippy_config attribute is honored. The library
129+
# passes with the default config but its clippy_config attribute points at
130+
# too_many_args/clippy.toml, which lowers the threshold so clippy fails.
131+
check_build_result $BUILD_FAILED bad_library_with_clippy_config_clippy
132+
133+
# Test that a per-target clippy_config attribute takes precedence over the
134+
# global label_flag. The global flag would fail this library, but the
135+
# per-target config raises the threshold so clippy passes.
136+
check_build_result $BUILD_OK ok_library_with_clippy_config_clippy $BAD_CLIPPY_TOML
137+
138+
# Test that a generated clippy_config (output under bazel-out/) is wired
139+
# through correctly via CLIPPY_CONF_DIR.
140+
check_build_result $BUILD_FAILED bad_library_with_generated_clippy_config_clippy
126141
}
127142

128143
test_all
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
exports_files(["clippy.toml"])
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# A benign per-target clippy configuration. The threshold is raised
2+
# above the default so that src/lib.rs continues to pass clippy when
3+
# this file is wired in via the `clippy_config` attribute.
4+
too-many-arguments-threshold = 10

test/unit/clippy/clippy_test.bzl

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
"""Unittest to verify properties of clippy rules"""
22

3-
load("@bazel_skylib//lib:unittest.bzl", "analysistest")
3+
load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts")
44
load("//rust:defs.bzl", "rust_clippy_aspect")
5-
load("//test/unit:common.bzl", "assert_argv_contains", "assert_argv_contains_prefix_suffix", "assert_list_contains_adjacent_elements")
5+
load("//test/unit:common.bzl", "assert_argv_contains", "assert_argv_contains_prefix_suffix", "assert_env_value", "assert_list_contains_adjacent_elements")
66

77
def _find_clippy_action(actions):
88
for action in actions:
@@ -83,6 +83,31 @@ def _clippy_aspect_with_explicit_flags_test_impl(ctx):
8383
_CLIPPY_EXPLICIT_FLAGS + _CLIPPY_INDIVIDUALLY_ADDED_EXPLICIT_FLAGS,
8484
)
8585

86+
def _clippy_aspect_conf_dir_test_impl(ctx, expected_dir):
87+
env = analysistest.begin(ctx)
88+
target = analysistest.target_under_test(env)
89+
90+
clippy_action = _find_clippy_action(target.actions)
91+
assert_env_value(
92+
env,
93+
clippy_action,
94+
"CLIPPY_CONF_DIR",
95+
"${{pwd}}/{}".format(expected_dir),
96+
)
97+
98+
config_inputs = [
99+
f
100+
for f in clippy_action.inputs.to_list()
101+
if f.dirname == expected_dir and f.basename in ("clippy.toml", ".clippy.toml")
102+
]
103+
asserts.true(
104+
env,
105+
len(config_inputs) == 1,
106+
"expected exactly one clippy config from {} in action inputs, got {}".format(expected_dir, config_inputs),
107+
)
108+
109+
return analysistest.end(env)
110+
86111
def make_clippy_aspect_unittest(impl, **kwargs):
87112
return analysistest.make(
88113
impl,
@@ -136,6 +161,14 @@ clippy_aspect_with_output_diagnostics_test = make_clippy_aspect_unittest(
136161
},
137162
)
138163

164+
clippy_aspect_uses_default_conf_dir_test = make_clippy_aspect_unittest(
165+
lambda ctx: _clippy_aspect_conf_dir_test_impl(ctx, "rust/settings"),
166+
)
167+
168+
clippy_aspect_uses_target_conf_dir_test = make_clippy_aspect_unittest(
169+
lambda ctx: _clippy_aspect_conf_dir_test_impl(ctx, "test/clippy/target_config"),
170+
)
171+
139172
def clippy_test_suite(name):
140173
"""Entry-point macro called from the BUILD file.
141174
@@ -183,6 +216,23 @@ def clippy_test_suite(name):
183216
target_under_test = Label("//test/clippy:ok_library"),
184217
)
185218

219+
clippy_aspect_uses_default_conf_dir_test(
220+
name = "clippy_aspect_uses_default_conf_dir_test",
221+
target_under_test = Label("//test/clippy:ok_library"),
222+
)
223+
clippy_aspect_uses_target_conf_dir_test(
224+
name = "clippy_aspect_uses_target_conf_dir_test",
225+
target_under_test = Label("//test/clippy:ok_library_with_clippy_config"),
226+
)
227+
clippy_aspect_uses_default_conf_dir_test(
228+
name = "clippy_aspect_crate_test_ignores_library_conf_dir_test",
229+
target_under_test = Label("//test/clippy:ok_crate_test_without_clippy_config"),
230+
)
231+
clippy_aspect_uses_target_conf_dir_test(
232+
name = "clippy_aspect_crate_test_uses_own_conf_dir_test",
233+
target_under_test = Label("//test/clippy:ok_crate_test_with_clippy_config"),
234+
)
235+
186236
native.test_suite(
187237
name = name,
188238
tests = [
@@ -195,5 +245,9 @@ def clippy_test_suite(name):
195245
":clippy_aspect_without_clippy_error_format_test",
196246
":clippy_aspect_with_clippy_error_format_test",
197247
":clippy_aspect_with_output_diagnostics_test",
248+
":clippy_aspect_uses_default_conf_dir_test",
249+
":clippy_aspect_uses_target_conf_dir_test",
250+
":clippy_aspect_crate_test_ignores_library_conf_dir_test",
251+
":clippy_aspect_crate_test_uses_own_conf_dir_test",
198252
],
199253
)

0 commit comments

Comments
 (0)