dioxus_rsx_hotreload/
collect.rs

1//! Compare two files and find any rsx calls that have changed
2//!
3//! This is used to determine if a hotreload is needed.
4//! We use a special syn visitor to find all the rsx! calls in the file and then compare them to see
5//! if they are the same. This visitor will actually remove the rsx! calls and replace them with a
6//! dummy rsx! call. The final file type is thus mutated in place, leaving the original file idents
7//! in place. We then compare the two files to see if they are the same. We're able to differentiate
8//! between rust code changes and rsx code changes with much less code than the previous manual diff
9//! approach.
10
11use syn::visit_mut::VisitMut;
12use syn::{File, Macro};
13
14pub struct ChangedRsx {
15    /// The old macro - the original RSX from the original file
16    pub old: Macro,
17
18    /// The new macro
19    pub new: Macro,
20}
21
22#[derive(Debug)]
23pub enum ReloadableRustCode {
24    Rsx { old: Macro, new: Macro },
25}
26
27/// Find any rsx calls in the given file and return a list of all the rsx calls that have changed.
28///
29/// Takes in the two files, clones them, removes the rsx! contents and prunes any doc comments.
30/// Then it compares the two files to see if they are different - if they are, the code changed.
31/// Otherwise, the code is the same and we can move on to handling the changed rsx
32///
33/// Returns `None` if the files are the same and `Some` if they are different
34/// If there are no rsx! calls in the files, the vec will be empty.
35pub fn diff_rsx(new: &File, old: &File) -> Option<Vec<ChangedRsx>> {
36    // Make a clone of these files in place so we don't have to worry about mutating the original
37    let mut old = old.clone();
38    let mut new = new.clone();
39
40    // Collect all the rsx! macros from the old file - modifying the files in place
41    let old_macros = collect_from_file(&mut old);
42    let new_macros = collect_from_file(&mut new);
43
44    // If the number of rsx! macros is different, then it's not hotreloadable
45    if old_macros.len() != new_macros.len() {
46        return None;
47    }
48
49    // If the files are not the same, then it's not hotreloadable
50    if old != new {
51        return None;
52    }
53
54    Some(
55        old_macros
56            .into_iter()
57            .zip(new_macros)
58            .map(|(old, new)| ChangedRsx { old, new })
59            .collect(),
60    )
61}
62
63pub fn collect_from_file(file: &mut File) -> Vec<Macro> {
64    struct MacroCollector(Vec<Macro>);
65    impl VisitMut for MacroCollector {
66        /// Take out the rsx! macros, leaving a default in their place
67        fn visit_macro_mut(&mut self, dest: &mut syn::Macro) {
68            let name = &dest.path.segments.last().map(|i| i.ident.to_string());
69            if let Some("rsx" | "render") = name.as_deref() {
70                let mut default: syn::Macro = syn::parse_quote! { rsx! {} };
71                std::mem::swap(dest, &mut default);
72                self.0.push(default)
73            }
74        }
75
76        /// Ignore doc comments by swapping them out with a default
77        fn visit_attribute_mut(&mut self, i: &mut syn::Attribute) {
78            if i.path().is_ident("doc") {
79                *i = syn::parse_quote! { #[doc = ""] };
80            }
81        }
82    }
83
84    let mut macros = MacroCollector(vec![]);
85    macros.visit_file_mut(file);
86    macros.0
87}
88
89#[test]
90fn changing_files() {
91    let old = r#"
92use dioxus::prelude::*;
93
94/// some comment
95pub fn CoolChild() -> Element {
96    let a = 123;
97
98    rsx! {
99        div {
100            {some_expr()}
101        }
102    }
103}"#;
104
105    let new = r#"
106use dioxus::prelude::*;
107
108/// some comment
109pub fn CoolChild() -> Element {
110    rsx! {
111        div {
112            {some_expr()}
113        }
114    }
115}"#;
116
117    let same = r#"
118use dioxus::prelude::*;
119
120/// some comment!!!!!
121pub fn CoolChild() -> Element {
122    let a = 123;
123
124    rsx! {
125        div {
126            {some_expr()}
127        }
128    }
129}"#;
130
131    let old = syn::parse_file(old).unwrap();
132    let new = syn::parse_file(new).unwrap();
133    let same = syn::parse_file(same).unwrap();
134
135    assert!(
136        diff_rsx(&old, &new).is_none(),
137        "Files with different expressions should not be hotreloadable"
138    );
139
140    assert!(
141        diff_rsx(&new, &new).is_some(),
142        "The same file should be reloadable with itself"
143    );
144
145    assert!(
146        diff_rsx(&old, &same).is_some(),
147        "Files with changed comments should be hotreloadable"
148    );
149}