thiserror_impl/
fmt.rs

1use crate::ast::Field;
2use crate::attr::{Display, Trait};
3use proc_macro2::TokenTree;
4use quote::{format_ident, quote_spanned};
5use std::collections::{BTreeSet as Set, HashMap as Map};
6use syn::ext::IdentExt;
7use syn::parse::{ParseStream, Parser};
8use syn::{Ident, Index, LitStr, Member, Result, Token};
9
10impl Display<'_> {
11    // Transform `"error {var}"` to `"error {}", var`.
12    pub fn expand_shorthand(&mut self, fields: &[Field]) {
13        let raw_args = self.args.clone();
14        let mut named_args = explicit_named_args.parse2(raw_args).unwrap();
15        let mut member_index = Map::new();
16        for (i, field) in fields.iter().enumerate() {
17            member_index.insert(&field.member, i);
18        }
19
20        let span = self.fmt.span();
21        let fmt = self.fmt.value();
22        let mut read = fmt.as_str();
23        let mut out = String::new();
24        let mut args = self.args.clone();
25        let mut has_bonus_display = false;
26        let mut implied_bounds = Set::new();
27
28        let mut has_trailing_comma = false;
29        if let Some(TokenTree::Punct(punct)) = args.clone().into_iter().last() {
30            if punct.as_char() == ',' {
31                has_trailing_comma = true;
32            }
33        }
34
35        while let Some(brace) = read.find('{') {
36            out += &read[..brace + 1];
37            read = &read[brace + 1..];
38            if read.starts_with('{') {
39                out.push('{');
40                read = &read[1..];
41                continue;
42            }
43            let next = match read.chars().next() {
44                Some(next) => next,
45                None => return,
46            };
47            let member = match next {
48                '0'..='9' => {
49                    let int = take_int(&mut read);
50                    let member = match int.parse::<u32>() {
51                        Ok(index) => Member::Unnamed(Index { index, span }),
52                        Err(_) => return,
53                    };
54                    if !member_index.contains_key(&member) {
55                        out += &int;
56                        continue;
57                    }
58                    member
59                }
60                'a'..='z' | 'A'..='Z' | '_' => {
61                    let mut ident = take_ident(&mut read);
62                    ident.set_span(span);
63                    Member::Named(ident)
64                }
65                _ => continue,
66            };
67            if let Some(&field) = member_index.get(&member) {
68                let end_spec = match read.find('}') {
69                    Some(end_spec) => end_spec,
70                    None => return,
71                };
72                let bound = match read[..end_spec].chars().next_back() {
73                    Some('?') => Trait::Debug,
74                    Some('o') => Trait::Octal,
75                    Some('x') => Trait::LowerHex,
76                    Some('X') => Trait::UpperHex,
77                    Some('p') => Trait::Pointer,
78                    Some('b') => Trait::Binary,
79                    Some('e') => Trait::LowerExp,
80                    Some('E') => Trait::UpperExp,
81                    Some(_) | None => Trait::Display,
82                };
83                implied_bounds.insert((field, bound));
84            }
85            let local = match &member {
86                Member::Unnamed(index) => format_ident!("_{}", index),
87                Member::Named(ident) => ident.clone(),
88            };
89            let mut formatvar = local.clone();
90            if formatvar.to_string().starts_with("r#") {
91                formatvar = format_ident!("r_{}", formatvar);
92            }
93            if formatvar.to_string().starts_with('_') {
94                // Work around leading underscore being rejected by 1.40 and
95                // older compilers. https://github.com/rust-lang/rust/pull/66847
96                formatvar = format_ident!("field_{}", formatvar);
97            }
98            out += &formatvar.to_string();
99            if !named_args.insert(formatvar.clone()) {
100                // Already specified in the format argument list.
101                continue;
102            }
103            if !has_trailing_comma {
104                args.extend(quote_spanned!(span=> ,));
105            }
106            args.extend(quote_spanned!(span=> #formatvar = #local));
107            if read.starts_with('}') && member_index.contains_key(&member) {
108                has_bonus_display = true;
109                args.extend(quote_spanned!(span=> .as_display()));
110            }
111            has_trailing_comma = false;
112        }
113
114        out += read;
115        self.fmt = LitStr::new(&out, self.fmt.span());
116        self.args = args;
117        self.has_bonus_display = has_bonus_display;
118        self.implied_bounds = implied_bounds;
119    }
120}
121
122fn explicit_named_args(input: ParseStream) -> Result<Set<Ident>> {
123    let mut named_args = Set::new();
124
125    while !input.is_empty() {
126        if input.peek(Token![,]) && input.peek2(Ident::peek_any) && input.peek3(Token![=]) {
127            input.parse::<Token![,]>()?;
128            let ident = input.call(Ident::parse_any)?;
129            input.parse::<Token![=]>()?;
130            named_args.insert(ident);
131        } else {
132            input.parse::<TokenTree>()?;
133        }
134    }
135
136    Ok(named_args)
137}
138
139fn take_int(read: &mut &str) -> String {
140    let mut int = String::new();
141    for (i, ch) in read.char_indices() {
142        match ch {
143            '0'..='9' => int.push(ch),
144            _ => {
145                *read = &read[i..];
146                break;
147            }
148        }
149    }
150    int
151}
152
153fn take_ident(read: &mut &str) -> Ident {
154    let mut ident = String::new();
155    let raw = read.starts_with("r#");
156    if raw {
157        ident.push_str("r#");
158        *read = &read[2..];
159    }
160    for (i, ch) in read.char_indices() {
161        match ch {
162            'a'..='z' | 'A'..='Z' | '0'..='9' | '_' => ident.push(ch),
163            _ => {
164                *read = &read[i..];
165                break;
166            }
167        }
168    }
169    Ident::parse_any.parse_str(&ident).unwrap()
170}