Skip to main content

reinhardt_admin_cli/migrate_v2/rules/
watch_unwrap.rs

1//! Rule §6.1 (2)+(3): `watch { body }` and `#reactive { body }` →
2//! splice `body` in place, dropping the wrapper.
3//!
4//! `#reactive` was an early-design wrapper that never reached `main`, but
5//! the rule keeps a defensive arm so any in-flight branch picking up the
6//! codemod still gets a clean result.
7
8use proc_macro2::{Delimiter, Group, TokenStream, TokenTree};
9use syn::visit_mut::{self, VisitMut};
10
11use crate::migrate_v2::rewriter::FileRewriter;
12
13/// `watch_unwrap` rule entry.
14pub struct Rule;
15
16impl FileRewriter for Rule {
17	fn name(&self) -> &'static str {
18		"watch_unwrap"
19	}
20
21	fn rewrite(&self, mut file: syn::File) -> syn::File {
22		PageMacroBodyVisitor.visit_file_mut(&mut file);
23		file
24	}
25}
26
27struct PageMacroBodyVisitor;
28
29impl VisitMut for PageMacroBodyVisitor {
30	fn visit_macro_mut(&mut self, m: &mut syn::Macro) {
31		if m.path
32			.segments
33			.last()
34			.map(|s| s.ident == "page")
35			.unwrap_or(false)
36		{
37			m.tokens = unwrap_watch(m.tokens.clone());
38		}
39		visit_mut::visit_macro_mut(self, m);
40	}
41}
42
43fn unwrap_watch(input: TokenStream) -> TokenStream {
44	let mut out: Vec<TokenTree> = Vec::new();
45	let mut iter = input.into_iter().peekable();
46
47	while let Some(tt) = iter.next() {
48		match &tt {
49			// `watch` followed by a brace group → splice body.
50			TokenTree::Ident(id) if id == "watch" => {
51				if let Some(TokenTree::Group(g)) = iter.peek()
52					&& g.delimiter() == Delimiter::Brace
53				{
54					let body = match iter.next() {
55						Some(TokenTree::Group(g)) => g.stream(),
56						_ => unreachable!("peek matched but next did not"),
57					};
58					out.extend(unwrap_watch(body));
59					continue;
60				}
61				out.push(tt);
62			}
63			// `#reactive` (Punct '#' then Ident "reactive") followed by a brace.
64			// Use a cloned lookahead iterator to avoid consuming the `reactive`
65			// token before confirming the brace.
66			TokenTree::Punct(p) if p.as_char() == '#' => {
67				let mut lookahead = iter.clone();
68				let reactive_with_brace = matches!(
69					(lookahead.next(), lookahead.next()),
70					(Some(TokenTree::Ident(id2)), Some(TokenTree::Group(g)))
71						if id2 == "reactive" && g.delimiter() == Delimiter::Brace
72				);
73				if reactive_with_brace {
74					let _ = iter.next(); // consume "reactive"
75					let body = match iter.next() {
76						Some(TokenTree::Group(g)) => g.stream(),
77						_ => unreachable!("lookahead matched but next did not"),
78					};
79					out.extend(unwrap_watch(body));
80					continue;
81				}
82				out.push(tt);
83			}
84			// Recurse into any other brace group.
85			TokenTree::Group(g) if g.delimiter() == Delimiter::Brace => {
86				let inner = unwrap_watch(g.stream());
87				out.push(TokenTree::Group(Group::new(Delimiter::Brace, inner)));
88			}
89			_ => out.push(tt),
90		}
91	}
92
93	out.into_iter().collect()
94}