ide_db/syntax_helpers/
format_string.rs

1//! Tools to work with format string literals for the `format_args!` family of macros.
2use syntax::{
3    AstNode, AstToken, TextRange, TextSize,
4    ast::{self, IsString},
5};
6
7// FIXME: This can probably be re-implemented via the HIR?
8pub fn is_format_string(string: &ast::String) -> bool {
9    // Check if `string` is a format string argument of a macro invocation.
10    // `string` is a string literal, mapped down into the innermost macro expansion.
11    // Since `format_args!` etc. remove the format string when expanding, but place all arguments
12    // in the expanded output, we know that the string token is (part of) the format string if it
13    // appears in `format_args!` (otherwise it would have been mapped down further).
14    //
15    // This setup lets us correctly highlight the components of `concat!("{}", "bla")` format
16    // strings. It still fails for `concat!("{", "}")`, but that is rare.
17    (|| {
18        let lit = string.syntax().parent().and_then(ast::Literal::cast)?;
19        let fa = lit.syntax().parent().and_then(ast::FormatArgsExpr::cast)?;
20        (fa.template()? == ast::Expr::Literal(lit)).then_some(|| ())
21    })()
22    .is_some()
23}
24
25#[derive(Debug)]
26pub enum FormatSpecifier {
27    Open,
28    Close,
29    Integer,
30    Identifier,
31    Colon,
32    Fill,
33    Align,
34    Sign,
35    NumberSign,
36    Zero,
37    DollarSign,
38    Dot,
39    Asterisk,
40    QuestionMark,
41    Escape,
42}
43
44// FIXME: Remove this, we can use rustc_format_parse instead
45pub fn lex_format_specifiers(
46    string: &ast::String,
47    mut callback: &mut dyn FnMut(TextRange, FormatSpecifier),
48) {
49    let mut char_ranges = Vec::new();
50    string.escaped_char_ranges(&mut |range, res| char_ranges.push((range, res)));
51    let mut chars = char_ranges
52        .iter()
53        .filter_map(|(range, res)| Some((*range, *res.as_ref().ok()?)))
54        .peekable();
55
56    while let Some((range, first_char)) = chars.next() {
57        if let '{' = first_char {
58            // Format specifier, see syntax at https://doc.rust-lang.org/std/fmt/index.html#syntax
59            if let Some((_, '{')) = chars.peek() {
60                // Escaped format specifier, `{{`
61                read_escaped_format_specifier(&mut chars, &mut callback);
62                continue;
63            }
64
65            callback(range, FormatSpecifier::Open);
66
67            // check for integer/identifier
68            let (_, int_char) = chars.peek().copied().unwrap_or_default();
69            match int_char {
70                // integer
71                '0'..='9' => read_integer(&mut chars, &mut callback),
72                // identifier
73                c if c == '_' || c.is_alphabetic() => read_identifier(&mut chars, &mut callback),
74                _ => {}
75            }
76
77            if let Some((_, ':')) = chars.peek() {
78                skip_char_and_emit(&mut chars, FormatSpecifier::Colon, &mut callback);
79
80                // check for fill/align
81                let mut cloned = chars.clone().take(2);
82                let (_, first) = cloned.next().unwrap_or_default();
83                let (_, second) = cloned.next().unwrap_or_default();
84                match second {
85                    '<' | '^' | '>' => {
86                        // alignment specifier, first char specifies fill
87                        skip_char_and_emit(&mut chars, FormatSpecifier::Fill, &mut callback);
88                        skip_char_and_emit(&mut chars, FormatSpecifier::Align, &mut callback);
89                    }
90                    _ => {
91                        if let '<' | '^' | '>' = first {
92                            skip_char_and_emit(&mut chars, FormatSpecifier::Align, &mut callback);
93                        }
94                    }
95                }
96
97                // check for sign
98                match chars.peek().copied().unwrap_or_default().1 {
99                    '+' | '-' => {
100                        skip_char_and_emit(&mut chars, FormatSpecifier::Sign, &mut callback);
101                    }
102                    _ => {}
103                }
104
105                // check for `#`
106                if let Some((_, '#')) = chars.peek() {
107                    skip_char_and_emit(&mut chars, FormatSpecifier::NumberSign, &mut callback);
108                }
109
110                // check for `0`
111                let mut cloned = chars.clone().take(2);
112                let first = cloned.next().map(|next| next.1);
113                let second = cloned.next().map(|next| next.1);
114
115                if first == Some('0') && second != Some('$') {
116                    skip_char_and_emit(&mut chars, FormatSpecifier::Zero, &mut callback);
117                }
118
119                // width
120                match chars.peek().copied().unwrap_or_default().1 {
121                    '0'..='9' => {
122                        read_integer(&mut chars, &mut callback);
123                        if let Some((_, '$')) = chars.peek() {
124                            skip_char_and_emit(
125                                &mut chars,
126                                FormatSpecifier::DollarSign,
127                                &mut callback,
128                            );
129                        }
130                    }
131                    c if c == '_' || c.is_alphabetic() => {
132                        read_identifier(&mut chars, &mut callback);
133
134                        if chars.peek().map(|&(_, c)| c) == Some('?') {
135                            skip_char_and_emit(
136                                &mut chars,
137                                FormatSpecifier::QuestionMark,
138                                &mut callback,
139                            );
140                        }
141
142                        // can be either width (indicated by dollar sign, or type in which case
143                        // the next sign has to be `}`)
144                        let next = chars.peek().map(|&(_, c)| c);
145
146                        match next {
147                            Some('$') => skip_char_and_emit(
148                                &mut chars,
149                                FormatSpecifier::DollarSign,
150                                &mut callback,
151                            ),
152                            Some('}') => {
153                                skip_char_and_emit(
154                                    &mut chars,
155                                    FormatSpecifier::Close,
156                                    &mut callback,
157                                );
158                                continue;
159                            }
160                            _ => continue,
161                        };
162                    }
163                    _ => {}
164                }
165
166                // precision
167                if let Some((_, '.')) = chars.peek() {
168                    skip_char_and_emit(&mut chars, FormatSpecifier::Dot, &mut callback);
169
170                    match chars.peek().copied().unwrap_or_default().1 {
171                        '*' => {
172                            skip_char_and_emit(
173                                &mut chars,
174                                FormatSpecifier::Asterisk,
175                                &mut callback,
176                            );
177                        }
178                        '0'..='9' => {
179                            read_integer(&mut chars, &mut callback);
180                            if let Some((_, '$')) = chars.peek() {
181                                skip_char_and_emit(
182                                    &mut chars,
183                                    FormatSpecifier::DollarSign,
184                                    &mut callback,
185                                );
186                            }
187                        }
188                        c if c == '_' || c.is_alphabetic() => {
189                            read_identifier(&mut chars, &mut callback);
190                            if chars.peek().map(|&(_, c)| c) != Some('$') {
191                                continue;
192                            }
193                            skip_char_and_emit(
194                                &mut chars,
195                                FormatSpecifier::DollarSign,
196                                &mut callback,
197                            );
198                        }
199                        _ => {
200                            continue;
201                        }
202                    }
203                }
204
205                // type
206                match chars.peek().copied().unwrap_or_default().1 {
207                    '?' => {
208                        skip_char_and_emit(
209                            &mut chars,
210                            FormatSpecifier::QuestionMark,
211                            &mut callback,
212                        );
213                    }
214                    c if c == '_' || c.is_alphabetic() => {
215                        read_identifier(&mut chars, &mut callback);
216
217                        if chars.peek().map(|&(_, c)| c) == Some('?') {
218                            skip_char_and_emit(
219                                &mut chars,
220                                FormatSpecifier::QuestionMark,
221                                &mut callback,
222                            );
223                        }
224                    }
225                    _ => {}
226                }
227            }
228
229            if let Some((_, '}')) = chars.peek() {
230                skip_char_and_emit(&mut chars, FormatSpecifier::Close, &mut callback);
231            }
232            continue;
233        } else if let '}' = first_char
234            && let Some((_, '}')) = chars.peek()
235        {
236            // Escaped format specifier, `}}`
237            read_escaped_format_specifier(&mut chars, &mut callback);
238        }
239    }
240
241    fn skip_char_and_emit<I, F>(
242        chars: &mut std::iter::Peekable<I>,
243        emit: FormatSpecifier,
244        callback: &mut F,
245    ) where
246        I: Iterator<Item = (TextRange, char)>,
247        F: FnMut(TextRange, FormatSpecifier),
248    {
249        let (range, _) = chars.next().unwrap();
250        callback(range, emit);
251    }
252
253    fn read_integer<I, F>(chars: &mut std::iter::Peekable<I>, callback: &mut F)
254    where
255        I: Iterator<Item = (TextRange, char)>,
256        F: FnMut(TextRange, FormatSpecifier),
257    {
258        let (mut range, c) = chars.next().unwrap();
259        assert!(c.is_ascii_digit());
260        while let Some(&(r, next_char)) = chars.peek() {
261            if next_char.is_ascii_digit() {
262                chars.next();
263                range = range.cover(r);
264            } else {
265                break;
266            }
267        }
268        callback(range, FormatSpecifier::Integer);
269    }
270
271    fn read_identifier<I, F>(chars: &mut std::iter::Peekable<I>, callback: &mut F)
272    where
273        I: Iterator<Item = (TextRange, char)>,
274        F: FnMut(TextRange, FormatSpecifier),
275    {
276        let (mut range, c) = chars.next().unwrap();
277        assert!(c.is_alphabetic() || c == '_');
278        while let Some(&(r, next_char)) = chars.peek() {
279            if next_char == '_' || next_char.is_ascii_digit() || next_char.is_alphabetic() {
280                chars.next();
281                range = range.cover(r);
282            } else {
283                break;
284            }
285        }
286        callback(range, FormatSpecifier::Identifier);
287    }
288
289    fn read_escaped_format_specifier<I, F>(chars: &mut std::iter::Peekable<I>, callback: &mut F)
290    where
291        I: Iterator<Item = (TextRange, char)>,
292        F: FnMut(TextRange, FormatSpecifier),
293    {
294        let (range, _) = chars.peek().unwrap();
295        let offset = TextSize::from(1);
296        callback(TextRange::new(range.start() - offset, range.end()), FormatSpecifier::Escape);
297        chars.next();
298    }
299}