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

rust-lang / annotate-snippets-rs / 16055339376

03 Jul 2025 04:05PM UTC coverage: 87.744%. Remained the same
16055339376

Pull #246

github

web-flow
Merge cf84eb679 into 172ba7581
Pull Request #246: fix: Address clippy::needless_doctest_main

1439 of 1640 relevant lines covered (87.74%)

4.62 hits per line

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

76.8
/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
///
26
/// # Example
27
///
28
/// ```rust
29
/// # #[allow(clippy::needless_doctest_main)]
30
#[doc = include_str!("../examples/highlight_message.rs")]
31
/// ```
32
#[doc = include_str!("../examples/highlight_message.svg")]
33
#[derive(Clone, Debug)]
34
pub struct Group<'a> {
35
    pub(crate) primary_level: Level<'a>,
36
    pub(crate) elements: Vec<Element<'a>>,
37
}
38

39
impl<'a> Group<'a> {
40
    /// Create group with a title, deriving the primary [`Level`] for [`Annotation`]s from it
41
    pub fn with_title(title: Title<'a>) -> Self {
9✔
42
        let level = title.level.clone();
9✔
43
        Self::with_level(level).element(title)
9✔
44
    }
45

46
    /// Create a title-less group with a primary [`Level`] for [`Annotation`]s
47
    ///
48
    /// # Example
49
    ///
50
    /// ```rust
51
    /// # #[allow(clippy::needless_doctest_main)]
52
    #[doc = include_str!("../examples/elide_header.rs")]
53
    /// ```
54
    #[doc = include_str!("../examples/elide_header.svg")]
55
    pub fn with_level(level: Level<'a>) -> Self {
9✔
56
        Self {
57
            primary_level: level,
58
            elements: vec![],
12✔
59
        }
60
    }
61

62
    pub fn element(mut self, section: impl Into<Element<'a>>) -> Self {
29✔
63
        self.elements.push(section.into());
61✔
64
        self
31✔
65
    }
66

67
    pub fn elements(mut self, sections: impl IntoIterator<Item = impl Into<Element<'a>>>) -> Self {
×
68
        self.elements.extend(sections.into_iter().map(Into::into));
×
69
        self
×
70
    }
71

72
    pub fn is_empty(&self) -> bool {
×
73
        self.elements.is_empty()
×
74
    }
75
}
76

77
/// A section of content within a [`Group`]
78
#[derive(Clone, Debug)]
79
#[non_exhaustive]
80
pub enum Element<'a> {
81
    Title(Title<'a>),
82
    Message(Message<'a>),
83
    Cause(Snippet<'a, Annotation<'a>>),
84
    Suggestion(Snippet<'a, Patch<'a>>),
85
    Origin(Origin<'a>),
86
    Padding(Padding),
87
}
88

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

95
impl<'a> From<Message<'a>> for Element<'a> {
96
    fn from(value: Message<'a>) -> Self {
2✔
97
        Element::Message(value)
2✔
98
    }
99
}
100

101
impl<'a> From<Snippet<'a, Annotation<'a>>> for Element<'a> {
102
    fn from(value: Snippet<'a, Annotation<'a>>) -> Self {
10✔
103
        Element::Cause(value)
10✔
104
    }
105
}
106

107
impl<'a> From<Snippet<'a, Patch<'a>>> for Element<'a> {
108
    fn from(value: Snippet<'a, Patch<'a>>) -> Self {
4✔
109
        Element::Suggestion(value)
4✔
110
    }
111
}
112

113
impl<'a> From<Origin<'a>> for Element<'a> {
114
    fn from(value: Origin<'a>) -> Self {
2✔
115
        Element::Origin(value)
2✔
116
    }
117
}
118

119
impl From<Padding> for Element<'_> {
120
    fn from(value: Padding) -> Self {
1✔
121
        Self::Padding(value)
1✔
122
    }
123
}
124

125
/// A whitespace [`Element`] in a [`Group`]
126
#[derive(Clone, Debug)]
127
pub struct Padding;
128

129
/// A text [`Element`] to start a [`Group`]
130
///
131
/// See [`Level::title`] to create this.
132
#[derive(Clone, Debug)]
133
pub struct Title<'a> {
134
    pub(crate) level: Level<'a>,
135
    pub(crate) id: Option<Id<'a>>,
136
    pub(crate) text: Cow<'a, str>,
137
}
138

139
impl<'a> Title<'a> {
140
    /// <div class="warning">
141
    ///
142
    /// This is only relevant if the title is the first element of a group.
143
    ///
144
    /// </div>
145
    /// <div class="warning">
146
    ///
147
    /// Text passed to this function is considered "untrusted input", as such
148
    /// all text is passed through a normalization function. Pre-styled text is
149
    /// not allowed to be passed to this function.
150
    ///
151
    /// </div>
152
    pub fn id(mut self, id: impl Into<Cow<'a, str>>) -> Self {
4✔
153
        self.id.get_or_insert(Id::default()).id = Some(id.into());
10✔
154
        self
4✔
155
    }
156

157
    /// <div class="warning">
158
    ///
159
    /// This is only relevant if the title is the first element of a group and
160
    /// `id` present
161
    ///
162
    /// </div>
163
    pub fn id_url(mut self, url: impl Into<Cow<'a, str>>) -> Self {
×
164
        self.id.get_or_insert(Id::default()).url = Some(url.into());
×
165
        self
×
166
    }
167
}
168

169
/// A text [`Element`] in a [`Group`]
170
///
171
/// See [`Level::message`] to create this.
172
#[derive(Clone, Debug)]
173
pub struct Message<'a> {
174
    pub(crate) level: Level<'a>,
175
    pub(crate) text: Cow<'a, str>,
176
}
177

178
/// A source view [`Element`] in a [`Group`]
179
///
180
/// If you do not have [source][Snippet::source] available, see instead [`Origin`]
181
#[derive(Clone, Debug)]
182
pub struct Snippet<'a, T> {
183
    pub(crate) path: Option<Cow<'a, str>>,
184
    pub(crate) line_start: usize,
185
    pub(crate) source: Cow<'a, str>,
186
    pub(crate) markers: Vec<T>,
187
    pub(crate) fold: bool,
188
}
189

190
impl<'a, T: Clone> Snippet<'a, T> {
191
    /// The source code to be rendered
192
    ///
193
    /// <div class="warning">
194
    ///
195
    /// Text passed to this function is considered "untrusted input", as such
196
    /// all text is passed through a normalization function. Pre-styled text is
197
    /// not allowed to be passed to this function.
198
    ///
199
    /// </div>
200
    pub fn source(source: impl Into<Cow<'a, str>>) -> Self {
16✔
201
        Self {
202
            path: None,
203
            line_start: 1,
204
            source: source.into(),
16✔
205
            markers: vec![],
16✔
206
            fold: true,
207
        }
208
    }
209

210
    /// When manually [`fold`][Self::fold]ing,
211
    /// the [`source`][Self::source]s line offset from the original start
212
    pub fn line_start(mut self, line_start: usize) -> Self {
12✔
213
        self.line_start = line_start;
13✔
214
        self
13✔
215
    }
216

217
    /// The location of the [`source`][Self::source] (e.g. a path)
218
    ///
219
    /// <div class="warning">
220
    ///
221
    /// Text passed to this function is considered "untrusted input", as such
222
    /// all text is passed through a normalization function. Pre-styled text is
223
    /// not allowed to be passed to this function.
224
    ///
225
    /// </div>
226
    pub fn path(mut self, path: impl Into<OptionCow<'a>>) -> Self {
13✔
227
        self.path = path.into().0;
28✔
228
        self
15✔
229
    }
230

231
    /// Hide lines without [`Annotation`]s
232
    pub fn fold(mut self, fold: bool) -> Self {
4✔
233
        self.fold = fold;
4✔
234
        self
4✔
235
    }
236
}
237

238
impl<'a> Snippet<'a, Annotation<'a>> {
239
    /// Highlight and describe a span of text within the [`source`][Self::source]
240
    pub fn annotation(mut self, annotation: Annotation<'a>) -> Snippet<'a, Annotation<'a>> {
6✔
241
        self.markers.push(annotation);
6✔
242
        self
8✔
243
    }
244

245
    /// Highlight and describe spans of text within the [`source`][Self::source]
246
    pub fn annotations(mut self, annotation: impl IntoIterator<Item = Annotation<'a>>) -> Self {
×
247
        self.markers.extend(annotation);
×
248
        self
×
249
    }
250
}
251

252
impl<'a> Snippet<'a, Patch<'a>> {
253
    /// Suggest to the user an edit to the [`source`][Self::source]
254
    pub fn patch(mut self, patch: Patch<'a>) -> Snippet<'a, Patch<'a>> {
4✔
255
        self.markers.push(patch);
4✔
256
        self
4✔
257
    }
258

259
    /// Suggest to the user edits to the [`source`][Self::source]
260
    pub fn patches(mut self, patches: impl IntoIterator<Item = Patch<'a>>) -> Self {
×
261
        self.markers.extend(patches);
×
262
        self
×
263
    }
264
}
265

266
/// Highlighted and describe a span of text within a [`Snippet`]
267
///
268
/// See [`AnnotationKind`] to create an annotation.
269
#[derive(Clone, Debug)]
270
pub struct Annotation<'a> {
271
    pub(crate) span: Range<usize>,
272
    pub(crate) label: Option<Cow<'a, str>>,
273
    pub(crate) kind: AnnotationKind,
274
    pub(crate) highlight_source: bool,
275
}
276

277
impl<'a> Annotation<'a> {
278
    /// Describe the reason the span is highlighted
279
    ///
280
    /// This will be styled according to the [`AnnotationKind`]
281
    ///
282
    /// <div class="warning">
283
    ///
284
    /// Text passed to this function is considered "untrusted input", as such
285
    /// all text is passed through a normalization function. Pre-styled text is
286
    /// not allowed to be passed to this function.
287
    ///
288
    /// </div>
289
    pub fn label(mut self, label: impl Into<OptionCow<'a>>) -> Self {
6✔
290
        self.label = label.into().0;
11✔
291
        self
5✔
292
    }
293

294
    /// Style the source according to the [`AnnotationKind`]
295
    pub fn highlight_source(mut self, highlight_source: bool) -> Self {
×
296
        self.highlight_source = highlight_source;
×
297
        self
×
298
    }
299
}
300

301
/// The category of the [`Annotation`]
302
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
303
#[non_exhaustive]
304
pub enum AnnotationKind {
305
    /// Shows the source that the [Group's Title][Group::with_title] references
306
    ///
307
    /// For [`Title`]-less groups, see [`Group::with_level`]
308
    Primary,
309
    /// Additional context to explain the [`Primary`][Self::Primary]
310
    /// [`Annotation`]
311
    ///
312
    /// See also [`Renderer::context`].
313
    ///
314
    /// [`Renderer::context`]: crate::renderer::Renderer
315
    Context,
316
}
317

318
impl AnnotationKind {
319
    pub fn span<'a>(self, span: Range<usize>) -> Annotation<'a> {
8✔
320
        Annotation {
321
            span,
322
            label: None,
323
            kind: self,
324
            highlight_source: false,
325
        }
326
    }
327

328
    pub(crate) fn is_primary(&self) -> bool {
7✔
329
        matches!(self, AnnotationKind::Primary)
5✔
330
    }
331
}
332

333
/// Suggested edit to the [`Snippet`]
334
#[derive(Clone, Debug)]
335
pub struct Patch<'a> {
336
    pub(crate) span: Range<usize>,
337
    pub(crate) replacement: Cow<'a, str>,
338
}
339

340
impl<'a> Patch<'a> {
341
    /// Splice `replacement` into the [`Snippet`] at the `span`
342
    ///
343
    /// <div class="warning">
344
    ///
345
    /// Text passed to this function is considered "untrusted input", as such
346
    /// all text is passed through a normalization function. Pre-styled text is
347
    /// not allowed to be passed to this function.
348
    ///
349
    /// </div>
350
    pub fn new(span: Range<usize>, replacement: impl Into<Cow<'a, str>>) -> Self {
4✔
351
        Self {
352
            span,
353
            replacement: replacement.into(),
4✔
354
        }
355
    }
356

357
    pub(crate) fn is_addition(&self, sm: &SourceMap<'_>) -> bool {
2✔
358
        !self.replacement.is_empty() && !self.replaces_meaningful_content(sm)
2✔
359
    }
360

361
    pub(crate) fn is_deletion(&self, sm: &SourceMap<'_>) -> bool {
3✔
362
        self.replacement.trim().is_empty() && self.replaces_meaningful_content(sm)
5✔
363
    }
364

365
    pub(crate) fn is_replacement(&self, sm: &SourceMap<'_>) -> bool {
2✔
366
        !self.replacement.is_empty() && self.replaces_meaningful_content(sm)
4✔
367
    }
368

369
    /// Whether this is a replacement that overwrites source with a snippet
370
    /// in a way that isn't a superset of the original string. For example,
371
    /// replacing "abc" with "abcde" is not destructive, but replacing it
372
    /// it with "abx" is, since the "c" character is lost.
373
    pub(crate) fn is_destructive_replacement(&self, sm: &SourceMap<'_>) -> bool {
2✔
374
        self.is_replacement(sm)
4✔
375
            && !sm
3✔
376
                .span_to_snippet(self.span.clone())
2✔
377
                // This should use `is_some_and` when our MSRV is >= 1.70
378
                .map_or(false, |s| {
2✔
379
                    as_substr(s.trim(), self.replacement.trim()).is_some()
2✔
380
                })
381
    }
382

383
    fn replaces_meaningful_content(&self, sm: &SourceMap<'_>) -> bool {
3✔
384
        sm.span_to_snippet(self.span.clone())
9✔
385
            .map_or(!self.span.is_empty(), |snippet| !snippet.trim().is_empty())
11✔
386
    }
387

388
    /// Try to turn a replacement into an addition when the span that is being
389
    /// overwritten matches either the prefix or suffix of the replacement.
390
    pub(crate) fn trim_trivial_replacements(&mut self, sm: &'a SourceMap<'a>) {
3✔
391
        if self.replacement.is_empty() {
3✔
392
            return;
×
393
        }
394
        let Some(snippet) = sm.span_to_snippet(self.span.clone()) else {
4✔
395
            return;
×
396
        };
397

398
        if let Some((prefix, substr, suffix)) = as_substr(snippet, &self.replacement) {
5✔
399
            self.span = self.span.start + prefix..self.span.end.saturating_sub(suffix);
2✔
400
            self.replacement = Cow::Owned(substr.to_owned());
5✔
401
        }
402
    }
403
}
404

405
/// A source location [`Element`] in a [`Group`]
406
///
407
/// If you have source available, see instead [`Snippet`]
408
///
409
/// # Example
410
///
411
/// ```rust
412
/// # use annotate_snippets::{Group, Snippet, AnnotationKind, Level, Origin};
413
/// let input = &[
414
///     Group::with_title(Level::ERROR.title("mismatched types").id("E0308"))
415
///         .element(
416
///             Origin::path("$DIR/mismatched-types.rs")
417
///         )
418
/// ];
419
/// ```
420
#[derive(Clone, Debug)]
421
pub struct Origin<'a> {
422
    pub(crate) path: Cow<'a, str>,
423
    pub(crate) line: Option<usize>,
424
    pub(crate) char_column: Option<usize>,
425
    pub(crate) primary: bool,
426
}
427

428
impl<'a> Origin<'a> {
429
    /// <div class="warning">
430
    ///
431
    /// Text passed to this function is considered "untrusted input", as such
432
    /// all text is passed through a normalization function. Pre-styled text is
433
    /// not allowed to be passed to this function.
434
    ///
435
    /// </div>
436
    pub fn path(path: impl Into<Cow<'a, str>>) -> Self {
7✔
437
        Self {
438
            path: path.into(),
5✔
439
            line: None,
440
            char_column: None,
441
            primary: false,
442
        }
443
    }
444

445
    /// Set the default line number to display
446
    pub fn line(mut self, line: usize) -> Self {
2✔
447
        self.line = Some(line);
2✔
448
        self
2✔
449
    }
450

451
    /// Set the default column to display
452
    ///
453
    /// <div class="warning">
454
    ///
455
    /// `char_column` is only be respected if [`Origin::line`] is also set.
456
    ///
457
    /// </div>
458
    pub fn char_column(mut self, char_column: usize) -> Self {
2✔
459
        self.char_column = Some(char_column);
2✔
460
        self
2✔
461
    }
462

463
    /// Mark this as the source that the [Group's Title][Group::with_title] references
464
    pub fn primary(mut self, primary: bool) -> Self {
1✔
465
        self.primary = primary;
1✔
466
        self
1✔
467
    }
468
}
469

470
impl<'a> From<Cow<'a, str>> for Origin<'a> {
471
    fn from(origin: Cow<'a, str>) -> Self {
×
472
        Self::path(origin)
×
473
    }
474
}
475

476
#[derive(Debug)]
477
pub struct OptionCow<'a>(pub(crate) Option<Cow<'a, str>>);
478

479
impl<'a, T: Into<Cow<'a, str>>> From<Option<T>> for OptionCow<'a> {
480
    fn from(value: Option<T>) -> Self {
1✔
481
        Self(value.map(Into::into))
1✔
482
    }
483
}
484

485
impl<'a> From<&'a Cow<'a, str>> for OptionCow<'a> {
486
    fn from(value: &'a Cow<'a, str>) -> Self {
×
487
        Self(Some(Cow::Borrowed(value)))
×
488
    }
489
}
490

491
impl<'a> From<Cow<'a, str>> for OptionCow<'a> {
492
    fn from(value: Cow<'a, str>) -> Self {
×
493
        Self(Some(value))
×
494
    }
495
}
496

497
impl<'a> From<&'a str> for OptionCow<'a> {
498
    fn from(value: &'a str) -> Self {
10✔
499
        Self(Some(Cow::Borrowed(value)))
12✔
500
    }
501
}
502
impl<'a> From<String> for OptionCow<'a> {
503
    fn from(value: String) -> Self {
×
504
        Self(Some(Cow::Owned(value)))
×
505
    }
506
}
507

508
impl<'a> From<&'a String> for OptionCow<'a> {
509
    fn from(value: &'a String) -> Self {
×
510
        Self(Some(Cow::Borrowed(value.as_str())))
×
511
    }
512
}
513

514
/// Given an original string like `AACC`, and a suggestion like `AABBCC`, try to detect
515
/// the case where a substring of the suggestion is "sandwiched" in the original, like
516
/// `BB` is. Return the length of the prefix, the "trimmed" suggestion, and the length
517
/// of the suffix.
518
fn as_substr<'a>(original: &'a str, suggestion: &'a str) -> Option<(usize, &'a str, usize)> {
3✔
519
    let common_prefix = original
6✔
520
        .chars()
521
        .zip(suggestion.chars())
3✔
522
        .take_while(|(c1, c2)| c1 == c2)
6✔
523
        .map(|(c, _)| c.len_utf8())
4✔
524
        .sum();
525
    let original = &original[common_prefix..];
2✔
526
    let suggestion = &suggestion[common_prefix..];
2✔
527
    if let Some(stripped) = suggestion.strip_suffix(original) {
5✔
528
        let common_suffix = original.len();
2✔
529
        Some((common_prefix, stripped, common_suffix))
2✔
530
    } else {
531
        None
3✔
532
    }
533
}
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