Respect `--instrumentation_filer` for Rust coverage (#4173)

re-implements https://github.com/bazelbuild/rules_rust/pull/4013

Full credits to @tamasvajk. Thank you so much!
diff --git a/.bazelci/presubmit.yml b/.bazelci/presubmit.yml
index 5620cba..60c7448 100644
--- a/.bazelci/presubmit.yml
+++ b/.bazelci/presubmit.yml
@@ -87,14 +87,20 @@
   - echo "coverage --experimental_fetch_all_coverage_outputs" >> user.bazelrc
   - echo "coverage --experimental_split_coverage_postprocessing" >> user.bazelrc
   - echo "build --//rust/settings:experimental_use_coverage_metadata_files" >> user.bazelrc
+coverage_flags: &coverage_flags
+  - "--instrumentation_filter=^//"
+  - "--instrument_test_targets"
 rbe_coverage_flags: &rbe_coverage_flags
   # https://github.com/bazelbuild/bazel/issues/20578
   - "--strategy=CoverageReport=local"
+  - "--instrumentation_filter=^//"
+  - "--instrument_test_targets"
 tasks:
   ubuntu2204:
     build_targets: *default_linux_targets
     test_targets: *default_linux_targets
     coverage_targets: *default_linux_targets
+    coverage_flags: *coverage_flags
     post_shell_commands: *coverage_validation_post_shell_commands
     run_targets:
       - //test:query_test_binary
@@ -112,6 +118,7 @@
     build_targets: *default_macos_targets
     test_targets: *default_macos_targets
     coverage_targets: *default_macos_targets
+    coverage_flags: *coverage_flags
     post_shell_commands: *coverage_validation_post_shell_commands
   windows:
     build_targets: *default_windows_targets
@@ -129,12 +136,14 @@
     platform: ubuntu2204
     shell_commands: *split_coverage_postprocessing_shell_commands
     coverage_targets: *default_linux_targets
+    coverage_flags: *coverage_flags
     post_shell_commands: *coverage_validation_post_shell_commands
   macos_split_coverage_postprocessing:
     name: Split Coverage Postprocessing
     platform: macos_arm64
     shell_commands: *split_coverage_postprocessing_shell_commands
     coverage_targets: *default_macos_targets
+    coverage_flags: *coverage_flags
     post_shell_commands: *coverage_validation_post_shell_commands
   ubuntu2204_opt:
     name: Opt Mode
@@ -170,6 +179,7 @@
     build_targets: *default_linux_targets
     test_targets: *default_linux_targets
     coverage_targets: *default_linux_targets
+    coverage_flags: *coverage_flags
     post_shell_commands: *coverage_validation_post_shell_commands
   rbe_ubuntu2204_with_aspects:
     name: With Aspects
@@ -204,6 +214,7 @@
     build_targets: *default_macos_targets
     test_targets: *default_macos_targets
     coverage_targets: *default_macos_targets
+    coverage_flags: *coverage_flags
     post_shell_commands: *coverage_validation_post_shell_commands
   macos_rolling_with_aspects:
     name: "Macos Rolling Bazel Version With Aspects"
@@ -212,6 +223,7 @@
     build_targets: *default_macos_targets
     test_targets: *default_macos_targets
     coverage_targets: *default_macos_targets
+    coverage_flags: *coverage_flags
     post_shell_commands: *coverage_validation_post_shell_commands
     soft_fail: yes
     bazel: "rolling"
@@ -371,6 +383,7 @@
     build_targets: *default_linux_targets
     test_targets: *default_linux_targets
     coverage_targets: *default_linux_targets
+    coverage_flags: *coverage_flags
     post_shell_commands: *coverage_validation_post_shell_commands
   linux_docs:
     name: Docs
@@ -436,6 +449,7 @@
     build_targets: *default_linux_targets
     test_targets: *default_linux_targets
     coverage_targets: *default_linux_targets
+    coverage_flags: *coverage_flags
     post_shell_commands: *coverage_validation_post_shell_commands
   path_mapping_rbe_ubuntu2204:
     name: Path Mapping RBE
@@ -470,6 +484,7 @@
     build_targets: *default_macos_targets
     test_targets: *default_macos_targets
     coverage_targets: *default_macos_targets
+    coverage_flags: *coverage_flags
     post_shell_commands: *coverage_validation_post_shell_commands
   # TODO: path_mapping requires sandboxing
   # https://github.com/bazelbuild/bazel/issues/7480
diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md
index 83897a6..4d30988 100644
--- a/docs/src/SUMMARY.md
+++ b/docs/src/SUMMARY.md
@@ -19,6 +19,7 @@
 - [Bzlmod](./rust_bzlmod.md)
 - [External Crates](./external_crates.md)
     - [crate_universe](crate_universe_bzlmod.md)
+- [Code Coverage](./coverage.md)
 - [Upstream Tooling](./upstream_tooling.md)
 - [IDE Integrations](./ide_integrations.md)
 - [Extensions](./extensions.md)
diff --git a/docs/src/coverage.md b/docs/src/coverage.md
new file mode 100644
index 0000000..ed2a62f
--- /dev/null
+++ b/docs/src/coverage.md
@@ -0,0 +1,51 @@
+# Code Coverage
+
+Rules Rust supports collecting code coverage data using `bazel coverage`, leveraging LLVM's source-based coverage instrumentation.
+
+## Basic Usage
+
+```sh
+bazel coverage //my_project/...
+```
+
+This instruments all targets matching the default `--instrumentation_filter` and produces an LCOV report at `bazel-out/_coverage/_coverage_report.dat`.
+
+## Controlling Instrumentation
+
+Rules Rust respects Bazel's standard [`--instrumentation_filter`](https://bazel.build/reference/command-line-reference#flag--instrumentation_filter) flag to control which targets get compiled with `-Cinstrument-coverage`. Only targets whose label matches the filter are instrumented, consistent with how coverage works for other languages (C++, Java).
+
+### Recommended `.bazelrc` Settings
+
+For projects with vendored or third-party dependencies, restrict instrumentation to workspace targets to avoid unnecessary recompilation:
+
+```text
+coverage --instrumentation_filter=^//
+```
+
+To further exclude specific directories:
+
+```text
+coverage --instrumentation_filter=^//,-^//third_party
+```
+
+### `rust_test` with a `crate` attribute
+
+When a `rust_test` target uses the `crate` attribute, Rust compiles the library source code directly into the test binary. Rules Rust automatically checks whether the underlying crate should be instrumented, so library code compiled into the test binary produces coverage data without needing `--instrument_test_targets`.
+
+Note: because the entire crate (including `#[cfg(test)]` code) is compiled as one unit, test-specific code in the crate will also be instrumented. This is a known inconsistency with the usual Bazel convention where test code is only instrumented when `--instrument_test_targets` is set.
+
+### `--instrument_test_targets`
+
+By default, Bazel excludes test targets from instrumentation. If you want coverage of the test code itself (not just the libraries it exercises), add:
+
+```text
+coverage --instrument_test_targets
+```
+
+### Flags Summary
+
+| Flag | Purpose |
+|------|---------|
+| `--instrumentation_filter=<regex>` | Controls which targets are instrumented. Default: `-/javatests[/:],-/test/java[/:]` |
+| `--instrument_test_targets` | Also instrument test targets. Not required for library coverage via `rust_test` with `crate`. |
+| `--combined_report=lcov` | Produce a combined LCOV report (set by default with `bazel coverage`). |
diff --git a/rust/private/rustc.bzl b/rust/private/rustc.bzl
index a41a8fb..ef902f7 100644
--- a/rust/private/rustc.bzl
+++ b/rust/private/rustc.bzl
@@ -1359,7 +1359,17 @@
         rustc_flags.add("--extern")
         rustc_flags.add("proc_macro")
 
-    if toolchain.coverage_supported and ctx.configuration.coverage_enabled:
+    # Use Bazel's standard instrumentation filter (--instrumentation_filter)
+    # so that only targets matching the filter get instrumented, consistent
+    # with how coverage works for other languages (Java, C++).
+    # For rust_test targets with a `crate` attribute, also check if the
+    # underlying crate should be instrumented. Rust compiles the crate
+    # sources directly into the test binary, so the test must be built
+    # with -Cinstrument-coverage for the crate's code to produce coverage.
+    is_coverage_instrumented = ctx.coverage_instrumented()
+    if not is_coverage_instrumented and crate_info.is_test and hasattr(ctx.attr, "crate") and ctx.attr.crate:
+        is_coverage_instrumented = ctx.coverage_instrumented(ctx.attr.crate)
+    if toolchain.coverage_supported and ctx.configuration.coverage_enabled and is_coverage_instrumented:
         # https://doc.rust-lang.org/rustc/instrument-coverage.html
         rustc_flags.add("--codegen=instrument-coverage")
 
diff --git a/util/collect_coverage/collect_coverage.rs b/util/collect_coverage/collect_coverage.rs
index 06097ea..7fbe08a 100644
--- a/util/collect_coverage/collect_coverage.rs
+++ b/util/collect_coverage/collect_coverage.rs
@@ -166,6 +166,16 @@
         })
         .collect();
 
+    // A test that ran without `-Cinstrument-coverage` (e.g. filtered out by
+    // `--instrumentation_filter` or a `rust_test` without `--instrument_test_targets`)
+    // produces no `.profraw` files. Skip the merge step and emit an empty lcov
+    // report instead of letting `llvm-profdata merge` fail on empty input.
+    if profraw_files.is_empty() {
+        debug_log!("No .profraw files in COVERAGE_DIR; writing empty report");
+        fs::write(&coverage_output_file, "").unwrap();
+        return;
+    }
+
     let mut llvm_profdata_cmd = process::Command::new(llvm_profdata);
     llvm_profdata_cmd
         .arg("merge")