1use std::io::{BufRead, BufReader, Read};
2
3#[derive(Debug, Clone, PartialEq, Eq)]
5pub enum MacroExpansionKind {
6 Bang,
8 Attribute,
10 Derive,
12}
13
14#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct MacroExpansion {
17 pub expanding: String,
18 pub arguments: String,
19 pub to: String,
20 pub name: String,
22 pub kind: MacroExpansionKind,
24 pub input: String,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct TraceGroup {
31 pub expansions: Vec<MacroExpansion>,
32}
33
34pub struct TraceParser<R: Read> {
36 reader: BufReader<R>,
37 current_line: String,
38 peeked_line: Option<String>,
39}
40
41impl<R: Read> TraceParser<R> {
42 pub fn new(reader: R) -> Self {
43 Self {
44 reader: BufReader::new(reader),
45 current_line: String::new(),
46 peeked_line: None,
47 }
48 }
49
50 fn strip_ansi_escape_sequences(s: &str) -> String {
51 let mut out = String::with_capacity(s.len());
52 let mut chars = s.chars().peekable();
53 while let Some(ch) = chars.next() {
54 if ch == '\u{1b}' {
55 if chars.peek() == Some(&'[') {
57 let _ = chars.next();
58 for c in chars.by_ref() {
59 if ('@'..='~').contains(&c) {
60 break;
61 }
62 }
63 }
64 continue;
65 }
66 out.push(ch);
67 }
68 out
69 }
70
71 fn read_line(&mut self) -> Option<String> {
72 if let Some(line) = self.peeked_line.take() {
73 return Some(line);
74 }
75 self.current_line.clear();
76 match self.reader.read_line(&mut self.current_line) {
77 Ok(0) => None,
78 Ok(_) => {
79 let line = self.current_line.trim_end_matches('\n');
80 Some(Self::strip_ansi_escape_sequences(line))
81 }
82 Err(_) => None,
83 }
84 }
85
86 fn peek_line(&mut self) -> Option<&str> {
87 if self.peeked_line.is_none() {
88 self.peeked_line = self.read_line();
89 }
90 self.peeked_line.as_deref()
91 }
92
93 fn extract_backtick_content(&mut self, first_line: &str) -> Option<String> {
103 let start_idx = first_line.find('`')?;
105 let after_backtick = &first_line[start_idx + 1..];
106
107 if let Some(end_idx) = after_backtick.rfind('`') {
109 return Some(after_backtick[..end_idx].to_string());
110 }
111
112 let mut content = after_backtick.to_string();
114
115 loop {
116 let line = self.read_line()?;
117 if line.ends_with('`') {
118 content.push('\n');
119 content.push_str(&line[..line.len() - 1]);
120 break;
121 } else {
122 content.push('\n');
123 content.push_str(&line);
124 }
125 }
126
127 Some(content)
128 }
129
130 fn extract_macro_name(expanding: &str) -> String {
135 if let Some(bang_pos) = expanding.find('!') {
136 expanding[..bang_pos].trim().to_string()
137 } else {
138 expanding.to_string()
139 }
140 }
141
142 fn extract_arguments(expanding: &str) -> String {
147 let bang_pos = match expanding.find('!') {
149 Some(p) => p,
150 None => return String::new(),
151 };
152 let after_bang = expanding[bang_pos + 1..].trim_start();
153 let first_char = match after_bang.chars().next() {
154 Some(c) => c,
155 None => return String::new(),
156 };
157 let (open, close) = match first_char {
158 '{' => ('{', '}'),
159 '(' => ('(', ')'),
160 '[' => ('[', ']'),
161 _ => return String::new(),
162 };
163 let mut depth = 0i32;
164 let mut start = None;
165 let mut end = None;
166 for (i, ch) in after_bang.char_indices() {
167 if ch == open {
168 depth += 1;
169 if start.is_none() {
170 start = Some(i + ch.len_utf8());
171 }
172 } else if ch == close {
173 depth -= 1;
174 if depth == 0 {
175 end = Some(i);
176 break;
177 }
178 }
179 }
180 match (start, end) {
181 (Some(s), Some(e)) => after_bang[s..e].trim().to_string(),
182 _ => String::new(),
183 }
184 }
185
186 fn parse_trace_group(&mut self) -> Option<TraceGroup> {
187 let mut expansions = Vec::new();
188
189 while let Some(l) = self.peek_line() {
190 let line = l.to_string();
191
192 if line.starts_with("note: trace_macro") {
193 if !expansions.is_empty() {
195 break;
196 }
197 self.read_line();
199 continue;
200 }
201
202 if line.contains("= note: expanding `") {
203 self.read_line(); let expanding = self.extract_backtick_content(&line)?;
205
206 loop {
208 let to_line = match self.peek_line() {
209 Some(l) => l.to_string(),
210 None => return None,
211 };
212
213 if to_line.contains("= note: to `") {
214 self.read_line(); let to = self.extract_backtick_content(&to_line)?;
216 let input = Self::extract_arguments(&expanding);
217 let name = Self::extract_macro_name(&expanding);
218 expansions.push(MacroExpansion {
219 expanding,
220 arguments: String::new(),
221 to,
222 name,
223 kind: MacroExpansionKind::Bang,
224 input,
225 });
226 break;
227 } else if to_line.starts_with("note: trace_macro")
228 || to_line.contains("= note: expanding `")
229 {
230 break;
232 } else {
233 self.read_line();
235 }
236 }
237 } else if line.trim().is_empty()
238 || line.starts_with(" -->")
239 || line.starts_with(" |")
240 || line.starts_with(" |")
241 || line.starts_with(" -->")
242 || line.starts_with("...")
243 || line.contains("= note: this note originates")
244 {
245 self.read_line();
247 } else if !expansions.is_empty() {
248 break;
250 } else {
251 self.read_line();
253 }
254 }
255
256 if expansions.is_empty() {
257 None
258 } else {
259 Some(TraceGroup { expansions })
260 }
261 }
262}
263
264impl<R: Read> Iterator for TraceParser<R> {
265 type Item = TraceGroup;
266
267 fn next(&mut self) -> Option<Self::Item> {
268 loop {
270 match self.peek_line() {
271 Some(line) if line.starts_with("note: trace_macro") => {
272 return self.parse_trace_group();
273 }
274 Some(_) => {
275 self.read_line();
276 }
277 None => return None,
278 }
279 }
280 }
281}
282
283pub fn parse_trace<R: Read>(reader: R) -> TraceParser<R> {
289 TraceParser::new(reader)
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295
296 #[test]
297 fn test_single_expansion() {
298 let input = r#"note: trace_macro
299 --> src/lib.rs:15:17
300 |
30115 | if !matches!(segment.arguments, PathArguments::None) {
302 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
303 |
304 = note: expanding `matches! { segment.arguments, PathArguments::None }`
305 = note: to `match segment.arguments { PathArguments::None => true, _ => false }`
306"#;
307
308 let groups: Vec<_> = parse_trace(input.as_bytes()).collect();
309 assert_eq!(groups.len(), 1);
310 assert_eq!(groups[0].expansions.len(), 1);
311 assert_eq!(
312 groups[0].expansions[0].expanding,
313 "matches! { segment.arguments, PathArguments::None }"
314 );
315 assert_eq!(
316 groups[0].expansions[0].to,
317 "match segment.arguments { PathArguments::None => true, _ => false }"
318 );
319 }
320
321 #[test]
322 fn test_multiple_expansions_in_group() {
323 let input = r#"note: trace_macro
324 --> macro/lib.rs:16:17
325 |
32616 | abort!(segment, "Path arguments are not allowed");
327 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
328 |
329 = note: expanding `abort! { segment, "message" }`
330 = note: to `diagnostic!(segment, Error, "message").abort()`
331 = note: expanding `diagnostic! { segment, Error, "message" }`
332 = note: to `Diagnostic::new(segment, Error, "message")`
333"#;
334
335 let groups: Vec<_> = parse_trace(input.as_bytes()).collect();
336 assert_eq!(groups.len(), 1);
337 assert_eq!(groups[0].expansions.len(), 2);
338 assert_eq!(
339 groups[0].expansions[0].expanding,
340 "abort! { segment, \"message\" }"
341 );
342 assert_eq!(
343 groups[0].expansions[0].to,
344 "diagnostic!(segment, Error, \"message\").abort()"
345 );
346 assert_eq!(
347 groups[0].expansions[1].expanding,
348 "diagnostic! { segment, Error, \"message\" }"
349 );
350 assert_eq!(
351 groups[0].expansions[1].to,
352 "Diagnostic::new(segment, Error, \"message\")"
353 );
354 }
355
356 #[test]
357 fn test_multiline_to_content() {
358 let input = r#"note: trace_macro
359 --> src/lib.rs:1:1
360 |
361 = note: expanding `vec! { 1, 2, 3 }`
362 = note: to `{
363 let mut v = Vec::new();
364 v.push(1);
365 v.push(2);
366 v.push(3);
367 v
368 }`
369"#;
370
371 let groups: Vec<_> = parse_trace(input.as_bytes()).collect();
372 assert_eq!(groups.len(), 1);
373 assert_eq!(groups[0].expansions.len(), 1);
374 assert_eq!(groups[0].expansions[0].expanding, "vec! { 1, 2, 3 }");
375 assert_eq!(
376 groups[0].expansions[0].to,
377 r#"{
378 let mut v = Vec::new();
379 v.push(1);
380 v.push(2);
381 v.push(3);
382 v
383 }"#
384 );
385 }
386
387 #[test]
388 fn test_multiple_trace_groups() {
389 let input = r#"note: trace_macro
390 --> src/lib.rs:1:1
391 |
392 = note: expanding `println! { "hello" }`
393 = note: to `print!("hello\n")`
394
395note: trace_macro
396 --> src/lib.rs:2:1
397 |
398 = note: expanding `dbg! { x }`
399 = note: to `{ eprintln!("{}", x); x }`
400"#;
401
402 let groups: Vec<_> = parse_trace(input.as_bytes()).collect();
403 assert_eq!(groups.len(), 2);
404 assert_eq!(groups[0].expansions[0].expanding, "println! { \"hello\" }");
405 assert_eq!(groups[1].expansions[0].expanding, "dbg! { x }");
406 }
407
408 #[test]
409 fn test_empty_input() {
410 let input = "";
411 let groups: Vec<_> = parse_trace(input.as_bytes()).collect();
412 assert!(groups.is_empty());
413 }
414
415 #[test]
416 fn test_no_trace_macro() {
417 let input = r#" Compiling myproject v0.1.0
418 Finished dev profile
419"#;
420 let groups: Vec<_> = parse_trace(input.as_bytes()).collect();
421 assert!(groups.is_empty());
422 }
423
424 #[test]
425 fn test_real_rustc_output() {
426 let input = r#"note: trace_macro
428 --> macro/lib.rs:15:17
429 |
43015 | if !matches!(segment.arguments, PathArguments::None) {
431 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
432 |
433 = note: expanding `matches! { segment.arguments, PathArguments::None }`
434 = note: to `#[allow(non_exhaustive_omitted_patterns)] match segment.arguments
435 { PathArguments::None => true, _ => false }`
436
437note: trace_macro
438 --> macro/lib.rs:16:17
439 |
44016 | abort!(segment, "Path arguments are not allowed");
441 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
442 |
443 = note: expanding `abort! { segment, "Path arguments are not allowed" }`
444 = note: to `$crate :: diagnostic!
445 (segment, $crate :: Level :: Error, "Path arguments are not allowed").abort()`
446 = note: expanding `diagnostic! { segment, $crate :: Level :: Error, "Path arguments are not allowed" }`
447 = note: to `{
448 #[allow(unused_imports)] use $crate :: __export ::
449 {
450 ToTokensAsSpanRange, Span2AsSpanRange, SpanAsSpanRange,
451 SpanRangeAsSpanRange
452 }; use $crate :: DiagnosticExt; let span_range =
453 (&
454 segment).FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange();
455 $crate :: Diagnostic ::
456 spanned_range(span_range, $crate :: Level :: Error,
457 "Path arguments are not allowed".to_string())
458 }`
459
460note: trace_macro
461 --> macro/lib.rs:43:41
462 |
46343 | if input.peek(Ident) && input.peek2(Token![=]) {
464 | ^^^^^^^^^
465 |
466 = note: expanding `Token! { = }`
467 = note: to `$crate :: token :: Eq`
468 Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.37s
469"#;
470
471 let groups: Vec<_> = parse_trace(input.as_bytes()).collect();
472 assert_eq!(groups.len(), 3);
473
474 assert_eq!(groups[0].expansions.len(), 1);
476 assert_eq!(
477 groups[0].expansions[0].expanding,
478 "matches! { segment.arguments, PathArguments::None }"
479 );
480 assert_eq!(
481 groups[0].expansions[0].to,
482 r#"#[allow(non_exhaustive_omitted_patterns)] match segment.arguments
483 { PathArguments::None => true, _ => false }"#
484 );
485
486 assert_eq!(groups[1].expansions.len(), 2);
488 assert_eq!(
489 groups[1].expansions[0].expanding,
490 "abort! { segment, \"Path arguments are not allowed\" }"
491 );
492 assert_eq!(
493 groups[1].expansions[0].to,
494 r#"$crate :: diagnostic!
495 (segment, $crate :: Level :: Error, "Path arguments are not allowed").abort()"#
496 );
497 assert_eq!(
498 groups[1].expansions[1].expanding,
499 "diagnostic! { segment, $crate :: Level :: Error, \"Path arguments are not allowed\" }"
500 );
501 assert_eq!(
502 groups[1].expansions[1].to,
503 r#"{
504 #[allow(unused_imports)] use $crate :: __export ::
505 {
506 ToTokensAsSpanRange, Span2AsSpanRange, SpanAsSpanRange,
507 SpanRangeAsSpanRange
508 }; use $crate :: DiagnosticExt; let span_range =
509 (&
510 segment).FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange();
511 $crate :: Diagnostic ::
512 spanned_range(span_range, $crate :: Level :: Error,
513 "Path arguments are not allowed".to_string())
514 }"#
515 );
516
517 assert_eq!(groups[2].expansions.len(), 1);
519 assert_eq!(groups[2].expansions[0].expanding, "Token! { = }");
520 assert_eq!(groups[2].expansions[0].to, "$crate :: token :: Eq");
521 }
522}