tauri_typegen/analysis/
channel_parser.rs

1use crate::analysis::type_resolver::TypeResolver;
2use crate::models::ChannelInfo;
3use std::path::Path;
4use syn::spanned::Spanned;
5use syn::{
6    AngleBracketedGenericArguments, FnArg, GenericArgument, ItemFn, PathArguments, PathSegment,
7    Type,
8};
9
10/// Parser for Tauri Channel parameters in command signatures
11#[derive(Debug)]
12pub struct ChannelParser;
13
14impl ChannelParser {
15    pub fn new() -> Self {
16        Self
17    }
18
19    /// Extract channel parameters from a command function signature
20    /// Looks for parameters with type Channel<T>, tauri::ipc::Channel<T>, etc.
21    pub fn extract_channels_from_command(
22        &self,
23        func: &ItemFn,
24        command_name: &str,
25        file_path: &Path,
26        type_resolver: &mut TypeResolver,
27    ) -> Result<Vec<ChannelInfo>, Box<dyn std::error::Error>> {
28        let mut channels = Vec::new();
29
30        // Iterate through function parameters
31        for input in &func.sig.inputs {
32            if let FnArg::Typed(pat_type) = input {
33                // Extract parameter name
34                let param_name = if let syn::Pat::Ident(pat_ident) = &*pat_type.pat {
35                    pat_ident.ident.to_string()
36                } else {
37                    continue;
38                };
39
40                // Check if this parameter is a Channel type
41                if let Some(message_type) = self.extract_channel_message_type(&pat_type.ty) {
42                    // Map Rust type to TypeScript
43                    let typescript_message_type =
44                        type_resolver.map_rust_type_to_typescript(&message_type);
45
46                    // Get line number from parameter span
47                    let line_number = pat_type.ty.span().start().line;
48
49                    channels.push(ChannelInfo {
50                        parameter_name: param_name,
51                        message_type: message_type.clone(),
52                        typescript_message_type,
53                        command_name: command_name.to_string(),
54                        file_path: file_path.to_string_lossy().to_string(),
55                        line_number,
56                    });
57                }
58            }
59        }
60
61        Ok(channels)
62    }
63
64    /// Extract the message type T from Channel<T>
65    /// Returns Some(T) if the type is a Channel, None otherwise
66    fn extract_channel_message_type(&self, ty: &Type) -> Option<String> {
67        match ty {
68            Type::Path(type_path) => {
69                // Get the last segment of the path (e.g., "Channel" from "tauri::ipc::Channel")
70                let last_segment = type_path.path.segments.last()?;
71
72                // Check if this is a Channel type
73                if self.is_channel_segment(last_segment, &type_path.path.segments) {
74                    // Extract the generic argument T from Channel<T>
75                    if let PathArguments::AngleBracketed(AngleBracketedGenericArguments {
76                        args,
77                        ..
78                    }) = &last_segment.arguments
79                    {
80                        if let Some(GenericArgument::Type(inner_type)) = args.first() {
81                            return Some(Self::type_to_string(inner_type));
82                        }
83                    }
84                }
85                None
86            }
87            _ => None,
88        }
89    }
90
91    /// Check if a path segment represents a Channel type
92    /// Handles: Channel, tauri::ipc::Channel, tauri::Channel
93    fn is_channel_segment(
94        &self,
95        segment: &PathSegment,
96        all_segments: &syn::punctuated::Punctuated<PathSegment, syn::Token![::]>,
97    ) -> bool {
98        let segment_name = segment.ident.to_string();
99
100        // Must be named "Channel"
101        if segment_name != "Channel" {
102            return false;
103        }
104
105        // Accept bare "Channel" or qualified paths like "tauri::ipc::Channel"
106        if all_segments.len() == 1 {
107            // Bare "Channel"
108            return true;
109        }
110
111        // Check for tauri::* namespace
112        if all_segments.len() >= 2 {
113            let first = all_segments.first().unwrap().ident.to_string();
114            if first == "tauri" {
115                return true;
116            }
117        }
118
119        false
120    }
121
122    /// Convert a syn::Type to a string representation
123    /// Simplified version - handles common cases
124    fn type_to_string(ty: &Type) -> String {
125        match ty {
126            Type::Path(type_path) => {
127                let segments: Vec<String> = type_path
128                    .path
129                    .segments
130                    .iter()
131                    .map(|seg| {
132                        let ident = seg.ident.to_string();
133                        // Handle generic arguments if present
134                        if let PathArguments::AngleBracketed(args) = &seg.arguments {
135                            let generic_args: Vec<String> = args
136                                .args
137                                .iter()
138                                .filter_map(|arg| {
139                                    if let GenericArgument::Type(t) = arg {
140                                        Some(Self::type_to_string(t))
141                                    } else {
142                                        None
143                                    }
144                                })
145                                .collect();
146                            if !generic_args.is_empty() {
147                                return format!("{}<{}>", ident, generic_args.join(", "));
148                            }
149                        }
150                        ident
151                    })
152                    .collect();
153                segments.join("::")
154            }
155            Type::Reference(type_ref) => {
156                format!("&{}", Self::type_to_string(&type_ref.elem))
157            }
158            Type::Tuple(tuple) => {
159                if tuple.elems.is_empty() {
160                    "()".to_string()
161                } else {
162                    let types: Vec<String> = tuple.elems.iter().map(Self::type_to_string).collect();
163                    format!("({})", types.join(", "))
164                }
165            }
166            Type::Array(arr) => {
167                format!("[{}]", Self::type_to_string(&arr.elem))
168            }
169            _ => "unknown".to_string(),
170        }
171    }
172}
173
174impl Default for ChannelParser {
175    fn default() -> Self {
176        Self::new()
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183    use syn::parse_quote;
184
185    #[test]
186    fn test_detect_simple_channel() {
187        let parser = ChannelParser::new();
188        let ty: Type = parse_quote!(Channel<ProgressUpdate>);
189        let result = parser.extract_channel_message_type(&ty);
190        assert_eq!(result, Some("ProgressUpdate".to_string()));
191    }
192
193    #[test]
194    fn test_detect_qualified_channel() {
195        let parser = ChannelParser::new();
196        let ty: Type = parse_quote!(tauri::ipc::Channel<DownloadEvent>);
197        let result = parser.extract_channel_message_type(&ty);
198        assert_eq!(result, Some("DownloadEvent".to_string()));
199    }
200
201    #[test]
202    fn test_detect_channel_with_primitive() {
203        let parser = ChannelParser::new();
204        let ty: Type = parse_quote!(Channel<i32>);
205        let result = parser.extract_channel_message_type(&ty);
206        assert_eq!(result, Some("i32".to_string()));
207    }
208
209    #[test]
210    fn test_non_channel_type() {
211        let parser = ChannelParser::new();
212        let ty: Type = parse_quote!(String);
213        let result = parser.extract_channel_message_type(&ty);
214        assert_eq!(result, None);
215    }
216
217    #[test]
218    fn test_channel_with_complex_type() {
219        let parser = ChannelParser::new();
220        let ty: Type = parse_quote!(Channel<Vec<String>>);
221        let result = parser.extract_channel_message_type(&ty);
222        assert_eq!(result, Some("Vec<String>".to_string()));
223    }
224
225    #[test]
226    fn test_type_to_string() {
227        let _parser = ChannelParser::new();
228
229        let ty: Type = parse_quote!(String);
230        assert_eq!(ChannelParser::type_to_string(&ty), "String");
231
232        let ty: Type = parse_quote!(Vec<i32>);
233        assert_eq!(ChannelParser::type_to_string(&ty), "Vec<i32>");
234
235        let ty: Type = parse_quote!(Option<User>);
236        assert_eq!(ChannelParser::type_to_string(&ty), "Option<User>");
237    }
238}