reinhardt_admin_cli/migrate_v2/rules/
bare_ident.rs1use proc_macro2::{Delimiter, Group, TokenStream, TokenTree};
9use quote::quote;
10use syn::visit_mut::{self, VisitMut};
11
12use crate::migrate_v2::rewriter::FileRewriter;
13
14pub struct Rule;
16
17impl FileRewriter for Rule {
18 fn name(&self) -> &'static str {
19 "bare_ident"
20 }
21
22 fn rewrite(&self, mut file: syn::File) -> syn::File {
23 PageMacroBodyVisitor.visit_file_mut(&mut file);
24 file
25 }
26}
27
28struct PageMacroBodyVisitor;
29
30impl VisitMut for PageMacroBodyVisitor {
31 fn visit_macro_mut(&mut self, m: &mut syn::Macro) {
32 if m.path
34 .segments
35 .last()
36 .map(|s| s.ident == "page")
37 .unwrap_or(false)
38 {
39 m.tokens = rewrite_page_body(m.tokens.clone());
40 }
41 visit_mut::visit_macro_mut(self, m);
42 }
43}
44
45fn rewrite_page_body(input: TokenStream) -> TokenStream {
48 let mut out: Vec<TokenTree> = Vec::new();
49 for tt in input {
50 match tt {
51 TokenTree::Group(g) if g.delimiter() == Delimiter::Brace => {
52 let inner = rewrite_brace_body(g.stream());
53 out.push(TokenTree::Group(Group::new(Delimiter::Brace, inner)));
54 }
55 other => out.push(other),
56 }
57 }
58 out.into_iter().collect()
59}
60
61fn rewrite_brace_body(input: TokenStream) -> TokenStream {
64 let mut out: Vec<TokenTree> = Vec::new();
65 let mut iter = input.into_iter().peekable();
66
67 while let Some(tt) = iter.next() {
68 if let TokenTree::Ident(id) = &tt
78 && starts_lowercase(&id.to_string())
79 && !is_reserved_keyword(&id.to_string())
80 {
81 let is_followed_by_continuation = match iter.peek() {
82 Some(TokenTree::Group(g))
83 if matches!(
84 g.delimiter(),
85 Delimiter::Brace | Delimiter::Parenthesis | Delimiter::Bracket
86 ) =>
87 {
88 true
89 }
90 Some(TokenTree::Punct(p)) if matches!(p.as_char(), ':' | '!' | '.' | ',') => true,
91 _ => false,
92 };
93
94 if !is_followed_by_continuation {
95 let ident = id.clone();
97 let wrapped = quote! { #ident };
98 out.push(TokenTree::Group(Group::new(Delimiter::Brace, wrapped)));
99 continue;
100 }
101 }
102
103 if let TokenTree::Group(g) = &tt
109 && g.delimiter() == Delimiter::Brace
110 {
111 if is_already_wrapped_expression_slot(&g.stream()) {
112 out.push(tt);
113 } else {
114 let inner = rewrite_brace_body(g.stream());
115 out.push(TokenTree::Group(Group::new(Delimiter::Brace, inner)));
116 }
117 continue;
118 }
119
120 out.push(tt);
121 }
122
123 out.into_iter().collect()
124}
125
126fn is_already_wrapped_expression_slot(stream: &TokenStream) -> bool {
130 let mut iter = stream.clone().into_iter();
131 let first = iter.next();
132 let rest = iter.next();
133 match (first, rest) {
134 (Some(TokenTree::Group(g)), None) => g.delimiter() == Delimiter::Brace,
135 _ => false,
136 }
137}
138
139fn starts_lowercase(s: &str) -> bool {
140 s.chars()
141 .next()
142 .map(|c| c.is_ascii_lowercase())
143 .unwrap_or(false)
144}
145
146fn is_reserved_keyword(s: &str) -> bool {
150 matches!(
151 s,
152 "if" | "else"
153 | "match" | "for"
154 | "while" | "loop"
155 | "let" | "return"
156 | "break" | "continue"
157 | "move" | "ref"
158 | "mut" | "async"
159 | "await" | "yield"
160 | "do" | "in"
161 | "as" | "where"
162 | "use" | "fn"
163 | "true" | "false"
164 | "self" | "Self"
165 | "super" | "crate"
166 | "impl" | "trait"
167 | "struct"
168 | "enum" | "type"
169 | "const" | "static"
170 | "pub" | "mod"
171 | "unsafe"
172 | "extern"
173 )
174}