diff --git a/compiler/rustc_mir_transform/src/coverage/mod.rs b/compiler/rustc_mir_transform/src/coverage/mod.rs
index d55bde311c1..2efca40d180 100644
--- a/compiler/rustc_mir_transform/src/coverage/mod.rs
+++ b/compiler/rustc_mir_transform/src/coverage/mod.rs
@@ -8,6 +8,10 @@ mod spans;
 mod tests;
 mod unexpand;
 
+use rustc_hir as hir;
+use rustc_hir::intravisit::{walk_expr, Visitor};
+use rustc_middle::hir::map::Map;
+use rustc_middle::hir::nested_filter;
 use rustc_middle::mir::coverage::{
     CodeRegion, CoverageKind, DecisionInfo, FunctionCoverageInfo, Mapping, MappingKind,
 };
@@ -465,6 +469,9 @@ struct ExtractedHirInfo {
     /// Must have the same context and filename as the body span.
     fn_sig_span_extended: Option<Span>,
     body_span: Span,
+    /// "Holes" are regions within the body span that should not be included in
+    /// coverage spans for this function (e.g. closures and nested items).
+    hole_spans: Vec<Span>,
 }
 
 fn extract_hir_info<'tcx>(tcx: TyCtxt<'tcx>, def_id: LocalDefId) -> ExtractedHirInfo {
@@ -480,7 +487,7 @@ fn extract_hir_info<'tcx>(tcx: TyCtxt<'tcx>, def_id: LocalDefId) -> ExtractedHir
 
     let mut body_span = hir_body.value.span;
 
-    use rustc_hir::{Closure, Expr, ExprKind, Node};
+    use hir::{Closure, Expr, ExprKind, Node};
     // Unexpand a closure's body span back to the context of its declaration.
     // This helps with closure bodies that consist of just a single bang-macro,
     // and also with closure bodies produced by async desugaring.
@@ -507,11 +514,78 @@ fn extract_hir_info<'tcx>(tcx: TyCtxt<'tcx>, def_id: LocalDefId) -> ExtractedHir
 
     let function_source_hash = hash_mir_source(tcx, hir_body);
 
-    ExtractedHirInfo { function_source_hash, is_async_fn, fn_sig_span_extended, body_span }
+    let hole_spans = extract_hole_spans_from_hir(tcx, body_span, hir_body);
+
+    ExtractedHirInfo {
+        function_source_hash,
+        is_async_fn,
+        fn_sig_span_extended,
+        body_span,
+        hole_spans,
+    }
 }
 
-fn hash_mir_source<'tcx>(tcx: TyCtxt<'tcx>, hir_body: &'tcx rustc_hir::Body<'tcx>) -> u64 {
+fn hash_mir_source<'tcx>(tcx: TyCtxt<'tcx>, hir_body: &'tcx hir::Body<'tcx>) -> u64 {
     // FIXME(cjgillot) Stop hashing HIR manually here.
     let owner = hir_body.id().hir_id.owner;
     tcx.hir_owner_nodes(owner).opt_hash_including_bodies.unwrap().to_smaller_hash().as_u64()
 }
+
+fn extract_hole_spans_from_hir<'tcx>(
+    tcx: TyCtxt<'tcx>,
+    body_span: Span, // Usually `hir_body.value.span`, but not always
+    hir_body: &hir::Body<'tcx>,
+) -> Vec<Span> {
+    struct HolesVisitor<'hir, F> {
+        hir: Map<'hir>,
+        visit_hole_span: F,
+    }
+
+    impl<'hir, F: FnMut(Span)> Visitor<'hir> for HolesVisitor<'hir, F> {
+        /// - We need `NestedFilter::INTRA = true` so that `visit_item` will be called.
+        /// - Bodies of nested items don't actually get visited, because of the
+        ///   `visit_item` override.
+        /// - For nested bodies that are not part of an item, we do want to visit any
+        ///   items contained within them.
+        type NestedFilter = nested_filter::All;
+
+        fn nested_visit_map(&mut self) -> Self::Map {
+            self.hir
+        }
+
+        fn visit_item(&mut self, item: &'hir hir::Item<'hir>) {
+            (self.visit_hole_span)(item.span);
+            // Having visited this item, we don't care about its children,
+            // so don't call `walk_item`.
+        }
+
+        // We override `visit_expr` instead of the more specific expression
+        // visitors, so that we have direct access to the expression span.
+        fn visit_expr(&mut self, expr: &'hir hir::Expr<'hir>) {
+            match expr.kind {
+                hir::ExprKind::Closure(_) | hir::ExprKind::ConstBlock(_) => {
+                    (self.visit_hole_span)(expr.span);
+                    // Having visited this expression, we don't care about its
+                    // children, so don't call `walk_expr`.
+                }
+
+                // For other expressions, recursively visit as normal.
+                _ => walk_expr(self, expr),
+            }
+        }
+    }
+
+    let mut hole_spans = vec![];
+    let mut visitor = HolesVisitor {
+        hir: tcx.hir(),
+        visit_hole_span: |hole_span| {
+            // Discard any holes that aren't directly visible within the body span.
+            if body_span.contains(hole_span) && body_span.eq_ctxt(hole_span) {
+                hole_spans.push(hole_span);
+            }
+        },
+    };
+
+    visitor.visit_body(hir_body);
+    hole_spans
+}
diff --git a/compiler/rustc_mir_transform/src/coverage/spans.rs b/compiler/rustc_mir_transform/src/coverage/spans.rs
index 7612c01c52e..dbc26a2808e 100644
--- a/compiler/rustc_mir_transform/src/coverage/spans.rs
+++ b/compiler/rustc_mir_transform/src/coverage/spans.rs
@@ -8,7 +8,7 @@ use rustc_span::Span;
 use crate::coverage::graph::{BasicCoverageBlock, CoverageGraph};
 use crate::coverage::mappings;
 use crate::coverage::spans::from_mir::{
-    extract_covspans_and_holes_from_mir, ExtractedCovspans, Hole, SpanFromMir,
+    extract_covspans_from_mir, ExtractedCovspans, Hole, SpanFromMir,
 };
 use crate::coverage::ExtractedHirInfo;
 
@@ -20,8 +20,8 @@ pub(super) fn extract_refined_covspans(
     basic_coverage_blocks: &CoverageGraph,
     code_mappings: &mut impl Extend<mappings::CodeMapping>,
 ) {
-    let ExtractedCovspans { mut covspans, mut holes } =
-        extract_covspans_and_holes_from_mir(mir_body, hir_info, basic_coverage_blocks);
+    let ExtractedCovspans { mut covspans } =
+        extract_covspans_from_mir(mir_body, hir_info, basic_coverage_blocks);
 
     // First, perform the passes that need macro information.
     covspans.sort_by(|a, b| basic_coverage_blocks.cmp_in_dominator_order(a.bcb, b.bcb));
@@ -45,6 +45,7 @@ pub(super) fn extract_refined_covspans(
     covspans.dedup_by(|b, a| a.span.source_equal(b.span));
 
     // Sort the holes, and merge overlapping/adjacent holes.
+    let mut holes = hir_info.hole_spans.iter().map(|&span| Hole { span }).collect::<Vec<_>>();
     holes.sort_by(|a, b| compare_spans(a.span, b.span));
     holes.dedup_by(|b, a| a.merge_if_overlapping_or_adjacent(b));
 
diff --git a/compiler/rustc_mir_transform/src/coverage/spans/from_mir.rs b/compiler/rustc_mir_transform/src/coverage/spans/from_mir.rs
index a0f8f580b1d..32bd25bf4b9 100644
--- a/compiler/rustc_mir_transform/src/coverage/spans/from_mir.rs
+++ b/compiler/rustc_mir_transform/src/coverage/spans/from_mir.rs
@@ -1,8 +1,7 @@
 use rustc_middle::bug;
 use rustc_middle::mir::coverage::CoverageKind;
 use rustc_middle::mir::{
-    self, AggregateKind, FakeReadCause, Rvalue, Statement, StatementKind, Terminator,
-    TerminatorKind,
+    self, FakeReadCause, Statement, StatementKind, Terminator, TerminatorKind,
 };
 use rustc_span::{Span, Symbol};
 
@@ -15,13 +14,12 @@ use crate::coverage::ExtractedHirInfo;
 
 pub(crate) struct ExtractedCovspans {
     pub(crate) covspans: Vec<SpanFromMir>,
-    pub(crate) holes: Vec<Hole>,
 }
 
 /// Traverses the MIR body to produce an initial collection of coverage-relevant
 /// spans, each associated with a node in the coverage graph (BCB) and possibly
 /// other metadata.
-pub(crate) fn extract_covspans_and_holes_from_mir(
+pub(crate) fn extract_covspans_from_mir(
     mir_body: &mir::Body<'_>,
     hir_info: &ExtractedHirInfo,
     basic_coverage_blocks: &CoverageGraph,
@@ -29,21 +27,13 @@ pub(crate) fn extract_covspans_and_holes_from_mir(
     let &ExtractedHirInfo { body_span, .. } = hir_info;
 
     let mut covspans = vec![];
-    let mut holes = vec![];
 
     for (bcb, bcb_data) in basic_coverage_blocks.iter_enumerated() {
-        bcb_to_initial_coverage_spans(
-            mir_body,
-            body_span,
-            bcb,
-            bcb_data,
-            &mut covspans,
-            &mut holes,
-        );
+        bcb_to_initial_coverage_spans(mir_body, body_span, bcb, bcb_data, &mut covspans);
     }
 
     // Only add the signature span if we found at least one span in the body.
-    if !covspans.is_empty() || !holes.is_empty() {
+    if !covspans.is_empty() {
         // If there is no usable signature span, add a fake one (before refinement)
         // to avoid an ugly gap between the body start and the first real span.
         // FIXME: Find a more principled way to solve this problem.
@@ -51,7 +41,7 @@ pub(crate) fn extract_covspans_and_holes_from_mir(
         covspans.push(SpanFromMir::for_fn_sig(fn_sig_span));
     }
 
-    ExtractedCovspans { covspans, holes }
+    ExtractedCovspans { covspans }
 }
 
 // Generate a set of coverage spans from the filtered set of `Statement`s and `Terminator`s of
@@ -65,7 +55,6 @@ fn bcb_to_initial_coverage_spans<'a, 'tcx>(
     bcb: BasicCoverageBlock,
     bcb_data: &'a BasicCoverageBlockData,
     initial_covspans: &mut Vec<SpanFromMir>,
-    holes: &mut Vec<Hole>,
 ) {
     for &bb in &bcb_data.basic_blocks {
         let data = &mir_body[bb];
@@ -81,13 +70,7 @@ fn bcb_to_initial_coverage_spans<'a, 'tcx>(
             let expn_span = filtered_statement_span(statement)?;
             let (span, visible_macro) = unexpand(expn_span)?;
 
-            // A statement that looks like the assignment of a closure expression
-            // is treated as a "hole" span, to be carved out of other spans.
-            if is_closure_like(statement) {
-                holes.push(Hole { span });
-            } else {
-                initial_covspans.push(SpanFromMir::new(span, visible_macro, bcb));
-            }
+            initial_covspans.push(SpanFromMir::new(span, visible_macro, bcb));
             Some(())
         };
         for statement in data.statements.iter() {
@@ -105,18 +88,6 @@ fn bcb_to_initial_coverage_spans<'a, 'tcx>(
     }
 }
 
-fn is_closure_like(statement: &Statement<'_>) -> bool {
-    match statement.kind {
-        StatementKind::Assign(box (_, Rvalue::Aggregate(box ref agg_kind, _))) => match agg_kind {
-            AggregateKind::Closure(_, _)
-            | AggregateKind::Coroutine(_, _)
-            | AggregateKind::CoroutineClosure(..) => true,
-            _ => false,
-        },
-        _ => false,
-    }
-}
-
 /// If the MIR `Statement` has a span contributive to computing coverage spans,
 /// return it; otherwise return `None`.
 fn filtered_statement_span(statement: &Statement<'_>) -> Option<Span> {
diff --git a/tests/coverage-run-rustdoc/doctest.coverage b/tests/coverage-run-rustdoc/doctest.coverage
index 1bbf364759b..396811c5487 100644
--- a/tests/coverage-run-rustdoc/doctest.coverage
+++ b/tests/coverage-run-rustdoc/doctest.coverage
@@ -34,10 +34,10 @@ $DIR/doctest.rs:
    LL|       |//!
    LL|       |//! doctest returning a result:
    LL|      1|//! ```
-   LL|      1|//! #[derive(Debug, PartialEq)]
-   LL|      1|//! struct SomeError {
-   LL|      1|//!     msg: String,
-   LL|      1|//! }
+   LL|       |//! #[derive(Debug, PartialEq)]
+   LL|       |//! struct SomeError {
+   LL|       |//!     msg: String,
+   LL|       |//! }
    LL|      1|//! let mut res = Err(SomeError { msg: String::from("a message") });
    LL|      1|//! if res.is_ok() {
    LL|      0|//!     res?;
diff --git a/tests/coverage/async.cov-map b/tests/coverage/async.cov-map
index 7d16372375a..9e5a4bdc60f 100644
--- a/tests/coverage/async.cov-map
+++ b/tests/coverage/async.cov-map
@@ -167,15 +167,16 @@ Number of file 0 mappings: 14
     = ((c6 + c7) + c8)
 
 Function name: async::j
-Raw bytes (53): 0x[01, 01, 02, 07, 0d, 05, 09, 09, 01, 35, 01, 13, 0c, 05, 14, 09, 00, 0a, 01, 00, 0e, 00, 1b, 05, 00, 1f, 00, 27, 09, 01, 09, 00, 0a, 11, 00, 0e, 00, 1a, 09, 00, 1e, 00, 20, 0d, 01, 0e, 00, 10, 03, 02, 01, 00, 02]
+Raw bytes (58): 0x[01, 01, 02, 07, 0d, 05, 09, 0a, 01, 35, 01, 00, 0d, 01, 0b, 0b, 00, 0c, 05, 01, 09, 00, 0a, 01, 00, 0e, 00, 1b, 05, 00, 1f, 00, 27, 09, 01, 09, 00, 0a, 11, 00, 0e, 00, 1a, 09, 00, 1e, 00, 20, 0d, 01, 0e, 00, 10, 03, 02, 01, 00, 02]
 Number of files: 1
 - file 0 => global file 1
 Number of expressions: 2
 - expression 0 operands: lhs = Expression(1, Add), rhs = Counter(3)
 - expression 1 operands: lhs = Counter(1), rhs = Counter(2)
-Number of file 0 mappings: 9
-- Code(Counter(0)) at (prev + 53, 1) to (start + 19, 12)
-- Code(Counter(1)) at (prev + 20, 9) to (start + 0, 10)
+Number of file 0 mappings: 10
+- Code(Counter(0)) at (prev + 53, 1) to (start + 0, 13)
+- Code(Counter(0)) at (prev + 11, 11) to (start + 0, 12)
+- Code(Counter(1)) at (prev + 1, 9) to (start + 0, 10)
 - Code(Counter(0)) at (prev + 0, 14) to (start + 0, 27)
 - Code(Counter(1)) at (prev + 0, 31) to (start + 0, 39)
 - Code(Counter(2)) at (prev + 1, 9) to (start + 0, 10)
@@ -186,7 +187,7 @@ Number of file 0 mappings: 9
     = ((c1 + c2) + c3)
 
 Function name: async::j::c
-Raw bytes (26): 0x[01, 01, 01, 01, 05, 04, 01, 37, 05, 01, 12, 05, 02, 0d, 00, 0e, 02, 0a, 0d, 00, 0e, 01, 02, 05, 00, 06]
+Raw bytes (26): 0x[01, 01, 01, 01, 05, 04, 01, 37, 05, 01, 12, 05, 02, 0d, 00, 0e, 02, 02, 0d, 00, 0e, 01, 02, 05, 00, 06]
 Number of files: 1
 - file 0 => global file 1
 Number of expressions: 1
@@ -194,40 +195,40 @@ Number of expressions: 1
 Number of file 0 mappings: 4
 - Code(Counter(0)) at (prev + 55, 5) to (start + 1, 18)
 - Code(Counter(1)) at (prev + 2, 13) to (start + 0, 14)
-- Code(Expression(0, Sub)) at (prev + 10, 13) to (start + 0, 14)
+- Code(Expression(0, Sub)) at (prev + 2, 13) to (start + 0, 14)
     = (c0 - c1)
 - Code(Counter(0)) at (prev + 2, 5) to (start + 0, 6)
 
 Function name: async::j::d
-Raw bytes (9): 0x[01, 01, 00, 01, 01, 46, 05, 00, 17]
+Raw bytes (9): 0x[01, 01, 00, 01, 01, 3e, 05, 00, 17]
 Number of files: 1
 - file 0 => global file 1
 Number of expressions: 0
 Number of file 0 mappings: 1
-- Code(Counter(0)) at (prev + 70, 5) to (start + 0, 23)
+- Code(Counter(0)) at (prev + 62, 5) to (start + 0, 23)
 
 Function name: async::j::f
-Raw bytes (9): 0x[01, 01, 00, 01, 01, 47, 05, 00, 17]
+Raw bytes (9): 0x[01, 01, 00, 01, 01, 3f, 05, 00, 17]
 Number of files: 1
 - file 0 => global file 1
 Number of expressions: 0
 Number of file 0 mappings: 1
-- Code(Counter(0)) at (prev + 71, 5) to (start + 0, 23)
+- Code(Counter(0)) at (prev + 63, 5) to (start + 0, 23)
 
 Function name: async::k (unused)
-Raw bytes (29): 0x[01, 01, 00, 05, 00, 4f, 01, 01, 0c, 00, 02, 0e, 00, 10, 00, 01, 0e, 00, 10, 00, 01, 0e, 00, 10, 00, 02, 01, 00, 02]
+Raw bytes (29): 0x[01, 01, 00, 05, 00, 47, 01, 01, 0c, 00, 02, 0e, 00, 10, 00, 01, 0e, 00, 10, 00, 01, 0e, 00, 10, 00, 02, 01, 00, 02]
 Number of files: 1
 - file 0 => global file 1
 Number of expressions: 0
 Number of file 0 mappings: 5
-- Code(Zero) at (prev + 79, 1) to (start + 1, 12)
+- Code(Zero) at (prev + 71, 1) to (start + 1, 12)
 - Code(Zero) at (prev + 2, 14) to (start + 0, 16)
 - Code(Zero) at (prev + 1, 14) to (start + 0, 16)
 - Code(Zero) at (prev + 1, 14) to (start + 0, 16)
 - Code(Zero) at (prev + 2, 1) to (start + 0, 2)
 
 Function name: async::l
-Raw bytes (37): 0x[01, 01, 04, 01, 07, 05, 09, 0f, 02, 09, 05, 05, 01, 57, 01, 01, 0c, 02, 02, 0e, 00, 10, 05, 01, 0e, 00, 10, 09, 01, 0e, 00, 10, 0b, 02, 01, 00, 02]
+Raw bytes (37): 0x[01, 01, 04, 01, 07, 05, 09, 0f, 02, 09, 05, 05, 01, 4f, 01, 01, 0c, 02, 02, 0e, 00, 10, 05, 01, 0e, 00, 10, 09, 01, 0e, 00, 10, 0b, 02, 01, 00, 02]
 Number of files: 1
 - file 0 => global file 1
 Number of expressions: 4
@@ -236,7 +237,7 @@ Number of expressions: 4
 - expression 2 operands: lhs = Expression(3, Add), rhs = Expression(0, Sub)
 - expression 3 operands: lhs = Counter(2), rhs = Counter(1)
 Number of file 0 mappings: 5
-- Code(Counter(0)) at (prev + 87, 1) to (start + 1, 12)
+- Code(Counter(0)) at (prev + 79, 1) to (start + 1, 12)
 - Code(Expression(0, Sub)) at (prev + 2, 14) to (start + 0, 16)
     = (c0 - (c1 + c2))
 - Code(Counter(1)) at (prev + 1, 14) to (start + 0, 16)
@@ -245,26 +246,26 @@ Number of file 0 mappings: 5
     = ((c2 + c1) + (c0 - (c1 + c2)))
 
 Function name: async::m
-Raw bytes (9): 0x[01, 01, 00, 01, 01, 5f, 01, 00, 19]
+Raw bytes (9): 0x[01, 01, 00, 01, 01, 57, 01, 00, 19]
 Number of files: 1
 - file 0 => global file 1
 Number of expressions: 0
 Number of file 0 mappings: 1
-- Code(Counter(0)) at (prev + 95, 1) to (start + 0, 25)
+- Code(Counter(0)) at (prev + 87, 1) to (start + 0, 25)
 
 Function name: async::m::{closure#0} (unused)
-Raw bytes (9): 0x[01, 01, 00, 01, 00, 5f, 19, 00, 22]
+Raw bytes (9): 0x[01, 01, 00, 01, 00, 57, 19, 00, 22]
 Number of files: 1
 - file 0 => global file 1
 Number of expressions: 0
 Number of file 0 mappings: 1
-- Code(Zero) at (prev + 95, 25) to (start + 0, 34)
+- Code(Zero) at (prev + 87, 25) to (start + 0, 34)
 
 Function name: async::main
-Raw bytes (9): 0x[01, 01, 00, 01, 01, 61, 01, 08, 02]
+Raw bytes (9): 0x[01, 01, 00, 01, 01, 59, 01, 08, 02]
 Number of files: 1
 - file 0 => global file 1
 Number of expressions: 0
 Number of file 0 mappings: 1
-- Code(Counter(0)) at (prev + 97, 1) to (start + 8, 2)
+- Code(Counter(0)) at (prev + 89, 1) to (start + 8, 2)
 
diff --git a/tests/coverage/async.coverage b/tests/coverage/async.coverage
index e943911d310..f5473829b02 100644
--- a/tests/coverage/async.coverage
+++ b/tests/coverage/async.coverage
@@ -53,25 +53,15 @@
    LL|      1|}
    LL|       |
    LL|      1|fn j(x: u8) {
-   LL|      1|    // non-async versions of `c()`, `d()`, and `f()` to make it similar to async `i()`.
+   LL|       |    // non-async versions of `c()`, `d()`, and `f()` to make it similar to async `i()`.
    LL|      1|    fn c(x: u8) -> u8 {
    LL|      1|        if x == 8 {
-   LL|      1|            1 // This line appears covered, but the 1-character expression span covering the `1`
-                          ^0
-   LL|      1|              // is not executed. (`llvm-cov show` displays a `^0` below the `1` ). This is because
-   LL|      1|              // `fn j()` executes the open brace for the function body, followed by the function's
-   LL|      1|              // first executable statement, `match x`. Inner function declarations are not
-   LL|      1|              // "visible" to the MIR for `j()`, so the code region counts all lines between the
-   LL|      1|              // open brace and the first statement as executed, which is, in a sense, true.
-   LL|      1|              // `llvm-cov show` overcomes this kind of situation by showing the actual counts
-   LL|      1|              // of the enclosed coverages, (that is, the `1` expression was not executed, and
-   LL|      1|              // accurately displays a `0`).
-   LL|      1|        } else {
+   LL|      0|            1
+   LL|       |        } else {
    LL|      1|            0
-   LL|      1|        }
+   LL|       |        }
    LL|      1|    }
-   LL|      1|    fn d() -> u8 { 1 } // inner function is defined in-line, but the function is not executed
-                  ^0
+   LL|      0|    fn d() -> u8 { 1 } // inner function is defined in-line, but the function is not executed
    LL|      1|    fn f() -> u8 { 1 }
    LL|      1|    match x {
    LL|      1|        y if c(x) == y + 1 => { d(); }
diff --git a/tests/coverage/async.rs b/tests/coverage/async.rs
index 5018ade0125..7e6ad761ecd 100644
--- a/tests/coverage/async.rs
+++ b/tests/coverage/async.rs
@@ -54,15 +54,7 @@ fn j(x: u8) {
     // non-async versions of `c()`, `d()`, and `f()` to make it similar to async `i()`.
     fn c(x: u8) -> u8 {
         if x == 8 {
-            1 // This line appears covered, but the 1-character expression span covering the `1`
-              // is not executed. (`llvm-cov show` displays a `^0` below the `1` ). This is because
-              // `fn j()` executes the open brace for the function body, followed by the function's
-              // first executable statement, `match x`. Inner function declarations are not
-              // "visible" to the MIR for `j()`, so the code region counts all lines between the
-              // open brace and the first statement as executed, which is, in a sense, true.
-              // `llvm-cov show` overcomes this kind of situation by showing the actual counts
-              // of the enclosed coverages, (that is, the `1` expression was not executed, and
-              // accurately displays a `0`).
+            1
         } else {
             0
         }
diff --git a/tests/coverage/attr/nested.cov-map b/tests/coverage/attr/nested.cov-map
index 0f2d5542f75..466aec8956e 100644
--- a/tests/coverage/attr/nested.cov-map
+++ b/tests/coverage/attr/nested.cov-map
@@ -1,18 +1,18 @@
 Function name: nested::closure_expr
-Raw bytes (14): 0x[01, 01, 00, 02, 01, 44, 01, 01, 0f, 01, 0b, 05, 01, 02]
+Raw bytes (14): 0x[01, 01, 00, 02, 01, 3f, 01, 01, 0f, 01, 0b, 05, 01, 02]
 Number of files: 1
 - file 0 => global file 1
 Number of expressions: 0
 Number of file 0 mappings: 2
-- Code(Counter(0)) at (prev + 68, 1) to (start + 1, 15)
+- Code(Counter(0)) at (prev + 63, 1) to (start + 1, 15)
 - Code(Counter(0)) at (prev + 11, 5) to (start + 1, 2)
 
 Function name: nested::closure_tail
-Raw bytes (14): 0x[01, 01, 00, 02, 01, 53, 01, 01, 0f, 01, 11, 05, 01, 02]
+Raw bytes (14): 0x[01, 01, 00, 02, 01, 4e, 01, 01, 0f, 01, 11, 05, 01, 02]
 Number of files: 1
 - file 0 => global file 1
 Number of expressions: 0
 Number of file 0 mappings: 2
-- Code(Counter(0)) at (prev + 83, 1) to (start + 1, 15)
+- Code(Counter(0)) at (prev + 78, 1) to (start + 1, 15)
 - Code(Counter(0)) at (prev + 17, 5) to (start + 1, 2)
 
diff --git a/tests/coverage/attr/nested.coverage b/tests/coverage/attr/nested.coverage
index bdd117b7dfa..2d64fe698ea 100644
--- a/tests/coverage/attr/nested.coverage
+++ b/tests/coverage/attr/nested.coverage
@@ -4,11 +4,6 @@
    LL|       |// Demonstrates the interaction between #[coverage(off)] and various kinds of
    LL|       |// nested function.
    LL|       |
-   LL|       |// FIXME(#126625): Coverage attributes should apply recursively to nested functions.
-   LL|       |// FIXME(#126626): When an inner (non-closure) function has `#[coverage(off)]`,
-   LL|       |// its lines can still be marked with misleading execution counts from its enclosing
-   LL|       |// function.
-   LL|       |
    LL|       |#[coverage(off)]
    LL|       |fn do_stuff() {}
    LL|       |
diff --git a/tests/coverage/attr/nested.rs b/tests/coverage/attr/nested.rs
index c7ff835f44f..8213e29b6fc 100644
--- a/tests/coverage/attr/nested.rs
+++ b/tests/coverage/attr/nested.rs
@@ -4,11 +4,6 @@
 // Demonstrates the interaction between #[coverage(off)] and various kinds of
 // nested function.
 
-// FIXME(#126625): Coverage attributes should apply recursively to nested functions.
-// FIXME(#126626): When an inner (non-closure) function has `#[coverage(off)]`,
-// its lines can still be marked with misleading execution counts from its enclosing
-// function.
-
 #[coverage(off)]
 fn do_stuff() {}
 
diff --git a/tests/coverage/attr/off-on-sandwich.cov-map b/tests/coverage/attr/off-on-sandwich.cov-map
index ed77d7d17e6..d5fbac6ebf7 100644
--- a/tests/coverage/attr/off-on-sandwich.cov-map
+++ b/tests/coverage/attr/off-on-sandwich.cov-map
@@ -1,24 +1,27 @@
 Function name: off_on_sandwich::dense_a::dense_b
-Raw bytes (9): 0x[01, 01, 00, 01, 01, 14, 05, 07, 06]
+Raw bytes (14): 0x[01, 01, 00, 02, 01, 0f, 05, 02, 12, 01, 07, 05, 00, 06]
 Number of files: 1
 - file 0 => global file 1
 Number of expressions: 0
-Number of file 0 mappings: 1
-- Code(Counter(0)) at (prev + 20, 5) to (start + 7, 6)
+Number of file 0 mappings: 2
+- Code(Counter(0)) at (prev + 15, 5) to (start + 2, 18)
+- Code(Counter(0)) at (prev + 7, 5) to (start + 0, 6)
 
 Function name: off_on_sandwich::sparse_a::sparse_b::sparse_c
-Raw bytes (9): 0x[01, 01, 00, 01, 01, 26, 09, 0b, 0a]
+Raw bytes (14): 0x[01, 01, 00, 02, 01, 21, 09, 02, 17, 01, 0b, 09, 00, 0a]
 Number of files: 1
 - file 0 => global file 1
 Number of expressions: 0
-Number of file 0 mappings: 1
-- Code(Counter(0)) at (prev + 38, 9) to (start + 11, 10)
+Number of file 0 mappings: 2
+- Code(Counter(0)) at (prev + 33, 9) to (start + 2, 23)
+- Code(Counter(0)) at (prev + 11, 9) to (start + 0, 10)
 
 Function name: off_on_sandwich::sparse_a::sparse_b::sparse_c::sparse_d
-Raw bytes (9): 0x[01, 01, 00, 01, 01, 29, 0d, 07, 0e]
+Raw bytes (14): 0x[01, 01, 00, 02, 01, 24, 0d, 02, 1b, 01, 07, 0d, 00, 0e]
 Number of files: 1
 - file 0 => global file 1
 Number of expressions: 0
-Number of file 0 mappings: 1
-- Code(Counter(0)) at (prev + 41, 13) to (start + 7, 14)
+Number of file 0 mappings: 2
+- Code(Counter(0)) at (prev + 36, 13) to (start + 2, 27)
+- Code(Counter(0)) at (prev + 7, 13) to (start + 0, 14)
 
diff --git a/tests/coverage/attr/off-on-sandwich.coverage b/tests/coverage/attr/off-on-sandwich.coverage
index 58c128b8342..675697906ee 100644
--- a/tests/coverage/attr/off-on-sandwich.coverage
+++ b/tests/coverage/attr/off-on-sandwich.coverage
@@ -4,11 +4,6 @@
    LL|       |// Demonstrates the interaction of `#[coverage(off)]` and `#[coverage(on)]`
    LL|       |// in nested functions.
    LL|       |
-   LL|       |// FIXME(#126625): Coverage attributes should apply recursively to nested functions.
-   LL|       |// FIXME(#126626): When an inner (non-closure) function has `#[coverage(off)]`,
-   LL|       |// its lines can still be marked with misleading execution counts from its enclosing
-   LL|       |// function.
-   LL|       |
    LL|       |#[coverage(off)]
    LL|       |fn do_stuff() {}
    LL|       |
@@ -20,10 +15,10 @@
    LL|      2|    fn dense_b() {
    LL|      2|        dense_c();
    LL|      2|        dense_c();
-   LL|      2|        #[coverage(off)]
-   LL|      2|        fn dense_c() {
-   LL|      2|            do_stuff();
-   LL|      2|        }
+   LL|       |        #[coverage(off)]
+   LL|       |        fn dense_c() {
+   LL|       |            do_stuff();
+   LL|       |        }
    LL|      2|    }
    LL|       |}
    LL|       |
@@ -41,10 +36,10 @@
    LL|      8|            fn sparse_d() {
    LL|      8|                sparse_e();
    LL|      8|                sparse_e();
-   LL|      8|                #[coverage(off)]
-   LL|      8|                fn sparse_e() {
-   LL|      8|                    do_stuff();
-   LL|      8|                }
+   LL|       |                #[coverage(off)]
+   LL|       |                fn sparse_e() {
+   LL|       |                    do_stuff();
+   LL|       |                }
    LL|      8|            }
    LL|      4|        }
    LL|       |    }
diff --git a/tests/coverage/attr/off-on-sandwich.rs b/tests/coverage/attr/off-on-sandwich.rs
index 6b21b180223..261634e0029 100644
--- a/tests/coverage/attr/off-on-sandwich.rs
+++ b/tests/coverage/attr/off-on-sandwich.rs
@@ -4,11 +4,6 @@
 // Demonstrates the interaction of `#[coverage(off)]` and `#[coverage(on)]`
 // in nested functions.
 
-// FIXME(#126625): Coverage attributes should apply recursively to nested functions.
-// FIXME(#126626): When an inner (non-closure) function has `#[coverage(off)]`,
-// its lines can still be marked with misleading execution counts from its enclosing
-// function.
-
 #[coverage(off)]
 fn do_stuff() {}
 
diff --git a/tests/coverage/closure_macro.cov-map b/tests/coverage/closure_macro.cov-map
index 156947f4e21..eb5f94d1080 100644
--- a/tests/coverage/closure_macro.cov-map
+++ b/tests/coverage/closure_macro.cov-map
@@ -7,15 +7,16 @@ Number of file 0 mappings: 1
 - Code(Counter(0)) at (prev + 29, 1) to (start + 2, 2)
 
 Function name: closure_macro::main
-Raw bytes (31): 0x[01, 01, 01, 01, 05, 05, 01, 21, 01, 01, 21, 02, 02, 09, 00, 0f, 05, 00, 54, 00, 55, 02, 02, 09, 02, 0b, 01, 03, 01, 00, 02]
+Raw bytes (36): 0x[01, 01, 01, 01, 05, 06, 01, 21, 01, 01, 21, 02, 02, 09, 00, 0f, 01, 00, 12, 00, 54, 05, 00, 54, 00, 55, 02, 02, 09, 02, 0b, 01, 03, 01, 00, 02]
 Number of files: 1
 - file 0 => global file 1
 Number of expressions: 1
 - expression 0 operands: lhs = Counter(0), rhs = Counter(1)
-Number of file 0 mappings: 5
+Number of file 0 mappings: 6
 - Code(Counter(0)) at (prev + 33, 1) to (start + 1, 33)
 - Code(Expression(0, Sub)) at (prev + 2, 9) to (start + 0, 15)
     = (c0 - c1)
+- Code(Counter(0)) at (prev + 0, 18) to (start + 0, 84)
 - Code(Counter(1)) at (prev + 0, 84) to (start + 0, 85)
 - Code(Expression(0, Sub)) at (prev + 2, 9) to (start + 2, 11)
     = (c0 - c1)
diff --git a/tests/coverage/closure_macro_async.cov-map b/tests/coverage/closure_macro_async.cov-map
index 0f2b4e01748..1286d663bd4 100644
--- a/tests/coverage/closure_macro_async.cov-map
+++ b/tests/coverage/closure_macro_async.cov-map
@@ -15,15 +15,16 @@ Number of file 0 mappings: 1
 - Code(Counter(0)) at (prev + 35, 1) to (start + 0, 43)
 
 Function name: closure_macro_async::test::{closure#0}
-Raw bytes (31): 0x[01, 01, 01, 01, 05, 05, 01, 23, 2b, 01, 21, 02, 02, 09, 00, 0f, 05, 00, 54, 00, 55, 02, 02, 09, 02, 0b, 01, 03, 01, 00, 02]
+Raw bytes (36): 0x[01, 01, 01, 01, 05, 06, 01, 23, 2b, 01, 21, 02, 02, 09, 00, 0f, 01, 00, 12, 00, 54, 05, 00, 54, 00, 55, 02, 02, 09, 02, 0b, 01, 03, 01, 00, 02]
 Number of files: 1
 - file 0 => global file 1
 Number of expressions: 1
 - expression 0 operands: lhs = Counter(0), rhs = Counter(1)
-Number of file 0 mappings: 5
+Number of file 0 mappings: 6
 - Code(Counter(0)) at (prev + 35, 43) to (start + 1, 33)
 - Code(Expression(0, Sub)) at (prev + 2, 9) to (start + 0, 15)
     = (c0 - c1)
+- Code(Counter(0)) at (prev + 0, 18) to (start + 0, 84)
 - Code(Counter(1)) at (prev + 0, 84) to (start + 0, 85)
 - Code(Expression(0, Sub)) at (prev + 2, 9) to (start + 2, 11)
     = (c0 - c1)
diff --git a/tests/coverage/holes.cov-map b/tests/coverage/holes.cov-map
new file mode 100644
index 00000000000..9350bd9a405
--- /dev/null
+++ b/tests/coverage/holes.cov-map
@@ -0,0 +1,47 @@
+Function name: <holes::main::MyStruct>::_method (unused)
+Raw bytes (9): 0x[01, 01, 00, 01, 00, 25, 09, 00, 1d]
+Number of files: 1
+- file 0 => global file 1
+Number of expressions: 0
+Number of file 0 mappings: 1
+- Code(Zero) at (prev + 37, 9) to (start + 0, 29)
+
+Function name: holes::main
+Raw bytes (44): 0x[01, 01, 00, 08, 01, 08, 01, 06, 11, 01, 0f, 05, 00, 12, 01, 04, 05, 00, 12, 01, 07, 05, 00, 12, 01, 06, 05, 00, 12, 01, 06, 05, 03, 0f, 01, 0a, 05, 03, 0f, 01, 0a, 05, 01, 02]
+Number of files: 1
+- file 0 => global file 1
+Number of expressions: 0
+Number of file 0 mappings: 8
+- Code(Counter(0)) at (prev + 8, 1) to (start + 6, 17)
+- Code(Counter(0)) at (prev + 15, 5) to (start + 0, 18)
+- Code(Counter(0)) at (prev + 4, 5) to (start + 0, 18)
+- Code(Counter(0)) at (prev + 7, 5) to (start + 0, 18)
+- Code(Counter(0)) at (prev + 6, 5) to (start + 0, 18)
+- Code(Counter(0)) at (prev + 6, 5) to (start + 3, 15)
+- Code(Counter(0)) at (prev + 10, 5) to (start + 3, 15)
+- Code(Counter(0)) at (prev + 10, 5) to (start + 1, 2)
+
+Function name: holes::main::_unused_fn (unused)
+Raw bytes (9): 0x[01, 01, 00, 01, 00, 19, 05, 00, 17]
+Number of files: 1
+- file 0 => global file 1
+Number of expressions: 0
+Number of file 0 mappings: 1
+- Code(Zero) at (prev + 25, 5) to (start + 0, 23)
+
+Function name: holes::main::{closure#0} (unused)
+Raw bytes (9): 0x[01, 01, 00, 01, 00, 12, 09, 02, 0a]
+Number of files: 1
+- file 0 => global file 1
+Number of expressions: 0
+Number of file 0 mappings: 1
+- Code(Zero) at (prev + 18, 9) to (start + 2, 10)
+
+Function name: holes::main::{closure#1} (unused)
+Raw bytes (9): 0x[01, 01, 00, 01, 00, 3d, 09, 02, 0a]
+Number of files: 1
+- file 0 => global file 1
+Number of expressions: 0
+Number of file 0 mappings: 1
+- Code(Zero) at (prev + 61, 9) to (start + 2, 10)
+
diff --git a/tests/coverage/holes.coverage b/tests/coverage/holes.coverage
new file mode 100644
index 00000000000..6e65435f7e3
--- /dev/null
+++ b/tests/coverage/holes.coverage
@@ -0,0 +1,68 @@
+   LL|       |//@ edition: 2021
+   LL|       |
+   LL|       |// Nested items/closures should be treated as "holes", so that their spans are
+   LL|       |// not displayed as executable code in the enclosing function.
+   LL|       |
+   LL|       |use core::hint::black_box;
+   LL|       |
+   LL|      1|fn main() {
+   LL|      1|    black_box(());
+   LL|      1|
+   LL|      1|    // Splitting this across multiple lines makes it easier to see where the
+   LL|      1|    // coverage mapping regions begin and end.
+   LL|      1|    #[rustfmt::skip]
+   LL|      1|    let _closure =
+   LL|       |        |
+   LL|       |            _arg: (),
+   LL|       |        |
+   LL|      0|        {
+   LL|      0|            black_box(());
+   LL|      0|        }
+   LL|       |        ;
+   LL|       |
+   LL|      1|    black_box(());
+   LL|       |
+   LL|      0|    fn _unused_fn() {}
+   LL|       |
+   LL|      1|    black_box(());
+   LL|       |
+   LL|       |    struct MyStruct {
+   LL|       |        _x: u32,
+   LL|       |        _y: u32,
+   LL|       |    }
+   LL|       |
+   LL|      1|    black_box(());
+   LL|       |
+   LL|       |    impl MyStruct {
+   LL|      0|        fn _method(&self) {}
+   LL|       |    }
+   LL|       |
+   LL|      1|    black_box(());
+   LL|       |
+   LL|       |    macro_rules! _my_macro {
+   LL|       |        () => {};
+   LL|       |    }
+   LL|       |
+   LL|      1|    black_box(());
+   LL|      1|
+   LL|      1|    #[rustfmt::skip]
+   LL|      1|    let _const =
+   LL|       |        const
+   LL|       |        {
+   LL|       |            7 + 4
+   LL|       |        }
+   LL|       |        ;
+   LL|       |
+   LL|      1|    black_box(());
+   LL|      1|
+   LL|      1|    #[rustfmt::skip]
+   LL|      1|    let _async =
+   LL|       |        async
+   LL|      0|        {
+   LL|      0|            7 + 4
+   LL|      0|        }
+   LL|       |        ;
+   LL|       |
+   LL|      1|    black_box(());
+   LL|      1|}
+
diff --git a/tests/coverage/holes.rs b/tests/coverage/holes.rs
new file mode 100644
index 00000000000..b3a71e759c8
--- /dev/null
+++ b/tests/coverage/holes.rs
@@ -0,0 +1,67 @@
+//@ edition: 2021
+
+// Nested items/closures should be treated as "holes", so that their spans are
+// not displayed as executable code in the enclosing function.
+
+use core::hint::black_box;
+
+fn main() {
+    black_box(());
+
+    // Splitting this across multiple lines makes it easier to see where the
+    // coverage mapping regions begin and end.
+    #[rustfmt::skip]
+    let _closure =
+        |
+            _arg: (),
+        |
+        {
+            black_box(());
+        }
+        ;
+
+    black_box(());
+
+    fn _unused_fn() {}
+
+    black_box(());
+
+    struct MyStruct {
+        _x: u32,
+        _y: u32,
+    }
+
+    black_box(());
+
+    impl MyStruct {
+        fn _method(&self) {}
+    }
+
+    black_box(());
+
+    macro_rules! _my_macro {
+        () => {};
+    }
+
+    black_box(());
+
+    #[rustfmt::skip]
+    let _const =
+        const
+        {
+            7 + 4
+        }
+        ;
+
+    black_box(());
+
+    #[rustfmt::skip]
+    let _async =
+        async
+        {
+            7 + 4
+        }
+        ;
+
+    black_box(());
+}
diff --git a/tests/coverage/no_cov_crate.cov-map b/tests/coverage/no_cov_crate.cov-map
index 281efb6d00d..75234f6c3b7 100644
--- a/tests/coverage/no_cov_crate.cov-map
+++ b/tests/coverage/no_cov_crate.cov-map
@@ -31,20 +31,22 @@ Number of file 0 mappings: 1
 - Code(Counter(0)) at (prev + 77, 1) to (start + 11, 2)
 
 Function name: no_cov_crate::nested_fns::outer
-Raw bytes (9): 0x[01, 01, 00, 01, 01, 31, 05, 0c, 06]
+Raw bytes (14): 0x[01, 01, 00, 02, 01, 31, 05, 02, 23, 01, 0c, 05, 00, 06]
 Number of files: 1
 - file 0 => global file 1
 Number of expressions: 0
-Number of file 0 mappings: 1
-- Code(Counter(0)) at (prev + 49, 5) to (start + 12, 6)
+Number of file 0 mappings: 2
+- Code(Counter(0)) at (prev + 49, 5) to (start + 2, 35)
+- Code(Counter(0)) at (prev + 12, 5) to (start + 0, 6)
 
 Function name: no_cov_crate::nested_fns::outer_both_covered
-Raw bytes (9): 0x[01, 01, 00, 01, 01, 3f, 05, 0b, 06]
+Raw bytes (14): 0x[01, 01, 00, 02, 01, 3f, 05, 02, 17, 01, 0b, 05, 00, 06]
 Number of files: 1
 - file 0 => global file 1
 Number of expressions: 0
-Number of file 0 mappings: 1
-- Code(Counter(0)) at (prev + 63, 5) to (start + 11, 6)
+Number of file 0 mappings: 2
+- Code(Counter(0)) at (prev + 63, 5) to (start + 2, 23)
+- Code(Counter(0)) at (prev + 11, 5) to (start + 0, 6)
 
 Function name: no_cov_crate::nested_fns::outer_both_covered::inner
 Raw bytes (26): 0x[01, 01, 01, 01, 05, 04, 01, 43, 09, 01, 17, 05, 01, 18, 02, 0e, 02, 02, 14, 02, 0e, 01, 03, 09, 00, 0a]
diff --git a/tests/coverage/no_cov_crate.coverage b/tests/coverage/no_cov_crate.coverage
index 29ad1f979cf..6a43e52652e 100644
--- a/tests/coverage/no_cov_crate.coverage
+++ b/tests/coverage/no_cov_crate.coverage
@@ -49,21 +49,21 @@
    LL|      1|    pub fn outer(is_true: bool) {
    LL|      1|        println!("called and covered");
    LL|      1|        inner_not_covered(is_true);
-   LL|      1|
-   LL|      1|        #[coverage(off)]
-   LL|      1|        fn inner_not_covered(is_true: bool) {
-   LL|      1|            if is_true {
-   LL|      1|                println!("called but not covered");
-   LL|      1|            } else {
-   LL|      1|                println!("absolutely not covered");
-   LL|      1|            }
-   LL|      1|        }
+   LL|       |
+   LL|       |        #[coverage(off)]
+   LL|       |        fn inner_not_covered(is_true: bool) {
+   LL|       |            if is_true {
+   LL|       |                println!("called but not covered");
+   LL|       |            } else {
+   LL|       |                println!("absolutely not covered");
+   LL|       |            }
+   LL|       |        }
    LL|      1|    }
    LL|       |
    LL|      1|    pub fn outer_both_covered(is_true: bool) {
    LL|      1|        println!("called and covered");
    LL|      1|        inner(is_true);
-   LL|      1|
+   LL|       |
    LL|      1|        fn inner(is_true: bool) {
    LL|      1|            if is_true {
    LL|      1|                println!("called and covered");