ferroui_hot_patch/
admission.rs1use quote::ToTokens;
2use std::collections::BTreeSet;
3use syn::visit::Visit;
4#[derive(Debug, PartialEq, Eq)]
5pub enum Admission {
6 Unchanged,
7 Patch { functions: Vec<String> },
8 RestartRequired(String),
9}
10pub fn admit(before: &str, after: &str, registered: &BTreeSet<String>) -> Admission {
13 let (Ok(mut old), Ok(mut new)) = (syn::parse_file(before), syn::parse_file(after)) else {
14 return Admission::RestartRequired("Rust source cannot be parsed".into());
15 };
16 if old.to_token_stream().to_string() == new.to_token_stream().to_string() {
17 return Admission::Unchanged;
18 }
19 let mut changed = Vec::new();
20 for item in &mut old.items {
21 let syn::Item::Fn(previous) = item else {
22 continue;
23 };
24 let name = previous.sig.ident.to_string();
25 if !registered.contains(&name) {
26 continue;
27 }
28 let Some(syn::Item::Fn(next)) = new.items.iter_mut().find(|item| matches!(item, syn::Item::Fn(function) if function.sig.ident == previous.sig.ident)) else {
29 return Admission::RestartRequired(format!("Registered function removed: {name}"));
30 };
31 if previous.block.to_token_stream().to_string() == next.block.to_token_stream().to_string()
32 {
33 continue;
34 }
35 if unsupported_signature(&previous.sig) {
36 return Admission::RestartRequired(format!("Unsupported patch boundary: {name}"));
37 }
38 for body in [&previous.block, &next.block] {
39 if unsupported_body(body) {
40 return Admission::RestartRequired(format!(
41 "Closure, macro, async block, unsafe block or declaration in {name}"
42 ));
43 }
44 }
45 previous.block = Box::new(syn::parse_quote!({}));
46 next.block = Box::new(syn::parse_quote!({}));
47 changed.push(name);
48 }
49 if old.to_token_stream().to_string() != new.to_token_stream().to_string() || changed.is_empty()
50 {
51 Admission::RestartRequired("Change outside registered function bodies".into())
52 } else {
53 Admission::Patch { functions: changed }
54 }
55}
56fn unsupported_signature(signature: &syn::Signature) -> bool {
57 signature.asyncness.is_some()
58 || signature.unsafety.is_some()
59 || signature.constness.is_some()
60 || !signature.generics.params.is_empty()
61}
62fn unsupported_body(body: &syn::Block) -> bool {
63 let mut unsupported = Unsupported(false);
64 unsupported.visit_block(body);
65 unsupported.0
66}
67pub(crate) fn validate_registration(source: &str, name: &str) -> Result<(), String> {
68 let file = syn::parse_file(source).map_err(|error| error.to_string())?;
69 let function = file
70 .items
71 .iter()
72 .find_map(|item| match item {
73 syn::Item::Fn(function) if function.sig.ident == name => Some(function),
74 _ => None,
75 })
76 .ok_or_else(|| format!("Registered top-level function not found: {name}"))?;
77 if unsupported_signature(&function.sig) || unsupported_body(&function.block) {
78 return Err(format!("Unsupported patch boundary: {name}"));
79 }
80 Ok(())
81}
82struct Unsupported(bool);
83impl<'ast> Visit<'ast> for Unsupported {
84 fn visit_expr_closure(&mut self, _: &'ast syn::ExprClosure) {
85 self.0 = true;
86 }
87 fn visit_expr_async(&mut self, _: &'ast syn::ExprAsync) {
88 self.0 = true;
89 }
90 fn visit_expr_unsafe(&mut self, _: &'ast syn::ExprUnsafe) {
91 self.0 = true;
92 }
93 fn visit_item(&mut self, _: &'ast syn::Item) {
94 self.0 = true;
95 }
96 fn visit_macro(&mut self, _: &'ast syn::Macro) {
97 self.0 = true;
98 }
99}
100#[cfg(test)]
101mod tests {
102 use super::*;
103 fn check(old: &str, new: &str) -> Admission {
104 admit(old, new, &BTreeSet::from(["click".into()]))
105 }
106 #[test]
107 fn admits_body_change() {
108 assert!(matches!(
109 check(
110 "fn click(x: i32) -> i32 { x+1 }",
111 "fn click(x: i32) -> i32 { x+2 }"
112 ),
113 Admission::Patch { .. }
114 ));
115 }
116 #[test]
117 fn rejects_signature() {
118 assert!(matches!(
119 check("fn click(x: i32) { }", "fn click(x: i64) { }"),
120 Admission::RestartRequired(_)
121 ));
122 }
123 #[test]
124 fn rejects_layout_and_static() {
125 for addition in [
126 "struct State { x: i64 }",
127 "static VALUE: i32 = 1;",
128 "fn other() {}",
129 ] {
130 assert!(matches!(
131 check("fn click() {}", &format!("fn click() {{}} {addition}")),
132 Admission::RestartRequired(_)
133 ));
134 }
135 }
136 #[test]
137 fn rejects_closure_and_macro() {
138 for body in [
139 "let f = || 1;",
140 "println!(\"changed\");",
141 "struct New;",
142 "let f = async { 1 };",
143 "unsafe {}",
144 ] {
145 assert!(matches!(
146 check("fn click() {}", &format!("fn click() {{ {body} }}")),
147 Admission::RestartRequired(_)
148 ));
149 }
150 }
151 #[test]
152 fn ignores_whitespace() {
153 assert_eq!(
154 check("fn click() {}", "fn click( ) { }"),
155 Admission::Unchanged
156 );
157 }
158}