errore_impl/
fmt.rs

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