Implement Clippy Aspect & Build Rule (#339)

To use as an aspect:

  $ bazel build --aspects=@io_bazel_rules_rust//rust:rust.bzl%rust_clippy_aspect
                       --output_groups=clippy_checks //...

To use as a rule:

  load("@io_bazel_rules_rust//rust:rust.bzl", "rust_clippy")
  rust_clippy(
      name = "my_clippy_target",
      deps = [
          ":library_on_which_we_run_clippy",
      ],
  )
diff --git a/.bazelci/presubmit.yml b/.bazelci/presubmit.yml
index 1a0c597..e11304e 100644
--- a/.bazelci/presubmit.yml
+++ b/.bazelci/presubmit.yml
@@ -50,3 +50,18 @@
     working_directory: examples
     test_targets:
     - //...
+  clippy_examples:
+    name: Clippy on Examples
+    platform: ubuntu1804
+    working_directory: examples
+    build_flags:
+    - "--aspects=@io_bazel_rules_rust//rust:rust.bzl%rust_clippy_aspect"
+    - "--output_groups=clippy_checks"
+    build_targets:
+    - //...
+  clippy_failure:
+    name: Negative Clippy Tests
+    platform: ubuntu1804
+    shell_commands:
+    - ./test/clippy/clippy_failure_test.sh
+
diff --git a/examples/ffi/rust_calling_c/src/matrix.rs b/examples/ffi/rust_calling_c/src/matrix.rs
index c9359a3..0c4c5fd 100644
--- a/examples/ffi/rust_calling_c/src/matrix.rs
+++ b/examples/ffi/rust_calling_c/src/matrix.rs
@@ -45,7 +45,7 @@
             if matrix.is_null() {
                 panic!("Failed to allocate Matrix.");
             }
-            Matrix { matrix: matrix }
+            Matrix { matrix }
         }
     }
 
diff --git a/examples/proto/helloworld/greeter_server/greeter_server.rs b/examples/proto/helloworld/greeter_server/greeter_server.rs
index fa46bca..f2018ff 100644
--- a/examples/proto/helloworld/greeter_server/greeter_server.rs
+++ b/examples/proto/helloworld/greeter_server/greeter_server.rs
@@ -36,7 +36,7 @@
 
 fn main() {
     let mut server = grpc::ServerBuilder::<tls_api_stub::TlsAcceptor>::new();
-    let port = u16::from_str(&env::args().nth(1).unwrap_or("50051".to_owned())).unwrap();
+    let port = u16::from_str(&env::args().nth(1).unwrap_or_else(|| "50051".to_owned())).unwrap();
     server.http.set_port(port);
     server.add_service(GreeterServer::new_service_def(GreeterImpl));
     server.http.set_cpu_pool_threads(4);
diff --git a/examples/proto/helloworld/helloworld_test.rs b/examples/proto/helloworld/helloworld_test.rs
index 3608a2d..0324a83 100644
--- a/examples/proto/helloworld/helloworld_test.rs
+++ b/examples/proto/helloworld/helloworld_test.rs
@@ -56,17 +56,19 @@
                     .expect("Waiting for server startup");
                 line = line.trim().to_owned();
                 if line.starts_with(port_prefix) {
-                    port = u16::from_str(&line[port_prefix.len()..]).expect(&format!(
-                        "Invalid port number {}",
-                        &line[port_prefix.len()..]
-                    ))
+                    port = u16::from_str(&line[port_prefix.len()..]).unwrap_or_else(|_|
+                        panic!(
+                            "Invalid port number {}",
+                            &line[port_prefix.len()..]
+                        )
+                    )
                 }
             }
         }
         println!("Started server on port {}", port);
         ServerInfo {
             process: c,
-            port: port,
+            port,
         }
     }
 
diff --git a/rust/private/clippy.bzl b/rust/private/clippy.bzl
new file mode 100644
index 0000000..f1f248e
--- /dev/null
+++ b/rust/private/clippy.bzl
@@ -0,0 +1,215 @@
+# Copyright 2020 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+load(
+    "@io_bazel_rules_rust//rust:private/rustc.bzl",
+    "CrateInfo",
+    "collect_deps",
+    "collect_inputs",
+    "construct_arguments",
+    "construct_compile_command",
+)
+load(
+    "@io_bazel_rules_rust//rust:private/rust.bzl",
+    "crate_root_src",
+)
+load("@io_bazel_rules_rust//rust:private/utils.bzl", "find_toolchain")
+
+_rust_extensions = [
+    "rs",
+]
+
+def _is_rust_target(srcs):
+    return any([src.extension in _rust_extensions for src in srcs])
+
+def _rust_sources(target, rule):
+    srcs = []
+    if "srcs" in dir(rule.attr):
+        srcs += [f for src in rule.attr.srcs for f in src.files.to_list()]
+    if "hdrs" in dir(rule.attr):
+        srcs += [f for hdr in rule.attr.hdrs for f in hdr.files.to_list()]
+    return [src for src in srcs if src.extension in _rust_extensions]
+
+def _clippy_aspect_impl(target, ctx):
+    if CrateInfo not in target:
+        return []
+    rust_srcs = _rust_sources(target, ctx.rule)
+    if rust_srcs == []:
+        return []
+
+    toolchain = find_toolchain(ctx)
+    root = crate_root_src(ctx.rule.attr, srcs = rust_srcs)
+    crate_info = target[CrateInfo]
+
+    dep_info, build_info = collect_deps(
+        ctx.label,
+        crate_info.deps,
+        crate_info.proc_macro_deps,
+        crate_info.aliases,
+        toolchain,
+    )
+
+    compile_inputs, out_dir = collect_inputs(
+        ctx,
+        ctx.rule.file,
+        ctx.rule.files,
+        toolchain,
+        crate_info,
+        dep_info,
+        build_info
+    )
+
+    args, env = construct_arguments(
+        ctx,
+        ctx.rule.file,
+        toolchain,
+        crate_info,
+        dep_info,
+        output_hash = repr(hash(root.path)),
+        rust_flags = [])
+
+    # A marker file indicating clippy has executed successfully.
+    # This file is necessary because "ctx.actions.run" mandates an output.
+    clippy_marker = ctx.actions.declare_file(ctx.label.name + "_clippy.ok")
+
+    command = construct_compile_command(
+        ctx,
+        toolchain.clippy_driver.path,
+        toolchain,
+        crate_info,
+        build_info,
+        out_dir,
+    ) + (" && touch %s" % clippy_marker.path)
+
+    # Deny the default-on clippy warning levels.
+    #
+    # If these are left as warnings, then 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.add("-Dclippy::style")
+    args.add("-Dclippy::correctness");
+    args.add("-Dclippy::complexity");
+    args.add("-Dclippy::perf");
+
+    ctx.actions.run_shell(
+        command = command,
+        inputs = compile_inputs,
+        outputs = [clippy_marker],
+        env = env,
+        tools = [toolchain.clippy_driver],
+        arguments = [args],
+        mnemonic = "Clippy",
+    )
+
+    return [
+        OutputGroupInfo(clippy_checks = depset([clippy_marker])),
+    ]
+
+# Example: Run the clippy checker on all targets in the codebase.
+#   bazel build --aspects=@io_bazel_rules_rust//rust:rust.bzl%rust_clippy_aspect \
+#               --output_groups=clippy_checks \
+#               //...
+rust_clippy_aspect = aspect(
+    fragments = ["cpp"],
+    host_fragments = ["cpp"],
+    attrs = {
+        "_cc_toolchain": attr.label(
+            default = Label("@bazel_tools//tools/cpp:current_cc_toolchain"),
+        ),
+    },
+    toolchains = [
+        "@io_bazel_rules_rust//rust:toolchain",
+        "@bazel_tools//tools/cpp:toolchain_type"
+    ],
+    implementation = _clippy_aspect_impl,
+    doc = """
+Executes the clippy checker on specified targets.
+
+This aspect applies to existing rust_library, rust_test, and rust_binary rules.
+
+As an example, if the following is defined in `hello_lib/BUILD`:
+
+```python
+package(default_visibility = ["//visibility:public"])
+
+load("@io_bazel_rules_rust//rust:rust.bzl", "rust_library", "rust_test")
+
+rust_library(
+    name = "hello_lib",
+    srcs = ["src/lib.rs"],
+)
+
+rust_test(
+    name = "greeting_test",
+    srcs = ["tests/greeting.rs"],
+    deps = [":hello_lib"],
+)
+```
+
+Then the targets can be analyzed with clippy using the following command:
+
+$ bazel build --aspects=@io_bazel_rules_rust//rust:rust.bzl%rust_clippy_aspect \
+              --output_groups=clippy_checks //hello_lib:all
+""",
+
+)
+
+def _rust_clippy_rule_impl(ctx):
+    files = depset([], transitive = [dep[OutputGroupInfo].clippy_checks for dep in ctx.attr.deps])
+    return [DefaultInfo(files = files)]
+
+rust_clippy = rule(
+    implementation = _rust_clippy_rule_impl,
+    attrs = {
+        'deps': attr.label_list(aspects = [rust_clippy_aspect]),
+    },
+    doc = """
+Executes the clippy checker on a specific target.
+
+Similar to `rust_clippy_aspect`, but allows specifying a list of dependencies
+within the build system.
+
+For example, given the following example targets:
+
+```python
+package(default_visibility = ["//visibility:public"])
+
+load("@io_bazel_rules_rust//rust:rust.bzl", "rust_library", "rust_test")
+
+rust_library(
+    name = "hello_lib",
+    srcs = ["src/lib.rs"],
+)
+
+rust_test(
+    name = "greeting_test",
+    srcs = ["tests/greeting.rs"],
+    deps = [":hello_lib"],
+)
+```
+
+Rust clippy can be set as a build target with the following:
+
+```python
+rust_clippy(
+    name = "hello_library_clippy",
+    testonly = True,
+    deps = [
+        ":hello_lib",
+        ":greeting_test",
+    ],
+)
+```
+""",
+)
diff --git a/rust/private/rust.bzl b/rust/private/rust.bzl
index 043dbdb..9ab873e 100644
--- a/rust/private/rust.bzl
+++ b/rust/private/rust.bzl
@@ -71,24 +71,28 @@
         extension = extension,
     )
 
-def _get_edition(ctx, toolchain):
-    if getattr(ctx.attr, "edition"):
-        return ctx.attr.edition
+def get_edition(attr, toolchain):
+    if getattr(attr, "edition"):
+        return attr.edition
     else:
         return toolchain.default_edition
 
-def _crate_root_src(ctx, file_name = "lib.rs"):
+def crate_root_src(attr, srcs, file_name = "lib.rs"):
     """Finds the source file for the crate root."""
-    srcs = ctx.files.srcs
 
-    crate_root = (
-        ctx.file.crate_root or
-        (srcs[0] if len(srcs) == 1 else None) or
-        _shortest_src_with_basename(srcs, file_name) or
-        _shortest_src_with_basename(srcs, ctx.attr.name + ".rs")
-    )
+    crate_root = None
+    if hasattr(attr, "crate_root"):
+        if attr.crate_root:
+            crate_root = attr.crate_root.files.to_list()[0]
+
     if not crate_root:
-        file_names = [file_name, ctx.attr.name + ".rs"]
+        crate_root = (
+            (srcs[0] if len(srcs) == 1 else None) or
+            _shortest_src_with_basename(srcs, file_name) or
+            _shortest_src_with_basename(srcs, attr.name + ".rs")
+        )
+    if not crate_root:
+        file_names = [file_name, attr.name + ".rs"]
         fail("No {} source file found.".format(" or ".join(file_names)), "srcs")
     return crate_root
 
@@ -105,7 +109,7 @@
 
 def _rust_library_impl(ctx):
     # Find lib.rs
-    lib_rs = _crate_root_src(ctx)
+    lib_rs = crate_root_src(ctx.attr, ctx.files.srcs)
 
     toolchain = find_toolchain(ctx)
 
@@ -133,7 +137,7 @@
             proc_macro_deps = ctx.attr.proc_macro_deps,
             aliases = ctx.attr.aliases,
             output = rust_lib,
-            edition = _get_edition(ctx, toolchain),
+            edition = get_edition(ctx.attr, toolchain),
             rustc_env = ctx.attr.rustc_env,
         ),
         output_hash = output_hash,
@@ -156,13 +160,13 @@
         crate_info = CrateInfo(
             name = crate_name,
             type = crate_type,
-            root = _crate_root_src(ctx, "main.rs"),
+            root = crate_root_src(ctx.attr, ctx.files.srcs, file_name = "main.rs"),
             srcs = ctx.files.srcs,
             deps = ctx.attr.deps,
             proc_macro_deps = ctx.attr.proc_macro_deps,
             aliases = ctx.attr.aliases,
             output = output,
-            edition = _get_edition(ctx, toolchain),
+            edition = get_edition(ctx.attr, toolchain),
             rustc_env = ctx.attr.rustc_env,
         ),
     )
@@ -205,13 +209,13 @@
         target = CrateInfo(
             name = test_binary.basename,
             type = "lib",
-            root = _crate_root_src(ctx),
+            root = crate_root_src(ctx.attr, ctx.files.srcs),
             srcs = ctx.files.srcs,
             deps = ctx.attr.deps,
             proc_macro_deps = ctx.attr.proc_macro_deps,
             aliases = ctx.attr.aliases,
             output = test_binary,
-            edition = _get_edition(ctx, toolchain),
+            edition = get_edition(ctx.attr, toolchain),
             rustc_env = ctx.attr.rustc_env,
         )
 
diff --git a/rust/private/rustc.bzl b/rust/private/rustc.bzl
index 48c8079..8a9d160 100644
--- a/rust/private/rustc.bzl
+++ b/rust/private/rustc.bzl
@@ -238,30 +238,33 @@
 
 def _add_out_dir_to_compile_inputs(
         ctx,
+        file,
         build_info,
         compile_inputs):
-    out_dir = _create_out_dir_action(ctx, build_info.out_dir if build_info else None)
+    out_dir = _create_out_dir_action(ctx, file, build_info.out_dir if build_info else None)
     if out_dir:
         compile_inputs = depset([out_dir], transitive = [compile_inputs])
     return compile_inputs, out_dir
 
-def _collect_inputs(
+def collect_inputs(
         ctx,
+        file,
+        files,
         toolchain,
         crate_info,
         dep_info,
         build_info):
-    linker_script = getattr(ctx.file, "linker_script") if hasattr(ctx.file, "linker_script") else None
+    linker_script = getattr(file, "linker_script") if hasattr(file, "linker_script") else None
 
     if (len(BAZEL_VERSION) == 0 or
         versions.is_at_least("0.25.0", BAZEL_VERSION)):
         linker_depset = find_cpp_toolchain(ctx).all_files
     else:
-        linker_depset = depset(ctx.files._cc_toolchain)
+        linker_depset = depset(files._cc_toolchain)
 
     compile_inputs = depset(
         crate_info.srcs +
-        getattr(ctx.files, "data", []) +
+        getattr(files, "data", []) +
         dep_info.transitive_libs +
         [toolchain.rustc] +
         toolchain.crosstool_files +
@@ -273,18 +276,19 @@
             linker_depset,
         ],
     )
-    return _add_out_dir_to_compile_inputs(ctx, build_info, compile_inputs)
+    return _add_out_dir_to_compile_inputs(ctx, file, build_info, compile_inputs)
 
-def _construct_arguments(
+def construct_arguments(
         ctx,
+        file,
         toolchain,
         crate_info,
         dep_info,
         output_hash,
         rust_flags):
-    output_dir = crate_info.output.dirname
+    output_dir = getattr(crate_info.output, "dirname") if hasattr(crate_info.output, "dirname") else None
 
-    linker_script = getattr(ctx.file, "linker_script") if hasattr(ctx.file, "linker_script") else None
+    linker_script = getattr(file, "linker_script") if hasattr(file, "linker_script") else None
 
     env = _get_rustc_env(ctx, toolchain)
 
@@ -296,7 +300,8 @@
     # Mangle symbols to disambiguate crates with the same name
     extra_filename = "-" + output_hash if output_hash else ""
     args.add("--codegen=metadata=" + extra_filename)
-    args.add("--out-dir=" + output_dir)
+    if output_dir:
+        args.add("--out-dir=" + output_dir)
     args.add("--codegen=extra-filename=" + extra_filename)
 
     compilation_mode = get_compilation_mode_opts(ctx, toolchain)
@@ -313,7 +318,6 @@
 
     # Gets the paths to the folders containing the standard library (or libcore)
     rust_lib_paths = depset([file.dirname for file in toolchain.rust_lib.files.to_list()]).to_list()
-
     # Tell Rustc where to find the standard library
     args.add_all(rust_lib_paths, before_each = "-L", format_each = "%s")
 
@@ -377,9 +381,13 @@
     package_dir = ctx.build_file_path[:ctx.build_file_path.rfind("/")]
     manifest_dir_env = "CARGO_MANIFEST_DIR=$(pwd)/{} ".format(package_dir)
 
-    return out_dir_env + manifest_dir_env
+    # This empty value satisfies Clippy, which otherwise complains about the
+    # sysroot being undefined.
+    sysroot_env= "SYSROOT= "
 
-def _construct_compile_command(
+    return out_dir_env + manifest_dir_env + sysroot_env
+
+def construct_compile_command(
         ctx,
         command,
         toolchain,
@@ -399,7 +407,7 @@
     # not the _ version.  So we rename the rustc-generated file (with _s) to
     # have -s if needed.
     maybe_rename = ""
-    if crate_info.type == "bin":
+    if crate_info.type == "bin" and crate_info.output != None:
         generated_file = crate_info.name
         if toolchain.target_arch == "wasm32":
             generated_file = generated_file + ".wasm"
@@ -411,7 +419,7 @@
     return '{}{}{} "$@" --remap-path-prefix="$(pwd)"=__bazel_redacted_pwd{}{}'.format(
         rustc_env_expansion,
         command_env,
-        toolchain.rustc.path,
+        command,
         build_flags_expansion,
         maybe_rename,
     )
@@ -439,16 +447,19 @@
         toolchain,
     )
 
-    compile_inputs, out_dir = _collect_inputs(
+    compile_inputs, out_dir = collect_inputs(
         ctx,
+        ctx.file,
+        ctx.files,
         toolchain,
         crate_info,
         dep_info,
         build_info
     )
 
-    args, env = _construct_arguments(
+    args, env = construct_arguments(
         ctx,
+        ctx.file,
         toolchain,
         crate_info,
         dep_info,
@@ -456,7 +467,7 @@
         rust_flags
     )
 
-    command = _construct_compile_command(
+    command = construct_compile_command(
         ctx,
         toolchain.rustc.path,
         toolchain,
@@ -508,8 +519,8 @@
     if crate.edition != "2015":
         args.add("--edition={}".format(crate.edition))
 
-def _create_out_dir_action(ctx, build_info_out_dir = None):
-    tar_file = getattr(ctx.file, "out_dir_tar", None)
+def _create_out_dir_action(ctx, file, build_info_out_dir = None):
+    tar_file = getattr(file, "out_dir_tar", None)
     if not tar_file:
         return build_info_out_dir
     else:
diff --git a/rust/rust.bzl b/rust/rust.bzl
index 2881ca1..98723db 100644
--- a/rust/rust.bzl
+++ b/rust/rust.bzl
@@ -28,6 +28,11 @@
     "@io_bazel_rules_rust//rust:private/rustdoc_test.bzl",
     _rust_doc_test = "rust_doc_test",
 )
+load(
+    "@io_bazel_rules_rust//rust:private/clippy.bzl",
+    _rust_clippy_aspect = "rust_clippy_aspect",
+    _rust_clippy = "rust_clippy",
+)
 
 rust_library = _rust_library
 """ See @io_bazel_rules_rust//rust:private/rust.bzl for a complete description. """
@@ -48,4 +53,10 @@
 """ See @io_bazel_rules_rust//rust:private/rustdoc.bzl for a complete description. """
 
 rust_doc_test = _rust_doc_test
-""" See @io_bazel_rules_rust//rust:private/rustdoc.bzl for a complete description. """
+""" See @io_bazel_rules_rust//rust:private/rustdoc_test.bzl for a complete description. """
+
+rust_clippy_aspect = _rust_clippy_aspect
+""" See @io_bazel_rules_rust//rust:private/clippy.bzl for a complete description. """
+
+rust_clippy = _rust_clippy
+""" See @io_bazel_rules_rust//rust:private/clippy.bzl for a complete description. """
diff --git a/test/clippy/BUILD b/test/clippy/BUILD
new file mode 100644
index 0000000..a56c1e9
--- /dev/null
+++ b/test/clippy/BUILD
@@ -0,0 +1,86 @@
+load(
+    "//rust:rust.bzl",
+    "rust_binary",
+    "rust_clippy",
+    "rust_library",
+    "rust_test",
+)
+
+# Declaration of passing targets.
+
+rust_binary(
+    name = "ok_binary",
+    srcs = ["src/main.rs"],
+    edition = "2018",
+)
+
+rust_library(
+    name = "ok_library",
+    srcs = ["src/lib.rs"],
+    edition = "2018",
+)
+
+rust_test(
+    name = "ok_test",
+    srcs = ["src/lib.rs"],
+    edition = "2018",
+)
+
+# Clippy analysis of passing targets.
+
+rust_clippy(
+    name = "ok_binary_clippy",
+    deps = [":ok_binary"],
+)
+
+rust_clippy(
+    name = "ok_library_clippy",
+    deps = [":ok_library"],
+)
+
+rust_clippy(
+    name = "ok_test_clippy",
+    deps = [":ok_test"],
+    testonly = True,
+)
+
+# Declaration of failing targets.
+
+rust_binary(
+    name = "bad_binary",
+    srcs = ["bad_src/main.rs"],
+    edition = "2018",
+)
+
+rust_library(
+    name = "bad_library",
+    srcs = ["bad_src/lib.rs"],
+    edition = "2018",
+)
+
+rust_test(
+    name = "bad_test",
+    srcs = ["bad_src/lib.rs"],
+    edition = "2018",
+)
+
+# Clippy analysis of failing targets.
+
+rust_clippy(
+    name = "bad_binary_clippy",
+    deps = [":bad_binary"],
+    tags = [ "manual" ],
+)
+
+rust_clippy(
+    name = "bad_library_clippy",
+    deps = [":bad_library"],
+    tags = [ "manual" ],
+)
+
+rust_clippy(
+    name = "bad_test_clippy",
+    deps = [":bad_test"],
+    tags = [ "manual" ],
+    testonly = True,
+)
diff --git a/test/clippy/README.md b/test/clippy/README.md
new file mode 100644
index 0000000..40db34f
--- /dev/null
+++ b/test/clippy/README.md
@@ -0,0 +1,12 @@
+# Clippy Tests
+
+This directory tests integration of the Clippy static analyzer aspect.
+
+It is split into a couple different directories:
+
+* [src](src) contains a simple binary, library and test which should compile
+successfully when built normally, and pass without error when analyzed
+with clippy.
+* [bad\_src](bad_src) also contains a binary, library, and test which compile
+normally, but which intentionally contain "junk code" which will trigger
+Clippy lints.
diff --git a/test/clippy/bad_src/lib.rs b/test/clippy/bad_src/lib.rs
new file mode 100644
index 0000000..ec29e04
--- /dev/null
+++ b/test/clippy/bad_src/lib.rs
@@ -0,0 +1,19 @@
+pub fn foobar() {
+    assert!(true);
+    loop {
+        println!("{}", "Hello World");
+        break;
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    #[test]
+    fn test_works() {
+        assert!(true);
+        loop {
+            println!("{}", "Hello World");
+            break;
+        }
+    }
+}
diff --git a/test/clippy/bad_src/main.rs b/test/clippy/bad_src/main.rs
new file mode 100644
index 0000000..cf9d4e6
--- /dev/null
+++ b/test/clippy/bad_src/main.rs
@@ -0,0 +1,6 @@
+fn main() {
+    loop {
+        println!("{}", "Hello World");
+        break;
+    }
+}
diff --git a/test/clippy/clippy_failure_test.sh b/test/clippy/clippy_failure_test.sh
new file mode 100755
index 0000000..b6c0002
--- /dev/null
+++ b/test/clippy/clippy_failure_test.sh
@@ -0,0 +1,42 @@
+#!/bin/bash
+
+# Runs Bazel build commands over clippy rules, where some are expected
+# to fail.
+#
+# Can be run from anywhere within the rules_rust workspace.
+
+set -euo pipefail
+
+# Executes a bazel build command and handles the return value, exiting
+# upon seeing an error.
+#
+# Takes two arguments:
+# ${1}: The expected return code.
+# ${2}: The target within "//test/clippy" to be tested.
+function check_build_result() {
+  local ret=0
+  echo -n "Testing ${2}... "
+  (bazel build //test/clippy:"${2}" &> /dev/null) || ret="$?" && true
+  if [[ "${ret}" -ne "${1}" ]]; then
+    echo "FAIL: Unexpected return code [saw: ${ret}, want: ${1}] building target //test/clippy:${2}"
+    echo "  Run \"bazel build //test/clippy:${2}\" to see the output"
+    exit 1
+  else
+    echo "OK"
+  fi
+}
+
+function test_all() {
+  local -r BUILD_OK=0
+  local -r BUILD_FAILED=1
+  local -r TEST_FAIL=3
+
+  check_build_result $BUILD_OK ok_binary_clippy
+  check_build_result $BUILD_OK ok_library_clippy
+  check_build_result $BUILD_OK ok_test_clippy
+  check_build_result $BUILD_FAILED bad_binary_clippy
+  check_build_result $BUILD_FAILED bad_library_clippy
+  check_build_result $BUILD_FAILED bad_test_clippy
+}
+
+test_all
diff --git a/test/clippy/src/lib.rs b/test/clippy/src/lib.rs
new file mode 100644
index 0000000..63fb7fd
--- /dev/null
+++ b/test/clippy/src/lib.rs
@@ -0,0 +1,11 @@
+pub fn foo() {
+    println!("Hello world");
+}
+
+#[cfg(test)]
+mod tests {
+    #[test]
+    fn it_works() {
+        assert_eq!(2 + 2, 4);
+    }
+}
diff --git a/test/clippy/src/main.rs b/test/clippy/src/main.rs
new file mode 100644
index 0000000..5bf256e
--- /dev/null
+++ b/test/clippy/src/main.rs
@@ -0,0 +1,3 @@
+fn main() {
+    println!("Hello world");
+}