1use syntax::{
3 AstNode, AstToken, TextRange, TextSize,
4 ast::{self, IsString},
5};
6
7pub fn is_format_string(string: &ast::String) -> bool {
9 (|| {
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
44pub 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 if let Some((_, '{')) = chars.peek() {
60 read_escaped_format_specifier(&mut chars, &mut callback);
62 continue;
63 }
64
65 callback(range, FormatSpecifier::Open);
66
67 let (_, int_char) = chars.peek().copied().unwrap_or_default();
69 match int_char {
70 '0'..='9' => read_integer(&mut chars, &mut callback),
72 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 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 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 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 if let Some((_, '#')) = chars.peek() {
107 skip_char_and_emit(&mut chars, FormatSpecifier::NumberSign, &mut callback);
108 }
109
110 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 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 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 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 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 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}