Skip to content

Commit 82da633

Browse files
committed
Add per-target clippy_config attribute to Rust rules
1 parent 175bf94 commit 82da633

8 files changed

Lines changed: 183 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: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,13 @@ def rust_clippy_action(ctx, clippy_executable, process_wrapper, crate_info, conf
223223
env["CLIPPY_CONF_DIR"] = "${{pwd}}/{}".format(config.dirname)
224224
compile_inputs = depset([config], transitive = [compile_inputs])
225225

226+
# `CLIPPY_CONF_DIR` is an env var, so it is not rewritten by Bazel's
227+
# path mapping. When the config is a generated file, advertising
228+
# `supports-path-mapping` would stage it under the mapped `bazel-out/`
229+
# prefix while the env var keeps the unmapped path, and clippy would
230+
# fail to find the config. Source files have no config segment to map.
231+
supports_path_mapping = args.supports_path_mapping and config.is_source
232+
226233
ctx.actions.run(
227234
executable = process_wrapper,
228235
inputs = compile_inputs,
@@ -233,7 +240,7 @@ def rust_clippy_action(ctx, clippy_executable, process_wrapper, crate_info, conf
233240
mnemonic = "Clippy",
234241
progress_message = "Clippy %{label}",
235242
toolchain = "@rules_rust//rust:toolchain_type",
236-
execution_requirements = {"supports-path-mapping": ""} if args.supports_path_mapping else None,
243+
execution_requirements = {"supports-path-mapping": ""} if supports_path_mapping else None,
237244
)
238245

239246
def _clippy_aspect_impl(target, ctx):
@@ -275,13 +282,17 @@ def _clippy_aspect_impl(target, ctx):
275282
if ctx.attr._clippy_output_diagnostics[ClippyOutputDiagnosticsInfo].output_diagnostics:
276283
clippy_diagnostics = ctx.actions.declare_file(ctx.label.name + ".clippy.diagnostics", sibling = crate_info.output)
277284

285+
# Prefer a per-target clippy.toml when the underlying rule sets one,
286+
# otherwise fall back to the global //rust/settings:clippy.toml flag.
287+
config = getattr(ctx.rule.file, "clippy_config", None) or ctx.file._config
288+
278289
# Run clippy using the extracted function
279290
rust_clippy_action(
280291
ctx = ctx,
281292
clippy_executable = toolchain.clippy_driver,
282293
process_wrapper = ctx.executable._process_wrapper,
283294
crate_info = crate_info,
284-
config = ctx.file._config,
295+
config = config,
285296
output = clippy_out,
286297
cap_at_warnings = clippy_out != None, # If we're capturing output, we want the build to continue.
287298
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 high
2+
# enough that src/lib.rs continues to pass clippy when this file is
3+
# wired in via the `clippy_config` attribute.
4+
too-many-arguments-threshold = 10

test/unit/clippy/clippy_test.bzl

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
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")
5+
load("//test/unit:common.bzl", "assert_argv_contains", "assert_argv_contains_prefix_suffix", "assert_env_value")
66

77
def _find_clippy_action(actions):
88
for action in actions:
@@ -93,6 +93,35 @@ def _clippy_aspect_with_explicit_flags_test_impl(ctx):
9393
_CLIPPY_EXPLICIT_FLAGS + _CLIPPY_INDIVIDUALLY_ADDED_EXPLICIT_FLAGS,
9494
)
9595

96+
def _clippy_aspect_conf_dir_test_impl(ctx, expected_dir):
97+
env = analysistest.begin(ctx)
98+
target = analysistest.target_under_test(env)
99+
100+
clippy_action = _find_clippy_action(target.actions)
101+
assert_env_value(
102+
env,
103+
clippy_action,
104+
"CLIPPY_CONF_DIR",
105+
"${{pwd}}/{}".format(expected_dir),
106+
)
107+
108+
# Exactly one clippy.toml should appear in the action inputs, and it
109+
# should come from `expected_dir`. Any other clippy.toml (e.g., the
110+
# unchosen global default) is a bug: the aspect would still rerun on
111+
# changes to a config it never reads.
112+
config_inputs = [
113+
f
114+
for f in clippy_action.inputs.to_list()
115+
if f.basename in ("clippy.toml", ".clippy.toml")
116+
]
117+
asserts.true(
118+
env,
119+
len(config_inputs) == 1 and config_inputs[0].dirname == expected_dir,
120+
"expected exactly one clippy config from {} in action inputs, got {}".format(expected_dir, config_inputs),
121+
)
122+
123+
return analysistest.end(env)
124+
96125
def make_clippy_aspect_unittest(impl, **kwargs):
97126
return analysistest.make(
98127
impl,
@@ -146,6 +175,14 @@ clippy_aspect_with_output_diagnostics_test = make_clippy_aspect_unittest(
146175
},
147176
)
148177

178+
clippy_aspect_uses_default_conf_dir_test = make_clippy_aspect_unittest(
179+
lambda ctx: _clippy_aspect_conf_dir_test_impl(ctx, "rust/settings"),
180+
)
181+
182+
clippy_aspect_uses_target_conf_dir_test = make_clippy_aspect_unittest(
183+
lambda ctx: _clippy_aspect_conf_dir_test_impl(ctx, "test/clippy/target_config"),
184+
)
185+
149186
def clippy_test_suite(name):
150187
"""Entry-point macro called from the BUILD file.
151188
@@ -193,6 +230,23 @@ def clippy_test_suite(name):
193230
target_under_test = Label("//test/clippy:ok_library"),
194231
)
195232

233+
clippy_aspect_uses_default_conf_dir_test(
234+
name = "clippy_aspect_uses_default_conf_dir_test",
235+
target_under_test = Label("//test/clippy:ok_library"),
236+
)
237+
clippy_aspect_uses_target_conf_dir_test(
238+
name = "clippy_aspect_uses_target_conf_dir_test",
239+
target_under_test = Label("//test/clippy:ok_library_with_clippy_config"),
240+
)
241+
clippy_aspect_uses_default_conf_dir_test(
242+
name = "clippy_aspect_crate_test_ignores_library_conf_dir_test",
243+
target_under_test = Label("//test/clippy:ok_crate_test_without_clippy_config"),
244+
)
245+
clippy_aspect_uses_target_conf_dir_test(
246+
name = "clippy_aspect_crate_test_uses_own_conf_dir_test",
247+
target_under_test = Label("//test/clippy:ok_crate_test_with_clippy_config"),
248+
)
249+
196250
native.test_suite(
197251
name = name,
198252
tests = [
@@ -205,5 +259,9 @@ def clippy_test_suite(name):
205259
":clippy_aspect_without_clippy_error_format_test",
206260
":clippy_aspect_with_clippy_error_format_test",
207261
":clippy_aspect_with_output_diagnostics_test",
262+
":clippy_aspect_uses_default_conf_dir_test",
263+
":clippy_aspect_uses_target_conf_dir_test",
264+
":clippy_aspect_crate_test_ignores_library_conf_dir_test",
265+
":clippy_aspect_crate_test_uses_own_conf_dir_test",
208266
],
209267
)

0 commit comments

Comments
 (0)