• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rust-lang / annotate-snippets-rs / 16010663667

01 Jul 2025 09:35PM UTC coverage: 87.43% (+0.3%) from 87.105%
16010663667

Pull #237

github

web-flow
Merge 49322a759 into 74c251759
Pull Request #237: fix: Force group levels to be explicitly given

5 of 5 new or added lines in 2 files covered. (100.0%)

2 existing lines in 1 file now uncovered.

1405 of 1607 relevant lines covered (87.43%)

4.93 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

76.42
/src/snippet.rs
1
//! Structures used as an input for the library.
2

3
use crate::renderer::source_map::SourceMap;
4
use crate::Level;
5
use std::borrow::Cow;
6
use std::ops::Range;
7

8
pub(crate) const ERROR_TXT: &str = "error";
9
pub(crate) const HELP_TXT: &str = "help";
10
pub(crate) const INFO_TXT: &str = "info";
11
pub(crate) const NOTE_TXT: &str = "note";
12
pub(crate) const WARNING_TXT: &str = "warning";
13

14
#[derive(Clone, Debug, Default)]
15
pub(crate) struct Id<'a> {
16
    pub(crate) id: Option<Cow<'a, str>>,
17
    pub(crate) url: Option<Cow<'a, str>>,
18
}
19

20
/// An [`Element`] container
21
#[derive(Clone, Debug)]
22
pub struct Group<'a> {
23
    pub(crate) primary_level: Level<'a>,
24
    pub(crate) elements: Vec<Element<'a>>,
25
}
26

27
impl<'a> Group<'a> {
28
    /// Create group with a title, deriving the primary [`Level`] for [`Annotation`]s from it
29
    pub fn with_title(title: Title<'a>) -> Self {
9✔
30
        let level = title.level.clone();
9✔
31
        Self::with_level(level).element(title)
9✔
32
    }
33

34
    /// Create a title-less group with a primary [`Level`] for [`Annotation`]s
35
    pub fn with_level(level: Level<'a>) -> Self {
9✔
36
        Self {
37
            primary_level: level,
38
            elements: vec![],
12✔
39
        }
40
    }
41

42
    pub fn element(mut self, section: impl Into<Element<'a>>) -> Self {
30✔
43
        self.elements.push(section.into());
64✔
44
        self
32✔
45
    }
46

47
    pub fn elements(mut self, sections: impl IntoIterator<Item = impl Into<Element<'a>>>) -> Self {
×
48
        self.elements.extend(sections.into_iter().map(Into::into));
×
49
        self
×
50
    }
51

UNCOV
52
    pub fn is_empty(&self) -> bool {
×
UNCOV
53
        self.elements.is_empty()
×
54
    }
55
}
56

57
/// A section of content within a [`Group`]
58
#[derive(Clone, Debug)]
59
#[non_exhaustive]
60
pub enum Element<'a> {
61
    Title(Title<'a>),
62
    Cause(Snippet<'a, Annotation<'a>>),
63
    Suggestion(Snippet<'a, Patch<'a>>),
64
    Origin(Origin<'a>),
65
    Padding(Padding),
66
}
67

68
impl<'a> From<Title<'a>> for Element<'a> {
69
    fn from(value: Title<'a>) -> Self {
12✔
70
        Element::Title(value)
12✔
71
    }
72
}
73

74
impl<'a> From<Snippet<'a, Annotation<'a>>> for Element<'a> {
75
    fn from(value: Snippet<'a, Annotation<'a>>) -> Self {
12✔
76
        Element::Cause(value)
12✔
77
    }
78
}
79

80
impl<'a> From<Snippet<'a, Patch<'a>>> for Element<'a> {
81
    fn from(value: Snippet<'a, Patch<'a>>) -> Self {
5✔
82
        Element::Suggestion(value)
5✔
83
    }
84
}
85

86
impl<'a> From<Origin<'a>> for Element<'a> {
87
    fn from(value: Origin<'a>) -> Self {
2✔
88
        Element::Origin(value)
2✔
89
    }
90
}
91

92
impl From<Padding> for Element<'_> {
93
    fn from(value: Padding) -> Self {
1✔
94
        Self::Padding(value)
1✔
95
    }
96
}
97

98
/// A whitespace [`Element`] in a [`Group`]
99
#[derive(Clone, Debug)]
100
pub struct Padding;
101

102
/// A text [`Element`] in a [`Group`]
103
///
104
/// See [`Level::title`] to create this.
105
#[derive(Clone, Debug)]
106
pub struct Title<'a> {
107
    pub(crate) level: Level<'a>,
108
    pub(crate) id: Option<Id<'a>>,
109
    pub(crate) title: Cow<'a, str>,
110
    pub(crate) is_pre_styled: bool,
111
}
112

113
impl<'a> Title<'a> {
114
    /// <div class="warning">
115
    ///
116
    /// This is only relevant if the title is the first element of a group.
117
    ///
118
    /// </div>
119
    /// <div class="warning">
120
    ///
121
    /// Text passed to this function is considered "untrusted input", as such
122
    /// all text is passed through a normalization function. Pre-styled text is
123
    /// not allowed to be passed to this function.
124
    ///
125
    /// </div>
126
    pub fn id(mut self, id: impl Into<Cow<'a, str>>) -> Self {
4✔
127
        self.id.get_or_insert(Id::default()).id = Some(id.into());
12✔
128
        self
6✔
129
    }
130

131
    /// <div class="warning">
132
    ///
133
    /// This is only relevant if the title is the first element of a group and
134
    /// `id` present
135
    ///
136
    /// </div>
137
    pub fn id_url(mut self, url: impl Into<Cow<'a, str>>) -> Self {
×
138
        self.id.get_or_insert(Id::default()).url = Some(url.into());
×
139
        self
×
140
    }
141
}
142

143
/// A source view [`Element`] in a [`Group`]
144
///
145
/// If you do not have [source][Snippet::source] available, see instead [`Origin`]
146
#[derive(Clone, Debug)]
147
pub struct Snippet<'a, T> {
148
    pub(crate) path: Option<Cow<'a, str>>,
149
    pub(crate) line_start: usize,
150
    pub(crate) source: Cow<'a, str>,
151
    pub(crate) markers: Vec<T>,
152
    pub(crate) fold: bool,
153
}
154

155
impl<'a, T: Clone> Snippet<'a, T> {
156
    /// The source code to be rendered
157
    ///
158
    /// <div class="warning">
159
    ///
160
    /// Text passed to this function is considered "untrusted input", as such
161
    /// all text is passed through a normalization function. Pre-styled text is
162
    /// not allowed to be passed to this function.
163
    ///
164
    /// </div>
165
    pub fn source(source: impl Into<Cow<'a, str>>) -> Self {
18✔
166
        Self {
167
            path: None,
168
            line_start: 1,
169
            source: source.into(),
18✔
170
            markers: vec![],
18✔
171
            fold: false,
172
        }
173
    }
174

175
    /// When manually [`fold`][Self::fold]ing,
176
    /// the [`source`][Self::source]s line offset from the original start
177
    pub fn line_start(mut self, line_start: usize) -> Self {
13✔
178
        self.line_start = line_start;
13✔
179
        self
13✔
180
    }
181

182
    /// The location of the [`source`][Self::source] (e.g. a path)
183
    ///
184
    /// <div class="warning">
185
    ///
186
    /// Text passed to this function is considered "untrusted input", as such
187
    /// all text is passed through a normalization function. Pre-styled text is
188
    /// not allowed to be passed to this function.
189
    ///
190
    /// </div>
191
    pub fn path(mut self, path: impl Into<OptionCow<'a>>) -> Self {
14✔
192
        self.path = path.into().0;
26✔
193
        self
14✔
194
    }
195

196
    /// Hide lines without [`Annotation`]s
197
    pub fn fold(mut self, fold: bool) -> Self {
10✔
198
        self.fold = fold;
10✔
199
        self
10✔
200
    }
201
}
202

203
impl<'a> Snippet<'a, Annotation<'a>> {
204
    /// Highlight and describe a span of text within the [`source`][Self::source]
205
    pub fn annotation(mut self, annotation: Annotation<'a>) -> Snippet<'a, Annotation<'a>> {
10✔
206
        self.markers.push(annotation);
10✔
207
        self
10✔
208
    }
209

210
    /// Highlight and describe spans of text within the [`source`][Self::source]
211
    pub fn annotations(mut self, annotation: impl IntoIterator<Item = Annotation<'a>>) -> Self {
×
212
        self.markers.extend(annotation);
×
213
        self
×
214
    }
215
}
216

217
impl<'a> Snippet<'a, Patch<'a>> {
218
    /// Suggest to the user an edit to the [`source`][Self::source]
219
    pub fn patch(mut self, patch: Patch<'a>) -> Snippet<'a, Patch<'a>> {
5✔
220
        self.markers.push(patch);
5✔
221
        self
5✔
222
    }
223

224
    /// Suggest to the user edits to the [`source`][Self::source]
225
    pub fn patches(mut self, patches: impl IntoIterator<Item = Patch<'a>>) -> Self {
×
226
        self.markers.extend(patches);
×
227
        self
×
228
    }
229
}
230

231
/// Highlighted and describe a span of text within a [`Snippet`]
232
///
233
/// See [`AnnotationKind`] to create an annotation.
234
#[derive(Clone, Debug)]
235
pub struct Annotation<'a> {
236
    pub(crate) span: Range<usize>,
237
    pub(crate) label: Option<Cow<'a, str>>,
238
    pub(crate) kind: AnnotationKind,
239
    pub(crate) highlight_source: bool,
240
}
241

242
impl<'a> Annotation<'a> {
243
    /// Describe the reason the span is highlighted
244
    ///
245
    /// This will be styled according to the [`AnnotationKind`]
246
    ///
247
    /// <div class="warning">
248
    ///
249
    /// Text passed to this function is considered "untrusted input", as such
250
    /// all text is passed through a normalization function. Pre-styled text is
251
    /// not allowed to be passed to this function.
252
    ///
253
    /// </div>
254
    pub fn label(mut self, label: impl Into<OptionCow<'a>>) -> Self {
7✔
255
        self.label = label.into().0;
16✔
256
        self
10✔
257
    }
258

259
    /// Style the source according to the [`AnnotationKind`]
260
    pub fn highlight_source(mut self, highlight_source: bool) -> Self {
×
261
        self.highlight_source = highlight_source;
×
262
        self
×
263
    }
264
}
265

266
/// The category of the [`Annotation`]
267
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
268
#[non_exhaustive]
269
pub enum AnnotationKind {
270
    /// Match the primary [`Level`] of the group.
271
    ///
272
    /// See [`Group::with_level`] for details about how this is determined
273
    Primary,
274
    /// Additional context to explain the [`Primary`][Self::Primary]
275
    /// [`Annotation`]
276
    ///
277
    /// See also [`Renderer::context`].
278
    ///
279
    /// [`Renderer::context`]: crate::renderer::Renderer
280
    Context,
281
}
282

283
impl AnnotationKind {
284
    pub fn span<'a>(self, span: Range<usize>) -> Annotation<'a> {
10✔
285
        Annotation {
286
            span,
287
            label: None,
288
            kind: self,
289
            highlight_source: false,
290
        }
291
    }
292

293
    pub(crate) fn is_primary(&self) -> bool {
5✔
294
        matches!(self, AnnotationKind::Primary)
7✔
295
    }
296
}
297

298
/// Suggested edit to the [`Snippet`]
299
#[derive(Clone, Debug)]
300
pub struct Patch<'a> {
301
    pub(crate) span: Range<usize>,
302
    pub(crate) replacement: Cow<'a, str>,
303
}
304

305
impl<'a> Patch<'a> {
306
    /// Splice `replacement` into the [`Snippet`] at the `span`
307
    ///
308
    /// <div class="warning">
309
    ///
310
    /// Text passed to this function is considered "untrusted input", as such
311
    /// all text is passed through a normalization function. Pre-styled text is
312
    /// not allowed to be passed to this function.
313
    ///
314
    /// </div>
315
    pub fn new(span: Range<usize>, replacement: impl Into<Cow<'a, str>>) -> Self {
4✔
316
        Self {
317
            span,
318
            replacement: replacement.into(),
5✔
319
        }
320
    }
321

322
    pub(crate) fn is_addition(&self, sm: &SourceMap<'_>) -> bool {
3✔
323
        !self.replacement.is_empty() && !self.replaces_meaningful_content(sm)
3✔
324
    }
325

326
    pub(crate) fn is_deletion(&self, sm: &SourceMap<'_>) -> bool {
3✔
327
        self.replacement.trim().is_empty() && self.replaces_meaningful_content(sm)
5✔
328
    }
329

330
    pub(crate) fn is_replacement(&self, sm: &SourceMap<'_>) -> bool {
2✔
331
        !self.replacement.is_empty() && self.replaces_meaningful_content(sm)
3✔
332
    }
333

334
    /// Whether this is a replacement that overwrites source with a snippet
335
    /// in a way that isn't a superset of the original string. For example,
336
    /// replacing "abc" with "abcde" is not destructive, but replacing it
337
    /// it with "abx" is, since the "c" character is lost.
338
    pub(crate) fn is_destructive_replacement(&self, sm: &SourceMap<'_>) -> bool {
2✔
339
        self.is_replacement(sm)
4✔
340
            && !sm
2✔
341
                .span_to_snippet(self.span.clone())
3✔
342
                // This should use `is_some_and` when our MSRV is >= 1.70
343
                .map_or(false, |s| {
3✔
344
                    as_substr(s.trim(), self.replacement.trim()).is_some()
2✔
345
                })
346
    }
347

348
    fn replaces_meaningful_content(&self, sm: &SourceMap<'_>) -> bool {
3✔
349
        sm.span_to_snippet(self.span.clone())
9✔
350
            .map_or(!self.span.is_empty(), |snippet| !snippet.trim().is_empty())
11✔
351
    }
352

353
    /// Try to turn a replacement into an addition when the span that is being
354
    /// overwritten matches either the prefix or suffix of the replacement.
355
    pub(crate) fn trim_trivial_replacements(&mut self, sm: &'a SourceMap<'a>) {
4✔
356
        if self.replacement.is_empty() {
4✔
357
            return;
×
358
        }
359
        let Some(snippet) = sm.span_to_snippet(self.span.clone()) else {
6✔
360
            return;
×
361
        };
362

363
        if let Some((prefix, substr, suffix)) = as_substr(snippet, &self.replacement) {
6✔
364
            self.span = self.span.start + prefix..self.span.end.saturating_sub(suffix);
2✔
365
            self.replacement = Cow::Owned(substr.to_owned());
5✔
366
        }
367
    }
368
}
369

370
/// The referenced location (e.g. a path)
371
///
372
/// If you have source available, see instead [`Snippet`]
373
#[derive(Clone, Debug)]
374
pub struct Origin<'a> {
375
    pub(crate) path: Cow<'a, str>,
376
    pub(crate) line: Option<usize>,
377
    pub(crate) char_column: Option<usize>,
378
    pub(crate) primary: bool,
379
}
380

381
impl<'a> Origin<'a> {
382
    /// <div class="warning">
383
    ///
384
    /// Text passed to this function is considered "untrusted input", as such
385
    /// all text is passed through a normalization function. Pre-styled text is
386
    /// not allowed to be passed to this function.
387
    ///
388
    /// </div>
389
    pub fn new(path: impl Into<Cow<'a, str>>) -> Self {
5✔
390
        Self {
391
            path: path.into(),
3✔
392
            line: None,
393
            char_column: None,
394
            primary: false,
395
        }
396
    }
397

398
    /// Set the default line number to display
399
    ///
400
    /// Otherwise this will be inferred from the primary [`Annotation`]
401
    pub fn line(mut self, line: usize) -> Self {
2✔
402
        self.line = Some(line);
2✔
403
        self
2✔
404
    }
405

406
    /// Set the default column to display
407
    ///
408
    /// Otherwise this will be inferred from the primary [`Annotation`]
409
    ///
410
    /// <div class="warning">
411
    ///
412
    /// `char_column` is only be respected if [`Origin::line`] is also set.
413
    ///
414
    /// </div>
415
    pub fn char_column(mut self, char_column: usize) -> Self {
2✔
416
        self.char_column = Some(char_column);
2✔
417
        self
2✔
418
    }
419

420
    pub fn primary(mut self, primary: bool) -> Self {
1✔
421
        self.primary = primary;
1✔
422
        self
1✔
423
    }
424
}
425

426
impl<'a> From<Cow<'a, str>> for Origin<'a> {
427
    fn from(origin: Cow<'a, str>) -> Self {
×
428
        Self::new(origin)
×
429
    }
430
}
431

432
#[derive(Debug)]
433
pub struct OptionCow<'a>(pub(crate) Option<Cow<'a, str>>);
434

435
impl<'a, T: Into<Cow<'a, str>>> From<Option<T>> for OptionCow<'a> {
436
    fn from(value: Option<T>) -> Self {
1✔
437
        Self(value.map(Into::into))
1✔
438
    }
439
}
440

441
impl<'a> From<&'a Cow<'a, str>> for OptionCow<'a> {
442
    fn from(value: &'a Cow<'a, str>) -> Self {
×
443
        Self(Some(Cow::Borrowed(value)))
×
444
    }
445
}
446

447
impl<'a> From<Cow<'a, str>> for OptionCow<'a> {
448
    fn from(value: Cow<'a, str>) -> Self {
×
449
        Self(Some(value))
×
450
    }
451
}
452

453
impl<'a> From<&'a str> for OptionCow<'a> {
454
    fn from(value: &'a str) -> Self {
7✔
455
        Self(Some(Cow::Borrowed(value)))
10✔
456
    }
457
}
458
impl<'a> From<String> for OptionCow<'a> {
459
    fn from(value: String) -> Self {
×
460
        Self(Some(Cow::Owned(value)))
×
461
    }
462
}
463

464
impl<'a> From<&'a String> for OptionCow<'a> {
465
    fn from(value: &'a String) -> Self {
×
466
        Self(Some(Cow::Borrowed(value.as_str())))
×
467
    }
468
}
469

470
/// Given an original string like `AACC`, and a suggestion like `AABBCC`, try to detect
471
/// the case where a substring of the suggestion is "sandwiched" in the original, like
472
/// `BB` is. Return the length of the prefix, the "trimmed" suggestion, and the length
473
/// of the suffix.
474
fn as_substr<'a>(original: &'a str, suggestion: &'a str) -> Option<(usize, &'a str, usize)> {
3✔
475
    let common_prefix = original
6✔
476
        .chars()
477
        .zip(suggestion.chars())
3✔
478
        .take_while(|(c1, c2)| c1 == c2)
6✔
479
        .map(|(c, _)| c.len_utf8())
4✔
480
        .sum();
481
    let original = &original[common_prefix..];
2✔
482
    let suggestion = &suggestion[common_prefix..];
2✔
483
    if let Some(stripped) = suggestion.strip_suffix(original) {
6✔
484
        let common_suffix = original.len();
2✔
485
        Some((common_prefix, stripped, common_suffix))
2✔
486
    } else {
487
        None
3✔
488
    }
489
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc