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

rust-lang / annotate-snippets-rs / 15916242166

27 Jun 2025 01:44AM UTC coverage: 87.744% (-0.4%) from 88.16%
15916242166

Pull #230

github

web-flow
Merge bff9dd5f8 into 24c8eaa17
Pull Request #230: Add suppost for Hyperlinks on IDs

5 of 12 new or added lines in 2 files covered. (41.67%)

1 existing line in 1 file now uncovered.

1439 of 1640 relevant lines covered (87.74%)

4.66 hits per line

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

78.1
/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<Id<'a>>, // 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.get_or_insert(Id::default()).id = Some(id);
9✔
30
        self
4✔
31
    }
32

33
    /// <div class="warning">
34
    ///
35
    /// This is only relevant if the `id` present
36
    ///
37
    /// </div>
NEW
38
    pub fn id_url(mut self, url: &'a str) -> Self {
×
NEW
39
        self.id.get_or_insert(Id::default()).url = Some(url);
×
UNCOV
40
        self
×
41
    }
42

43
    /// Add an [`Element`] container
44
    pub fn group(mut self, group: Group<'a>) -> Self {
6✔
45
        self.groups.push(group);
8✔
46
        self
7✔
47
    }
48

49
    pub(crate) fn max_line_number(&self) -> usize {
7✔
50
        self.groups
7✔
51
            .iter()
52
            .map(|v| {
7✔
53
                v.elements
7✔
54
                    .iter()
×
55
                    .map(|s| match s {
14✔
56
                        Element::Title(_) | Element::Origin(_) | Element::Padding(_) => 0,
7✔
57
                        Element::Cause(cause) => {
7✔
58
                            let end = cause
19✔
59
                                .markers
×
60
                                .iter()
×
61
                                .map(|a| a.span.end)
13✔
62
                                .max()
×
63
                                .unwrap_or(cause.source.len())
7✔
64
                                .min(cause.source.len());
4✔
65

66
                            cause.line_start + newline_count(&cause.source[..end])
15✔
67
                        }
68
                        Element::Suggestion(suggestion) => {
3✔
69
                            let end = suggestion
9✔
70
                                .markers
×
71
                                .iter()
×
72
                                .map(|a| a.span.end)
6✔
73
                                .max()
×
74
                                .unwrap_or(suggestion.source.len())
3✔
75
                                .min(suggestion.source.len());
3✔
76

77
                            suggestion.line_start + newline_count(&suggestion.source[..end])
6✔
78
                        }
79
                    })
80
                    .max()
×
81
                    .unwrap_or(1)
×
82
            })
83
            .max()
84
            .unwrap_or(1)
85
    }
86
}
87

88
#[derive(Clone, Debug, Default)]
89
pub(crate) struct Id<'a> {
90
    pub(crate) id: Option<&'a str>,
91
    pub(crate) url: Option<&'a str>,
92
}
93

94
/// An [`Element`] container
95
#[derive(Clone, Debug)]
96
pub struct Group<'a> {
97
    pub(crate) elements: Vec<Element<'a>>,
98
}
99

100
impl Default for Group<'_> {
101
    fn default() -> Self {
×
102
        Self::new()
×
103
    }
104
}
105

106
impl<'a> Group<'a> {
107
    pub fn new() -> Self {
12✔
108
        Self { elements: vec![] }
12✔
109
    }
110

111
    pub fn element(mut self, section: impl Into<Element<'a>>) -> Self {
35✔
112
        self.elements.push(section.into());
67✔
113
        self
30✔
114
    }
115

116
    pub fn elements(mut self, sections: impl IntoIterator<Item = impl Into<Element<'a>>>) -> Self {
×
117
        self.elements.extend(sections.into_iter().map(Into::into));
×
118
        self
×
119
    }
120

121
    pub fn is_empty(&self) -> bool {
×
122
        self.elements.is_empty()
×
123
    }
124
}
125

126
/// A section of content within a [`Group`]
127
#[derive(Clone, Debug)]
128
#[non_exhaustive]
129
pub enum Element<'a> {
130
    Title(Title<'a>),
131
    Cause(Snippet<'a, Annotation<'a>>),
132
    Suggestion(Snippet<'a, Patch<'a>>),
133
    Origin(Origin<'a>),
134
    Padding(Padding),
135
}
136

137
impl<'a> From<Title<'a>> for Element<'a> {
138
    fn from(value: Title<'a>) -> Self {
4✔
139
        Element::Title(value)
4✔
140
    }
141
}
142

143
impl<'a> From<Snippet<'a, Annotation<'a>>> for Element<'a> {
144
    fn from(value: Snippet<'a, Annotation<'a>>) -> Self {
10✔
145
        Element::Cause(value)
10✔
146
    }
147
}
148

149
impl<'a> From<Snippet<'a, Patch<'a>>> for Element<'a> {
150
    fn from(value: Snippet<'a, Patch<'a>>) -> Self {
4✔
151
        Element::Suggestion(value)
4✔
152
    }
153
}
154

155
impl<'a> From<Origin<'a>> for Element<'a> {
156
    fn from(value: Origin<'a>) -> Self {
2✔
157
        Element::Origin(value)
2✔
158
    }
159
}
160

161
impl From<Padding> for Element<'_> {
162
    fn from(value: Padding) -> Self {
1✔
163
        Self::Padding(value)
1✔
164
    }
165
}
166

167
/// A whitespace [`Element`] in a [`Group`]
168
#[derive(Clone, Debug)]
169
pub struct Padding;
170

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

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

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

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

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

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

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

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

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

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

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

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

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

303
/// The category of the [`Annotation`]
304
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
305
pub enum AnnotationKind {
306
    /// Color to [`Message`]'s [`Level`]
307
    Primary,
308
    /// "secondary"; fixed color
309
    Context,
310
}
311

312
impl AnnotationKind {
313
    pub fn span<'a>(self, span: Range<usize>) -> Annotation<'a> {
10✔
314
        Annotation {
315
            span,
316
            label: None,
317
            kind: self,
318
            highlight_source: false,
319
        }
320
    }
321

322
    pub(crate) fn is_primary(&self) -> bool {
6✔
323
        matches!(self, AnnotationKind::Primary)
4✔
324
    }
325
}
326

327
/// Suggested edit to the [`Snippet`]
328
#[derive(Clone, Debug)]
329
pub struct Patch<'a> {
330
    pub(crate) span: Range<usize>,
331
    pub(crate) replacement: &'a str,
332
}
333

334
impl<'a> Patch<'a> {
335
    /// Splice `replacement` into the [`Snippet`] at the `span`
336
    ///
337
    /// <div class="warning">
338
    ///
339
    /// Text passed to this function is considered "untrusted input", as such
340
    /// all text is passed through a normalization function. Pre-styled text is
341
    /// not allowed to be passed to this function.
342
    ///
343
    /// </div>
344
    pub fn new(span: Range<usize>, replacement: &'a str) -> Self {
4✔
345
        Self { span, replacement }
346
    }
347

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

352
    pub(crate) fn is_deletion(&self, sm: &SourceMap<'_>) -> bool {
5✔
353
        self.replacement.trim().is_empty() && self.replaces_meaningful_content(sm)
3✔
354
    }
355

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

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

374
    fn replaces_meaningful_content(&self, sm: &SourceMap<'_>) -> bool {
4✔
375
        sm.span_to_snippet(self.span.clone())
7✔
376
            .map_or(!self.span.is_empty(), |snippet| !snippet.trim().is_empty())
12✔
377
    }
378

379
    /// Try to turn a replacement into an addition when the span that is being
380
    /// overwritten matches either the prefix or suffix of the replacement.
381
    pub(crate) fn trim_trivial_replacements(&mut self, sm: &'a SourceMap<'a>) {
4✔
382
        if self.replacement.is_empty() {
4✔
383
            return;
×
384
        }
385
        let Some(snippet) = sm.span_to_snippet(self.span.clone()) else {
4✔
386
            return;
×
387
        };
388

389
        if let Some((prefix, substr, suffix)) = as_substr(snippet, self.replacement) {
5✔
390
            self.span = self.span.start + prefix..self.span.end.saturating_sub(suffix);
7✔
391
            self.replacement = substr;
4✔
392
        }
393
    }
394
}
395

396
/// The referenced location (e.g. a path)
397
///
398
/// If you have source available, see instead [`Snippet`]
399
#[derive(Clone, Debug)]
400
pub struct Origin<'a> {
401
    pub(crate) path: &'a str,
402
    pub(crate) line: Option<usize>,
403
    pub(crate) char_column: Option<usize>,
404
    pub(crate) primary: bool,
405
}
406

407
impl<'a> Origin<'a> {
408
    /// <div class="warning">
409
    ///
410
    /// Text passed to this function is considered "untrusted input", as such
411
    /// all text is passed through a normalization function. Pre-styled text is
412
    /// not allowed to be passed to this function.
413
    ///
414
    /// </div>
415
    pub fn new(path: &'a str) -> Self {
8✔
416
        Self {
417
            path,
418
            line: None,
419
            char_column: None,
420
            primary: false,
421
        }
422
    }
423

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

432
    /// Set the default column to display
433
    ///
434
    /// Otherwise this will be inferred from the primary [`Annotation`]
435
    ///
436
    /// <div class="warning">
437
    ///
438
    /// `char_column` is only be respected if [`Origin::line`] is also set.
439
    ///
440
    /// </div>
441
    pub fn char_column(mut self, char_column: usize) -> Self {
2✔
442
        self.char_column = Some(char_column);
2✔
443
        self
2✔
444
    }
445

446
    pub fn primary(mut self, primary: bool) -> Self {
1✔
447
        self.primary = primary;
1✔
448
        self
1✔
449
    }
450
}
451

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

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