tauri_typegen/analysis/
command_parser.rs1use 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#[derive(Debug)]
8pub struct CommandParser;
9
10impl CommandParser {
11 pub fn new() -> Self {
12 Self
13 }
14
15 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 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 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 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(), })
74 }
75
76 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 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 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 if segments.len() >= 2 {
122 if segments[0].ident == "tauri" {
124 if segments.len() == 2 {
125 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 let third = &segments[2].ident;
135 return third == "Request" || third == "Channel";
136 }
137 }
138 }
139
140 if let Some(last_segment) = segments.last() {
142 let type_ident = &last_segment.ident;
143
144 if type_ident == "AppHandle" || type_ident == "WebviewWindow" {
147 return true;
148 }
149
150 if type_ident == "Channel"
152 && matches!(
153 last_segment.arguments,
154 syn::PathArguments::AngleBracketed(_)
155 )
156 {
157 return true;
158 }
159
160 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 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 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 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}