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

rust-lang / annotate-snippets-rs / 15937257238

27 Jun 2025 10:38PM UTC coverage: 87.75% (-0.5%) from 88.235%
15937257238

Pull #232

github

web-flow
Merge 37542efce into 8280ae09b
Pull Request #232: fix: Take Into<Cow> instead of &str

52 of 67 new or added lines in 4 files covered. (77.61%)

3 existing lines in 3 files now uncovered.

1404 of 1600 relevant lines covered (87.75%)

4.83 hits per line

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

74.59
/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) elements: Vec<Element<'a>>,
24
}
25

26
impl Default for Group<'_> {
27
    fn default() -> Self {
×
28
        Self::new()
×
29
    }
30
}
31

32
impl<'a> Group<'a> {
33
    pub fn new() -> Self {
12✔
34
        Self { elements: vec![] }
11✔
35
    }
36

37
    pub fn element(mut self, section: impl Into<Element<'a>>) -> Self {
23✔
38
        self.elements.push(section.into());
52✔
39
        self
28✔
40
    }
41

42
    pub fn elements(mut self, sections: impl IntoIterator<Item = impl Into<Element<'a>>>) -> Self {
×
43
        self.elements.extend(sections.into_iter().map(Into::into));
×
44
        self
×
45
    }
46

47
    pub fn is_empty(&self) -> bool {
×
48
        self.elements.is_empty()
×
49
    }
50
}
51

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

63
impl<'a> From<Title<'a>> for Element<'a> {
64
    fn from(value: Title<'a>) -> Self {
7✔
65
        Element::Title(value)
8✔
66
    }
67
}
68

69
impl<'a> From<Snippet<'a, Annotation<'a>>> for Element<'a> {
70
    fn from(value: Snippet<'a, Annotation<'a>>) -> Self {
10✔
71
        Element::Cause(value)
10✔
72
    }
73
}
74

75
impl<'a> From<Snippet<'a, Patch<'a>>> for Element<'a> {
76
    fn from(value: Snippet<'a, Patch<'a>>) -> Self {
4✔
77
        Element::Suggestion(value)
5✔
78
    }
79
}
80

81
impl<'a> From<Origin<'a>> for Element<'a> {
82
    fn from(value: Origin<'a>) -> Self {
2✔
83
        Element::Origin(value)
2✔
84
    }
85
}
86

87
impl From<Padding> for Element<'_> {
88
    fn from(value: Padding) -> Self {
1✔
89
        Self::Padding(value)
1✔
90
    }
91
}
92

93
/// A whitespace [`Element`] in a [`Group`]
94
#[derive(Clone, Debug)]
95
pub struct Padding;
96

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

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

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

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

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

170
    /// When manually [`fold`][Self::fold]ing,
171
    /// the [`source`][Self::source]s line offset from the original start
172
    pub fn line_start(mut self, line_start: usize) -> Self {
12✔
173
        self.line_start = line_start;
12✔
174
        self
12✔
175
    }
176

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

191
    /// Hide lines without [`Annotation`]s
192
    pub fn fold(mut self, fold: bool) -> Self {
10✔
193
        self.fold = fold;
9✔
194
        self
10✔
195
    }
196
}
197

198
impl<'a> Snippet<'a, Annotation<'a>> {
199
    /// Highlight and describe a span of text within the [`source`][Self::source]
200
    pub fn annotation(mut self, annotation: Annotation<'a>) -> Snippet<'a, Annotation<'a>> {
10✔
201
        self.markers.push(annotation);
10✔
202
        self
11✔
203
    }
204

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

212
impl<'a> Snippet<'a, Patch<'a>> {
213
    /// Suggest to the user an edit to the [`source`][Self::source]
214
    pub fn patch(mut self, patch: Patch<'a>) -> Snippet<'a, Patch<'a>> {
4✔
215
        self.markers.push(patch);
4✔
216
        self
4✔
217
    }
218

219
    /// Suggest to the user edits to the [`source`][Self::source]
220
    pub fn patches(mut self, patches: impl IntoIterator<Item = Patch<'a>>) -> Self {
×
221
        self.markers.extend(patches);
×
222
        self
×
223
    }
224
}
225

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

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

254
    /// Style the source according to the [`AnnotationKind`]
255
    pub fn highlight_source(mut self, highlight_source: bool) -> Self {
×
256
        self.highlight_source = highlight_source;
×
257
        self
×
258
    }
259
}
260

261
/// The category of the [`Annotation`]
262
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
263
pub enum AnnotationKind {
264
    /// Color to the [`Level`] the first [`Title`] in [`Group`]. If no [`Title`]
265
    /// is present, it will default to `error`.
266
    Primary,
267
    /// "secondary"; fixed color
268
    Context,
269
}
270

271
impl AnnotationKind {
272
    pub fn span<'a>(self, span: Range<usize>) -> Annotation<'a> {
7✔
273
        Annotation {
274
            span,
275
            label: None,
276
            kind: self,
277
            highlight_source: false,
278
        }
279
    }
280

281
    pub(crate) fn is_primary(&self) -> bool {
8✔
282
        matches!(self, AnnotationKind::Primary)
6✔
283
    }
284
}
285

286
/// Suggested edit to the [`Snippet`]
287
#[derive(Clone, Debug)]
288
pub struct Patch<'a> {
289
    pub(crate) span: Range<usize>,
290
    pub(crate) replacement: Cow<'a, str>,
291
}
292

293
impl<'a> Patch<'a> {
294
    /// Splice `replacement` into the [`Snippet`] at the `span`
295
    ///
296
    /// <div class="warning">
297
    ///
298
    /// Text passed to this function is considered "untrusted input", as such
299
    /// all text is passed through a normalization function. Pre-styled text is
300
    /// not allowed to be passed to this function.
301
    ///
302
    /// </div>
303
    pub fn new(span: Range<usize>, replacement: impl Into<Cow<'a, str>>) -> Self {
3✔
304
        Self {
305
            span,
306
            replacement: replacement.into(),
4✔
307
        }
308
    }
309

310
    pub(crate) fn is_addition(&self, sm: &SourceMap<'_>) -> bool {
3✔
311
        !self.replacement.is_empty() && !self.replaces_meaningful_content(sm)
3✔
312
    }
313

314
    pub(crate) fn is_deletion(&self, sm: &SourceMap<'_>) -> bool {
6✔
315
        self.replacement.trim().is_empty() && self.replaces_meaningful_content(sm)
5✔
316
    }
317

318
    pub(crate) fn is_replacement(&self, sm: &SourceMap<'_>) -> bool {
5✔
319
        !self.replacement.is_empty() && self.replaces_meaningful_content(sm)
4✔
320
    }
321

322
    /// Whether this is a replacement that overwrites source with a snippet
323
    /// in a way that isn't a superset of the original string. For example,
324
    /// replacing "abc" with "abcde" is not destructive, but replacing it
325
    /// it with "abx" is, since the "c" character is lost.
326
    pub(crate) fn is_destructive_replacement(&self, sm: &SourceMap<'_>) -> bool {
5✔
327
        self.is_replacement(sm)
4✔
328
            && !sm
2✔
329
                .span_to_snippet(self.span.clone())
2✔
330
                // This should use `is_some_and` when our MSRV is >= 1.70
331
                .map_or(false, |s| {
2✔
332
                    as_substr(s.trim(), self.replacement.trim()).is_some()
2✔
333
                })
334
    }
335

336
    fn replaces_meaningful_content(&self, sm: &SourceMap<'_>) -> bool {
4✔
337
        sm.span_to_snippet(self.span.clone())
12✔
338
            .map_or(!self.span.is_empty(), |snippet| !snippet.trim().is_empty())
20✔
339
    }
340

341
    /// Try to turn a replacement into an addition when the span that is being
342
    /// overwritten matches either the prefix or suffix of the replacement.
343
    pub(crate) fn trim_trivial_replacements(&mut self, sm: &'a SourceMap<'a>) {
3✔
344
        if self.replacement.is_empty() {
3✔
345
            return;
×
346
        }
347
        let Some(snippet) = sm.span_to_snippet(self.span.clone()) else {
6✔
348
            return;
×
349
        };
350

351
        if let Some((prefix, substr, suffix)) = as_substr(snippet, &self.replacement) {
5✔
352
            self.span = self.span.start + prefix..self.span.end.saturating_sub(suffix);
3✔
353
            self.replacement = Cow::Owned(substr.to_owned());
4✔
354
        }
355
    }
356
}
357

358
/// The referenced location (e.g. a path)
359
///
360
/// If you have source available, see instead [`Snippet`]
361
#[derive(Clone, Debug)]
362
pub struct Origin<'a> {
363
    pub(crate) path: Cow<'a, str>,
364
    pub(crate) line: Option<usize>,
365
    pub(crate) char_column: Option<usize>,
366
    pub(crate) primary: bool,
367
}
368

369
impl<'a> Origin<'a> {
370
    /// <div class="warning">
371
    ///
372
    /// Text passed to this function is considered "untrusted input", as such
373
    /// all text is passed through a normalization function. Pre-styled text is
374
    /// not allowed to be passed to this function.
375
    ///
376
    /// </div>
377
    pub fn new(path: impl Into<Cow<'a, str>>) -> Self {
4✔
378
        Self {
379
            path: path.into(),
6✔
380
            line: None,
381
            char_column: None,
382
            primary: false,
383
        }
384
    }
385

386
    /// Set the default line number to display
387
    ///
388
    /// Otherwise this will be inferred from the primary [`Annotation`]
389
    pub fn line(mut self, line: usize) -> Self {
2✔
390
        self.line = Some(line);
2✔
391
        self
2✔
392
    }
393

394
    /// Set the default column to display
395
    ///
396
    /// Otherwise this will be inferred from the primary [`Annotation`]
397
    ///
398
    /// <div class="warning">
399
    ///
400
    /// `char_column` is only be respected if [`Origin::line`] is also set.
401
    ///
402
    /// </div>
403
    pub fn char_column(mut self, char_column: usize) -> Self {
2✔
404
        self.char_column = Some(char_column);
2✔
405
        self
2✔
406
    }
407

408
    pub fn primary(mut self, primary: bool) -> Self {
1✔
409
        self.primary = primary;
1✔
410
        self
1✔
411
    }
412
}
413

414
impl<'a> From<Cow<'a, str>> for Origin<'a> {
NEW
415
    fn from(origin: Cow<'a, str>) -> Self {
×
NEW
416
        Self::new(origin)
×
417
    }
418
}
419

420
#[derive(Debug)]
421
pub struct OptionCow<'a>(pub(crate) Option<Cow<'a, str>>);
422

423
impl<'a, T: Into<Cow<'a, str>>> From<Option<T>> for OptionCow<'a> {
424
    fn from(value: Option<T>) -> Self {
1✔
425
        Self(value.map(Into::into))
1✔
426
    }
427
}
428

429
impl<'a> From<&'a Cow<'a, str>> for OptionCow<'a> {
NEW
430
    fn from(value: &'a Cow<'a, str>) -> Self {
×
NEW
431
        Self(Some(Cow::Borrowed(value)))
×
432
    }
433
}
434

435
impl<'a> From<Cow<'a, str>> for OptionCow<'a> {
NEW
436
    fn from(value: Cow<'a, str>) -> Self {
×
NEW
437
        Self(Some(value))
×
438
    }
439
}
440

441
impl<'a> From<&'a str> for OptionCow<'a> {
442
    fn from(value: &'a str) -> Self {
10✔
443
        Self(Some(Cow::Borrowed(value)))
9✔
444
    }
445
}
446
impl<'a> From<String> for OptionCow<'a> {
NEW
447
    fn from(value: String) -> Self {
×
NEW
448
        Self(Some(Cow::Owned(value)))
×
449
    }
450
}
451

452
impl<'a> From<&'a String> for OptionCow<'a> {
NEW
453
    fn from(value: &'a String) -> Self {
×
NEW
454
        Self(Some(Cow::Borrowed(value.as_str())))
×
455
    }
456
}
457

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