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 | |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 48 | def _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 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` |
| 75 | if rust_common.crate_info not in target: |
| 76 | return None |
| 77 | |
| 78 | return target[rust_common.crate_info] |
| 79 | |
| 80 | def _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 Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame^] | 132 | clippy_flags = ctx.attr._clippy_flags[ClippyFlagsInfo].clippy_flags |
| 133 | |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 134 | # 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 Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame^] | 140 | if clippy_flags: |
| 141 | fail("""Combining @rules_rust//:clippy_flags with @rules_rust//:capture_clippy_output=true is currently not supported. |
| 142 | See https://github.com/bazelbuild/rules_rust/pull/1264#discussion_r853241339 for more detail.""") |
| 143 | |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 144 | # 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 Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame^] | 153 | if clippy_flags: |
| 154 | args.rustc_flags.add_all(clippy_flags) |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 155 | else: |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame^] | 156 | # 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 Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 161 | 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 | # //... |
| 190 | rust_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 Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame^] | 205 | "_clippy_flags": attr.label( |
| 206 | doc = "Arguments to pass to clippy", |
| 207 | default = Label("//:clippy_flags"), |
| 208 | ), |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 209 | "_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 Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame^] | 218 | "_extra_rustc_flag": attr.label(default = "//:extra_rustc_flag"), |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 219 | "_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 Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame^] | 229 | str(Label("//rust:toolchain_type")), |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 230 | "@bazel_tools//tools/cpp:toolchain_type", |
| 231 | ], |
| 232 | incompatible_use_toolchain_transition = True, |
| 233 | implementation = _clippy_aspect_impl, |
| 234 | doc = """\ |
| 235 | Executes the clippy checker on specified targets. |
| 236 | |
| 237 | This aspect applies to existing rust_library, rust_test, and rust_binary rules. |
| 238 | |
| 239 | As an example, if the following is defined in `examples/hello_lib/BUILD.bazel`: |
| 240 | |
| 241 | ```python |
| 242 | load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") |
| 243 | |
| 244 | rust_library( |
| 245 | name = "hello_lib", |
| 246 | srcs = ["src/lib.rs"], |
| 247 | ) |
| 248 | |
| 249 | rust_test( |
| 250 | name = "greeting_test", |
| 251 | srcs = ["tests/greeting.rs"], |
| 252 | deps = [":hello_lib"], |
| 253 | ) |
| 254 | ``` |
| 255 | |
| 256 | Then 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 | |
| 265 | def _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 | |
| 270 | rust_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 = """\ |
| 280 | Executes the clippy checker on a specific target. |
| 281 | |
| 282 | Similar to `rust_clippy_aspect`, but allows specifying a list of dependencies \ |
| 283 | within the build system. |
| 284 | |
| 285 | For example, given the following example targets: |
| 286 | |
| 287 | ```python |
| 288 | load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") |
| 289 | |
| 290 | rust_library( |
| 291 | name = "hello_lib", |
| 292 | srcs = ["src/lib.rs"], |
| 293 | ) |
| 294 | |
| 295 | rust_test( |
| 296 | name = "greeting_test", |
| 297 | srcs = ["tests/greeting.rs"], |
| 298 | deps = [":hello_lib"], |
| 299 | ) |
| 300 | ``` |
| 301 | |
| 302 | Rust clippy can be set as a build target with the following: |
| 303 | |
| 304 | ```python |
| 305 | load("@rules_rust//rust:defs.bzl", "rust_clippy") |
| 306 | |
| 307 | rust_clippy( |
| 308 | name = "hello_library_clippy", |
| 309 | testonly = True, |
| 310 | deps = [ |
| 311 | ":hello_lib", |
| 312 | ":greeting_test", |
| 313 | ], |
| 314 | ) |
| 315 | ``` |
| 316 | """, |
| 317 | ) |
| 318 | |
| 319 | def _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 | |
| 330 | capture_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 | ) |