reinhardt_admin_cli/migrate_v2/rules/
bare_ident.rs1use proc_macro2::{Delimiter, Group, TokenStream, TokenTree};
9use quote::quote;
10use std::iter::Peekable;
11use syn::visit_mut::{self, VisitMut};
12
13use crate::migrate_v2::rewriter::FileRewriter;
14
15pub struct Rule;
17
18impl FileRewriter for Rule {
19 fn name(&self) -> &'static str {
20 "bare_ident"
21 }
22
23 fn rewrite(&self, mut file: syn::File) -> syn::File {
24 PageMacroBodyVisitor.visit_file_mut(&mut file);
25 file
26 }
27}
28
29struct PageMacroBodyVisitor;
30
31impl VisitMut for PageMacroBodyVisitor {
32 fn visit_macro_mut(&mut self, m: &mut syn::Macro) {
33 if m.path
35 .segments
36 .last()
37 .map(|s| s.ident == "page")
38 .unwrap_or(false)
39 {
40 m.tokens = rewrite_page_body(m.tokens.clone());
41 }
42 visit_mut::visit_macro_mut(self, m);
43 }
44}
45
46fn rewrite_page_body(input: TokenStream) -> TokenStream {
49 let mut out: Vec<TokenTree> = Vec::new();
50 for tt in input {
51 match tt {
52 TokenTree::Group(g) if g.delimiter() == Delimiter::Brace => {
53 let inner = rewrite_brace_body(g.stream());
54 out.push(TokenTree::Group(Group::new(Delimiter::Brace, inner)));
55 }
56 other => out.push(other),
57 }
58 }
59 out.into_iter().collect()
60}
61
62fn rewrite_brace_body(input: TokenStream) -> TokenStream {
65 let mut out: Vec<TokenTree> = Vec::new();
66 let mut iter = input.into_iter().peekable();
67 let mut previous_token_can_own_brace_body = false;
68
69 while let Some(tt) = iter.next() {
70 if let TokenTree::Ident(id) = &tt {
71 match id.to_string().as_str() {
72 "if" | "for" | "while" => {
73 out.push(tt);
74 push_control_prefix_and_rewrite_body(&mut iter, &mut out);
75 previous_token_can_own_brace_body = false;
76 continue;
77 }
78 "match" => {
79 out.push(tt);
80 push_match_prefix_and_rewrite_body(&mut iter, &mut out);
81 previous_token_can_own_brace_body = false;
82 continue;
83 }
84 "let" => {
85 out.push(tt);
86 push_statement_until_semicolon(&mut iter, &mut out);
87 previous_token_can_own_brace_body = false;
88 continue;
89 }
90 "else" => {
91 out.push(tt);
92 if let Some(TokenTree::Ident(next)) = iter.peek()
93 && next == "if"
94 {
95 let if_token = iter.next().expect("peeked token disappeared");
96 out.push(if_token);
97 push_control_prefix_and_rewrite_body(&mut iter, &mut out);
98 } else {
99 push_immediate_body_if_present(&mut iter, &mut out);
100 }
101 previous_token_can_own_brace_body = false;
102 continue;
103 }
104 "loop" | "unsafe" | "async" => {
105 out.push(tt);
106 push_immediate_body_if_present(&mut iter, &mut out);
107 previous_token_can_own_brace_body = false;
108 continue;
109 }
110 _ => {}
111 }
112 }
113
114 if let TokenTree::Ident(id) = &tt
124 && starts_lowercase(&id.to_string())
125 && !is_reserved_keyword(&id.to_string())
126 {
127 let is_followed_by_continuation = match iter.peek() {
128 Some(TokenTree::Group(g))
129 if matches!(
130 g.delimiter(),
131 Delimiter::Brace | Delimiter::Parenthesis | Delimiter::Bracket
132 ) =>
133 {
134 true
135 }
136 Some(TokenTree::Punct(p)) if matches!(p.as_char(), ':' | '!' | '.' | ',') => true,
137 _ => false,
138 };
139
140 if !is_followed_by_continuation {
141 let ident = id.clone();
143 let wrapped = quote! { #ident };
144 out.push(TokenTree::Group(Group::new(Delimiter::Brace, wrapped)));
145 previous_token_can_own_brace_body = false;
146 continue;
147 }
148 }
149
150 if let TokenTree::Group(g) = &tt
155 && g.delimiter() == Delimiter::Brace
156 {
157 if previous_token_can_own_brace_body {
158 let inner = rewrite_brace_body(g.stream());
159 out.push(TokenTree::Group(Group::new(Delimiter::Brace, inner)));
160 } else {
161 out.push(tt);
162 }
163 previous_token_can_own_brace_body = false;
164 continue;
165 }
166
167 previous_token_can_own_brace_body = match &tt {
168 TokenTree::Ident(id) => !is_reserved_keyword(&id.to_string()),
169 TokenTree::Punct(p) => matches!(p.as_char(), ')' | ']'),
170 _ => false,
171 };
172 out.push(tt);
173 }
174
175 out.into_iter().collect()
176}
177
178fn push_control_prefix_and_rewrite_body(
179 iter: &mut Peekable<proc_macro2::token_stream::IntoIter>,
180 out: &mut Vec<TokenTree>,
181) {
182 for next in iter.by_ref() {
183 if let TokenTree::Group(g) = &next
184 && g.delimiter() == Delimiter::Brace
185 {
186 let inner = rewrite_brace_body(g.stream());
187 out.push(TokenTree::Group(Group::new(Delimiter::Brace, inner)));
188 return;
189 }
190 out.push(next);
191 }
192}
193
194fn push_match_prefix_and_rewrite_body(
195 iter: &mut Peekable<proc_macro2::token_stream::IntoIter>,
196 out: &mut Vec<TokenTree>,
197) {
198 for next in iter.by_ref() {
199 if let TokenTree::Group(g) = &next
200 && g.delimiter() == Delimiter::Brace
201 {
202 let inner = rewrite_match_body(g.stream());
203 out.push(TokenTree::Group(Group::new(Delimiter::Brace, inner)));
204 return;
205 }
206 out.push(next);
207 }
208}
209
210fn rewrite_match_body(input: TokenStream) -> TokenStream {
211 let mut out: Vec<TokenTree> = Vec::new();
212 let mut arm_value: Vec<TokenTree> = Vec::new();
213 let mut iter = input.into_iter().peekable();
214 let mut in_arm_value = false;
215
216 while let Some(tt) = iter.next() {
217 if !in_arm_value {
218 if is_fat_arrow_start(&tt, iter.peek()) {
219 out.push(tt);
220 if let Some(next) = iter.next() {
221 out.push(next);
222 }
223 in_arm_value = true;
224 } else {
225 out.push(tt);
226 }
227 continue;
228 }
229
230 if is_top_level_comma(&tt) {
231 out.extend(rewrite_brace_body(arm_value.into_iter().collect()));
232 arm_value = Vec::new();
233 out.push(tt);
234 in_arm_value = false;
235 } else {
236 arm_value.push(tt);
237 }
238 }
239
240 if in_arm_value {
241 out.extend(rewrite_brace_body(arm_value.into_iter().collect()));
242 }
243
244 out.into_iter().collect()
245}
246
247fn is_fat_arrow_start(tt: &TokenTree, next: Option<&TokenTree>) -> bool {
248 matches!(tt, TokenTree::Punct(p) if p.as_char() == '=')
249 && matches!(next, Some(TokenTree::Punct(p)) if p.as_char() == '>')
250}
251
252fn is_top_level_comma(tt: &TokenTree) -> bool {
253 matches!(tt, TokenTree::Punct(p) if p.as_char() == ',')
254}
255
256fn push_statement_until_semicolon(
257 iter: &mut Peekable<proc_macro2::token_stream::IntoIter>,
258 out: &mut Vec<TokenTree>,
259) {
260 let mut initializer: Vec<TokenTree> = Vec::new();
261 let mut in_initializer = false;
262
263 for next in iter.by_ref() {
264 let is_semicolon = matches!(&next, TokenTree::Punct(p) if p.as_char() == ';');
265 let is_assignment = matches!(&next, TokenTree::Punct(p) if p.as_char() == '=');
266
267 if in_initializer {
268 if is_semicolon {
269 out.extend(rewrite_let_initializer(initializer.into_iter().collect()));
270 out.push(next);
271 return;
272 }
273 initializer.push(next);
274 continue;
275 }
276
277 out.push(next);
278 if is_assignment {
279 in_initializer = true;
280 }
281 if is_semicolon {
282 return;
283 }
284 }
285
286 if in_initializer {
287 out.extend(rewrite_let_initializer(initializer.into_iter().collect()));
288 }
289}
290
291fn rewrite_let_initializer(input: TokenStream) -> TokenStream {
292 let mut out: Vec<TokenTree> = Vec::new();
293 let mut previous_token_can_own_brace_body = false;
294
295 for tt in input {
296 match tt {
297 TokenTree::Group(g) if g.delimiter() == Delimiter::Brace => {
298 let inner = if previous_token_can_own_brace_body {
299 rewrite_brace_body(g.stream())
300 } else {
301 rewrite_let_initializer(g.stream())
302 };
303 out.push(TokenTree::Group(Group::new(Delimiter::Brace, inner)));
304 previous_token_can_own_brace_body = false;
305 }
306 TokenTree::Group(g) => {
307 let delimiter = g.delimiter();
308 let inner = rewrite_let_initializer(g.stream());
309 out.push(TokenTree::Group(Group::new(delimiter, inner)));
310 previous_token_can_own_brace_body = false;
311 }
312 other => {
313 previous_token_can_own_brace_body = token_can_own_let_initializer_body(&other);
314 out.push(other);
315 }
316 }
317 }
318
319 out.into_iter().collect()
320}
321
322fn token_can_own_let_initializer_body(tt: &TokenTree) -> bool {
323 matches!(
324 tt,
325 TokenTree::Ident(id)
326 if starts_lowercase(&id.to_string()) && !is_reserved_keyword(&id.to_string())
327 )
328}
329
330fn push_immediate_body_if_present(
331 iter: &mut Peekable<proc_macro2::token_stream::IntoIter>,
332 out: &mut Vec<TokenTree>,
333) {
334 if let Some(TokenTree::Group(g)) = iter.peek()
335 && g.delimiter() == Delimiter::Brace
336 {
337 let body = match iter.next() {
338 Some(TokenTree::Group(g)) => g,
339 _ => unreachable!("peek matched but next did not"),
340 };
341 let inner = rewrite_brace_body(body.stream());
342 out.push(TokenTree::Group(Group::new(Delimiter::Brace, inner)));
343 }
344}
345
346fn starts_lowercase(s: &str) -> bool {
347 s.chars()
348 .next()
349 .map(|c| c.is_ascii_lowercase())
350 .unwrap_or(false)
351}
352
353fn is_reserved_keyword(s: &str) -> bool {
357 matches!(
358 s,
359 "if" | "else"
360 | "match" | "for"
361 | "while" | "loop"
362 | "let" | "return"
363 | "break" | "continue"
364 | "move" | "ref"
365 | "mut" | "async"
366 | "await" | "yield"
367 | "do" | "in"
368 | "as" | "where"
369 | "use" | "fn"
370 | "true" | "false"
371 | "self" | "Self"
372 | "super" | "crate"
373 | "impl" | "trait"
374 | "struct"
375 | "enum" | "type"
376 | "const" | "static"
377 | "pub" | "mod"
378 | "unsafe"
379 | "extern"
380 )
381}