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

rust-lang / annotate-snippets-rs / 15594595085

11 Jun 2025 08:07PM UTC coverage: 86.693%. First build
15594595085

Pull #210

github

web-flow
Merge d4300058b into 9fda85f5c
Pull Request #210: chore: Fix clippy warning

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

1342 of 1548 relevant lines covered (86.69%)

4.31 hits per line

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

77.14
/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 {
5✔
29
        self.id = Some(id);
6✔
30
        self
7✔
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
10✔
37
    }
38

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

56
                            cause.line_start + newline_count(&cause.source[..end])
18✔
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 {
12✔
92
        Self { elements: vec![] }
10✔
93
    }
94

95
    pub fn element(mut self, section: impl Into<Element<'a>>) -> Self {
25✔
96
        self.elements.push(section.into());
55✔
97
        self
26✔
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 {
9✔
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 {
2✔
135
        Element::Suggestion(value)
2✔
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 {
×
147
        Self::Padding(value)
×
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
    pub(crate) primary: bool,
163
}
164

165
impl Title<'_> {
166
    pub fn primary(mut self, primary: bool) -> Self {
×
167
        self.primary = primary;
×
168
        self
×
169
    }
170
}
171

172
/// A source view [`Element`] in a [`Group`]
173
#[derive(Clone, Debug)]
174
pub struct Snippet<'a, T> {
175
    pub(crate) origin: Option<&'a str>,
176
    pub(crate) line_start: usize,
177
    pub(crate) source: &'a str,
178
    pub(crate) markers: Vec<T>,
179
    pub(crate) fold: bool,
180
}
181

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

202
    /// When manually [`fold`][Self::fold]ing,
203
    /// the [`source`][Self::source]s line offset from the original start
204
    pub fn line_start(mut self, line_start: usize) -> Self {
10✔
205
        self.line_start = line_start;
11✔
206
        self
11✔
207
    }
208

209
    /// The location of the [`source`][Self::source] (e.g. a path)
210
    ///
211
    /// <div class="warning">
212
    ///
213
    /// Text passed to this function is considered "untrusted input", as such
214
    /// all text is passed through a normalization function. Pre-styled text is
215
    /// not allowed to be passed to this function.
216
    ///
217
    /// </div>
218
    pub fn origin(mut self, origin: &'a str) -> Self {
8✔
219
        self.origin = Some(origin);
10✔
220
        self
10✔
221
    }
222

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

230
impl<'a> Snippet<'a, Annotation<'a>> {
231
    /// Highlight and describe a span of text within the [`source`][Self::source]
232
    pub fn annotation(mut self, annotation: Annotation<'a>) -> Snippet<'a, Annotation<'a>> {
11✔
233
        self.markers.push(annotation);
9✔
234
        self
11✔
235
    }
236

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

244
impl<'a> Snippet<'a, Patch<'a>> {
245
    /// Suggest to the user an edit to the [`source`][Self::source]
246
    pub fn patch(mut self, patch: Patch<'a>) -> Snippet<'a, Patch<'a>> {
1✔
247
        self.markers.push(patch);
1✔
248
        self
1✔
249
    }
250

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

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

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

286
    /// Style the source according to the [`AnnotationKind`]
287
    pub fn highlight_source(mut self, highlight_source: bool) -> Self {
×
288
        self.highlight_source = highlight_source;
×
289
        self
×
290
    }
291
}
292

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

302
impl AnnotationKind {
303
    pub fn span<'a>(self, span: Range<usize>) -> Annotation<'a> {
7✔
304
        Annotation {
305
            span,
306
            label: None,
307
            kind: self,
308
            highlight_source: false,
309
        }
310
    }
311

312
    pub(crate) fn is_primary(&self) -> bool {
5✔
313
        matches!(self, AnnotationKind::Primary)
5✔
314
    }
315
}
316

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

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

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

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

346
    pub(crate) fn is_replacement(&self, sm: &SourceMap<'_>) -> bool {
2✔
347
        !self.replacement.is_empty() && self.replaces_meaningful_content(sm)
2✔
348
    }
349

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

364
    fn replaces_meaningful_content(&self, sm: &SourceMap<'_>) -> bool {
2✔
365
        sm.span_to_snippet(self.span.clone())
4✔
366
            .map_or(!self.span.is_empty(), |snippet| !snippet.trim().is_empty())
5✔
367
    }
368

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

379
        if let Some((prefix, substr, suffix)) = as_substr(snippet, self.replacement) {
2✔
380
            self.span = self.span.start + prefix..self.span.end.saturating_sub(suffix);
1✔
381
            self.replacement = substr;
1✔
382
        }
383
    }
384
}
385

386
/// The location of the [`Snippet`] (e.g. a path)
387
#[derive(Clone, Debug)]
388
pub struct Origin<'a> {
389
    pub(crate) origin: &'a str,
390
    pub(crate) line: Option<usize>,
391
    pub(crate) char_column: Option<usize>,
392
    pub(crate) primary: bool,
393
    pub(crate) label: Option<&'a str>,
394
}
395

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

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

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

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

435
    /// Like [`Annotation::label`], but when there is no source
436
    ///
437
    /// <div class="warning">
438
    ///
439
    /// Text passed to this function is considered "untrusted input", as such
440
    /// all text is passed through a normalization function. Pre-styled text is
441
    /// not allowed to be passed to this function.
442
    ///
443
    /// </div>
444
    pub fn label(mut self, label: &'a str) -> Self {
1✔
445
        self.label = Some(label);
1✔
446
        self
1✔
447
    }
448
}
449

450
fn newline_count(body: &str) -> usize {
9✔
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)
9✔
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)> {
1✔
468
    let common_prefix = original
2✔
469
        .chars()
470
        .zip(suggestion.chars())
1✔
471
        .take_while(|(c1, c2)| c1 == c2)
2✔
472
        .map(|(c, _)| c.len_utf8())
2✔
473
        .sum();
474
    let original = &original[common_prefix..];
1✔
475
    let suggestion = &suggestion[common_prefix..];
1✔
476
    if let Some(stripped) = suggestion.strip_suffix(original) {
2✔
477
        let common_suffix = original.len();
1✔
478
        Some((common_prefix, stripped, common_suffix))
1✔
479
    } else {
480
        None
1✔
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