feat(#514): pass extra flags to clippy (#1264)

* feat(#514): pass extra flags to clippy

This change adds a new configuration rule, `clippy_flags`.
This rule allows you to override clippy flags that the
`rust_clippy_aspect` aspect and the `rust_clippy` rule use.
The flags affect all the targets.
Adding the configuration to `.bazelrc` allows you to have consistent
clippy settings for the whole team.

Example:

```
--@rules_rust//:clippy_flags=-D,clippy::all,-A,clippy::too_many_arguments
```

* sort dict entries

* Apply review feedback

* fail on unsupported clippy configuration
diff --git a/BUILD.bazel b/BUILD.bazel
index 281a3d8..260db43 100644
--- a/BUILD.bazel
+++ b/BUILD.bazel
@@ -1,5 +1,5 @@
 load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
-load("//rust:defs.bzl", "capture_clippy_output", "error_format", "extra_exec_rustc_flags", "extra_rustc_flags")
+load("//rust:defs.bzl", "capture_clippy_output", "clippy_flags", "error_format", "extra_exec_rustc_flags", "extra_rustc_flags")
 
 exports_files(["LICENSE"])
 
@@ -37,6 +37,14 @@
     visibility = ["//visibility:public"],
 )
 
+# This setting may be used to pass extra options to clippy from the command line.
+# It applies across all targets.
+clippy_flags(
+    name = "clippy_flags",
+    build_setting_default = [],
+    visibility = ["//visibility:public"],
+)
+
 # This setting may be used to pass extra options to rustc from the command line
 # in non-exec configuration.
 # It applies across all targets whereas the rustc_flags option on targets applies only
diff --git a/rust/defs.bzl b/rust/defs.bzl
index dd1b445..8fef5af 100644
--- a/rust/defs.bzl
+++ b/rust/defs.bzl
@@ -21,6 +21,7 @@
 load(
     "//rust/private:clippy.bzl",
     _capture_clippy_output = "capture_clippy_output",
+    _clippy_flags = "clippy_flags",
     _rust_clippy = "rust_clippy",
     _rust_clippy_aspect = "rust_clippy_aspect",
 )
@@ -87,6 +88,9 @@
 rust_doc_test = _rust_doc_test
 # See @rules_rust//rust/private:rustdoc_test.bzl for a complete description.
 
+clippy_flags = _clippy_flags
+# See @rules_rust//rust/private:clippy.bzl for a complete description.
+
 rust_clippy_aspect = _rust_clippy_aspect
 # See @rules_rust//rust/private:clippy.bzl for a complete description.
 
diff --git a/rust/private/clippy.bzl b/rust/private/clippy.bzl
index d1464e9..29074f4 100644
--- a/rust/private/clippy.bzl
+++ b/rust/private/clippy.bzl
@@ -29,6 +29,22 @@
     "find_toolchain",
 )
 
+ClippyFlagsInfo = provider(
+    doc = "Pass each value as an additional flag to clippy invocations",
+    fields = {"clippy_flags": "List[string] Flags to pass to clippy"},
+)
+
+def _clippy_flags_impl(ctx):
+    return ClippyFlagsInfo(clippy_flags = ctx.build_setting_value)
+
+clippy_flags = rule(
+    doc = (
+        "Add custom clippy flags from the command line with `--@rules_rust//:clippy_flags`."
+    ),
+    implementation = _clippy_flags_impl,
+    build_setting = config.string_list(flag = True),
+)
+
 def _get_clippy_ready_crate_info(target, aspect_ctx):
     """Check that a target is suitable for clippy and extract the `CrateInfo` provider from it.
 
@@ -106,12 +122,18 @@
     if crate_info.is_test:
         args.rustc_flags.add("--test")
 
+    clippy_flags = ctx.attr._clippy_flags[ClippyFlagsInfo].clippy_flags
+
     # For remote execution purposes, the clippy_out file must be a sibling of crate_info.output
     # or rustc may fail to create intermediate output files because the directory does not exist.
     if ctx.attr._capture_output[CaptureClippyOutputInfo].capture_output:
         clippy_out = ctx.actions.declare_file(ctx.label.name + ".clippy.out", sibling = crate_info.output)
         args.process_wrapper_flags.add("--stderr-file", clippy_out.path)
 
+        if clippy_flags:
+            fail("""Combining @rules_rust//:clippy_flags with @rules_rust//:capture_clippy_output=true is currently not supported.
+See https://github.com/bazelbuild/rules_rust/pull/1264#discussion_r853241339 for more detail.""")
+
         # If we are capturing the output, we want the build system to be able to keep going
         # and consume the output. Some clippy lints are denials, so we treat them as warnings.
         args.rustc_flags.add("-Wclippy::all")
@@ -121,10 +143,10 @@
         clippy_out = ctx.actions.declare_file(ctx.label.name + ".clippy.ok", sibling = crate_info.output)
         args.process_wrapper_flags.add("--touch-file", clippy_out.path)
 
-        # Turn any warnings from clippy or rustc into an error, as otherwise
-        # Bazel will consider the execution result of the aspect to be "success",
-        # and Clippy won't be re-triggered unless the source file is modified.
-        if "__bindgen" in ctx.rule.attr.tags:
+        if clippy_flags:
+            args.rustc_flags.extend(clippy_flags)
+
+        elif "__bindgen" in ctx.rule.attr.tags:
             # bindgen-generated content is likely to trigger warnings, so
             # only fail on clippy warnings
             args.rustc_flags.add("-Dclippy::style")
@@ -132,7 +154,11 @@
             args.rustc_flags.add("-Dclippy::complexity")
             args.rustc_flags.add("-Dclippy::perf")
         else:
-            # fail on any warning
+            # The user didn't provide any clippy flags explicitly so we apply conservative defaults.
+
+            # Turn any warnings from clippy or rustc into an error, as otherwise
+            # Bazel will consider the execution result of the aspect to be "success",
+            # and Clippy won't be re-triggered unless the source file is modified.
             args.rustc_flags.add("-Dwarnings")
 
     # Upstream clippy requires one of these two filenames or it silently uses
@@ -177,6 +203,10 @@
             ),
             default = Label("@bazel_tools//tools/cpp:current_cc_toolchain"),
         ),
+        "_clippy_flags": attr.label(
+            doc = "Arguments to pass to clippy",
+            default = Label("//:clippy_flags"),
+        ),
         "_config": attr.label(
             doc = "The `clippy.toml` file used for configuration",
             allow_single_file = True,