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, return_type_ts) =
58            self.extract_return_types(&func.sig.output, type_resolver);
59        let is_async = func.sig.asyncness.is_some();
60
61        // Get line number from the function's span
62        let line_number = func.sig.ident.span().start().line;
63
64        Some(CommandInfo {
65            name,
66            parameters,
67            return_type,
68            return_type_ts,
69            file_path: file_path.to_string_lossy().to_string(),
70            line_number,
71            is_async,
72            channels: Vec::new(), // Will be populated by channel_parser
73        })
74    }
75
76    /// Extract parameters from function signature
77    fn extract_parameters(
78        &self,
79        inputs: &syn::punctuated::Punctuated<FnArg, syn::token::Comma>,
80        type_resolver: &mut TypeResolver,
81    ) -> Vec<ParameterInfo> {
82        inputs
83            .iter()
84            .filter_map(|input| {
85                if let FnArg::Typed(PatType { pat, ty, .. }) = input {
86                    if let syn::Pat::Ident(pat_ident) = pat.as_ref() {
87                        let name = pat_ident.ident.to_string();
88
89                        // Skip Tauri-specific parameters
90                        if self.is_tauri_parameter_type(ty) {
91                            return None;
92                        }
93
94                        let rust_type = Self::type_to_string(ty);
95                        let typescript_type = type_resolver.map_rust_type_to_typescript(&rust_type);
96                        let is_optional = self.is_optional_type(ty);
97
98                        return Some(ParameterInfo {
99                            name,
100                            rust_type,
101                            typescript_type,
102                            is_optional,
103                        });
104                    }
105                }
106                None
107            })
108            .collect()
109    }
110
111    /// Check if a parameter type is a Tauri-specific type that should be skipped
112    /// This checks the actual syn::Type to properly handle both imported and fully-qualified types
113    fn is_tauri_parameter_type(&self, ty: &Type) -> bool {
114        if let Type::Path(type_path) = ty {
115            let segments = &type_path.path.segments;
116
117            // Check various patterns:
118            // 1. Fully qualified: tauri::AppHandle, tauri::State<T>, tauri::ipc::Request
119            // 2. Imported: AppHandle, State<T>, Window<T>
120
121            if segments.len() >= 2 {
122                // Check for tauri::* or tauri::ipc::*
123                if segments[0].ident == "tauri" {
124                    if segments.len() == 2 {
125                        // tauri::AppHandle, tauri::Window, etc.
126                        let second = &segments[1].ident;
127                        return second == "AppHandle"
128                            || second == "Window"
129                            || second == "WebviewWindow"
130                            || second == "State"
131                            || second == "Manager";
132                    } else if segments.len() == 3 && segments[1].ident == "ipc" {
133                        // tauri::ipc::Request, tauri::ipc::Channel
134                        let third = &segments[2].ident;
135                        return third == "Request" || third == "Channel";
136                    }
137                }
138            }
139
140            // Check for imported types (single segment)
141            if let Some(last_segment) = segments.last() {
142                let type_ident = &last_segment.ident;
143
144                // Only match specific Tauri types that are commonly imported
145                // Be careful not to match user types with similar names
146                if type_ident == "AppHandle" || type_ident == "WebviewWindow" {
147                    return true;
148                }
149
150                // Channel should be filtered if it has generic parameters (indicating it's the Tauri IPC channel)
151                if type_ident == "Channel"
152                    && matches!(
153                        last_segment.arguments,
154                        syn::PathArguments::AngleBracketed(_)
155                    )
156                {
157                    return true;
158                }
159
160                // State and Window are common names, only match if they have generic params
161                // (Tauri's State and Window types always have generics like State<T>, Window<R>)
162                if (type_ident == "State" || type_ident == "Window")
163                    && !last_segment.arguments.is_empty()
164                {
165                    return true;
166                }
167            }
168        }
169
170        false
171    }
172
173    /// Extract return type from function signature - returns (rust_type, typescript_type)
174    fn extract_return_types(
175        &self,
176        output: &ReturnType,
177        type_resolver: &mut TypeResolver,
178    ) -> (String, String) {
179        match output {
180            ReturnType::Default => ("()".to_string(), "void".to_string()),
181            ReturnType::Type(_, ty) => {
182                let rust_type = Self::type_to_string(ty);
183                let typescript_type = type_resolver.map_rust_type_to_typescript(&rust_type);
184                (rust_type, typescript_type)
185            }
186        }
187    }
188
189    /// Convert a Type to its string representation
190    fn type_to_string(ty: &Type) -> String {
191        match ty {
192            Type::Path(type_path) => {
193                let segments: Vec<String> = type_path
194                    .path
195                    .segments
196                    .iter()
197                    .map(|segment| {
198                        if segment.arguments.is_empty() {
199                            segment.ident.to_string()
200                        } else {
201                            match &segment.arguments {
202                                syn::PathArguments::AngleBracketed(args) => {
203                                    let inner_types: Vec<String> = args
204                                        .args
205                                        .iter()
206                                        .filter_map(|arg| {
207                                            if let syn::GenericArgument::Type(inner_ty) = arg {
208                                                Some(Self::type_to_string(inner_ty))
209                                            } else {
210                                                None
211                                            }
212                                        })
213                                        .collect();
214                                    format!("{}<{}>", segment.ident, inner_types.join(", "))
215                                }
216                                _ => segment.ident.to_string(),
217                            }
218                        }
219                    })
220                    .collect();
221                segments.join("::")
222            }
223            Type::Reference(type_ref) => {
224                format!("&{}", Self::type_to_string(&type_ref.elem))
225            }
226            Type::Tuple(type_tuple) => {
227                if type_tuple.elems.is_empty() {
228                    "()".to_string()
229                } else {
230                    let types: Vec<String> =
231                        type_tuple.elems.iter().map(Self::type_to_string).collect();
232                    format!("({})", types.join(", "))
233                }
234            }
235            _ => "unknown".to_string(),
236        }
237    }
238
239    /// Check if a type is Option<T>
240    fn is_optional_type(&self, ty: &Type) -> bool {
241        if let Type::Path(type_path) = ty {
242            if let Some(segment) = type_path.path.segments.last() {
243                return segment.ident == "Option";
244            }
245        }
246        false
247    }
248}
249
250impl Default for CommandParser {
251    fn default() -> Self {
252        Self::new()
253    }
254}