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 | |
| 32 | def _get_clippy_ready_crate_info(target, aspect_ctx): |
| 33 | """Check that a target is suitable for clippy and extract the `CrateInfo` provider from it. |
| 34 | |
| 35 | Args: |
| 36 | target (Target): The target the aspect is running on. |
| 37 | aspect_ctx (ctx, optional): The aspect's context object. |
| 38 | |
| 39 | Returns: |
| 40 | CrateInfo, optional: A `CrateInfo` provider if clippy should be run or `None`. |
| 41 | """ |
| 42 | |
| 43 | # Ignore external targets |
| 44 | if target.label.workspace_root.startswith("external"): |
| 45 | return None |
| 46 | |
| 47 | # Targets annotated with `noclippy` will not be formatted |
| 48 | if aspect_ctx and "noclippy" in aspect_ctx.rule.attr.tags: |
| 49 | return None |
| 50 | |
| 51 | # Obviously ignore any targets that don't contain `CrateInfo` |
| 52 | if rust_common.crate_info not in target: |
| 53 | return None |
| 54 | |
| 55 | return target[rust_common.crate_info] |
| 56 | |
| 57 | def _clippy_aspect_impl(target, ctx): |
| 58 | crate_info = _get_clippy_ready_crate_info(target, ctx) |
| 59 | if not crate_info: |
| 60 | return [ClippyInfo(output = depset([]))] |
| 61 | |
| 62 | toolchain = find_toolchain(ctx) |
| 63 | cc_toolchain, feature_configuration = find_cc_toolchain(ctx) |
| 64 | |
| 65 | dep_info, build_info, linkstamps = collect_deps( |
| 66 | deps = crate_info.deps, |
| 67 | proc_macro_deps = crate_info.proc_macro_deps, |
| 68 | aliases = crate_info.aliases, |
| 69 | # Clippy doesn't need to invoke transitive linking, therefore doesn't need linkstamps. |
| 70 | are_linkstamps_supported = False, |
| 71 | ) |
| 72 | |
| 73 | compile_inputs, out_dir, build_env_files, build_flags_files, linkstamp_outs, ambiguous_libs = collect_inputs( |
| 74 | ctx, |
| 75 | ctx.rule.file, |
| 76 | ctx.rule.files, |
| 77 | linkstamps, |
| 78 | toolchain, |
| 79 | cc_toolchain, |
| 80 | feature_configuration, |
| 81 | crate_info, |
| 82 | dep_info, |
| 83 | build_info, |
| 84 | ) |
| 85 | |
| 86 | args, env = construct_arguments( |
| 87 | ctx = ctx, |
| 88 | attr = ctx.rule.attr, |
| 89 | file = ctx.file, |
| 90 | toolchain = toolchain, |
| 91 | tool_path = toolchain.clippy_driver.path, |
| 92 | cc_toolchain = cc_toolchain, |
| 93 | feature_configuration = feature_configuration, |
| 94 | crate_info = crate_info, |
| 95 | dep_info = dep_info, |
| 96 | linkstamp_outs = linkstamp_outs, |
| 97 | ambiguous_libs = ambiguous_libs, |
| 98 | output_hash = determine_output_hash(crate_info.root, ctx.label), |
| 99 | rust_flags = [], |
| 100 | out_dir = out_dir, |
| 101 | build_env_files = build_env_files, |
| 102 | build_flags_files = build_flags_files, |
| 103 | emit = ["dep-info", "metadata"], |
| 104 | ) |
| 105 | |
| 106 | if crate_info.is_test: |
| 107 | args.rustc_flags.add("--test") |
| 108 | |
| 109 | # For remote execution purposes, the clippy_out file must be a sibling of crate_info.output |
| 110 | # or rustc may fail to create intermediate output files because the directory does not exist. |
| 111 | if ctx.attr._capture_output[CaptureClippyOutputInfo].capture_output: |
| 112 | clippy_out = ctx.actions.declare_file(ctx.label.name + ".clippy.out", sibling = crate_info.output) |
| 113 | args.process_wrapper_flags.add("--stderr-file", clippy_out.path) |
| 114 | |
| 115 | # If we are capturing the output, we want the build system to be able to keep going |
| 116 | # and consume the output. Some clippy lints are denials, so we treat them as warnings. |
| 117 | args.rustc_flags.add("-Wclippy::all") |
| 118 | else: |
| 119 | # A marker file indicating clippy has executed successfully. |
| 120 | # This file is necessary because "ctx.actions.run" mandates an output. |
| 121 | clippy_out = ctx.actions.declare_file(ctx.label.name + ".clippy.ok", sibling = crate_info.output) |
| 122 | args.process_wrapper_flags.add("--touch-file", clippy_out.path) |
| 123 | |
| 124 | # Turn any warnings from clippy or rustc into an error, as otherwise |
| 125 | # Bazel will consider the execution result of the aspect to be "success", |
| 126 | # and Clippy won't be re-triggered unless the source file is modified. |
| 127 | if "__bindgen" in ctx.rule.attr.tags: |
| 128 | # bindgen-generated content is likely to trigger warnings, so |
| 129 | # only fail on clippy warnings |
| 130 | args.rustc_flags.add("-Dclippy::style") |
| 131 | args.rustc_flags.add("-Dclippy::correctness") |
| 132 | args.rustc_flags.add("-Dclippy::complexity") |
| 133 | args.rustc_flags.add("-Dclippy::perf") |
| 134 | else: |
| 135 | # fail on any warning |
| 136 | args.rustc_flags.add("-Dwarnings") |
| 137 | |
| 138 | # Upstream clippy requires one of these two filenames or it silently uses |
| 139 | # the default config. Enforce the naming so users are not confused. |
| 140 | valid_config_file_names = [".clippy.toml", "clippy.toml"] |
| 141 | if ctx.file._config.basename not in valid_config_file_names: |
| 142 | fail("The clippy config file must be named one of: {}".format(valid_config_file_names)) |
| 143 | env["CLIPPY_CONF_DIR"] = "${{pwd}}/{}".format(ctx.file._config.dirname) |
| 144 | compile_inputs = depset([ctx.file._config], transitive = [compile_inputs]) |
| 145 | |
| 146 | ctx.actions.run( |
| 147 | executable = ctx.executable._process_wrapper, |
| 148 | inputs = compile_inputs, |
| 149 | outputs = [clippy_out], |
| 150 | env = env, |
| 151 | tools = [toolchain.clippy_driver], |
| 152 | arguments = args.all, |
| 153 | mnemonic = "Clippy", |
| 154 | ) |
| 155 | |
| 156 | return [ |
| 157 | OutputGroupInfo(clippy_checks = depset([clippy_out])), |
| 158 | ClippyInfo(output = depset([clippy_out])), |
| 159 | ] |
| 160 | |
| 161 | # Example: Run the clippy checker on all targets in the codebase. |
| 162 | # bazel build --aspects=@rules_rust//rust:defs.bzl%rust_clippy_aspect \ |
| 163 | # --output_groups=clippy_checks \ |
| 164 | # //... |
| 165 | rust_clippy_aspect = aspect( |
| 166 | fragments = ["cpp"], |
| 167 | host_fragments = ["cpp"], |
| 168 | attrs = { |
| 169 | "_capture_output": attr.label( |
| 170 | doc = "Value of the `capture_clippy_output` build setting", |
| 171 | default = Label("//:capture_clippy_output"), |
| 172 | ), |
| 173 | "_cc_toolchain": attr.label( |
| 174 | doc = ( |
| 175 | "Required attribute to access the cc_toolchain. See [Accessing the C++ toolchain]" + |
| 176 | "(https://docs.bazel.build/versions/master/integrating-with-rules-cc.html#accessing-the-c-toolchain)" |
| 177 | ), |
| 178 | default = Label("@bazel_tools//tools/cpp:current_cc_toolchain"), |
| 179 | ), |
| 180 | "_config": attr.label( |
| 181 | doc = "The `clippy.toml` file used for configuration", |
| 182 | allow_single_file = True, |
| 183 | default = Label("//:clippy.toml"), |
| 184 | ), |
| 185 | "_error_format": attr.label( |
| 186 | doc = "The desired `--error-format` flags for clippy", |
| 187 | default = "//:error_format", |
| 188 | ), |
| 189 | "_extra_rustc_flags": attr.label(default = "//:extra_rustc_flags"), |
| 190 | "_process_wrapper": attr.label( |
| 191 | doc = "A process wrapper for running clippy on all platforms", |
| 192 | default = Label("//util/process_wrapper"), |
| 193 | executable = True, |
| 194 | cfg = "exec", |
| 195 | ), |
| 196 | }, |
| 197 | provides = [ClippyInfo], |
| 198 | toolchains = [ |
| 199 | str(Label("//rust:toolchain")), |
| 200 | "@bazel_tools//tools/cpp:toolchain_type", |
| 201 | ], |
| 202 | incompatible_use_toolchain_transition = True, |
| 203 | implementation = _clippy_aspect_impl, |
| 204 | doc = """\ |
| 205 | Executes the clippy checker on specified targets. |
| 206 | |
| 207 | This aspect applies to existing rust_library, rust_test, and rust_binary rules. |
| 208 | |
| 209 | As an example, if the following is defined in `examples/hello_lib/BUILD.bazel`: |
| 210 | |
| 211 | ```python |
| 212 | load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") |
| 213 | |
| 214 | rust_library( |
| 215 | name = "hello_lib", |
| 216 | srcs = ["src/lib.rs"], |
| 217 | ) |
| 218 | |
| 219 | rust_test( |
| 220 | name = "greeting_test", |
| 221 | srcs = ["tests/greeting.rs"], |
| 222 | deps = [":hello_lib"], |
| 223 | ) |
| 224 | ``` |
| 225 | |
| 226 | Then the targets can be analyzed with clippy using the following command: |
| 227 | |
| 228 | ```output |
| 229 | $ bazel build --aspects=@rules_rust//rust:defs.bzl%rust_clippy_aspect \ |
| 230 | --output_groups=clippy_checks //hello_lib:all |
| 231 | ``` |
| 232 | """, |
| 233 | ) |
| 234 | |
| 235 | def _rust_clippy_rule_impl(ctx): |
| 236 | clippy_ready_targets = [dep for dep in ctx.attr.deps if "clippy_checks" in dir(dep[OutputGroupInfo])] |
| 237 | files = depset([], transitive = [dep[OutputGroupInfo].clippy_checks for dep in clippy_ready_targets]) |
| 238 | return [DefaultInfo(files = files)] |
| 239 | |
| 240 | rust_clippy = rule( |
| 241 | implementation = _rust_clippy_rule_impl, |
| 242 | attrs = { |
| 243 | "deps": attr.label_list( |
| 244 | doc = "Rust targets to run clippy on.", |
| 245 | providers = [rust_common.crate_info], |
| 246 | aspects = [rust_clippy_aspect], |
| 247 | ), |
| 248 | }, |
| 249 | doc = """\ |
| 250 | Executes the clippy checker on a specific target. |
| 251 | |
| 252 | Similar to `rust_clippy_aspect`, but allows specifying a list of dependencies \ |
| 253 | within the build system. |
| 254 | |
| 255 | For example, given the following example targets: |
| 256 | |
| 257 | ```python |
| 258 | load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") |
| 259 | |
| 260 | rust_library( |
| 261 | name = "hello_lib", |
| 262 | srcs = ["src/lib.rs"], |
| 263 | ) |
| 264 | |
| 265 | rust_test( |
| 266 | name = "greeting_test", |
| 267 | srcs = ["tests/greeting.rs"], |
| 268 | deps = [":hello_lib"], |
| 269 | ) |
| 270 | ``` |
| 271 | |
| 272 | Rust clippy can be set as a build target with the following: |
| 273 | |
| 274 | ```python |
| 275 | load("@rules_rust//rust:defs.bzl", "rust_clippy") |
| 276 | |
| 277 | rust_clippy( |
| 278 | name = "hello_library_clippy", |
| 279 | testonly = True, |
| 280 | deps = [ |
| 281 | ":hello_lib", |
| 282 | ":greeting_test", |
| 283 | ], |
| 284 | ) |
| 285 | ``` |
| 286 | """, |
| 287 | ) |
| 288 | |
| 289 | def _capture_clippy_output_impl(ctx): |
| 290 | """Implementation of the `capture_clippy_output` rule |
| 291 | |
| 292 | Args: |
| 293 | ctx (ctx): The rule's context object |
| 294 | |
| 295 | Returns: |
| 296 | list: A list containing the CaptureClippyOutputInfo provider |
| 297 | """ |
| 298 | return [CaptureClippyOutputInfo(capture_output = ctx.build_setting_value)] |
| 299 | |
| 300 | capture_clippy_output = rule( |
| 301 | doc = "Control whether to print clippy output or store it to a file, using the configured error_format.", |
| 302 | implementation = _capture_clippy_output_impl, |
| 303 | build_setting = config.bool(flag = True), |
| 304 | ) |