proc_macro_faithful_display/
lib.rs

1#![feature(proc_macro_span)]
2
3//! An alternative [`Display`] impl for [proc_macro], respecting the input layout and formatting.
4//!
5//! The idea is that the impl of [`Display`] for [proc_macro] types doesn’t respect the input’s
6//! layout. For most commun Rust use cases, this is okay, because the language doesn’t depend on
7//! whitespaces and has its own grammar for floating point numbers, field access, etc. However, for
8//! all other use cases, you will lose your formatting and indentation. Plus, some [EDSLs] might
9//! require strict use of newlines or symbols with a leading colon, comma, etc. without whitespaces.
10//!
11//! This crate provides an implementation of [`Display`] that respects the input’s formatting, so
12//! that one can display a [`TokenStream`] and parse it with a more esoteric parser than [syn].
13//!
14//! > Currently, this crate highly depends on *nightly* features. You cannot use it on the *stable*
15//! > channel… just yet.
16//!
17//! You can get a faithful [`Display`] object by calling the [`faithful_display`] function on your
18//! [`TokenStream`].
19//!
20//! > At the time of writing, traits don’t allow [existential `impl Trait`] to be used in methods.
21//! > This is unfortunate, then the feature is accessed through a function instead of a method.
22//!
23//! [EDSLs]: https://wiki.haskell.org/Embedded_domain_specific_language
24//! [syn]: https://crates.io/crates/syn
25//! [existential `impl Trait`]: https://rust-lang-nursery.github.io/edition-guide/rust-2018/trait-system/impl-trait-for-returning-complex-types-with-ease.html#return-position
26
27extern crate proc_macro;
28
29use proc_macro::{Delimiter, Group, Ident, Literal, Punct, Span, TokenStream, TokenTree};
30use std::fmt::{self, Display, Write};
31use std::iter::FromIterator;
32
33/// A more faithful [`Display`].
34///
35/// This trait works by accumulating a [`Span`] as it formats tokens. By recomputing on the
36/// fly on the layout of each token, it’s possible to insert newlines and spaces to respect the
37/// initial formatting.
38pub trait FaithfulDisplay {
39    /// Display a token in a faithful way.
40    fn faithful_fmt(&self, f: &mut fmt::Formatter, prev_span: Span) -> Result<Span, fmt::Error>;
41}
42
43impl FaithfulDisplay for Ident {
44    fn faithful_fmt(&self, f: &mut fmt::Formatter, prev_span: Span) -> Result<Span, fmt::Error> {
45        let current_span = self.span();
46        whitespace_adjust_span(f, prev_span, current_span.start())?;
47
48        self.fmt(f).map(|_| current_span.end())
49    }
50}
51
52impl FaithfulDisplay for Literal {
53    fn faithful_fmt(&self, f: &mut fmt::Formatter, prev_span: Span) -> Result<Span, fmt::Error> {
54        let current_span = self.span();
55        whitespace_adjust_span(f, prev_span, current_span.start())?;
56
57        self.fmt(f).map(|_| current_span.end())
58    }
59}
60
61impl FaithfulDisplay for Punct {
62    fn faithful_fmt(&self, f: &mut fmt::Formatter, prev_span: Span) -> Result<Span, fmt::Error> {
63        let current_span = self.span();
64        whitespace_adjust_span(f, prev_span, current_span.start())?;
65
66        f.write_char(self.as_char()).map(|_| current_span.end())
67    }
68}
69
70impl FaithfulDisplay for Group {
71    fn faithful_fmt(&self, f: &mut fmt::Formatter, prev_span: Span) -> Result<Span, fmt::Error> {
72        let current_span = self.span_open();
73        whitespace_adjust_span(f, prev_span, current_span.start())?;
74
75        match self.delimiter() {
76            Delimiter::Parenthesis => {
77                faithful_delimited(
78                    f,
79                    '(',
80                    ')',
81                    self.stream(),
82                    current_span.end(),
83                    self.span_close().start(),
84                )?;
85            }
86
87            Delimiter::Brace => {
88                faithful_delimited(
89                    f,
90                    '{',
91                    '}',
92                    self.stream(),
93                    current_span.end(),
94                    self.span_close().start(),
95                )?;
96            }
97
98            Delimiter::Bracket => {
99                faithful_delimited(
100                    f,
101                    '[',
102                    ']',
103                    self.stream(),
104                    current_span.end(),
105                    self.span_close().start(),
106                )?;
107            }
108
109            Delimiter::None => {
110                let line_col = self.stream().faithful_fmt(f, current_span.end())?;
111                whitespace_adjust_span(f, prev_span, line_col)?;
112            }
113        }
114
115        Ok(self.span_close().end())
116    }
117}
118
119impl FaithfulDisplay for TokenStream {
120    fn faithful_fmt(&self, f: &mut fmt::Formatter, prev_span: Span) -> Result<Span, fmt::Error> {
121        let mut current_span = prev_span;
122
123        for tree in self.clone() {
124            current_span = tree.faithful_fmt(f, current_span)?;
125        }
126
127        Ok(current_span)
128    }
129}
130
131impl FaithfulDisplay for TokenTree {
132    fn faithful_fmt(&self, f: &mut fmt::Formatter, prev_span: Span) -> Result<Span, fmt::Error> {
133        match self {
134            TokenTree::Group(gr) => gr.faithful_fmt(f, prev_span),
135            TokenTree::Ident(ident) => ident.faithful_fmt(f, prev_span),
136            TokenTree::Punct(p) => p.faithful_fmt(f, prev_span),
137            TokenTree::Literal(lit) => lit.faithful_fmt(f, prev_span),
138        }
139    }
140}
141
142/// Create a [`Display`] object out of a [`TokenStream`] that respects as closely as possible its
143/// formatting.
144///
145/// > Disclaimer: because this function takes a reference and because [`TokenStream`] – at the time
146/// > of writing – doesn’t support reference-based iteration, a complete deep clone of the token
147/// > tree has to be performed prior to displaying it.
148pub fn faithful_display(stream: &TokenStream) -> impl Display + '_ {
149    struct D<'a>(&'a TokenStream);
150
151    impl<'a> fmt::Display for D<'a> {
152        fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
153            // get the first span, if any
154            let mut iter = self.0.clone().into_iter();
155            let first = iter.next();
156
157            if let Some(tree) = first {
158                let first_line_col = tree.span().start();
159                let line_col = tree.faithful_fmt(f, first_line_col)?;
160
161                TokenStream::from_iter(iter)
162                    .faithful_fmt(f, line_col)
163                    .map(|_| ())
164            } else {
165                Ok(())
166            }
167        }
168    }
169
170    D(stream)
171}
172
173/// Automatically adjust with whitespaces a formatter based on the current span and the previous
174/// one.
175///
176/// This function is key to the overall implementation, has it enables to respect the input
177/// indentation and general formatting.
178fn whitespace_adjust_span(
179    f: &mut fmt::Formatter,
180    prev_span: Span,
181    current_span: Span,
182) -> Result<(), fmt::Error> {
183    if current_span.line() == prev_span.line() {
184        // we are on the same line, we just have to adjust the number of spaces
185        let nb_spaces = current_span.column() - prev_span.column();
186        f.write_str(" ".repeat(nb_spaces).as_str())
187    } else {
188        // we are on different lines; first add the newlines difference, then adjust with spaces
189        let nb_newlines = current_span.line() - prev_span.line();
190        let nb_spaces = current_span.column();
191        f.write_str("\n".repeat(nb_newlines).as_str())?;
192        f.write_str(" ".repeat(nb_spaces).as_str())
193    }
194}
195
196/// Display a token stream that is surrounded by two matching characters.
197fn faithful_delimited(
198    f: &mut fmt::Formatter,
199    del_first: char,
200    del_end: char,
201    stream: TokenStream,
202    prev_span: Span,
203    final_span: Span,
204) -> Result<(), fmt::Error> {
205    f.write_char(del_first)?;
206
207    let current_span = stream.faithful_fmt(f, prev_span)?;
208
209    whitespace_adjust_span(f, current_span, final_span)?;
210    f.write_char(del_end)
211}