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

rust-lang / annotate-snippets-rs / 16028937566

02 Jul 2025 03:11PM UTC coverage: 87.321% (-0.1%) from 87.43%
16028937566

Pull #242

github

web-flow
Merge 141206a6e into 128156280
Pull Request #242: feat: Add Level::no_name

2 of 2 new or added lines in 1 file covered. (100.0%)

2 existing lines in 1 file now uncovered.

1405 of 1609 relevant lines covered (87.32%)

4.66 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
///
22
/// A [diagnostic][crate::Renderer::render] is made of several `Group`s.
23
/// `Group`s are used to [annotate][AnnotationKind::Primary] [`Snippet`]s
24
/// with different [semantic reasons][Title].
25
#[derive(Clone, Debug)]
26
pub struct Group<'a> {
27
    pub(crate) primary_level: Level<'a>,
28
    pub(crate) elements: Vec<Element<'a>>,
29
}
30

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

38
    /// Create a title-less group with a primary [`Level`] for [`Annotation`]s
39
    pub fn with_level(level: Level<'a>) -> Self {
8✔
40
        Self {
41
            primary_level: level,
42
            elements: vec![],
7✔
43
        }
44
    }
45

46
    pub fn element(mut self, section: impl Into<Element<'a>>) -> Self {
19✔
47
        self.elements.push(section.into());
40✔
48
        self
22✔
49
    }
50

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

56
    pub fn is_empty(&self) -> bool {
×
57
        self.elements.is_empty()
×
58
    }
59
}
60

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

72
impl<'a> From<Title<'a>> for Element<'a> {
73
    fn from(value: Title<'a>) -> Self {
8✔
74
        Element::Title(value)
7✔
75
    }
76
}
77

78
impl<'a> From<Snippet<'a, Annotation<'a>>> for Element<'a> {
79
    fn from(value: Snippet<'a, Annotation<'a>>) -> Self {
4✔
80
        Element::Cause(value)
6✔
81
    }
82
}
83

84
impl<'a> From<Snippet<'a, Patch<'a>>> for Element<'a> {
85
    fn from(value: Snippet<'a, Patch<'a>>) -> Self {
4✔
86
        Element::Suggestion(value)
4✔
87
    }
88
}
89

90
impl<'a> From<Origin<'a>> for Element<'a> {
91
    fn from(value: Origin<'a>) -> Self {
2✔
92
        Element::Origin(value)
2✔
93
    }
94
}
95

96
impl From<Padding> for Element<'_> {
97
    fn from(value: Padding) -> Self {
1✔
98
        Self::Padding(value)
1✔
99
    }
100
}
101

102
/// A whitespace [`Element`] in a [`Group`]
103
#[derive(Clone, Debug)]
104
pub struct Padding;
105

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

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

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

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

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

179
    /// When manually [`fold`][Self::fold]ing,
180
    /// the [`source`][Self::source]s line offset from the original start
181
    pub fn line_start(mut self, line_start: usize) -> Self {
10✔
182
        self.line_start = line_start;
10✔
183
        self
10✔
184
    }
185

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

200
    /// Hide lines without [`Annotation`]s
201
    pub fn fold(mut self, fold: bool) -> Self {
4✔
202
        self.fold = fold;
4✔
203
        self
4✔
204
    }
205
}
206

207
impl<'a> Snippet<'a, Annotation<'a>> {
208
    /// Highlight and describe a span of text within the [`source`][Self::source]
209
    pub fn annotation(mut self, annotation: Annotation<'a>) -> Snippet<'a, Annotation<'a>> {
5✔
210
        self.markers.push(annotation);
5✔
211
        self
7✔
212
    }
213

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

221
impl<'a> Snippet<'a, Patch<'a>> {
222
    /// Suggest to the user an edit to the [`source`][Self::source]
223
    pub fn patch(mut self, patch: Patch<'a>) -> Snippet<'a, Patch<'a>> {
3✔
224
        self.markers.push(patch);
3✔
225
        self
4✔
226
    }
227

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

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

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

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

270
/// The category of the [`Annotation`]
271
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
272
#[non_exhaustive]
273
pub enum AnnotationKind {
274
    /// Shows the source that the [Group's Title][Group::with_title] references
275
    ///
276
    /// For [`Title`]-less groups, see [`Group::with_level`]
277
    Primary,
278
    /// Additional context to explain the [`Primary`][Self::Primary]
279
    /// [`Annotation`]
280
    ///
281
    /// See also [`Renderer::context`].
282
    ///
283
    /// [`Renderer::context`]: crate::renderer::Renderer
284
    Context,
285
}
286

287
impl AnnotationKind {
288
    pub fn span<'a>(self, span: Range<usize>) -> Annotation<'a> {
5✔
289
        Annotation {
290
            span,
291
            label: None,
292
            kind: self,
293
            highlight_source: false,
294
        }
295
    }
296

297
    pub(crate) fn is_primary(&self) -> bool {
5✔
298
        matches!(self, AnnotationKind::Primary)
7✔
299
    }
300
}
301

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

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

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

330
    pub(crate) fn is_deletion(&self, sm: &SourceMap<'_>) -> bool {
5✔
331
        self.replacement.trim().is_empty() && self.replaces_meaningful_content(sm)
6✔
332
    }
333

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

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

352
    fn replaces_meaningful_content(&self, sm: &SourceMap<'_>) -> bool {
5✔
353
        sm.span_to_snippet(self.span.clone())
12✔
354
            .map_or(!self.span.is_empty(), |snippet| !snippet.trim().is_empty())
15✔
355
    }
356

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

367
        if let Some((prefix, substr, suffix)) = as_substr(snippet, &self.replacement) {
6✔
368
            self.span = self.span.start + prefix..self.span.end.saturating_sub(suffix);
6✔
369
            self.replacement = Cow::Owned(substr.to_owned());
6✔
370
        }
371
    }
372
}
373

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

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

402
    /// Set the default line number to display
403
    pub fn line(mut self, line: usize) -> Self {
2✔
404
        self.line = Some(line);
2✔
405
        self
2✔
406
    }
407

408
    /// Set the default column to display
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
    /// Mark this as the source that the [Group's Title][Group::with_title] references
421
    pub fn primary(mut self, primary: bool) -> Self {
1✔
422
        self.primary = primary;
1✔
423
        self
1✔
424
    }
425
}
426

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

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

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

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

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

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

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

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