egui_cha_analyzer/
lib.rs

1//! egui-cha-analyzer: Static analyzer for egui UI flow
2//!
3//! Extracts and visualizes the UI -> Action -> State flow from egui code.
4//!
5//! # Example
6//!
7//! ```ignore
8//! use egui_cha_analyzer::Analyzer;
9//!
10//! let analyzer = Analyzer::new();
11//! let result = analyzer.analyze_file("src/app.rs")?;
12//!
13//! // Generate Mermaid flowchart
14//! let mermaid = result.to_mermaid();
15//! ```
16
17pub 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
30/// Build TEA flows by matching emissions to handlers
31fn build_tea_flows(emissions: &[MsgEmission], handlers: &[MsgHandler]) -> Vec<TeaFlow> {
32    emissions
33        .iter()
34        .map(|emission| {
35            // Try to find a matching handler
36            let handler = handlers.iter().find(|h| {
37                // Match by message name (e.g., "Msg::Increment" matches "Msg::Increment")
38                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
54/// Main analyzer for egui UI flow
55pub struct Analyzer;
56
57impl Analyzer {
58    pub fn new() -> Self {
59        Self
60    }
61
62    /// Analyze a single Rust source file
63    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    /// Analyze source code directly
74    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        // Extract UI elements (standard egui)
79        let ui_elements = ui_extractor::extract_ui_elements(file_path, &syntax_tree);
80
81        // Extract actions (response checks)
82        let actions = action_extractor::extract_actions(file_path, &syntax_tree);
83
84        // Extract state mutations
85        let state_mutations = state_extractor::extract_state_mutations(file_path, &syntax_tree);
86
87        // Extract flows with scope tracking (causality)
88        let flows = flow_extractor::extract_flows(file_path, &syntax_tree);
89
90        // Extract TEA patterns (DS components -> Msg)
91        let msg_emissions = tea_extractor::extract_msg_emissions(file_path, &syntax_tree);
92
93        // Extract Msg handlers (update function)
94        let msg_handlers = tea_extractor::extract_msg_handlers(file_path, &syntax_tree);
95
96        // Build TEA flows by matching emissions to handlers
97        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        // Check flow causality
140        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}