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

rust-lang / annotate-snippets-rs / 15861679393

24 Jun 2025 09:16PM UTC coverage: 86.51% (-0.1%) from 86.621%
15861679393

Pull #221

github

web-flow
Merge 156604040 into a81dc31d2
Pull Request #221: refactor: Use Origin for Snippet::origin

40 of 49 new or added lines in 2 files covered. (81.63%)

6 existing lines in 1 file now uncovered.

1398 of 1616 relevant lines covered (86.51%)

4.44 hits per line

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

78.99
/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::ops::Range;
6

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

13
/// Top-level user message
14
#[derive(Clone, Debug)]
15
pub struct Message<'a> {
16
    pub(crate) id: Option<&'a str>, // for "correctness", could be sloppy and be on Title
17
    pub(crate) groups: Vec<Group<'a>>,
18
}
19

20
impl<'a> Message<'a> {
21
    /// <div class="warning">
22
    ///
23
    /// Text passed to this function is considered "untrusted input", as such
24
    /// all text is passed through a normalization function. Pre-styled text is
25
    /// not allowed to be passed to this function.
26
    ///
27
    /// </div>
28
    pub fn id(mut self, id: &'a str) -> Self {
4✔
29
        self.id = Some(id);
5✔
30
        self
6✔
31
    }
32

33
    /// Add an [`Element`] container
34
    pub fn group(mut self, group: Group<'a>) -> Self {
11✔
35
        self.groups.push(group);
9✔
36
        self
11✔
37
    }
38

39
    pub(crate) fn max_line_number(&self) -> usize {
4✔
40
        self.groups
8✔
41
            .iter()
42
            .map(|v| {
4✔
43
                v.elements
8✔
44
                    .iter()
×
45
                    .map(|s| match s {
12✔
46
                        Element::Title(_) | Element::Origin(_) | Element::Padding(_) => 0,
4✔
47
                        Element::Cause(cause) => {
8✔
48
                            let end = cause
12✔
49
                                .markers
×
50
                                .iter()
×
51
                                .map(|a| a.span.end)
12✔
52
                                .max()
×
53
                                .unwrap_or(cause.source.len())
8✔
54
                                .min(cause.source.len());
8✔
55

56
                            cause.line_start + newline_count(&cause.source[..end])
12✔
57
                        }
58
                        Element::Suggestion(suggestion) => {
1✔
59
                            let end = suggestion
3✔
60
                                .markers
×
61
                                .iter()
×
62
                                .map(|a| a.span.end)
2✔
63
                                .max()
×
64
                                .unwrap_or(suggestion.source.len())
1✔
65
                                .min(suggestion.source.len());
1✔
66

67
                            suggestion.line_start + newline_count(&suggestion.source[..end])
2✔
68
                        }
69
                    })
70
                    .max()
×
71
                    .unwrap_or(1)
×
72
            })
73
            .max()
74
            .unwrap_or(1)
75
    }
76
}
77

78
/// An [`Element`] container
79
#[derive(Clone, Debug)]
80
pub struct Group<'a> {
81
    pub(crate) elements: Vec<Element<'a>>,
82
}
83

84
impl Default for Group<'_> {
85
    fn default() -> Self {
×
86
        Self::new()
×
87
    }
88
}
89

90
impl<'a> Group<'a> {
91
    pub fn new() -> Self {
9✔
92
        Self { elements: vec![] }
11✔
93
    }
94

95
    pub fn element(mut self, section: impl Into<Element<'a>>) -> Self {
29✔
96
        self.elements.push(section.into());
58✔
97
        self
29✔
98
    }
99

100
    pub fn elements(mut self, sections: impl IntoIterator<Item = impl Into<Element<'a>>>) -> Self {
×
101
        self.elements.extend(sections.into_iter().map(Into::into));
×
102
        self
×
103
    }
104

105
    pub fn is_empty(&self) -> bool {
×
106
        self.elements.is_empty()
×
107
    }
108
}
109

110
/// A section of content within a [`Group`]
111
#[derive(Clone, Debug)]
112
#[non_exhaustive]
113
pub enum Element<'a> {
114
    Title(Title<'a>),
115
    Cause(Snippet<'a, Annotation<'a>>),
116
    Suggestion(Snippet<'a, Patch<'a>>),
117
    Origin(Origin<'a>),
118
    Padding(Padding),
119
}
120

121
impl<'a> From<Title<'a>> for Element<'a> {
122
    fn from(value: Title<'a>) -> Self {
4✔
123
        Element::Title(value)
4✔
124
    }
125
}
126

127
impl<'a> From<Snippet<'a, Annotation<'a>>> for Element<'a> {
128
    fn from(value: Snippet<'a, Annotation<'a>>) -> Self {
11✔
129
        Element::Cause(value)
9✔
130
    }
131
}
132

133
impl<'a> From<Snippet<'a, Patch<'a>>> for Element<'a> {
134
    fn from(value: Snippet<'a, Patch<'a>>) -> Self {
3✔
135
        Element::Suggestion(value)
3✔
136
    }
137
}
138

139
impl<'a> From<Origin<'a>> for Element<'a> {
140
    fn from(value: Origin<'a>) -> Self {
1✔
141
        Element::Origin(value)
1✔
142
    }
143
}
144

145
impl From<Padding> for Element<'_> {
146
    fn from(value: Padding) -> Self {
1✔
147
        Self::Padding(value)
1✔
148
    }
149
}
150

151
/// A whitespace [`Element`] in a [`Group`]
152
#[derive(Clone, Debug)]
153
pub struct Padding;
154

155
/// A text [`Element`] in a [`Group`]
156
///
157
/// See [`Level::title`] to create this.
158
#[derive(Clone, Debug)]
159
pub struct Title<'a> {
160
    pub(crate) level: Level<'a>,
161
    pub(crate) title: &'a str,
162
}
163

164
/// A source view [`Element`] in a [`Group`]
165
#[derive(Clone, Debug)]
166
pub struct Snippet<'a, T> {
167
    pub(crate) origin: Option<Origin<'a>>,
168
    pub(crate) line_start: usize,
169
    pub(crate) source: &'a str,
170
    pub(crate) markers: Vec<T>,
171
    pub(crate) fold: bool,
172
}
173

174
impl<'a, T: Clone> Snippet<'a, T> {
175
    /// The source code to be rendered
176
    ///
177
    /// <div class="warning">
178
    ///
179
    /// Text passed to this function is considered "untrusted input", as such
180
    /// all text is passed through a normalization function. Pre-styled text is
181
    /// not allowed to be passed to this function.
182
    ///
183
    /// </div>
184
    pub fn source(source: &'a str) -> Self {
11✔
185
        Self {
186
            origin: None,
187
            line_start: 1,
188
            source,
189
            markers: vec![],
11✔
190
            fold: false,
191
        }
192
    }
193

194
    /// When manually [`fold`][Self::fold]ing,
195
    /// the [`source`][Self::source]s line offset from the original start
196
    pub fn line_start(mut self, line_start: usize) -> Self {
11✔
197
        self.line_start = line_start;
10✔
198
        self
12✔
199
    }
200

201
    /// The location of the [`source`][Self::source] (e.g. a path)
202
    ///
203
    /// If just a location is passed (i.e. a string) in the line and column
204
    /// numbers are automatically inferred. If you want to explicitly set the
205
    /// line and column you can create an [`Origin`] and pass it in.
206
    ///
207
    /// <div class="warning">
208
    ///
209
    /// Text passed to this function is considered "untrusted input", as such
210
    /// all text is passed through a normalization function. Pre-styled text is
211
    /// not allowed to be passed to this function.
212
    ///
213
    /// </div>
214
    pub fn origin(mut self, origin: impl Into<Origin<'a>>) -> Self {
8✔
215
        self.origin = Some(origin.into());
20✔
216
        self
11✔
217
    }
218

219
    /// Hide lines without [`Annotation`]s
220
    pub fn fold(mut self, fold: bool) -> Self {
8✔
221
        self.fold = fold;
8✔
222
        self
8✔
223
    }
224
}
225

226
impl<'a> Snippet<'a, Annotation<'a>> {
227
    /// Highlight and describe a span of text within the [`source`][Self::source]
228
    pub fn annotation(mut self, annotation: Annotation<'a>) -> Snippet<'a, Annotation<'a>> {
6✔
229
        self.markers.push(annotation);
6✔
230
        self
6✔
231
    }
232

233
    /// Highlight and describe spans of text within the [`source`][Self::source]
UNCOV
234
    pub fn annotations(mut self, annotation: impl IntoIterator<Item = Annotation<'a>>) -> Self {
×
235
        self.markers.extend(annotation);
×
236
        self
×
237
    }
238
}
239

240
impl<'a> Snippet<'a, Patch<'a>> {
241
    /// Suggest to the user an edit to the [`source`][Self::source]
242
    pub fn patch(mut self, patch: Patch<'a>) -> Snippet<'a, Patch<'a>> {
3✔
243
        self.markers.push(patch);
3✔
244
        self
3✔
245
    }
246

247
    /// Suggest to the user edits to the [`source`][Self::source]
UNCOV
248
    pub fn patches(mut self, patches: impl IntoIterator<Item = Patch<'a>>) -> Self {
×
249
        self.markers.extend(patches);
×
250
        self
×
251
    }
252
}
253

254
/// Highlighted and describe a span of text within a [`Snippet`]
255
///
256
/// See [`AnnotationKind`] to create an annotation.
257
#[derive(Clone, Debug)]
258
pub struct Annotation<'a> {
259
    pub(crate) span: Range<usize>,
260
    pub(crate) label: Option<&'a str>,
261
    pub(crate) kind: AnnotationKind,
262
    pub(crate) highlight_source: bool,
263
}
264

265
impl<'a> Annotation<'a> {
266
    /// Describe the reason the span is highlighted
267
    ///
268
    /// This will be styled according to the [`AnnotationKind`]
269
    ///
270
    /// <div class="warning">
271
    ///
272
    /// Text passed to this function is considered "untrusted input", as such
273
    /// all text is passed through a normalization function. Pre-styled text is
274
    /// not allowed to be passed to this function.
275
    ///
276
    /// </div>
277
    pub fn label(mut self, label: &'a str) -> Self {
7✔
278
        self.label = Some(label);
7✔
279
        self
7✔
280
    }
281

282
    /// Style the source according to the [`AnnotationKind`]
UNCOV
283
    pub fn highlight_source(mut self, highlight_source: bool) -> Self {
×
284
        self.highlight_source = highlight_source;
×
285
        self
×
286
    }
287
}
288

289
/// The category of the [`Annotation`]
290
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
291
pub enum AnnotationKind {
292
    /// Color to [`Message`]'s [`Level`]
293
    Primary,
294
    /// "secondary"; fixed color
295
    Context,
296
}
297

298
impl AnnotationKind {
299
    pub fn span<'a>(self, span: Range<usize>) -> Annotation<'a> {
9✔
300
        Annotation {
301
            span,
302
            label: None,
303
            kind: self,
304
            highlight_source: false,
305
        }
306
    }
307

308
    pub(crate) fn is_primary(&self) -> bool {
7✔
309
        matches!(self, AnnotationKind::Primary)
3✔
310
    }
311
}
312

313
/// Suggested edit to the [`Snippet`]
314
#[derive(Clone, Debug)]
315
pub struct Patch<'a> {
316
    pub(crate) span: Range<usize>,
317
    pub(crate) replacement: &'a str,
318
}
319

320
impl<'a> Patch<'a> {
321
    /// Splice `replacement` into the [`Snippet`] at the `span`
322
    ///
323
    /// <div class="warning">
324
    ///
325
    /// Text passed to this function is considered "untrusted input", as such
326
    /// all text is passed through a normalization function. Pre-styled text is
327
    /// not allowed to be passed to this function.
328
    ///
329
    /// </div>
330
    pub fn new(span: Range<usize>, replacement: &'a str) -> Self {
2✔
331
        Self { span, replacement }
332
    }
333

334
    pub(crate) fn is_addition(&self, sm: &SourceMap<'_>) -> bool {
2✔
335
        !self.replacement.is_empty() && !self.replaces_meaningful_content(sm)
2✔
336
    }
337

338
    pub(crate) fn is_deletion(&self, sm: &SourceMap<'_>) -> bool {
6✔
339
        self.replacement.trim().is_empty() && self.replaces_meaningful_content(sm)
6✔
340
    }
341

342
    pub(crate) fn is_replacement(&self, sm: &SourceMap<'_>) -> bool {
4✔
343
        !self.replacement.is_empty() && self.replaces_meaningful_content(sm)
4✔
344
    }
345

346
    /// Whether this is a replacement that overwrites source with a snippet
347
    /// in a way that isn't a superset of the original string. For example,
348
    /// replacing "abc" with "abcde" is not destructive, but replacing it
349
    /// it with "abx" is, since the "c" character is lost.
350
    pub(crate) fn is_destructive_replacement(&self, sm: &SourceMap<'_>) -> bool {
4✔
351
        self.is_replacement(sm)
4✔
352
            && !sm
4✔
353
                .span_to_snippet(self.span.clone())
2✔
354
                // This should use `is_some_and` when our MSRV is >= 1.70
355
                .map_or(false, |s| {
4✔
356
                    as_substr(s.trim(), self.replacement.trim()).is_some()
2✔
357
                })
358
    }
359

360
    fn replaces_meaningful_content(&self, sm: &SourceMap<'_>) -> bool {
4✔
361
        sm.span_to_snippet(self.span.clone())
8✔
362
            .map_or(!self.span.is_empty(), |snippet| !snippet.trim().is_empty())
16✔
363
    }
364

365
    /// Try to turn a replacement into an addition when the span that is being
366
    /// overwritten matches either the prefix or suffix of the replacement.
367
    pub(crate) fn trim_trivial_replacements(&mut self, sm: &'a SourceMap<'a>) {
2✔
368
        if self.replacement.is_empty() {
2✔
UNCOV
369
            return;
×
370
        }
371
        let Some(snippet) = sm.span_to_snippet(self.span.clone()) else {
4✔
UNCOV
372
            return;
×
373
        };
374

375
        if let Some((prefix, substr, suffix)) = as_substr(snippet, self.replacement) {
5✔
376
            self.span = self.span.start + prefix..self.span.end.saturating_sub(suffix);
3✔
377
            self.replacement = substr;
3✔
378
        }
379
    }
380
}
381

382
/// The location of the [`Snippet`] (e.g. a path).
383
///
384
/// This should be used if you want to display the file details by
385
/// itself, or you want to set the line number and column explicitly.
386
/// Otherwise, it is better to use [`Snippet::origin`].
387
///
388
/// Note: `line` is always respected if set, but `char_column` is only
389
/// respected if `line` has been set. `primary` is respected unless the origin
390
/// is the first one in a [`Group`], in which case it is ignored.
391
#[derive(Clone, Debug, Eq, PartialEq)]
392
pub struct Origin<'a> {
393
    pub(crate) origin: &'a str,
394
    pub(crate) line: Option<usize>,
395
    pub(crate) char_column: Option<usize>,
396
    pub(crate) primary: bool,
397
}
398

399
impl<'a> Origin<'a> {
400
    /// <div class="warning">
401
    ///
402
    /// Text passed to this function is considered "untrusted input", as such
403
    /// all text is passed through a normalization function. Pre-styled text is
404
    /// not allowed to be passed to this function.
405
    ///
406
    /// </div>
407
    pub fn new(origin: &'a str) -> Self {
8✔
408
        Self {
409
            origin,
410
            line: None,
411
            char_column: None,
412
            primary: false,
413
        }
414
    }
415

416
    /// Set the default line number to display
417
    ///
418
    /// Otherwise this will be inferred from the primary [`Annotation`]
419
    pub fn line(mut self, line: usize) -> Self {
1✔
420
        self.line = Some(line);
1✔
421
        self
1✔
422
    }
423

424
    /// Set the default column to display
425
    ///
426
    /// Otherwise this will be inferred from the primary [`Annotation`]
427
    pub fn char_column(mut self, char_column: usize) -> Self {
1✔
428
        self.char_column = Some(char_column);
1✔
429
        self
1✔
430
    }
431

432
    pub fn primary(mut self, primary: bool) -> Self {
1✔
433
        self.primary = primary;
1✔
434
        self
1✔
435
    }
436
}
437

438
impl<'a> From<&'a str> for Origin<'a> {
439
    fn from(origin: &'a str) -> Self {
6✔
440
        Self::new(origin)
8✔
441
    }
442
}
443

444
impl<'a> From<&'a String> for Origin<'a> {
NEW
445
    fn from(origin: &'a String) -> Self {
×
NEW
UNCOV
446
        Self::new(origin)
×
447
    }
448
}
449

450
fn newline_count(body: &str) -> usize {
4✔
451
    #[cfg(feature = "simd")]
452
    {
453
        memchr::memchr_iter(b'\n', body.as_bytes())
454
            .count()
455
            .saturating_sub(1)
456
    }
457
    #[cfg(not(feature = "simd"))]
458
    {
459
        body.lines().count().saturating_sub(1)
8✔
460
    }
461
}
462

463
/// Given an original string like `AACC`, and a suggestion like `AABBCC`, try to detect
464
/// the case where a substring of the suggestion is "sandwiched" in the original, like
465
/// `BB` is. Return the length of the prefix, the "trimmed" suggestion, and the length
466
/// of the suffix.
467
fn as_substr<'a>(original: &'a str, suggestion: &'a str) -> Option<(usize, &'a str, usize)> {
3✔
468
    let common_prefix = original
6✔
469
        .chars()
470
        .zip(suggestion.chars())
3✔
471
        .take_while(|(c1, c2)| c1 == c2)
4✔
472
        .map(|(c, _)| c.len_utf8())
4✔
473
        .sum();
474
    let original = &original[common_prefix..];
2✔
475
    let suggestion = &suggestion[common_prefix..];
2✔
476
    if let Some(stripped) = suggestion.strip_suffix(original) {
5✔
477
        let common_suffix = original.len();
2✔
478
        Some((common_prefix, stripped, common_suffix))
3✔
479
    } else {
480
        None
2✔
481
    }
482
}
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