Skip to content

Commit db0b7e4

Browse files
authoredSep 20, 2023
feat(labels): Add support for primary label in specifying line/col information (#291)
1 parent cc81382 commit db0b7e4

File tree

6 files changed

+174
-51
lines changed

6 files changed

+174
-51
lines changed
 

‎miette-derive/src/label.rs

+61-18
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@ struct Label {
2020
label: Option<Display>,
2121
ty: syn::Type,
2222
span: syn::Member,
23+
primary: bool,
2324
}
2425

2526
struct LabelAttr {
2627
label: Option<Display>,
28+
primary: bool,
2729
}
2830

2931
impl Parse for LabelAttr {
@@ -40,10 +42,22 @@ impl Parse for LabelAttr {
4042
}
4143
});
4244
let la = input.lookahead1();
43-
let label = if la.peek(syn::token::Paren) {
44-
// #[label("{}", x)]
45+
let (primary, label) = if la.peek(syn::token::Paren) {
46+
// #[label(primary?, "{}", x)]
4547
let content;
4648
parenthesized!(content in input);
49+
50+
let primary = if content.peek(syn::Ident) {
51+
let ident: syn::Ident = content.parse()?;
52+
if ident != "primary" {
53+
return Err(syn::Error::new(input.span(), "Invalid argument to label() attribute. The argument must be a literal string or the keyword `primary`."));
54+
}
55+
let _ = content.parse::<Token![,]>();
56+
true
57+
} else {
58+
false
59+
};
60+
4761
if content.peek(syn::LitStr) {
4862
let fmt = content.parse()?;
4963
let args = if content.is_empty() {
@@ -56,22 +70,27 @@ impl Parse for LabelAttr {
5670
args,
5771
has_bonus_display: false,
5872
};
59-
Some(display)
73+
(primary, Some(display))
74+
} else if !primary {
75+
return Err(syn::Error::new(input.span(), "Invalid argument to label() attribute. The argument must be a literal string or the keyword `primary`."));
6076
} else {
61-
return Err(syn::Error::new(input.span(), "Invalid argument to label() attribute. The first argument must be a literal string."));
77+
(primary, None)
6278
}
6379
} else if la.peek(Token![=]) {
6480
// #[label = "blabla"]
6581
input.parse::<Token![=]>()?;
66-
Some(Display {
67-
fmt: input.parse()?,
68-
args: TokenStream::new(),
69-
has_bonus_display: false,
70-
})
82+
(
83+
false,
84+
Some(Display {
85+
fmt: input.parse()?,
86+
args: TokenStream::new(),
87+
has_bonus_display: false,
88+
}),
89+
)
7190
} else {
72-
None
91+
(false, None)
7392
};
74-
Ok(LabelAttr { label })
93+
Ok(LabelAttr { label, primary })
7594
}
7695
}
7796

@@ -100,12 +119,21 @@ impl Labels {
100119
})
101120
};
102121
use quote::ToTokens;
103-
let LabelAttr { label } =
122+
let LabelAttr { label, primary } =
104123
syn::parse2::<LabelAttr>(attr.meta.to_token_stream())?;
124+
125+
if primary && labels.iter().any(|l: &Label| l.primary) {
126+
return Err(syn::Error::new(
127+
field.span(),
128+
"Cannot have more than one primary label.",
129+
));
130+
}
131+
105132
labels.push(Label {
106133
label,
107134
span,
108135
ty: field.ty.clone(),
136+
primary,
109137
});
110138
}
111139
}
@@ -120,21 +148,31 @@ impl Labels {
120148
pub(crate) fn gen_struct(&self, fields: &syn::Fields) -> Option<TokenStream> {
121149
let (display_pat, display_members) = display_pat_members(fields);
122150
let labels = self.0.iter().map(|highlight| {
123-
let Label { span, label, ty } = highlight;
151+
let Label {
152+
span,
153+
label,
154+
ty,
155+
primary,
156+
} = highlight;
124157
let var = quote! { __miette_internal_var };
158+
let ctor = if *primary {
159+
quote! { miette::LabeledSpan::new_primary_with_span }
160+
} else {
161+
quote! { miette::LabeledSpan::new_with_span }
162+
};
125163
if let Some(display) = label {
126164
let (fmt, args) = display.expand_shorthand_cloned(&display_members);
127165
quote! {
128166
miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(&self.#span)
129-
.map(|#var| miette::LabeledSpan::new_with_span(
167+
.map(|#var| #ctor(
130168
std::option::Option::Some(format!(#fmt #args)),
131169
#var.clone(),
132170
))
133171
}
134172
} else {
135173
quote! {
136174
miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(&self.#span)
137-
.map(|#var| miette::LabeledSpan::new_with_span(
175+
.map(|#var| #ctor(
138176
std::option::Option::None,
139177
#var.clone(),
140178
))
@@ -161,27 +199,32 @@ impl Labels {
161199
let (display_pat, display_members) = display_pat_members(fields);
162200
labels.as_ref().and_then(|labels| {
163201
let variant_labels = labels.0.iter().map(|label| {
164-
let Label { span, label, ty } = label;
202+
let Label { span, label, ty, primary } = label;
165203
let field = match &span {
166204
syn::Member::Named(ident) => ident.clone(),
167205
syn::Member::Unnamed(syn::Index { index, .. }) => {
168206
format_ident!("_{}", index)
169207
}
170208
};
171209
let var = quote! { __miette_internal_var };
210+
let ctor = if *primary {
211+
quote! { miette::LabeledSpan::new_primary_with_span }
212+
} else {
213+
quote! { miette::LabeledSpan::new_with_span }
214+
};
172215
if let Some(display) = label {
173216
let (fmt, args) = display.expand_shorthand_cloned(&display_members);
174217
quote! {
175218
miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(#field)
176-
.map(|#var| miette::LabeledSpan::new_with_span(
219+
.map(|#var| #ctor(
177220
std::option::Option::Some(format!(#fmt #args)),
178221
#var.clone(),
179222
))
180223
}
181224
} else {
182225
quote! {
183226
miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(#field)
184-
.map(|#var| miette::LabeledSpan::new_with_span(
227+
.map(|#var| #ctor(
185228
std::option::Option::None,
186229
#var.clone(),
187230
))

‎src/handlers/graphical.rs

+23-4
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,11 @@ impl GraphicalReportHandler {
391391
) -> fmt::Result {
392392
let (contents, lines) = self.get_lines(source, context.inner())?;
393393

394+
let primary_label = labels
395+
.iter()
396+
.find(|label| label.primary())
397+
.or_else(|| labels.first());
398+
394399
// sorting is your friend
395400
let labels = labels
396401
.iter()
@@ -431,19 +436,33 @@ impl GraphicalReportHandler {
431436
self.theme.characters.hbar,
432437
)?;
433438

434-
if let Some(source_name) = contents.name() {
439+
// If there is a primary label, then use its span
440+
// as the reference point for line/column information.
441+
let primary_contents = match primary_label {
442+
Some(label) => source
443+
.read_span(label.inner(), 0, 0)
444+
.map_err(|_| fmt::Error)?,
445+
None => contents,
446+
};
447+
448+
if let Some(source_name) = primary_contents.name() {
435449
let source_name = source_name.style(self.theme.styles.link);
436450
writeln!(
437451
f,
438452
"[{}:{}:{}]",
439453
source_name,
440-
contents.line() + 1,
441-
contents.column() + 1
454+
primary_contents.line() + 1,
455+
primary_contents.column() + 1
442456
)?;
443457
} else if lines.len() <= 1 {
444458
writeln!(f, "{}", self.theme.characters.hbar.to_string().repeat(3))?;
445459
} else {
446-
writeln!(f, "[{}:{}]", contents.line() + 1, contents.column() + 1)?;
460+
writeln!(
461+
f,
462+
"[{}:{}]",
463+
primary_contents.line() + 1,
464+
primary_contents.column() + 1
465+
)?;
447466
}
448467

449468
// Now it's time for the fun part--actually rendering everything!

‎src/miette_diagnostic.rs

+8-4
Original file line numberDiff line numberDiff line change
@@ -292,14 +292,16 @@ fn test_serialize_miette_diagnostic() {
292292
"offset": 0,
293293
"length": 0
294294
},
295-
"label": "label1"
295+
"label": "label1",
296+
"primary": false
296297
},
297298
{
298299
"span": {
299300
"offset": 1,
300301
"length": 2
301302
},
302-
"label": "label2"
303+
"label": "label2",
304+
"primary": false
303305
}
304306
]
305307
});
@@ -350,14 +352,16 @@ fn test_deserialize_miette_diagnostic() {
350352
"offset": 0,
351353
"length": 0
352354
},
353-
"label": "label1"
355+
"label": "label1",
356+
"primary": false
354357
},
355358
{
356359
"span": {
357360
"offset": 1,
358361
"length": 2
359362
},
360-
"label": "label2"
363+
"label": "label2",
364+
"primary": false
361365
}
362366
]
363367
});

‎src/protocol.rs

+27-5
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@ pub struct LabeledSpan {
249249
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
250250
label: Option<String>,
251251
span: SourceSpan,
252+
primary: bool,
252253
}
253254

254255
impl LabeledSpan {
@@ -257,6 +258,7 @@ impl LabeledSpan {
257258
Self {
258259
label,
259260
span: SourceSpan::new(SourceOffset(offset), SourceOffset(len)),
261+
primary: false,
260262
}
261263
}
262264

@@ -265,6 +267,16 @@ impl LabeledSpan {
265267
Self {
266268
label,
267269
span: span.into(),
270+
primary: false,
271+
}
272+
}
273+
274+
/// Makes a new labeled primary span using an existing span.
275+
pub fn new_primary_with_span(label: Option<String>, span: impl Into<SourceSpan>) -> Self {
276+
Self {
277+
label,
278+
span: span.into(),
279+
primary: true,
268280
}
269281
}
270282

@@ -340,6 +352,11 @@ impl LabeledSpan {
340352
pub const fn is_empty(&self) -> bool {
341353
self.span.is_empty()
342354
}
355+
356+
/// True if this `LabeledSpan` is a primary span.
357+
pub const fn primary(&self) -> bool {
358+
self.primary
359+
}
343360
}
344361

345362
#[cfg(feature = "serde")]
@@ -350,15 +367,17 @@ fn test_serialize_labeled_span() {
350367
assert_eq!(
351368
json!(LabeledSpan::new(None, 0, 0)),
352369
json!({
353-
"span": { "offset": 0, "length": 0 }
370+
"span": { "offset": 0, "length": 0, },
371+
"primary": false,
354372
})
355373
);
356374

357375
assert_eq!(
358376
json!(LabeledSpan::new(Some("label".to_string()), 0, 0)),
359377
json!({
360378
"label": "label",
361-
"span": { "offset": 0, "length": 0 }
379+
"span": { "offset": 0, "length": 0, },
380+
"primary": false,
362381
})
363382
)
364383
}
@@ -370,20 +389,23 @@ fn test_deserialize_labeled_span() {
370389

371390
let span: LabeledSpan = serde_json::from_value(json!({
372391
"label": null,
373-
"span": { "offset": 0, "length": 0 }
392+
"span": { "offset": 0, "length": 0, },
393+
"primary": false,
374394
}))
375395
.unwrap();
376396
assert_eq!(span, LabeledSpan::new(None, 0, 0));
377397

378398
let span: LabeledSpan = serde_json::from_value(json!({
379-
"span": { "offset": 0, "length": 0 }
399+
"span": { "offset": 0, "length": 0, },
400+
"primary": false
380401
}))
381402
.unwrap();
382403
assert_eq!(span, LabeledSpan::new(None, 0, 0));
383404

384405
let span: LabeledSpan = serde_json::from_value(json!({
385406
"label": "label",
386-
"span": { "offset": 0, "length": 0 }
407+
"span": { "offset": 0, "length": 0, },
408+
"primary": false
387409
}))
388410
.unwrap();
389411
assert_eq!(span, LabeledSpan::new(Some("label".to_string()), 0, 0))

‎tests/graphical.rs

+54-19
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ fn single_line_highlight_span_full_line() {
8686
println!("Error: {}", out);
8787

8888
let expected = r#" × oops!
89-
╭─[issue:1:1]
89+
╭─[issue:2:1]
9090
1 │ source
9191
2 │ text
9292
· ──┬─
@@ -120,7 +120,7 @@ fn single_line_with_wide_char() -> Result<(), MietteError> {
120120
let expected = r#"oops::my::bad
121121
122122
× oops!
123-
╭─[bad_file.rs:1:1]
123+
╭─[bad_file.rs:2:7]
124124
1 │ source
125125
2 │ 👼🏼text
126126
· ───┬──
@@ -159,7 +159,7 @@ fn single_line_with_two_tabs() -> Result<(), MietteError> {
159159
let expected = r#"oops::my::bad
160160
161161
× oops!
162-
╭─[bad_file.rs:1:1]
162+
╭─[bad_file.rs:2:3]
163163
1 │ source
164164
2 │ text
165165
· ──┬─
@@ -198,7 +198,7 @@ fn single_line_with_tab_in_middle() -> Result<(), MietteError> {
198198
let expected = r#"oops::my::bad
199199
200200
× oops!
201-
╭─[bad_file.rs:1:1]
201+
╭─[bad_file.rs:2:8]
202202
1 │ source
203203
2 │ text = text
204204
· ──┬─
@@ -235,7 +235,7 @@ fn single_line_highlight() -> Result<(), MietteError> {
235235
let expected = r#"oops::my::bad
236236
237237
× oops!
238-
╭─[bad_file.rs:1:1]
238+
╭─[bad_file.rs:2:3]
239239
1 │ source
240240
2 │ text
241241
· ──┬─
@@ -270,7 +270,7 @@ fn external_source() -> Result<(), MietteError> {
270270
let expected = r#"oops::my::bad
271271
272272
× oops!
273-
╭─[bad_file.rs:1:1]
273+
╭─[bad_file.rs:2:3]
274274
1 │ source
275275
2 │ text
276276
· ──┬─
@@ -343,7 +343,7 @@ fn single_line_highlight_offset_end_of_line() -> Result<(), MietteError> {
343343
let expected = r#"oops::my::bad
344344
345345
× oops!
346-
╭─[bad_file.rs:1:1]
346+
╭─[bad_file.rs:1:7]
347347
1 │ source
348348
· ▲
349349
· ╰── this bit here
@@ -379,7 +379,7 @@ fn single_line_highlight_include_end_of_line() -> Result<(), MietteError> {
379379
let expected = r#"oops::my::bad
380380
381381
× oops!
382-
╭─[bad_file.rs:1:1]
382+
╭─[bad_file.rs:2:3]
383383
1 │ source
384384
2 │ text
385385
· ──┬──
@@ -416,7 +416,7 @@ fn single_line_highlight_include_end_of_line_crlf() -> Result<(), MietteError> {
416416
let expected = r#"oops::my::bad
417417
418418
× oops!
419-
╭─[bad_file.rs:1:1]
419+
╭─[bad_file.rs:2:3]
420420
1 │ source
421421
2 │ text
422422
· ──┬──
@@ -453,7 +453,7 @@ fn single_line_highlight_with_empty_span() -> Result<(), MietteError> {
453453
let expected = r#"oops::my::bad
454454
455455
× oops!
456-
╭─[bad_file.rs:1:1]
456+
╭─[bad_file.rs:2:3]
457457
1 │ source
458458
2 │ text
459459
· ▲
@@ -490,7 +490,7 @@ fn single_line_highlight_no_label() -> Result<(), MietteError> {
490490
let expected = r#"oops::my::bad
491491
492492
× oops!
493-
╭─[bad_file.rs:1:1]
493+
╭─[bad_file.rs:2:3]
494494
1 │ source
495495
2 │ text
496496
· ────
@@ -526,7 +526,7 @@ fn single_line_highlight_at_line_start() -> Result<(), MietteError> {
526526
let expected = r#"oops::my::bad
527527
528528
× oops!
529-
╭─[bad_file.rs:1:1]
529+
╭─[bad_file.rs:2:1]
530530
1 │ source
531531
2 │ text
532532
· ──┬─
@@ -569,7 +569,7 @@ fn multiple_same_line_highlights() -> Result<(), MietteError> {
569569
let expected = r#"oops::my::bad
570570
571571
× oops!
572-
╭─[bad_file.rs:1:1]
572+
╭─[bad_file.rs:2:3]
573573
1 │ source
574574
2 │ text text text text text
575575
· ──┬─ ──┬─ ──┬─
@@ -616,7 +616,7 @@ fn multiple_same_line_highlights_with_tabs_in_middle() -> Result<(), MietteError
616616
let expected = r#"oops::my::bad
617617
618618
× oops!
619-
╭─[bad_file.rs:1:1]
619+
╭─[bad_file.rs:2:3]
620620
1 │ source
621621
2 │ text text text text text
622622
· ──┬─ ──┬─ ──┬─
@@ -655,7 +655,7 @@ fn multiline_highlight_adjacent() -> Result<(), MietteError> {
655655
let expected = r#"oops::my::bad
656656
657657
× oops!
658-
╭─[bad_file.rs:1:1]
658+
╭─[bad_file.rs:2:3]
659659
1 │ source
660660
2 │ ╭─▶ text
661661
3 │ ├─▶ here
@@ -969,7 +969,7 @@ fn related() -> Result<(), MietteError> {
969969
let expected = r#"oops::my::bad
970970
971971
× oops!
972-
╭─[bad_file.rs:1:1]
972+
╭─[bad_file.rs:2:3]
973973
1 │ source
974974
2 │ text
975975
· ──┬─
@@ -1031,7 +1031,7 @@ fn related_source_code_propagation() -> Result<(), MietteError> {
10311031
let expected = r#"oops::my::bad
10321032
10331033
× oops!
1034-
╭─[bad_file.rs:1:1]
1034+
╭─[bad_file.rs:2:3]
10351035
1 │ source
10361036
2 │ text
10371037
· ──┬─
@@ -1136,7 +1136,7 @@ fn related_severity() -> Result<(), MietteError> {
11361136
let expected = r#"oops::my::bad
11371137
11381138
× oops!
1139-
╭─[bad_file.rs:1:1]
1139+
╭─[bad_file.rs:2:3]
11401140
1 │ source
11411141
2 │ text
11421142
· ──┬─
@@ -1201,7 +1201,7 @@ fn zero_length_eol_span() {
12011201
println!("Error: {}", out);
12021202

12031203
let expected = r#" × oops!
1204-
╭─[issue:1:1]
1204+
╭─[issue:2:1]
12051205
1 │ this is the first line
12061206
2 │ this is the second line
12071207
· ▲
@@ -1212,3 +1212,38 @@ fn zero_length_eol_span() {
12121212

12131213
assert_eq!(expected, out);
12141214
}
1215+
1216+
#[test]
1217+
fn primary_label() {
1218+
#[derive(Error, Debug, Diagnostic)]
1219+
#[error("oops!")]
1220+
struct MyBad {
1221+
#[source_code]
1222+
src: NamedSource,
1223+
#[label]
1224+
first_label: SourceSpan,
1225+
#[label(primary, "nope")]
1226+
second_label: SourceSpan,
1227+
}
1228+
let err = MyBad {
1229+
src: NamedSource::new("issue", "this is the first line\nthis is the second line"),
1230+
first_label: (2, 4).into(),
1231+
second_label: (24, 4).into(),
1232+
};
1233+
let out = fmt_report(err.into());
1234+
println!("Error: {}", out);
1235+
1236+
// line 2 should be the primary, not line 1
1237+
let expected = r#" × oops!
1238+
╭─[issue:2:2]
1239+
1 │ this is the first line
1240+
· ────
1241+
2 │ this is the second line
1242+
· ──┬─
1243+
· ╰── nope
1244+
╰────
1245+
"#
1246+
.to_string();
1247+
1248+
assert_eq!(expected, out);
1249+
}

‎tests/test_diagnostic_source_macro.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ fn test_diagnostic_source_pass_extra_info() {
9191
println!("Error: {}", out);
9292
let expected = r#" × TestError
9393
╰─▶ × A complex error happened
94-
╭─[1:1]
94+
╭─[1:2]
9595
1 │ Hello
9696
· ──┬─
9797
· ╰── here

0 commit comments

Comments
 (0)
Please sign in to comment.