debug_span/
lib.rs

1//! This crate provides a simple way to debug proc-macro2 spans. It is useful when you are working
2//! with procedural macros and you want to see the location of a span in the source code. It can be
3//! used for testing or debugging.
4//!
5//! # Example
6//!
7//! ```rust
8//! use debug_span::debug_span;
9//! use syn::spanned::Spanned;
10//! use syn::Data;
11//! use unindent::Unindent;
12//!
13//! let input = r###"
14//!     struct Foo {
15//!         a: i32,
16//!         b: i32,
17//!     }
18//! "###
19//! .unindent();
20//! let derive_input: syn::DeriveInput = syn::parse_str(&input).unwrap();
21//! let span = match derive_input.data {
22//!     Data::Struct(s) => s.fields.span(),
23//!     _ => panic!("expected struct"),
24//! };
25//!     
26//! let output = debug_span(span, &input);
27//! insta::assert_snapshot!(output, @r###"
28//!  --> 1:11..4:1
29//!   |
30//!   |            ┌────╮
31//! 1 | struct Foo {    │
32//! 2 |     a: i32,     │
33//! 3 |     b: i32,     │
34//! 4 | }               │
35//!   | └───────────────╯
36//!   |
37//! "###);
38//! ```
39//!
40
41/// A trait for types that represent a span in the source code.
42///
43/// This trait is implemented for `proc_macro2::Span`
44pub trait Span {
45    fn start_line(&self) -> usize;
46    fn end_line(&self) -> usize;
47    fn start_column(&self) -> usize;
48    fn end_column(&self) -> usize;
49
50    #[doc(hidden)]
51    fn is_empty(&self) -> bool {
52        self.start_line() == self.end_line() && self.start_column() == self.end_column()
53    }
54    #[doc(hidden)]
55    fn is_single_line(&self) -> bool {
56        self.start_line() == self.end_line()
57    }
58
59    /// Returns a string representation of the span in the format `start_line:start_column..end_line:end_column`
60    ///
61    /// # Example
62    ///
63    /// ```text
64    /// 1:7..1:10
65    /// ```
66    fn to_range(&self) -> String {
67        format!(
68            "{}:{}..{}:{}",
69            self.start_line(),
70            self.start_column(),
71            self.end_line(),
72            self.end_column(),
73        )
74    }
75
76    /// Generate a debug representation of the span and the source code it points to.
77    ///
78    /// see [`debug_span`] for more information.
79    fn debug(&self, code: &str) -> String {
80        internal::debug_span(self, code)
81    }
82}
83
84#[cfg(feature = "proc-macro2")]
85mod proc_macro2_span {
86    impl crate::Span for proc_macro2::Span {
87        fn start_line(&self) -> usize {
88            self.start().line
89        }
90        fn end_line(&self) -> usize {
91            self.end().line
92        }
93        fn start_column(&self) -> usize {
94            self.start().column
95        }
96        fn end_column(&self) -> usize {
97            self.end().column
98        }
99    }
100}
101
102/// Generate a debug representation of a span and the source code it points to.
103///
104/// It accepts any type that implements the [`Span`] trait. `Span` is implemented for [`proc_macro2::Span`].
105///
106/// ## Single line span example
107///
108/// ```text
109///  --> 1:7..1:10
110///   |
111/// 1 | struct Foo;
112///   |        ^^^
113/// ````
114/// ## Multi line span example
115///
116/// ```text
117/// --> 1:11..4:1
118///   |
119///   |            ┌────╮
120/// 1 | struct Foo {    │
121/// 2 |     a: i32,     │
122/// 3 |     b: i32,     │
123/// 4 | }               │
124///   | └───────────────╯
125/// ```
126///
127pub fn debug_span(span: impl Span, code: &str) -> String {
128    internal::debug_span(&span, code)
129}
130
131#[doc(hidden)]
132pub mod internal {
133    use crate::Span;
134
135    pub fn debug_span(span: &(impl Span + ?Sized), code: &str) -> String {
136        if span.is_empty() {
137            debug_empty_span(span, code)
138        } else if span.is_single_line() {
139            debug_single_line_span(span, code)
140        } else {
141            debug_multi_line_span(span, code)
142        }
143    }
144
145    pub fn debug_empty_span(_span: &(impl Span + ?Sized), _code: &str) -> String {
146        "".to_string()
147    }
148
149    pub fn debug_single_line_span(span: &(impl Span + ?Sized), code: &str) -> String {
150        let empty_line = empty_line(span);
151        let range_line = range_line(span);
152        let code_line = code_line(span, code);
153        let marker_line = marker_line(span);
154        format!(
155            "{}\n{}\n{}\n{}\n{}\n",
156            range_line, empty_line, code_line, marker_line, empty_line,
157        )
158    }
159
160    pub fn debug_multi_line_span(span: &(impl Span + ?Sized), code: &str) -> String {
161        let empty_line = empty_line(span);
162        let range_line = range_line(span);
163        let start_line = start_line(span, code);
164        let code_lines = code_lines(span, code);
165        let end_line = end_line(span, code);
166        format!(
167            "{}\n{}\n{}\n{}\n{}\n{}\n",
168            range_line, empty_line, start_line, code_lines, end_line, empty_line,
169        )
170    }
171
172    pub fn range_line(span: &(impl Span + ?Sized)) -> String {
173        let line_number_width = span.end_line().to_string().len();
174        let range = span.to_range();
175        format!("{:width$}--> {}", "", range, width = line_number_width,)
176    }
177
178    pub fn empty_line(span: &(impl Span + ?Sized)) -> String {
179        let line_number_width = span.end_line().to_string().len();
180        format!("{:width$} |", "", width = line_number_width)
181    }
182
183    pub fn marker_line(span: &(impl Span + ?Sized)) -> String {
184        let line_number_width = span.end_line().to_string().len();
185        let start_column = span.start_column();
186        let end_column = span.end_column();
187
188        let marker = "^".repeat(end_column - start_column);
189        format!(
190            "{:width$} | {:space$}{}",
191            "",
192            "",
193            marker,
194            space = start_column,
195            width = line_number_width,
196        )
197    }
198
199    pub fn code_line(span: &(impl Span + ?Sized), code: &str) -> String {
200        let line_number_width = span.end_line().to_string().len();
201        let line = code.lines().nth(span.start_line() - 1).unwrap();
202        format!(
203            "{:width$} | {}",
204            span.start_line(),
205            line,
206            width = line_number_width,
207        )
208    }
209
210    const PADDING: usize = 3;
211
212    pub fn start_line(span: &(impl Span + ?Sized), code: &str) -> String {
213        let line_number_width = span.end_line().to_string().len();
214        let start_line = span.start_line();
215        let end_line = span.end_line();
216        let start_column = span.start_column();
217
218        let lines = code
219            .lines()
220            .skip(start_line - 1)
221            .take(end_line - start_line + 1);
222        let max_line_len = lines.map(|line| line.len()).max().unwrap();
223        format!(
224            "{:width$} | {}┌{}╮",
225            "",
226            " ".repeat(start_column),
227            "─".repeat(max_line_len + PADDING - start_column),
228            width = line_number_width,
229        )
230    }
231
232    pub fn code_lines(span: &(impl Span + ?Sized), code: &str) -> String {
233        let line_number_width = span.end_line().to_string().len();
234        let start_line = span.start_line();
235        let end_line = span.end_line();
236        let lines = code
237            .lines()
238            .skip(start_line - 1)
239            .take(end_line - start_line + 1);
240        let max_line_len = lines.clone().map(|line| line.len()).max().unwrap();
241        lines
242            .into_iter()
243            .enumerate()
244            .map(|(i, line)| {
245                let line_number = start_line + i;
246                format!(
247                    "{: >line_number_width$} | {}{}│",
248                    line_number,
249                    line,
250                    " ".repeat(max_line_len + PADDING + 1 - line.len()),
251                )
252            })
253            .collect::<Vec<_>>()
254            .join("\n")
255    }
256
257    pub fn end_line(span: &(impl Span + ?Sized), code: &str) -> String {
258        let line_number_width = span.end_line().to_string().len();
259        let start_line = span.start_line();
260        let end_line = span.end_line();
261        let end_column = span.end_column();
262
263        let lines = code
264            .lines()
265            .skip(start_line - 1)
266            .take(end_line - start_line + 1);
267        let max_line_len = lines.map(|line| line.len()).max().unwrap();
268        format!(
269            "{:width$} | {}└{}╯",
270            "",
271            " ".repeat(end_column - 1),
272            "─".repeat(max_line_len + PADDING - end_column + 1),
273            width = line_number_width,
274        )
275    }
276}
277
278#[cfg(test)]
279mod tests {
280    use super::*;
281    use syn::spanned::Spanned;
282    use syn::Data;
283    use unindent::Unindent;
284
285    #[test]
286    fn test_empty_span() {
287        let input = r###"
288            struct Foo;
289        "###
290        .unindent();
291        let span = proc_macro2::Span::call_site();
292        let output = debug_span(span, &input);
293        insta::assert_snapshot!(output, @"");
294    }
295
296    #[test]
297    fn test_single_line() {
298        let input = r###"
299            struct Foo;
300        "###
301        .unindent();
302        let derive_input: syn::DeriveInput = syn::parse_str(&input).unwrap();
303        let span = derive_input.ident.span();
304        let output = debug_span(span, &input);
305        insta::assert_snapshot!(output, @r###"
306         --> 1:7..1:10
307          |
308        1 | struct Foo;
309          |        ^^^
310          |
311        "###);
312    }
313    #[test]
314    fn test_single_line_large_line_number() {
315        let input = r###"
316            struct Foo;
317        "###
318        .unindent();
319        let input = "\n".repeat(120) + &input;
320        let derive_input: syn::DeriveInput = syn::parse_str(&input).unwrap();
321        let span = derive_input.ident.span();
322        let output = debug_span(span, &input);
323        insta::assert_snapshot!(output, @r###"
324           --> 121:7..121:10
325            |
326        121 | struct Foo;
327            |        ^^^
328            |
329        "###);
330    }
331
332    #[test]
333    fn test_multi_line() {
334        let input = r###"
335            struct Foo {
336                a: i32,
337                b: i32,
338            }
339        "###
340        .unindent();
341        let derive_input: syn::DeriveInput = syn::parse_str(&input).unwrap();
342        let span = match derive_input.data {
343            Data::Struct(s) => s.fields.span(),
344            _ => panic!("expected struct"),
345        };
346
347        let output = debug_span(span, &input);
348        insta::assert_snapshot!(output, @r###"
349         --> 1:11..4:1
350          |
351          |            ┌────╮
352        1 | struct Foo {    │
353        2 |     a: i32,     │
354        3 |     b: i32,     │
355        4 | }               │
356          | └───────────────╯
357          |
358        "###);
359    }
360
361    #[test]
362    fn test_multi_line_large_line_number() {
363        let input = r###"
364            struct Foo {
365                a: i32,
366                b: i32,
367            }
368        "###
369        .unindent();
370        let input = "\n".repeat(120) + &input;
371        let derive_input: syn::DeriveInput = syn::parse_str(&input).unwrap();
372        let span = match derive_input.data {
373            Data::Struct(s) => s.fields.span(),
374            _ => panic!("expected struct"),
375        };
376
377        let output = debug_span(span, &input);
378        insta::assert_snapshot!(output, @r###"
379           --> 121:11..124:1
380            |
381            |            ┌────╮
382        121 | struct Foo {    │
383        122 |     a: i32,     │
384        123 |     b: i32,     │
385        124 | }               │
386            | └───────────────╯
387            |
388        "###);
389    }
390
391    #[test]
392    fn test_multi_line_large_line() {
393        let input = r###"
394            struct Foo {
395                a: std::collections::HashMap<i32, i32>,
396                b: i32,
397            }
398        "###
399        .unindent();
400        let derive_input: syn::DeriveInput = syn::parse_str(&input).unwrap();
401        let span = match derive_input.data {
402            Data::Struct(s) => s.fields.span(),
403            _ => panic!("expected struct"),
404        };
405
406        let output = debug_span(span, &input);
407        insta::assert_snapshot!(output, @r###"
408         --> 1:11..4:1
409          |
410          |            ┌───────────────────────────────────╮
411        1 | struct Foo {                                   │
412        2 |     a: std::collections::HashMap<i32, i32>,    │
413        3 |     b: i32,                                    │
414        4 | }                                              │
415          | └──────────────────────────────────────────────╯
416          |
417        "###);
418    }
419
420    #[test]
421    fn test_syn_error() {
422        let input = r###"
423            struct Foo {
424                a: i32
425                bar: i32,
426            }
427        "###
428        .unindent();
429        let derive_input: Result<syn::DeriveInput, _> = syn::parse_str(&input);
430        let error = match derive_input {
431            Ok(_) => panic!("expected error"),
432            Err(e) => e,
433        };
434        let span = error.span();
435        let output = debug_span(span, &input);
436        insta::assert_snapshot!(error.to_string(), @"expected `,`");
437        insta::assert_snapshot!(output, @r###"
438         --> 3:4..3:7
439          |
440        3 |     bar: i32,
441          |     ^^^
442          |
443        "###);
444    }
445
446    #[test]
447    fn test_debug_method() {
448        let input = r###"
449            struct Foo;
450        "###
451        .unindent();
452        let derive_input: syn::DeriveInput = syn::parse_str(&input).unwrap();
453        let span = derive_input.ident.span();
454        let output = span.debug(&input);
455        insta::assert_snapshot!(output, @r###"
456         --> 1:7..1:10
457          |
458        1 | struct Foo;
459          |        ^^^
460          |
461        "###);
462    }
463}