1pub mod action_extractor;
18pub mod flow_extractor;
19pub mod graph_generator;
20pub mod state_extractor;
21pub mod tea_extractor;
22pub mod types;
23pub mod ui_extractor;
24
25use std::path::Path;
26use types::{FileAnalysis, MsgEmission, MsgHandler, TeaFlow};
27
28pub use types::AnalysisResult;
29
30fn build_tea_flows(emissions: &[MsgEmission], handlers: &[MsgHandler]) -> Vec<TeaFlow> {
32 emissions
33 .iter()
34 .map(|emission| {
35 let handler = handlers.iter().find(|h| {
37 emission.msg == h.msg_pattern
39 || emission.msg.ends_with(&format!("::{}", h.msg_pattern))
40 || h.msg_pattern.ends_with(&format!(
41 "::{}",
42 emission.msg.split("::").last().unwrap_or("")
43 ))
44 });
45
46 TeaFlow {
47 emission: emission.clone(),
48 handler: handler.cloned(),
49 }
50 })
51 .collect()
52}
53
54pub struct Analyzer;
56
57impl Analyzer {
58 pub fn new() -> Self {
59 Self
60 }
61
62 pub fn analyze_file<P: AsRef<Path>>(&self, file_path: P) -> Result<FileAnalysis, String> {
64 let path = file_path.as_ref();
65 let path_str = path.to_string_lossy().to_string();
66
67 let content = std::fs::read_to_string(path)
68 .map_err(|e| format!("Failed to read {}: {}", path_str, e))?;
69
70 self.analyze_source(&path_str, &content)
71 }
72
73 pub fn analyze_source(&self, file_path: &str, content: &str) -> Result<FileAnalysis, String> {
75 let syntax_tree =
76 syn::parse_file(content).map_err(|e| format!("Parse error in {}: {}", file_path, e))?;
77
78 let ui_elements = ui_extractor::extract_ui_elements(file_path, &syntax_tree);
80
81 let actions = action_extractor::extract_actions(file_path, &syntax_tree);
83
84 let state_mutations = state_extractor::extract_state_mutations(file_path, &syntax_tree);
86
87 let flows = flow_extractor::extract_flows(file_path, &syntax_tree);
89
90 let msg_emissions = tea_extractor::extract_msg_emissions(file_path, &syntax_tree);
92
93 let msg_handlers = tea_extractor::extract_msg_handlers(file_path, &syntax_tree);
95
96 let tea_flows = build_tea_flows(&msg_emissions, &msg_handlers);
98
99 Ok(FileAnalysis {
100 path: file_path.to_string(),
101 ui_elements,
102 actions,
103 state_mutations,
104 flows,
105 msg_emissions,
106 msg_handlers,
107 tea_flows,
108 })
109 }
110}
111
112impl Default for Analyzer {
113 fn default() -> Self {
114 Self::new()
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 #[test]
123 fn test_basic_analysis() {
124 let code = r#"
125 fn show_ui(ui: &mut egui::Ui, state: &mut AppState) {
126 if ui.button("Click me").clicked() {
127 state.counter += 1;
128 }
129 }
130 "#;
131
132 let analyzer = Analyzer::new();
133 let result = analyzer.analyze_source("test.rs", code).unwrap();
134
135 assert!(!result.ui_elements.is_empty(), "Should find UI elements");
136 assert!(!result.actions.is_empty(), "Should find actions");
137 assert!(!result.flows.is_empty(), "Should find flows");
138
139 let flow = &result.flows[0];
141 assert_eq!(flow.ui_element.element_type, "button");
142 assert_eq!(flow.ui_element.label, Some("Click me".to_string()));
143 assert_eq!(flow.action.action_type, "clicked");
144 assert_eq!(flow.state_mutations[0].target, "state.counter");
145 }
146}