rustc_mir_transform/coverage/
spans.rs

1use rustc_data_structures::fx::FxHashSet;
2use rustc_middle::mir;
3use rustc_middle::mir::coverage::{Mapping, MappingKind, START_BCB};
4use rustc_middle::ty::TyCtxt;
5use rustc_span::source_map::SourceMap;
6use rustc_span::{BytePos, DesugaringKind, ExpnKind, MacroKind, Span};
7use tracing::instrument;
8
9use crate::coverage::graph::{BasicCoverageBlock, CoverageGraph};
10use crate::coverage::hir_info::ExtractedHirInfo;
11use crate::coverage::spans::from_mir::{Hole, RawSpanFromMir, SpanFromMir};
12use crate::coverage::unexpand;
13
14mod from_mir;
15
16pub(super) fn extract_refined_covspans<'tcx>(
17    tcx: TyCtxt<'tcx>,
18    mir_body: &mir::Body<'tcx>,
19    hir_info: &ExtractedHirInfo,
20    graph: &CoverageGraph,
21    mappings: &mut Vec<Mapping>,
22) {
23    if hir_info.is_async_fn {
24        // An async function desugars into a function that returns a future,
25        // with the user code wrapped in a closure. Any spans in the desugared
26        // outer function will be unhelpful, so just keep the signature span
27        // and ignore all of the spans in the MIR body.
28        if let Some(span) = hir_info.fn_sig_span {
29            mappings.push(Mapping { span, kind: MappingKind::Code { bcb: START_BCB } })
30        }
31        return;
32    }
33
34    let &ExtractedHirInfo { body_span, .. } = hir_info;
35
36    let raw_spans = from_mir::extract_raw_spans_from_mir(mir_body, graph);
37    let mut covspans = raw_spans
38        .into_iter()
39        .filter_map(|RawSpanFromMir { raw_span, bcb }| try {
40            let (span, expn_kind) =
41                unexpand::unexpand_into_body_span_with_expn_kind(raw_span, body_span)?;
42            // Discard any spans that fill the entire body, because they tend
43            // to represent compiler-inserted code, e.g. implicitly returning `()`.
44            if span.source_equal(body_span) {
45                return None;
46            };
47            SpanFromMir { span, expn_kind, bcb }
48        })
49        .collect::<Vec<_>>();
50
51    // Only proceed if we found at least one usable span.
52    if covspans.is_empty() {
53        return;
54    }
55
56    // Also add the function signature span, if available.
57    // Otherwise, add a fake span at the start of the body, to avoid an ugly
58    // gap between the start of the body and the first real span.
59    // FIXME: Find a more principled way to solve this problem.
60    covspans.push(SpanFromMir::for_fn_sig(
61        hir_info.fn_sig_span.unwrap_or_else(|| body_span.shrink_to_lo()),
62    ));
63
64    // First, perform the passes that need macro information.
65    covspans.sort_by(|a, b| graph.cmp_in_dominator_order(a.bcb, b.bcb));
66    remove_unwanted_expansion_spans(&mut covspans);
67    shrink_visible_macro_spans(tcx, &mut covspans);
68
69    // We no longer need the extra information in `SpanFromMir`, so convert to `Covspan`.
70    let mut covspans = covspans.into_iter().map(SpanFromMir::into_covspan).collect::<Vec<_>>();
71
72    let compare_covspans = |a: &Covspan, b: &Covspan| {
73        compare_spans(a.span, b.span)
74            // After deduplication, we want to keep only the most-dominated BCB.
75            .then_with(|| graph.cmp_in_dominator_order(a.bcb, b.bcb).reverse())
76    };
77    covspans.sort_by(compare_covspans);
78
79    // Among covspans with the same span, keep only one,
80    // preferring the one with the most-dominated BCB.
81    // (Ideally we should try to preserve _all_ non-dominating BCBs, but that
82    // requires a lot more complexity in the span refiner, for little benefit.)
83    covspans.dedup_by(|b, a| a.span.source_equal(b.span));
84
85    // Sort the holes, and merge overlapping/adjacent holes.
86    let mut holes = hir_info
87        .hole_spans
88        .iter()
89        .copied()
90        // Discard any holes that aren't directly visible within the body span.
91        .filter(|&hole_span| body_span.contains(hole_span) && body_span.eq_ctxt(hole_span))
92        .map(|span| Hole { span })
93        .collect::<Vec<_>>();
94    holes.sort_by(|a, b| compare_spans(a.span, b.span));
95    holes.dedup_by(|b, a| a.merge_if_overlapping_or_adjacent(b));
96
97    // Discard any span that overlaps with a hole.
98    discard_spans_overlapping_holes(&mut covspans, &holes);
99
100    // Discard spans that overlap in unwanted ways.
101    let mut covspans = remove_unwanted_overlapping_spans(covspans);
102
103    // For all empty spans, either enlarge them to be non-empty, or discard them.
104    let source_map = tcx.sess.source_map();
105    covspans.retain_mut(|covspan| {
106        let Some(span) = ensure_non_empty_span(source_map, covspan.span) else { return false };
107        covspan.span = span;
108        true
109    });
110
111    // Merge covspans that can be merged.
112    covspans.dedup_by(|b, a| a.merge_if_eligible(b));
113
114    mappings.extend(covspans.into_iter().map(|Covspan { span, bcb }| {
115        // Each span produced by the refiner represents an ordinary code region.
116        Mapping { span, kind: MappingKind::Code { bcb } }
117    }));
118}
119
120/// Macros that expand into branches (e.g. `assert!`, `trace!`) tend to generate
121/// multiple condition/consequent blocks that have the span of the whole macro
122/// invocation, which is unhelpful. Keeping only the first such span seems to
123/// give better mappings, so remove the others.
124///
125/// Similarly, `await` expands to a branch on the discriminant of `Poll`, which
126/// leads to incorrect coverage if the `Future` is immediately ready (#98712).
127///
128/// (The input spans should be sorted in BCB dominator order, so that the
129/// retained "first" span is likely to dominate the others.)
130fn remove_unwanted_expansion_spans(covspans: &mut Vec<SpanFromMir>) {
131    let mut deduplicated_spans = FxHashSet::default();
132
133    covspans.retain(|covspan| {
134        match covspan.expn_kind {
135            // Retain only the first await-related or macro-expanded covspan with this span.
136            Some(ExpnKind::Desugaring(DesugaringKind::Await)) => {
137                deduplicated_spans.insert(covspan.span)
138            }
139            Some(ExpnKind::Macro(MacroKind::Bang, _)) => deduplicated_spans.insert(covspan.span),
140            // Ignore (retain) other spans.
141            _ => true,
142        }
143    });
144}
145
146/// When a span corresponds to a macro invocation that is visible from the
147/// function body, truncate it to just the macro name plus `!`.
148/// This seems to give better results for code that uses macros.
149fn shrink_visible_macro_spans(tcx: TyCtxt<'_>, covspans: &mut Vec<SpanFromMir>) {
150    let source_map = tcx.sess.source_map();
151
152    for covspan in covspans {
153        if matches!(covspan.expn_kind, Some(ExpnKind::Macro(MacroKind::Bang, _))) {
154            covspan.span = source_map.span_through_char(covspan.span, '!');
155        }
156    }
157}
158
159/// Discard all covspans that overlap a hole.
160///
161/// The lists of covspans and holes must be sorted, and any holes that overlap
162/// with each other must have already been merged.
163fn discard_spans_overlapping_holes(covspans: &mut Vec<Covspan>, holes: &[Hole]) {
164    debug_assert!(covspans.is_sorted_by(|a, b| compare_spans(a.span, b.span).is_le()));
165    debug_assert!(holes.is_sorted_by(|a, b| compare_spans(a.span, b.span).is_le()));
166    debug_assert!(holes.array_windows().all(|[a, b]| !a.span.overlaps_or_adjacent(b.span)));
167
168    let mut curr_hole = 0usize;
169    let mut overlaps_hole = |covspan: &Covspan| -> bool {
170        while let Some(hole) = holes.get(curr_hole) {
171            // Both lists are sorted, so we can permanently skip any holes that
172            // end before the start of the current span.
173            if hole.span.hi() <= covspan.span.lo() {
174                curr_hole += 1;
175                continue;
176            }
177
178            return hole.span.overlaps(covspan.span);
179        }
180
181        // No holes left, so this covspan doesn't overlap with any holes.
182        false
183    };
184
185    covspans.retain(|covspan| !overlaps_hole(covspan));
186}
187
188/// Takes a list of sorted spans extracted from MIR, and "refines"
189/// those spans by removing spans that overlap in unwanted ways.
190#[instrument(level = "debug")]
191fn remove_unwanted_overlapping_spans(sorted_spans: Vec<Covspan>) -> Vec<Covspan> {
192    debug_assert!(sorted_spans.is_sorted_by(|a, b| compare_spans(a.span, b.span).is_le()));
193
194    // Holds spans that have been read from the input vector, but haven't yet
195    // been committed to the output vector.
196    let mut pending = vec![];
197    let mut refined = vec![];
198
199    for curr in sorted_spans {
200        pending.retain(|prev: &Covspan| {
201            if prev.span.hi() <= curr.span.lo() {
202                // There's no overlap between the previous/current covspans,
203                // so move the previous one into the refined list.
204                refined.push(prev.clone());
205                false
206            } else {
207                // Otherwise, retain the previous covspan only if it has the
208                // same BCB. This tends to discard long outer spans that enclose
209                // smaller inner spans with different control flow.
210                prev.bcb == curr.bcb
211            }
212        });
213        pending.push(curr);
214    }
215
216    // Drain the rest of the pending list into the refined list.
217    refined.extend(pending);
218    refined
219}
220
221#[derive(Clone, Debug)]
222struct Covspan {
223    span: Span,
224    bcb: BasicCoverageBlock,
225}
226
227impl Covspan {
228    /// If `self` and `other` can be merged, mutates `self.span` to also
229    /// include `other.span` and returns true.
230    ///
231    /// Two covspans can be merged if they have the same BCB, and they are
232    /// overlapping or adjacent.
233    fn merge_if_eligible(&mut self, other: &Self) -> bool {
234        let eligible_for_merge =
235            |a: &Self, b: &Self| (a.bcb == b.bcb) && a.span.overlaps_or_adjacent(b.span);
236
237        if eligible_for_merge(self, other) {
238            self.span = self.span.to(other.span);
239            true
240        } else {
241            false
242        }
243    }
244}
245
246/// Compares two spans in (lo ascending, hi descending) order.
247fn compare_spans(a: Span, b: Span) -> std::cmp::Ordering {
248    // First sort by span start.
249    Ord::cmp(&a.lo(), &b.lo())
250        // If span starts are the same, sort by span end in reverse order.
251        // This ensures that if spans A and B are adjacent in the list,
252        // and they overlap but are not equal, then either:
253        // - Span A extends further left, or
254        // - Both have the same start and span A extends further right
255        .then_with(|| Ord::cmp(&a.hi(), &b.hi()).reverse())
256}
257
258fn ensure_non_empty_span(source_map: &SourceMap, span: Span) -> Option<Span> {
259    if !span.is_empty() {
260        return Some(span);
261    }
262
263    // The span is empty, so try to enlarge it to cover an adjacent '{' or '}'.
264    source_map
265        .span_to_source(span, |src, start, end| try {
266            // Adjusting span endpoints by `BytePos(1)` is normally a bug,
267            // but in this case we have specifically checked that the character
268            // we're skipping over is one of two specific ASCII characters, so
269            // adjusting by exactly 1 byte is correct.
270            if src.as_bytes().get(end).copied() == Some(b'{') {
271                Some(span.with_hi(span.hi() + BytePos(1)))
272            } else if start > 0 && src.as_bytes()[start - 1] == b'}' {
273                Some(span.with_lo(span.lo() - BytePos(1)))
274            } else {
275                None
276            }
277        })
278        .ok()?
279}