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

rust-lang / annotate-snippets-rs / 14500801022

16 Apr 2025 07:20PM UTC coverage: 87.161% (+1.3%) from 85.84%
14500801022

Pull #195

github

web-flow
Merge 353a04044 into 5e302a9f1
Pull Request #195: New api

1246 of 1415 new or added lines in 7 files covered. (88.06%)

2 existing lines in 1 file now uncovered.

1351 of 1550 relevant lines covered (87.16%)

3.8 hits per line

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

81.43
/src/snippet.rs
1
//! Structures used as an input for the library.
2

3
use crate::level::Level;
4
use crate::renderer::source_map::SourceMap;
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
#[derive(Debug)]
14
pub struct Message<'a> {
15
    pub(crate) id: Option<&'a str>, // for "correctness", could be sloppy and be on Title
16
    pub(crate) groups: Vec<Group<'a>>,
17
}
18

19
impl<'a> Message<'a> {
20
    pub fn id(mut self, id: &'a str) -> Self {
3✔
21
        self.id = Some(id);
3✔
22
        self
5✔
23
    }
24

25
    pub fn group(mut self, group: Group<'a>) -> Self {
6✔
26
        self.groups.push(group);
4✔
27
        self
6✔
28
    }
29

30
    pub(crate) fn max_line_number(&self) -> usize {
6✔
31
        self.groups
6✔
32
            .iter()
33
            .map(|v| {
6✔
34
                v.elements
6✔
NEW
35
                    .iter()
×
36
                    .map(|s| match s {
12✔
37
                        Element::Title(_) | Element::Origin(_) | Element::ColumnSeparator(_) => 0,
5✔
38
                        Element::Cause(cause) => {
7✔
39
                            let end = cause
15✔
NEW
40
                                .markers
×
NEW
41
                                .iter()
×
42
                                .map(|a| a.range.end)
12✔
NEW
43
                                .max()
×
44
                                .unwrap_or(cause.source.len())
7✔
45
                                .min(cause.source.len());
7✔
46

47
                            cause.line_start + newline_count(&cause.source[..end])
12✔
48
                        }
49
                        Element::Suggestion(suggestion) => {
1✔
50
                            let end = suggestion
3✔
NEW
51
                                .markers
×
NEW
52
                                .iter()
×
53
                                .map(|a| a.range.end)
2✔
NEW
54
                                .max()
×
55
                                .unwrap_or(suggestion.source.len())
1✔
56
                                .min(suggestion.source.len());
1✔
57

58
                            suggestion.line_start + newline_count(&suggestion.source[..end])
2✔
59
                        }
60
                    })
NEW
61
                    .max()
×
NEW
62
                    .unwrap_or(1)
×
63
            })
64
            .max()
65
            .unwrap_or(1)
66
    }
67
}
68

69
#[derive(Debug)]
70
pub struct Group<'a> {
71
    pub(crate) elements: Vec<Element<'a>>,
72
}
73

74
impl Default for Group<'_> {
NEW
75
    fn default() -> Self {
×
NEW
76
        Self::new()
×
77
    }
78
}
79

80
impl<'a> Group<'a> {
81
    pub fn new() -> Self {
6✔
82
        Self { elements: vec![] }
8✔
83
    }
84

85
    pub fn element(mut self, section: impl Into<Element<'a>>) -> Self {
18✔
86
        self.elements.push(section.into());
36✔
87
        self
16✔
88
    }
89

90
    pub fn elements(mut self, sections: impl IntoIterator<Item = impl Into<Element<'a>>>) -> Self {
1✔
91
        self.elements.extend(sections.into_iter().map(Into::into));
2✔
92
        self
1✔
93
    }
94

NEW
95
    pub fn is_empty(&self) -> bool {
×
NEW
96
        self.elements.is_empty()
×
97
    }
98
}
99

100
#[derive(Debug)]
101
#[non_exhaustive]
102
pub enum Element<'a> {
103
    Title(Title<'a>),
104
    Cause(Snippet<'a, Annotation<'a>>),
105
    Suggestion(Snippet<'a, Patch<'a>>),
106
    Origin(Origin<'a>),
107
    ColumnSeparator(ColumnSeparator),
108
}
109

110
impl<'a> From<Title<'a>> for Element<'a> {
111
    fn from(value: Title<'a>) -> Self {
4✔
112
        Element::Title(value)
4✔
113
    }
114
}
115

116
impl<'a> From<Snippet<'a, Annotation<'a>>> for Element<'a> {
117
    fn from(value: Snippet<'a, Annotation<'a>>) -> Self {
5✔
118
        Element::Cause(value)
3✔
119
    }
120
}
121

122
impl<'a> From<Snippet<'a, Patch<'a>>> for Element<'a> {
123
    fn from(value: Snippet<'a, Patch<'a>>) -> Self {
2✔
124
        Element::Suggestion(value)
2✔
125
    }
126
}
127

128
impl<'a> From<Origin<'a>> for Element<'a> {
129
    fn from(value: Origin<'a>) -> Self {
1✔
130
        Element::Origin(value)
1✔
131
    }
132
}
133

134
impl From<ColumnSeparator> for Element<'_> {
NEW
135
    fn from(value: ColumnSeparator) -> Self {
×
NEW
136
        Self::ColumnSeparator(value)
×
137
    }
138
}
139

140
#[derive(Debug)]
141
pub struct ColumnSeparator;
142

143
#[derive(Debug)]
144
pub struct Title<'a> {
145
    pub(crate) level: Level<'a>,
146
    pub(crate) title: &'a str,
147
    pub(crate) primary: bool,
148
}
149

150
impl Title<'_> {
NEW
151
    pub fn primary(mut self, primary: bool) -> Self {
×
NEW
152
        self.primary = primary;
×
UNCOV
153
        self
×
154
    }
155
}
156

157
#[derive(Debug)]
158
pub struct Snippet<'a, T> {
159
    pub(crate) origin: Option<&'a str>,
160
    pub(crate) line_start: usize,
161
    pub(crate) source: &'a str,
162
    pub(crate) markers: Vec<T>,
163
    pub(crate) fold: bool,
164
}
165

166
impl<'a, T: Clone> Snippet<'a, T> {
167
    /// Text passed to this function is considered "untrusted input", as such
168
    /// all text is passed through a normalization function. Pre-styled text is
169
    /// not allowed to be passed to this function.
170
    pub fn source(source: &'a str) -> Self {
8✔
171
        Self {
172
            origin: None,
173
            line_start: 1,
174
            source,
175
            markers: vec![],
8✔
176
            fold: false,
177
        }
178
    }
179

180
    pub fn line_start(mut self, line_start: usize) -> Self {
7✔
181
        self.line_start = line_start;
8✔
182
        self
8✔
183
    }
184

185
    /// Text passed to this function is considered "untrusted input", as such
186
    /// all text is passed through a normalization function. Pre-styled text is
187
    /// not allowed to be passed to this function.
188
    pub fn origin(mut self, origin: &'a str) -> Self {
7✔
189
        self.origin = Some(origin);
7✔
190
        self
7✔
191
    }
192

193
    pub fn fold(mut self, fold: bool) -> Self {
4✔
194
        self.fold = fold;
6✔
195
        self
4✔
196
    }
197
}
198

199
impl<'a> Snippet<'a, Annotation<'a>> {
200
    pub fn annotation(mut self, annotation: Annotation<'a>) -> Snippet<'a, Annotation<'a>> {
3✔
201
        self.markers.push(annotation);
5✔
202
        self
3✔
203
    }
204

205
    pub fn annotations(mut self, annotation: impl IntoIterator<Item = Annotation<'a>>) -> Self {
1✔
206
        self.markers.extend(annotation);
1✔
207
        self
1✔
208
    }
209
}
210

211
impl<'a> Snippet<'a, Patch<'a>> {
212
    pub fn patch(mut self, patch: Patch<'a>) -> Snippet<'a, Patch<'a>> {
2✔
213
        self.markers.push(patch);
2✔
214
        self
2✔
215
    }
216

NEW
217
    pub fn patches(mut self, patches: impl IntoIterator<Item = Patch<'a>>) -> Self {
×
NEW
218
        self.markers.extend(patches);
×
UNCOV
219
        self
×
220
    }
221
}
222

223
#[derive(Clone, Debug)]
224
pub struct Annotation<'a> {
225
    pub(crate) range: Range<usize>,
226
    pub(crate) label: Option<&'a str>,
227
    pub(crate) kind: AnnotationKind,
228
    pub(crate) highlight_source: bool,
229
}
230

231
impl<'a> Annotation<'a> {
232
    /// Text passed to this function is considered "untrusted input", as such
233
    /// all text is passed through a normalization function. Pre-styled text is
234
    /// not allowed to be passed to this function.
235
    pub fn label(mut self, label: &'a str) -> Self {
8✔
236
        self.label = Some(label);
6✔
237
        self
8✔
238
    }
239

NEW
240
    pub fn highlight_source(mut self, highlight_source: bool) -> Self {
×
NEW
241
        self.highlight_source = highlight_source;
×
NEW
242
        self
×
243
    }
244
}
245

246
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
247
pub enum AnnotationKind {
248
    /// Color to [`Message`]'s [`Level`]
249
    Primary,
250
    /// "secondary"; fixed color
251
    Context,
252
}
253

254
impl AnnotationKind {
255
    pub fn span<'a>(self, span: Range<usize>) -> Annotation<'a> {
7✔
256
        Annotation {
257
            range: span,
258
            label: None,
259
            kind: self,
260
            highlight_source: false,
261
        }
262
    }
263

264
    pub(crate) fn is_primary(&self) -> bool {
5✔
265
        matches!(self, AnnotationKind::Primary)
3✔
266
    }
267
}
268

269
#[derive(Clone, Debug)]
270
pub struct Patch<'a> {
271
    pub(crate) range: Range<usize>,
272
    pub(crate) replacement: &'a str,
273
}
274

275
impl<'a> Patch<'a> {
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
    pub fn new(range: Range<usize>, replacement: &'a str) -> Self {
2✔
280
        Self { range, replacement }
281
    }
282

283
    pub(crate) fn is_addition(&self, sm: &SourceMap<'_>) -> bool {
1✔
284
        !self.replacement.is_empty() && !self.replaces_meaningful_content(sm)
1✔
285
    }
286

287
    pub(crate) fn is_deletion(&self, sm: &SourceMap<'_>) -> bool {
2✔
288
        self.replacement.trim().is_empty() && self.replaces_meaningful_content(sm)
2✔
289
    }
290

291
    pub(crate) fn is_replacement(&self, sm: &SourceMap<'_>) -> bool {
2✔
292
        !self.replacement.is_empty() && self.replaces_meaningful_content(sm)
2✔
293
    }
294

295
    /// Whether this is a replacement that overwrites source with a snippet
296
    /// in a way that isn't a superset of the original string. For example,
297
    /// replacing "abc" with "abcde" is not destructive, but replacing it
298
    /// it with "abx" is, since the "c" character is lost.
299
    pub(crate) fn is_destructive_replacement(&self, sm: &SourceMap<'_>) -> bool {
2✔
300
        self.is_replacement(sm)
2✔
301
            && !sm
4✔
302
                .span_to_snippet(self.range.clone())
1✔
303
                // This should use `is_some_and` when our MSRV is >= 1.70
304
                .map_or(false, |s| {
4✔
305
                    as_substr(s.trim(), self.replacement.trim()).is_some()
3✔
306
                })
307
    }
308

309
    fn replaces_meaningful_content(&self, sm: &SourceMap<'_>) -> bool {
2✔
310
        sm.span_to_snippet(self.range.clone())
4✔
311
            .map_or(!self.range.is_empty(), |snippet| !snippet.trim().is_empty())
6✔
312
    }
313

314
    /// Try to turn a replacement into an addition when the span that is being
315
    /// overwritten matches either the prefix or suffix of the replacement.
316
    pub(crate) fn trim_trivial_replacements(&mut self, sm: &'a SourceMap<'a>) {
1✔
317
        if self.replacement.is_empty() {
1✔
NEW
318
            return;
×
319
        }
320
        let Some(snippet) = sm.span_to_snippet(self.range.clone()) else {
2✔
NEW
321
            return;
×
322
        };
323

324
        if let Some((prefix, substr, suffix)) = as_substr(snippet, self.replacement) {
2✔
325
            self.range = self.range.start + prefix..self.range.end.saturating_sub(suffix);
1✔
326
            self.replacement = substr;
1✔
327
        }
328
    }
329
}
330

331
#[derive(Clone, Debug)]
332
pub struct Origin<'a> {
333
    pub(crate) origin: &'a str,
334
    pub(crate) line: Option<usize>,
335
    pub(crate) char_column: Option<usize>,
336
    pub(crate) primary: bool,
337
    pub(crate) label: Option<&'a str>,
338
}
339

340
impl<'a> Origin<'a> {
341
    /// Text passed to this function is considered "untrusted input", as such
342
    /// all text is passed through a normalization function. Pre-styled text is
343
    /// not allowed to be passed to this function.
344
    pub fn new(origin: &'a str) -> Self {
4✔
345
        Self {
346
            origin,
347
            line: None,
348
            char_column: None,
349
            primary: false,
350
            label: None,
351
        }
352
    }
353

354
    pub fn line(mut self, line: usize) -> Self {
1✔
355
        self.line = Some(line);
1✔
356
        self
1✔
357
    }
358

359
    pub fn char_column(mut self, char_column: usize) -> Self {
1✔
360
        self.char_column = Some(char_column);
1✔
361
        self
1✔
362
    }
363

364
    pub fn primary(mut self, primary: bool) -> Self {
1✔
365
        self.primary = primary;
1✔
366
        self
1✔
367
    }
368

369
    /// Text passed to this function is considered "untrusted input", as such
370
    /// all text is passed through a normalization function. Pre-styled text is
371
    /// not allowed to be passed to this function.
372
    pub fn label(mut self, label: &'a str) -> Self {
1✔
373
        self.label = Some(label);
1✔
374
        self
1✔
375
    }
376
}
377

378
fn newline_count(body: &str) -> usize {
5✔
379
    #[cfg(feature = "simd")]
380
    {
381
        memchr::memchr_iter(b'\n', body.as_bytes())
382
            .count()
383
            .saturating_sub(1)
384
    }
385
    #[cfg(not(feature = "simd"))]
386
    {
387
        body.lines().count().saturating_sub(1)
6✔
388
    }
389
}
390

391
/// Given an original string like `AACC`, and a suggestion like `AABBCC`, try to detect
392
/// the case where a substring of the suggestion is "sandwiched" in the original, like
393
/// `BB` is. Return the length of the prefix, the "trimmed" suggestion, and the length
394
/// of the suffix.
395
fn as_substr<'a>(original: &'a str, suggestion: &'a str) -> Option<(usize, &'a str, usize)> {
1✔
396
    let common_prefix = original
2✔
397
        .chars()
398
        .zip(suggestion.chars())
1✔
399
        .take_while(|(c1, c2)| c1 == c2)
2✔
400
        .map(|(c, _)| c.len_utf8())
2✔
401
        .sum();
402
    let original = &original[common_prefix..];
1✔
403
    let suggestion = &suggestion[common_prefix..];
1✔
404
    if let Some(stripped) = suggestion.strip_suffix(original) {
2✔
405
        let common_suffix = original.len();
1✔
406
        Some((common_prefix, stripped, common_suffix))
1✔
407
    } else {
408
        None
1✔
409
    }
410
}
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