tauri_typegen/analysis/
command_parser.rs

1use crate::analysis::type_resolver::TypeResolver;
2use crate::models::{CommandInfo, ParameterInfo};
3use std::path::Path;
4use syn::{File as SynFile, FnArg, ItemFn, PatType, ReturnType, Type};
5
6/// Parser for Tauri command functions
7#[derive(Debug)]
8pub struct CommandParser;
9
10impl CommandParser {
11    pub fn new() -> Self {
12        Self
13    }
14
15    /// Extract commands from a cached AST
16    pub fn extract_commands_from_ast(
17        &self,
18        ast: &SynFile,
19        file_path: &Path,
20        type_resolver: &mut TypeResolver,
21    ) -> Result<Vec<CommandInfo>, Box<dyn std::error::Error>> {
22        let commands = ast
23            .items
24            .iter()
25            .filter_map(|item| {
26                if let syn::Item::Fn(func) = item {
27                    if self.is_tauri_command(func) {
28                        return self.extract_command_info(func, file_path, type_resolver);
29                    }
30                }
31                None
32            })
33            .collect();
34
35        Ok(commands)
36    }
37
38    /// Check if a function is a Tauri command
39    fn is_tauri_command(&self, func: &ItemFn) -> bool {
40        func.attrs.iter().any(|attr| {
41            attr.path().segments.len() == 2
42                && attr.path().segments[0].ident == "tauri"
43                && attr.path().segments[1].ident == "command"
44                || attr.path().is_ident("command")
45        })
46    }
47
48    /// Extract command information from a function
49    fn extract_command_info(
50        &self,
51        func: &ItemFn,
52        file_path: &Path,
53        type_resolver: &mut TypeResolver,
54    ) -> Option<CommandInfo> {
55        let name = func.sig.ident.to_string();
56        let parameters = self.extract_parameters(&func.sig.inputs, type_resolver);
57        let return_type = self.extract_return_type(&func.sig.output, type_resolver);
58        let is_async = func.sig.asyncness.is_some();
59
60        // Get line number from the function's span
61        let line_number = func.sig.ident.span().start().line;
62
63        Some(CommandInfo {
64            name,
65            parameters,
66            return_type,
67            file_path: file_path.to_string_lossy().to_string(),
68            line_number,
69            is_async,
70            channels: Vec::new(), // Will be populated by channel_parser
71        })
72    }
73
74    /// Extract parameters from function signature
75    fn extract_parameters(
76        &self,
77        inputs: &syn::punctuated::Punctuated<FnArg, syn::token::Comma>,
78        type_resolver: &mut TypeResolver,
79    ) -> Vec<ParameterInfo> {
80        inputs
81            .iter()
82            .filter_map(|input| {
83                if let FnArg::Typed(PatType { pat, ty, .. }) = input {
84                    if let syn::Pat::Ident(pat_ident) = pat.as_ref() {
85                        let name = pat_ident.ident.to_string();
86
87                        // Skip Tauri-specific parameters
88                        if self.is_tauri_parameter_type(ty) {
89                            return None;
90                        }
91
92                        let rust_type = Self::type_to_string(ty);
93                        let typescript_type = type_resolver.map_rust_type_to_typescript(&rust_type);
94                        let is_optional = self.is_optional_type(ty);
95
96                        return Some(ParameterInfo {
97                            name,
98                            rust_type,
99                            typescript_type,
100                            is_optional,
101                        });
102                    }
103                }
104                None
105            })
106            .collect()
107    }
108
109    /// Check if a parameter type is a Tauri-specific type that should be skipped
110    /// This checks the actual syn::Type to properly handle both imported and fully-qualified types
111    fn is_tauri_parameter_type(&self, ty: &Type) -> bool {
112        if let Type::Path(type_path) = ty {
113            let segments = &type_path.path.segments;
114
115            // Check various patterns:
116            // 1. Fully qualified: tauri::AppHandle, tauri::State<T>, tauri::ipc::Request
117            // 2. Imported: AppHandle, State<T>, Window<T>
118
119            if segments.len() >= 2 {
120                // Check for tauri::* or tauri::ipc::*
121                if segments[0].ident == "tauri" {
122                    if segments.len() == 2 {
123                        // tauri::AppHandle, tauri::Window, etc.
124                        let second = &segments[1].ident;
125                        return second == "AppHandle"
126                            || second == "Window"
127                            || second == "WebviewWindow"
128                            || second == "State"
129                            || second == "Manager";
130                    } else if segments.len() == 3 && segments[1].ident == "ipc" {
131                        // tauri::ipc::Request, tauri::ipc::Channel
132                        let third = &segments[2].ident;
133                        return third == "Request" || third == "Channel";
134                    }
135                }
136            }
137
138            // Check for imported types (single segment)
139            if let Some(last_segment) = segments.last() {
140                let type_ident = &last_segment.ident;
141
142                // Only match specific Tauri types that are commonly imported
143                // Be careful not to match user types with similar names
144                if type_ident == "AppHandle" || type_ident == "WebviewWindow" {
145                    return true;
146                }
147
148                // Channel should be filtered if it has generic parameters (indicating it's the Tauri IPC channel)
149                if type_ident == "Channel"
150                    && matches!(
151                        last_segment.arguments,
152                        syn::PathArguments::AngleBracketed(_)
153                    )
154                {
155                    return true;
156                }
157
158                // State and Window are common names, only match if they have generic params
159                // (Tauri's State and Window types always have generics like State<T>, Window<R>)
160                if (type_ident == "State" || type_ident == "Window")
161                    && !last_segment.arguments.is_empty()
162                {
163                    return true;
164                }
165            }
166        }
167
168        false
169    }
170
171    /// Extract return type from function signature
172    fn extract_return_type(&self, output: &ReturnType, type_resolver: &mut TypeResolver) -> String {
173        match output {
174            ReturnType::Default => "void".to_string(),
175            ReturnType::Type(_, ty) => {
176                let rust_type = Self::type_to_string(ty);
177                type_resolver.map_rust_type_to_typescript(&rust_type)
178            }
179        }
180    }
181
182    /// Convert a Type to its string representation
183    fn type_to_string(ty: &Type) -> String {
184        match ty {
185            Type::Path(type_path) => {
186                let segments: Vec<String> = type_path
187                    .path
188                    .segments
189                    .iter()
190                    .map(|segment| {
191                        if segment.arguments.is_empty() {
192                            segment.ident.to_string()
193                        } else {
194                            match &segment.arguments {
195                                syn::PathArguments::AngleBracketed(args) => {
196                                    let inner_types: Vec<String> = args
197                                        .args
198                                        .iter()
199                                        .filter_map(|arg| {
200                                            if let syn::GenericArgument::Type(inner_ty) = arg {
201                                                Some(Self::type_to_string(inner_ty))
202                                            } else {
203                                                None
204                                            }
205                                        })
206                                        .collect();
207                                    format!("{}<{}>", segment.ident, inner_types.join(", "))
208                                }
209                                _ => segment.ident.to_string(),
210                            }
211                        }
212                    })
213                    .collect();
214                segments.join("::")
215            }
216            Type::Reference(type_ref) => {
217                format!("&{}", Self::type_to_string(&type_ref.elem))
218            }
219            Type::Tuple(type_tuple) => {
220                if type_tuple.elems.is_empty() {
221                    "()".to_string()
222                } else {
223                    let types: Vec<String> =
224                        type_tuple.elems.iter().map(Self::type_to_string).collect();
225                    format!("({})", types.join(", "))
226                }
227            }
228            _ => "unknown".to_string(),
229        }
230    }
231
232    /// Check if a type is Option<T>
233    fn is_optional_type(&self, ty: &Type) -> bool {
234        if let Type::Path(type_path) = ty {
235            if let Some(segment) = type_path.path.segments.last() {
236                return segment.ident == "Option";
237            }
238        }
239        false
240    }
241}
242
243impl Default for CommandParser {
244    fn default() -> Self {
245        Self::new()
246    }
247}