Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 1 | # 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 | |
| 17 | load("//rust/private:common.bzl", "rust_common") |
| 18 | load("//rust/private:providers.bzl", "CaptureClippyOutputInfo", "ClippyInfo") |
| 19 | load( |
| 20 | "//rust/private:rustc.bzl", |
| 21 | "collect_deps", |
| 22 | "collect_inputs", |
| 23 | "construct_arguments", |
| 24 | ) |
| 25 | load( |
| 26 | "//rust/private:utils.bzl", |
| 27 | "determine_output_hash", |
| 28 | "find_cc_toolchain", |
| 29 | "find_toolchain", |
| 30 | ) |
| 31 | |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 32 | ClippyFlagsInfo = 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 | |
| 37 | def _clippy_flags_impl(ctx): |
| 38 | return ClippyFlagsInfo(clippy_flags = ctx.build_setting_value) |
| 39 | |
| 40 | clippy_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 | |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 48 | def _get_clippy_ready_crate_info(target, aspect_ctx = None): |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 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 Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 63 | # 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 Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 73 | |
| 74 | # Obviously ignore any targets that don't contain `CrateInfo` |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 75 | if rust_common.crate_info in target: |
| 76 | return target[rust_common.crate_info] |
| 77 | elif rust_common.test_crate_info in target: |
| 78 | return target[rust_common.test_crate_info].crate |
| 79 | else: |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 80 | return None |
| 81 | |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 82 | def _clippy_aspect_impl(target, ctx): |
| 83 | crate_info = _get_clippy_ready_crate_info(target, ctx) |
| 84 | if not crate_info: |
| 85 | return [ClippyInfo(output = depset([]))] |
| 86 | |
| 87 | toolchain = find_toolchain(ctx) |
| 88 | cc_toolchain, feature_configuration = find_cc_toolchain(ctx) |
| 89 | |
| 90 | dep_info, build_info, linkstamps = collect_deps( |
| 91 | deps = crate_info.deps, |
| 92 | proc_macro_deps = crate_info.proc_macro_deps, |
| 93 | aliases = crate_info.aliases, |
| 94 | # Clippy doesn't need to invoke transitive linking, therefore doesn't need linkstamps. |
| 95 | are_linkstamps_supported = False, |
| 96 | ) |
| 97 | |
| 98 | compile_inputs, out_dir, build_env_files, build_flags_files, linkstamp_outs, ambiguous_libs = collect_inputs( |
| 99 | ctx, |
| 100 | ctx.rule.file, |
| 101 | ctx.rule.files, |
| 102 | linkstamps, |
| 103 | toolchain, |
| 104 | cc_toolchain, |
| 105 | feature_configuration, |
| 106 | crate_info, |
| 107 | dep_info, |
| 108 | build_info, |
| 109 | ) |
| 110 | |
| 111 | args, env = construct_arguments( |
| 112 | ctx = ctx, |
| 113 | attr = ctx.rule.attr, |
| 114 | file = ctx.file, |
| 115 | toolchain = toolchain, |
| 116 | tool_path = toolchain.clippy_driver.path, |
| 117 | cc_toolchain = cc_toolchain, |
| 118 | feature_configuration = feature_configuration, |
| 119 | crate_info = crate_info, |
| 120 | dep_info = dep_info, |
| 121 | linkstamp_outs = linkstamp_outs, |
| 122 | ambiguous_libs = ambiguous_libs, |
| 123 | output_hash = determine_output_hash(crate_info.root, ctx.label), |
| 124 | rust_flags = [], |
| 125 | out_dir = out_dir, |
| 126 | build_env_files = build_env_files, |
| 127 | build_flags_files = build_flags_files, |
| 128 | emit = ["dep-info", "metadata"], |
| 129 | ) |
| 130 | |
| 131 | if crate_info.is_test: |
| 132 | args.rustc_flags.add("--test") |
| 133 | |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 134 | clippy_flags = ctx.attr._clippy_flags[ClippyFlagsInfo].clippy_flags |
| 135 | |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 136 | # For remote execution purposes, the clippy_out file must be a sibling of crate_info.output |
| 137 | # or rustc may fail to create intermediate output files because the directory does not exist. |
| 138 | if ctx.attr._capture_output[CaptureClippyOutputInfo].capture_output: |
| 139 | clippy_out = ctx.actions.declare_file(ctx.label.name + ".clippy.out", sibling = crate_info.output) |
| 140 | args.process_wrapper_flags.add("--stderr-file", clippy_out.path) |
| 141 | |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 142 | if clippy_flags: |
| 143 | fail("""Combining @rules_rust//:clippy_flags with @rules_rust//:capture_clippy_output=true is currently not supported. |
| 144 | See https://github.com/bazelbuild/rules_rust/pull/1264#discussion_r853241339 for more detail.""") |
| 145 | |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 146 | # If we are capturing the output, we want the build system to be able to keep going |
| 147 | # and consume the output. Some clippy lints are denials, so we treat them as warnings. |
| 148 | args.rustc_flags.add("-Wclippy::all") |
| 149 | else: |
| 150 | # A marker file indicating clippy has executed successfully. |
| 151 | # This file is necessary because "ctx.actions.run" mandates an output. |
| 152 | clippy_out = ctx.actions.declare_file(ctx.label.name + ".clippy.ok", sibling = crate_info.output) |
| 153 | args.process_wrapper_flags.add("--touch-file", clippy_out.path) |
| 154 | |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 155 | if clippy_flags: |
| 156 | args.rustc_flags.add_all(clippy_flags) |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 157 | else: |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 158 | # The user didn't provide any clippy flags explicitly so we apply conservative defaults. |
| 159 | |
| 160 | # Turn any warnings from clippy or rustc into an error, as otherwise |
| 161 | # Bazel will consider the execution result of the aspect to be "success", |
| 162 | # and Clippy won't be re-triggered unless the source file is modified. |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 163 | args.rustc_flags.add("-Dwarnings") |
| 164 | |
| 165 | # Upstream clippy requires one of these two filenames or it silently uses |
| 166 | # the default config. Enforce the naming so users are not confused. |
| 167 | valid_config_file_names = [".clippy.toml", "clippy.toml"] |
| 168 | if ctx.file._config.basename not in valid_config_file_names: |
| 169 | fail("The clippy config file must be named one of: {}".format(valid_config_file_names)) |
| 170 | env["CLIPPY_CONF_DIR"] = "${{pwd}}/{}".format(ctx.file._config.dirname) |
| 171 | compile_inputs = depset([ctx.file._config], transitive = [compile_inputs]) |
| 172 | |
| 173 | ctx.actions.run( |
| 174 | executable = ctx.executable._process_wrapper, |
| 175 | inputs = compile_inputs, |
| 176 | outputs = [clippy_out], |
| 177 | env = env, |
| 178 | tools = [toolchain.clippy_driver], |
| 179 | arguments = args.all, |
| 180 | mnemonic = "Clippy", |
| 181 | ) |
| 182 | |
| 183 | return [ |
| 184 | OutputGroupInfo(clippy_checks = depset([clippy_out])), |
| 185 | ClippyInfo(output = depset([clippy_out])), |
| 186 | ] |
| 187 | |
| 188 | # Example: Run the clippy checker on all targets in the codebase. |
| 189 | # bazel build --aspects=@rules_rust//rust:defs.bzl%rust_clippy_aspect \ |
| 190 | # --output_groups=clippy_checks \ |
| 191 | # //... |
| 192 | rust_clippy_aspect = aspect( |
| 193 | fragments = ["cpp"], |
| 194 | host_fragments = ["cpp"], |
| 195 | attrs = { |
| 196 | "_capture_output": attr.label( |
| 197 | doc = "Value of the `capture_clippy_output` build setting", |
| 198 | default = Label("//:capture_clippy_output"), |
| 199 | ), |
| 200 | "_cc_toolchain": attr.label( |
| 201 | doc = ( |
| 202 | "Required attribute to access the cc_toolchain. See [Accessing the C++ toolchain]" + |
| 203 | "(https://docs.bazel.build/versions/master/integrating-with-rules-cc.html#accessing-the-c-toolchain)" |
| 204 | ), |
| 205 | default = Label("@bazel_tools//tools/cpp:current_cc_toolchain"), |
| 206 | ), |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 207 | "_clippy_flags": attr.label( |
| 208 | doc = "Arguments to pass to clippy", |
| 209 | default = Label("//:clippy_flags"), |
| 210 | ), |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 211 | "_config": attr.label( |
| 212 | doc = "The `clippy.toml` file used for configuration", |
| 213 | allow_single_file = True, |
| 214 | default = Label("//:clippy.toml"), |
| 215 | ), |
| 216 | "_error_format": attr.label( |
| 217 | doc = "The desired `--error-format` flags for clippy", |
| 218 | default = "//:error_format", |
| 219 | ), |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 220 | "_extra_rustc_flag": attr.label( |
| 221 | default = Label("//:extra_rustc_flag"), |
| 222 | ), |
| 223 | "_per_crate_rustc_flag": attr.label( |
| 224 | default = Label("//:experimental_per_crate_rustc_flag"), |
| 225 | ), |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 226 | "_process_wrapper": attr.label( |
| 227 | doc = "A process wrapper for running clippy on all platforms", |
| 228 | default = Label("//util/process_wrapper"), |
| 229 | executable = True, |
| 230 | cfg = "exec", |
| 231 | ), |
| 232 | }, |
| 233 | provides = [ClippyInfo], |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 234 | required_providers = [ |
| 235 | [rust_common.crate_info], |
| 236 | [rust_common.test_crate_info], |
| 237 | ], |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 238 | toolchains = [ |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 239 | str(Label("//rust:toolchain_type")), |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 240 | "@bazel_tools//tools/cpp:toolchain_type", |
| 241 | ], |
| 242 | incompatible_use_toolchain_transition = True, |
| 243 | implementation = _clippy_aspect_impl, |
| 244 | doc = """\ |
| 245 | Executes the clippy checker on specified targets. |
| 246 | |
| 247 | This aspect applies to existing rust_library, rust_test, and rust_binary rules. |
| 248 | |
| 249 | As an example, if the following is defined in `examples/hello_lib/BUILD.bazel`: |
| 250 | |
| 251 | ```python |
| 252 | load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") |
| 253 | |
| 254 | rust_library( |
| 255 | name = "hello_lib", |
| 256 | srcs = ["src/lib.rs"], |
| 257 | ) |
| 258 | |
| 259 | rust_test( |
| 260 | name = "greeting_test", |
| 261 | srcs = ["tests/greeting.rs"], |
| 262 | deps = [":hello_lib"], |
| 263 | ) |
| 264 | ``` |
| 265 | |
| 266 | Then the targets can be analyzed with clippy using the following command: |
| 267 | |
| 268 | ```output |
| 269 | $ bazel build --aspects=@rules_rust//rust:defs.bzl%rust_clippy_aspect \ |
| 270 | --output_groups=clippy_checks //hello_lib:all |
| 271 | ``` |
| 272 | """, |
| 273 | ) |
| 274 | |
| 275 | def _rust_clippy_rule_impl(ctx): |
| 276 | clippy_ready_targets = [dep for dep in ctx.attr.deps if "clippy_checks" in dir(dep[OutputGroupInfo])] |
| 277 | files = depset([], transitive = [dep[OutputGroupInfo].clippy_checks for dep in clippy_ready_targets]) |
| 278 | return [DefaultInfo(files = files)] |
| 279 | |
| 280 | rust_clippy = rule( |
| 281 | implementation = _rust_clippy_rule_impl, |
| 282 | attrs = { |
| 283 | "deps": attr.label_list( |
| 284 | doc = "Rust targets to run clippy on.", |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 285 | providers = [ |
| 286 | [rust_common.crate_info], |
| 287 | [rust_common.test_crate_info], |
| 288 | ], |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 289 | aspects = [rust_clippy_aspect], |
| 290 | ), |
| 291 | }, |
| 292 | doc = """\ |
| 293 | Executes the clippy checker on a specific target. |
| 294 | |
| 295 | Similar to `rust_clippy_aspect`, but allows specifying a list of dependencies \ |
| 296 | within the build system. |
| 297 | |
| 298 | For example, given the following example targets: |
| 299 | |
| 300 | ```python |
| 301 | load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") |
| 302 | |
| 303 | rust_library( |
| 304 | name = "hello_lib", |
| 305 | srcs = ["src/lib.rs"], |
| 306 | ) |
| 307 | |
| 308 | rust_test( |
| 309 | name = "greeting_test", |
| 310 | srcs = ["tests/greeting.rs"], |
| 311 | deps = [":hello_lib"], |
| 312 | ) |
| 313 | ``` |
| 314 | |
| 315 | Rust clippy can be set as a build target with the following: |
| 316 | |
| 317 | ```python |
| 318 | load("@rules_rust//rust:defs.bzl", "rust_clippy") |
| 319 | |
| 320 | rust_clippy( |
| 321 | name = "hello_library_clippy", |
| 322 | testonly = True, |
| 323 | deps = [ |
| 324 | ":hello_lib", |
| 325 | ":greeting_test", |
| 326 | ], |
| 327 | ) |
| 328 | ``` |
| 329 | """, |
| 330 | ) |
| 331 | |
| 332 | def _capture_clippy_output_impl(ctx): |
| 333 | """Implementation of the `capture_clippy_output` rule |
| 334 | |
| 335 | Args: |
| 336 | ctx (ctx): The rule's context object |
| 337 | |
| 338 | Returns: |
| 339 | list: A list containing the CaptureClippyOutputInfo provider |
| 340 | """ |
| 341 | return [CaptureClippyOutputInfo(capture_output = ctx.build_setting_value)] |
| 342 | |
| 343 | capture_clippy_output = rule( |
| 344 | doc = "Control whether to print clippy output or store it to a file, using the configured error_format.", |
| 345 | implementation = _capture_clippy_output_impl, |
| 346 | build_setting = config.bool(flag = True), |
| 347 | ) |