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