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 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 32ClippyFlagsInfo = 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 48def _get_clippy_ready_crate_info(target, aspect_ctx = None): 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 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 73 74 # Obviously ignore any targets that don't contain `CrateInfo` 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: 80 return None 81 82def _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 skip_expanding_rustc_env = True, 130 ) 131 132 if crate_info.is_test: 133 args.rustc_flags.add("--test") 134 135 clippy_flags = ctx.attr._clippy_flags[ClippyFlagsInfo].clippy_flags 136 137 # For remote execution purposes, the clippy_out file must be a sibling of crate_info.output 138 # or rustc may fail to create intermediate output files because the directory does not exist. 139 if ctx.attr._capture_output[CaptureClippyOutputInfo].capture_output: 140 clippy_out = ctx.actions.declare_file(ctx.label.name + ".clippy.out", sibling = crate_info.output) 141 args.process_wrapper_flags.add("--stderr-file", clippy_out) 142 143 if clippy_flags: 144 fail("""Combining @rules_rust//:clippy_flags with @rules_rust//:capture_clippy_output=true is currently not supported. 145See https://github.com/bazelbuild/rules_rust/pull/1264#discussion_r853241339 for more detail.""") 146 147 # If we are capturing the output, we want the build system to be able to keep going 148 # and consume the output. Some clippy lints are denials, so we treat them as warnings. 149 args.rustc_flags.add("-Wclippy::all") 150 else: 151 # A marker file indicating clippy has executed successfully. 152 # This file is necessary because "ctx.actions.run" mandates an output. 153 clippy_out = ctx.actions.declare_file(ctx.label.name + ".clippy.ok", sibling = crate_info.output) 154 args.process_wrapper_flags.add("--touch-file", clippy_out) 155 156 if clippy_flags: 157 args.rustc_flags.add_all(clippy_flags) 158 else: 159 # The user didn't provide any clippy flags explicitly so we apply conservative defaults. 160 161 # Turn any warnings from clippy or rustc into an error, as otherwise 162 # Bazel will consider the execution result of the aspect to be "success", 163 # and Clippy won't be re-triggered unless the source file is modified. 164 args.rustc_flags.add("-Dwarnings") 165 166 # Upstream clippy requires one of these two filenames or it silently uses 167 # the default config. Enforce the naming so users are not confused. 168 valid_config_file_names = [".clippy.toml", "clippy.toml"] 169 if ctx.file._config.basename not in valid_config_file_names: 170 fail("The clippy config file must be named one of: {}".format(valid_config_file_names)) 171 env["CLIPPY_CONF_DIR"] = "${{pwd}}/{}".format(ctx.file._config.dirname) 172 compile_inputs = depset([ctx.file._config], transitive = [compile_inputs]) 173 174 ctx.actions.run( 175 executable = ctx.executable._process_wrapper, 176 inputs = compile_inputs, 177 outputs = [clippy_out], 178 env = env, 179 tools = [toolchain.clippy_driver], 180 arguments = args.all, 181 mnemonic = "Clippy", 182 toolchain = "@rules_rust//rust:toolchain_type", 183 ) 184 185 return [ 186 OutputGroupInfo(clippy_checks = depset([clippy_out])), 187 ClippyInfo(output = depset([clippy_out])), 188 ] 189 190# Example: Run the clippy checker on all targets in the codebase. 191# bazel build --aspects=@rules_rust//rust:defs.bzl%rust_clippy_aspect \ 192# --output_groups=clippy_checks \ 193# //... 194rust_clippy_aspect = aspect( 195 fragments = ["cpp"], 196 attrs = { 197 "_capture_output": attr.label( 198 doc = "Value of the `capture_clippy_output` build setting", 199 default = Label("//:capture_clippy_output"), 200 ), 201 "_cc_toolchain": attr.label( 202 doc = ( 203 "Required attribute to access the cc_toolchain. See [Accessing the C++ toolchain]" + 204 "(https://docs.bazel.build/versions/master/integrating-with-rules-cc.html#accessing-the-c-toolchain)" 205 ), 206 default = Label("@bazel_tools//tools/cpp:current_cc_toolchain"), 207 ), 208 "_clippy_flags": attr.label( 209 doc = "Arguments to pass to clippy", 210 default = Label("//:clippy_flags"), 211 ), 212 "_config": attr.label( 213 doc = "The `clippy.toml` file used for configuration", 214 allow_single_file = True, 215 default = Label("//:clippy.toml"), 216 ), 217 "_error_format": attr.label( 218 doc = "The desired `--error-format` flags for clippy", 219 default = "//:error_format", 220 ), 221 "_extra_rustc_flag": attr.label( 222 default = Label("//:extra_rustc_flag"), 223 ), 224 "_per_crate_rustc_flag": attr.label( 225 default = Label("//:experimental_per_crate_rustc_flag"), 226 ), 227 "_process_wrapper": attr.label( 228 doc = "A process wrapper for running clippy on all platforms", 229 default = Label("//util/process_wrapper"), 230 executable = True, 231 cfg = "exec", 232 ), 233 }, 234 provides = [ClippyInfo], 235 required_providers = [ 236 [rust_common.crate_info], 237 [rust_common.test_crate_info], 238 ], 239 toolchains = [ 240 str(Label("//rust:toolchain_type")), 241 "@bazel_tools//tools/cpp:toolchain_type", 242 ], 243 implementation = _clippy_aspect_impl, 244 doc = """\ 245Executes the clippy checker on specified targets. 246 247This aspect applies to existing rust_library, rust_test, and rust_binary rules. 248 249As an example, if the following is defined in `examples/hello_lib/BUILD.bazel`: 250 251```python 252load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") 253 254rust_library( 255 name = "hello_lib", 256 srcs = ["src/lib.rs"], 257) 258 259rust_test( 260 name = "greeting_test", 261 srcs = ["tests/greeting.rs"], 262 deps = [":hello_lib"], 263) 264``` 265 266Then 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 275def _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 280rust_clippy = rule( 281 implementation = _rust_clippy_rule_impl, 282 attrs = { 283 "deps": attr.label_list( 284 doc = "Rust targets to run clippy on.", 285 providers = [ 286 [rust_common.crate_info], 287 [rust_common.test_crate_info], 288 ], 289 aspects = [rust_clippy_aspect], 290 ), 291 }, 292 doc = """\ 293Executes the clippy checker on a specific target. 294 295Similar to `rust_clippy_aspect`, but allows specifying a list of dependencies \ 296within the build system. 297 298For example, given the following example targets: 299 300```python 301load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") 302 303rust_library( 304 name = "hello_lib", 305 srcs = ["src/lib.rs"], 306) 307 308rust_test( 309 name = "greeting_test", 310 srcs = ["tests/greeting.rs"], 311 deps = [":hello_lib"], 312) 313``` 314 315Rust clippy can be set as a build target with the following: 316 317```python 318load("@rules_rust//rust:defs.bzl", "rust_clippy") 319 320rust_clippy( 321 name = "hello_library_clippy", 322 testonly = True, 323 deps = [ 324 ":hello_lib", 325 ":greeting_test", 326 ], 327) 328``` 329""", 330) 331 332def _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 343capture_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) 348