Skip to content

Commit 84ef26a

Browse files
authored
feat: Extract rust_clippy_action to allow other rulesets to run clippy (#3660)
Extract the action to run clippy into an importable struct, so that other rulesets can use it. This is an alternative fix for #3607. Since `rules_lint` does allow transitive semantics for linting, we can use the clippy action there, and users who want clippy to be transitive can use `rules_lint` to run it. The tests pass, and I've also tested it manually against a branch of `rules_lint`. Reviewing instructions: - Start from `rust/defs.bzl`. We expose two new functions: One to get the crate info, and one to create the clippy action itself. - Then, read the docstring for `run_clippy`. I've tried to select a reasonable boundary for the arguments, but I'm definitely open to opinions. For instance, we don't ever read `ctx.attrs` in `run_clippy`, because we don't want to force users to implement the same arguments and flags, but that may be wrong. - Then, go on to the call site of `run_clippy`, to see if it preserves the semantics you'd expect, particularly around capturing output. - Finally, go on to the implementation of `run_clippy`
1 parent a8a520f commit 84ef26a

File tree

2 files changed

+111
-54
lines changed

2 files changed

+111
-54
lines changed

rust/defs.bzl

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ load(
2323
_capture_clippy_output = "capture_clippy_output",
2424
_clippy_flag = "clippy_flag",
2525
_clippy_flags = "clippy_flags",
26+
_get_clippy_ready_crate_info = "get_clippy_ready_crate_info",
2627
_rust_clippy = "rust_clippy",
28+
_rust_clippy_action = "rust_clippy_action",
2729
_rust_clippy_aspect = "rust_clippy_aspect",
2830
)
2931
load("//rust/private:common.bzl", _rust_common = "rust_common")
@@ -119,6 +121,12 @@ rust_clippy = _rust_clippy
119121
capture_clippy_output = _capture_clippy_output
120122
# See @rules_rust//rust/private:clippy.bzl for a complete description.
121123

124+
rust_clippy_action = struct(
125+
action = _rust_clippy_action,
126+
get_clippy_ready_crate_info = _get_clippy_ready_crate_info,
127+
)
128+
# See @rules_rust//rust/private:clippy.bzl for a complete description.
129+
122130
rustc_output_diagnostics = _rustc_output_diagnostics
123131
# See @rules_rust//rust/private:rustc.bzl for a complete description.
124132

rust/private/clippy.bzl

Lines changed: 103 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ clippy_flags = rule(
6666
build_setting = config.string_list(flag = True),
6767
)
6868

69-
def _get_clippy_ready_crate_info(target, aspect_ctx = None):
69+
def get_clippy_ready_crate_info(target, aspect_ctx = None):
7070
"""Check that a target is suitable for clippy and extract the `CrateInfo` provider from it.
7171
7272
Args:
@@ -101,17 +101,25 @@ def _get_clippy_ready_crate_info(target, aspect_ctx = None):
101101
else:
102102
return None
103103

104-
def _clippy_aspect_impl(target, ctx):
105-
# Exit early if a target already has a clippy output group. This
106-
# can be useful for rules which always want to inhibit clippy.
107-
if OutputGroupInfo in target:
108-
if hasattr(target[OutputGroupInfo], "clippy_checks"):
109-
return []
104+
def rust_clippy_action(ctx, clippy_executable, process_wrapper, crate_info, config, output = None, success_marker = None, cap_at_warnings = False, extra_clippy_flags = [], error_format = None, clippy_diagnostics_file = None):
105+
"""Run clippy with the specified parameters.
110106
111-
crate_info = _get_clippy_ready_crate_info(target, ctx)
112-
if not crate_info:
113-
return [ClippyInfo(output = depset([]))]
107+
Args:
108+
ctx (ctx): The aspect's context object. This function should not read ctx.attr, but it might read ctx.rule.attr
109+
clippy_executable (File): The clippy executable to run
110+
process_wrapper (File): An executable process wrapper that can run clippy, usually @rules_rust//utils/process_wrapper
111+
crate_info (CrateInfo): The source crate information
112+
config (File): The clippy configuration file. Reference: https://doc.rust-lang.org/clippy/configuration.html#configuring-clippy
113+
output (File): The output file for clippy stdout/stderr. If None, no output will be captured
114+
success_marker (File): A file that will be written if clippy succeeds
115+
cap_at_warnings (bool): If set, it will cap all reports as warnings, allowing the build to continue even with clippy failures
116+
extra_clippy_flags (List[str]): A list of extra options to pass to clippy. If not set, every warnings will be turned into errors
117+
error_format (str): Which error format to use. Must be acceptable by rustc: https://doc.rust-lang.org/beta/rustc/command-line-arguments.html#--error-format-control-how-errors-are-produced
118+
clippy_diagnostics_file (File): File to output diagnostics to. If None, no diagnostics will be written
114119
120+
Returns:
121+
None
122+
"""
115123
toolchain = find_toolchain(ctx)
116124
cc_toolchain, feature_configuration = find_cc_toolchain(ctx)
117125

@@ -147,25 +155,17 @@ def _clippy_aspect_impl(target, ctx):
147155
lint_files,
148156
)
149157

150-
use_clippy_error_format = ctx.attr._incompatible_change_clippy_error_format[IncompatibleFlagInfo].enabled
151-
error_format = get_error_format(
152-
ctx.attr,
153-
"_clippy_error_format" if use_clippy_error_format else "_error_format",
154-
)
155-
156-
clippy_diagnostics = None
157-
if ctx.attr._clippy_output_diagnostics[ClippyOutputDiagnosticsInfo].output_diagnostics:
158-
clippy_diagnostics = ctx.actions.declare_file(ctx.label.name + ".clippy.diagnostics", sibling = crate_info.output)
158+
if clippy_diagnostics_file:
159159
crate_info_dict = structs.to_dict(crate_info)
160-
crate_info_dict["rustc_output"] = clippy_diagnostics
160+
crate_info_dict["rustc_output"] = clippy_diagnostics_file
161161
crate_info = rust_common.create_crate_info(**crate_info_dict)
162162

163163
args, env = construct_arguments(
164164
ctx = ctx,
165165
attr = ctx.rule.attr,
166166
file = ctx.file,
167167
toolchain = toolchain,
168-
tool_path = toolchain.clippy_driver.path,
168+
tool_path = clippy_executable.path,
169169
cc_toolchain = cc_toolchain,
170170
feature_configuration = feature_configuration,
171171
crate_info = crate_info,
@@ -179,7 +179,7 @@ def _clippy_aspect_impl(target, ctx):
179179
build_flags_files = build_flags_files,
180180
emit = ["dep-info", "metadata"],
181181
skip_expanding_rustc_env = True,
182-
use_json_output = bool(clippy_diagnostics),
182+
use_json_output = bool(clippy_diagnostics_file),
183183
error_format = error_format,
184184
)
185185

@@ -188,60 +188,109 @@ def _clippy_aspect_impl(target, ctx):
188188

189189
# Then append the clippy flags specified from the command line, so they override what is
190190
# specified on the library.
191-
clippy_flags = clippy_flags + ctx.attr._clippy_flags[ClippyFlagsInfo].clippy_flags
191+
clippy_flags += extra_clippy_flags
192192

193-
if hasattr(ctx.attr, "_clippy_flag"):
194-
clippy_flags = clippy_flags + ctx.attr._clippy_flag[ClippyFlagsInfo].clippy_flags
193+
outputs = []
195194

196-
# For remote execution purposes, the clippy_out file must be a sibling of crate_info.output
197-
# or rustc may fail to create intermediate output files because the directory does not exist.
198-
if ctx.attr._capture_output[CaptureClippyOutputInfo].capture_output:
199-
clippy_out = ctx.actions.declare_file(ctx.label.name + ".clippy.out", sibling = crate_info.output)
200-
args.process_wrapper_flags.add("--stderr-file", clippy_out)
195+
if output != None:
196+
args.process_wrapper_flags.add("--stderr-file", output)
197+
outputs.append(output)
201198

202-
if clippy_flags or lint_files:
203-
args.rustc_flags.add_all(clippy_flags)
199+
if success_marker != None:
200+
args.process_wrapper_flags.add("--touch-file", success_marker)
201+
outputs.append(success_marker)
204202

205-
# If we are capturing the output, we want the build system to be able to keep going
206-
# and consume the output. Some clippy lints are denials, so we cap everything at warn.
207-
args.rustc_flags.add("--cap-lints=warn")
203+
if clippy_flags or lint_files:
204+
args.rustc_flags.add_all(clippy_flags)
208205
else:
209-
# A marker file indicating clippy has executed successfully.
210-
# This file is necessary because "ctx.actions.run" mandates an output.
211-
clippy_out = ctx.actions.declare_file(ctx.label.name + ".clippy.ok", sibling = crate_info.output)
212-
args.process_wrapper_flags.add("--touch-file", clippy_out)
206+
# The user didn't provide any clippy flags explicitly so we apply conservative defaults.
213207

214-
if clippy_flags or lint_files:
215-
args.rustc_flags.add_all(clippy_flags)
216-
else:
217-
# The user didn't provide any clippy flags explicitly so we apply conservative defaults.
208+
# Turn any warnings from clippy or rustc into an error, as otherwise
209+
# Bazel will consider the execution result of the aspect to be "success",
210+
# and Clippy won't be re-triggered unless the source file is modified.
211+
args.rustc_flags.add("-Dwarnings")
218212

219-
# Turn any warnings from clippy or rustc into an error, as otherwise
220-
# Bazel will consider the execution result of the aspect to be "success",
221-
# and Clippy won't be re-triggered unless the source file is modified.
222-
args.rustc_flags.add("-Dwarnings")
213+
if cap_at_warnings:
214+
# If we are capturing the output, we want the build system to be able to keep going
215+
# and consume the output. Some clippy lints are denials, so we cap everything at warn.
216+
args.rustc_flags.add("--cap-lints=warn")
223217

224218
# Upstream clippy requires one of these two filenames or it silently uses
225219
# the default config. Enforce the naming so users are not confused.
226220
valid_config_file_names = [".clippy.toml", "clippy.toml"]
227-
if ctx.file._config.basename not in valid_config_file_names:
221+
if config.basename not in valid_config_file_names:
228222
fail("The clippy config file must be named one of: {}".format(valid_config_file_names))
229-
env["CLIPPY_CONF_DIR"] = "${{pwd}}/{}".format(ctx.file._config.dirname)
230-
compile_inputs = depset([ctx.file._config], transitive = [compile_inputs])
223+
env["CLIPPY_CONF_DIR"] = "${{pwd}}/{}".format(config.dirname)
224+
compile_inputs = depset([config], transitive = [compile_inputs])
231225

232226
ctx.actions.run(
233-
executable = ctx.executable._process_wrapper,
227+
executable = process_wrapper,
234228
inputs = compile_inputs,
235-
outputs = [clippy_out] + [x for x in [clippy_diagnostics] if x],
229+
outputs = outputs + [x for x in [clippy_diagnostics_file] if x],
236230
env = env,
237-
tools = [toolchain.clippy_driver],
231+
tools = [clippy_executable],
238232
arguments = args.all,
239233
mnemonic = "Clippy",
240234
progress_message = "Clippy %{label}",
241235
toolchain = "@rules_rust//rust:toolchain_type",
242236
)
243237

244-
output_group_info = {"clippy_checks": depset([clippy_out])}
238+
def _clippy_aspect_impl(target, ctx):
239+
# Exit early if a target already has a clippy output group. This
240+
# can be useful for rules which always want to inhibit clippy.
241+
if OutputGroupInfo in target:
242+
if hasattr(target[OutputGroupInfo], "clippy_checks"):
243+
return []
244+
245+
crate_info = get_clippy_ready_crate_info(target, ctx)
246+
if not crate_info:
247+
return [ClippyInfo(output = depset([]))]
248+
249+
toolchain = find_toolchain(ctx)
250+
251+
# For remote execution purposes, the clippy_out file must be a sibling of crate_info.output
252+
# or rustc may fail to create intermediate output files because the directory does not exist.
253+
if ctx.attr._capture_output[CaptureClippyOutputInfo].capture_output:
254+
clippy_out = ctx.actions.declare_file(ctx.label.name + ".clippy.out", sibling = crate_info.output)
255+
clippy_success_marker = None
256+
else:
257+
# A marker file indicating clippy has executed successfully.
258+
# This file is necessary because "ctx.actions.run" mandates an output.
259+
clippy_success_marker = ctx.actions.declare_file(ctx.label.name + ".clippy.ok", sibling = crate_info.output)
260+
clippy_out = None
261+
262+
use_clippy_error_format = ctx.attr._incompatible_change_clippy_error_format[IncompatibleFlagInfo].enabled
263+
error_format = get_error_format(
264+
ctx.attr,
265+
"_clippy_error_format" if use_clippy_error_format else "_error_format",
266+
)
267+
268+
clippy_flags = ctx.attr._clippy_flags[ClippyFlagsInfo].clippy_flags
269+
270+
if hasattr(ctx.attr, "_clippy_flag"):
271+
clippy_flags = clippy_flags + ctx.attr._clippy_flag[ClippyFlagsInfo].clippy_flags
272+
273+
clippy_diagnostics = None
274+
if ctx.attr._clippy_output_diagnostics[ClippyOutputDiagnosticsInfo].output_diagnostics:
275+
clippy_diagnostics = ctx.actions.declare_file(ctx.label.name + ".clippy.diagnostics", sibling = crate_info.output)
276+
277+
# Run clippy using the extracted function
278+
rust_clippy_action(
279+
ctx = ctx,
280+
clippy_executable = toolchain.clippy_driver,
281+
process_wrapper = ctx.executable._process_wrapper,
282+
crate_info = crate_info,
283+
config = ctx.file._config,
284+
output = clippy_out,
285+
cap_at_warnings = clippy_out != None, # If we're capturing output, we want the build to continue.
286+
success_marker = clippy_success_marker,
287+
extra_clippy_flags = clippy_flags,
288+
error_format = error_format,
289+
clippy_diagnostics_file = clippy_diagnostics,
290+
)
291+
292+
clippy_checks = [file for file in [clippy_out, clippy_success_marker] if file != None]
293+
output_group_info = {"clippy_checks": depset(clippy_checks)}
245294
if clippy_diagnostics:
246295
output_group_info["clippy_output"] = depset([clippy_diagnostics])
247296

0 commit comments

Comments
 (0)