blob: 21f39656649f51233f7d51dbf21d1feb5350fca5 [file] [log] [blame]
Brian Silvermancc09f182022-03-09 15:40:20 -08001# Copyright 2020 The Bazel Authors. All rights reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""A module defining clippy rules"""
16
17load("//rust/private:common.bzl", "rust_common")
18load("//rust/private:providers.bzl", "CaptureClippyOutputInfo", "ClippyInfo")
19load(
20 "//rust/private:rustc.bzl",
21 "collect_deps",
22 "collect_inputs",
23 "construct_arguments",
24)
25load(
26 "//rust/private:utils.bzl",
27 "determine_output_hash",
28 "find_cc_toolchain",
29 "find_toolchain",
30)
31
Brian Silverman5f6f2762022-08-13 19:30:05 -070032ClippyFlagsInfo = provider(
33 doc = "Pass each value as an additional flag to clippy invocations",
34 fields = {"clippy_flags": "List[string] Flags to pass to clippy"},
35)
36
37def _clippy_flags_impl(ctx):
38 return ClippyFlagsInfo(clippy_flags = ctx.build_setting_value)
39
40clippy_flags = rule(
41 doc = (
42 "Add custom clippy flags from the command line with `--@rules_rust//:clippy_flags`."
43 ),
44 implementation = _clippy_flags_impl,
45 build_setting = config.string_list(flag = True),
46)
47
Brian Silvermancc09f182022-03-09 15:40:20 -080048def _get_clippy_ready_crate_info(target, aspect_ctx):
49 """Check that a target is suitable for clippy and extract the `CrateInfo` provider from it.
50
51 Args:
52 target (Target): The target the aspect is running on.
53 aspect_ctx (ctx, optional): The aspect's context object.
54
55 Returns:
56 CrateInfo, optional: A `CrateInfo` provider if clippy should be run or `None`.
57 """
58
59 # Ignore external targets
60 if target.label.workspace_root.startswith("external"):
61 return None
62
Brian Silverman5f6f2762022-08-13 19:30:05 -070063 # Targets with specific tags will not be formatted
64 if aspect_ctx:
65 ignore_tags = [
66 "noclippy",
67 "no-clippy",
68 ]
69
70 for tag in ignore_tags:
71 if tag in aspect_ctx.rule.attr.tags:
72 return None
Brian Silvermancc09f182022-03-09 15:40:20 -080073
74 # Obviously ignore any targets that don't contain `CrateInfo`
75 if rust_common.crate_info not in target:
76 return None
77
78 return target[rust_common.crate_info]
79
80def _clippy_aspect_impl(target, ctx):
81 crate_info = _get_clippy_ready_crate_info(target, ctx)
82 if not crate_info:
83 return [ClippyInfo(output = depset([]))]
84
85 toolchain = find_toolchain(ctx)
86 cc_toolchain, feature_configuration = find_cc_toolchain(ctx)
87
88 dep_info, build_info, linkstamps = collect_deps(
89 deps = crate_info.deps,
90 proc_macro_deps = crate_info.proc_macro_deps,
91 aliases = crate_info.aliases,
92 # Clippy doesn't need to invoke transitive linking, therefore doesn't need linkstamps.
93 are_linkstamps_supported = False,
94 )
95
96 compile_inputs, out_dir, build_env_files, build_flags_files, linkstamp_outs, ambiguous_libs = collect_inputs(
97 ctx,
98 ctx.rule.file,
99 ctx.rule.files,
100 linkstamps,
101 toolchain,
102 cc_toolchain,
103 feature_configuration,
104 crate_info,
105 dep_info,
106 build_info,
107 )
108
109 args, env = construct_arguments(
110 ctx = ctx,
111 attr = ctx.rule.attr,
112 file = ctx.file,
113 toolchain = toolchain,
114 tool_path = toolchain.clippy_driver.path,
115 cc_toolchain = cc_toolchain,
116 feature_configuration = feature_configuration,
117 crate_info = crate_info,
118 dep_info = dep_info,
119 linkstamp_outs = linkstamp_outs,
120 ambiguous_libs = ambiguous_libs,
121 output_hash = determine_output_hash(crate_info.root, ctx.label),
122 rust_flags = [],
123 out_dir = out_dir,
124 build_env_files = build_env_files,
125 build_flags_files = build_flags_files,
126 emit = ["dep-info", "metadata"],
127 )
128
129 if crate_info.is_test:
130 args.rustc_flags.add("--test")
131
Brian Silverman5f6f2762022-08-13 19:30:05 -0700132 clippy_flags = ctx.attr._clippy_flags[ClippyFlagsInfo].clippy_flags
133
Brian Silvermancc09f182022-03-09 15:40:20 -0800134 # For remote execution purposes, the clippy_out file must be a sibling of crate_info.output
135 # or rustc may fail to create intermediate output files because the directory does not exist.
136 if ctx.attr._capture_output[CaptureClippyOutputInfo].capture_output:
137 clippy_out = ctx.actions.declare_file(ctx.label.name + ".clippy.out", sibling = crate_info.output)
138 args.process_wrapper_flags.add("--stderr-file", clippy_out.path)
139
Brian Silverman5f6f2762022-08-13 19:30:05 -0700140 if clippy_flags:
141 fail("""Combining @rules_rust//:clippy_flags with @rules_rust//:capture_clippy_output=true is currently not supported.
142See https://github.com/bazelbuild/rules_rust/pull/1264#discussion_r853241339 for more detail.""")
143
Brian Silvermancc09f182022-03-09 15:40:20 -0800144 # If we are capturing the output, we want the build system to be able to keep going
145 # and consume the output. Some clippy lints are denials, so we treat them as warnings.
146 args.rustc_flags.add("-Wclippy::all")
147 else:
148 # A marker file indicating clippy has executed successfully.
149 # This file is necessary because "ctx.actions.run" mandates an output.
150 clippy_out = ctx.actions.declare_file(ctx.label.name + ".clippy.ok", sibling = crate_info.output)
151 args.process_wrapper_flags.add("--touch-file", clippy_out.path)
152
Brian Silverman5f6f2762022-08-13 19:30:05 -0700153 if clippy_flags:
154 args.rustc_flags.add_all(clippy_flags)
Brian Silvermancc09f182022-03-09 15:40:20 -0800155 else:
Brian Silverman5f6f2762022-08-13 19:30:05 -0700156 # The user didn't provide any clippy flags explicitly so we apply conservative defaults.
157
158 # Turn any warnings from clippy or rustc into an error, as otherwise
159 # Bazel will consider the execution result of the aspect to be "success",
160 # and Clippy won't be re-triggered unless the source file is modified.
Brian Silvermancc09f182022-03-09 15:40:20 -0800161 args.rustc_flags.add("-Dwarnings")
162
163 # Upstream clippy requires one of these two filenames or it silently uses
164 # the default config. Enforce the naming so users are not confused.
165 valid_config_file_names = [".clippy.toml", "clippy.toml"]
166 if ctx.file._config.basename not in valid_config_file_names:
167 fail("The clippy config file must be named one of: {}".format(valid_config_file_names))
168 env["CLIPPY_CONF_DIR"] = "${{pwd}}/{}".format(ctx.file._config.dirname)
169 compile_inputs = depset([ctx.file._config], transitive = [compile_inputs])
170
171 ctx.actions.run(
172 executable = ctx.executable._process_wrapper,
173 inputs = compile_inputs,
174 outputs = [clippy_out],
175 env = env,
176 tools = [toolchain.clippy_driver],
177 arguments = args.all,
178 mnemonic = "Clippy",
179 )
180
181 return [
182 OutputGroupInfo(clippy_checks = depset([clippy_out])),
183 ClippyInfo(output = depset([clippy_out])),
184 ]
185
186# Example: Run the clippy checker on all targets in the codebase.
187# bazel build --aspects=@rules_rust//rust:defs.bzl%rust_clippy_aspect \
188# --output_groups=clippy_checks \
189# //...
190rust_clippy_aspect = aspect(
191 fragments = ["cpp"],
192 host_fragments = ["cpp"],
193 attrs = {
194 "_capture_output": attr.label(
195 doc = "Value of the `capture_clippy_output` build setting",
196 default = Label("//:capture_clippy_output"),
197 ),
198 "_cc_toolchain": attr.label(
199 doc = (
200 "Required attribute to access the cc_toolchain. See [Accessing the C++ toolchain]" +
201 "(https://docs.bazel.build/versions/master/integrating-with-rules-cc.html#accessing-the-c-toolchain)"
202 ),
203 default = Label("@bazel_tools//tools/cpp:current_cc_toolchain"),
204 ),
Brian Silverman5f6f2762022-08-13 19:30:05 -0700205 "_clippy_flags": attr.label(
206 doc = "Arguments to pass to clippy",
207 default = Label("//:clippy_flags"),
208 ),
Brian Silvermancc09f182022-03-09 15:40:20 -0800209 "_config": attr.label(
210 doc = "The `clippy.toml` file used for configuration",
211 allow_single_file = True,
212 default = Label("//:clippy.toml"),
213 ),
214 "_error_format": attr.label(
215 doc = "The desired `--error-format` flags for clippy",
216 default = "//:error_format",
217 ),
Brian Silverman5f6f2762022-08-13 19:30:05 -0700218 "_extra_rustc_flag": attr.label(default = "//:extra_rustc_flag"),
Brian Silvermancc09f182022-03-09 15:40:20 -0800219 "_extra_rustc_flags": attr.label(default = "//:extra_rustc_flags"),
220 "_process_wrapper": attr.label(
221 doc = "A process wrapper for running clippy on all platforms",
222 default = Label("//util/process_wrapper"),
223 executable = True,
224 cfg = "exec",
225 ),
226 },
227 provides = [ClippyInfo],
228 toolchains = [
Brian Silverman5f6f2762022-08-13 19:30:05 -0700229 str(Label("//rust:toolchain_type")),
Brian Silvermancc09f182022-03-09 15:40:20 -0800230 "@bazel_tools//tools/cpp:toolchain_type",
231 ],
232 incompatible_use_toolchain_transition = True,
233 implementation = _clippy_aspect_impl,
234 doc = """\
235Executes the clippy checker on specified targets.
236
237This aspect applies to existing rust_library, rust_test, and rust_binary rules.
238
239As an example, if the following is defined in `examples/hello_lib/BUILD.bazel`:
240
241```python
242load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test")
243
244rust_library(
245 name = "hello_lib",
246 srcs = ["src/lib.rs"],
247)
248
249rust_test(
250 name = "greeting_test",
251 srcs = ["tests/greeting.rs"],
252 deps = [":hello_lib"],
253)
254```
255
256Then the targets can be analyzed with clippy using the following command:
257
258```output
259$ bazel build --aspects=@rules_rust//rust:defs.bzl%rust_clippy_aspect \
260 --output_groups=clippy_checks //hello_lib:all
261```
262""",
263)
264
265def _rust_clippy_rule_impl(ctx):
266 clippy_ready_targets = [dep for dep in ctx.attr.deps if "clippy_checks" in dir(dep[OutputGroupInfo])]
267 files = depset([], transitive = [dep[OutputGroupInfo].clippy_checks for dep in clippy_ready_targets])
268 return [DefaultInfo(files = files)]
269
270rust_clippy = rule(
271 implementation = _rust_clippy_rule_impl,
272 attrs = {
273 "deps": attr.label_list(
274 doc = "Rust targets to run clippy on.",
275 providers = [rust_common.crate_info],
276 aspects = [rust_clippy_aspect],
277 ),
278 },
279 doc = """\
280Executes the clippy checker on a specific target.
281
282Similar to `rust_clippy_aspect`, but allows specifying a list of dependencies \
283within the build system.
284
285For example, given the following example targets:
286
287```python
288load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test")
289
290rust_library(
291 name = "hello_lib",
292 srcs = ["src/lib.rs"],
293)
294
295rust_test(
296 name = "greeting_test",
297 srcs = ["tests/greeting.rs"],
298 deps = [":hello_lib"],
299)
300```
301
302Rust clippy can be set as a build target with the following:
303
304```python
305load("@rules_rust//rust:defs.bzl", "rust_clippy")
306
307rust_clippy(
308 name = "hello_library_clippy",
309 testonly = True,
310 deps = [
311 ":hello_lib",
312 ":greeting_test",
313 ],
314)
315```
316""",
317)
318
319def _capture_clippy_output_impl(ctx):
320 """Implementation of the `capture_clippy_output` rule
321
322 Args:
323 ctx (ctx): The rule's context object
324
325 Returns:
326 list: A list containing the CaptureClippyOutputInfo provider
327 """
328 return [CaptureClippyOutputInfo(capture_output = ctx.build_setting_value)]
329
330capture_clippy_output = rule(
331 doc = "Control whether to print clippy output or store it to a file, using the configured error_format.",
332 implementation = _capture_clippy_output_impl,
333 build_setting = config.bool(flag = True),
334)