use crate::analysis::type_resolver::TypeResolver;
use crate::models::{CommandInfo, ParameterInfo};
use std::path::Path;
use syn::{File as SynFile, FnArg, ItemFn, PatType, ReturnType, Type};
#[derive(Debug)]
pub struct CommandParser;
impl CommandParser {
pub fn new() -> Self {
Self
}
pub fn extract_commands_from_ast(
&self,
ast: &SynFile,
file_path: &Path,
type_resolver: &mut TypeResolver,
) -> Result<Vec<CommandInfo>, Box<dyn std::error::Error>> {
let commands = ast
.items
.iter()
.filter_map(|item| {
if let syn::Item::Fn(func) = item {
if self.is_tauri_command(func) {
return self.extract_command_info(func, file_path, type_resolver);
}
}
None
})
.collect();
Ok(commands)
}
fn is_tauri_command(&self, func: &ItemFn) -> bool {
func.attrs.iter().any(|attr| {
attr.path().segments.len() == 2
&& attr.path().segments[0].ident == "tauri"
&& attr.path().segments[1].ident == "command"
|| attr.path().is_ident("command")
})
}
fn extract_command_info(
&self,
func: &ItemFn,
file_path: &Path,
type_resolver: &mut TypeResolver,
) -> Option<CommandInfo> {
let name = func.sig.ident.to_string();
let parameters = self.extract_parameters(&func.sig.inputs, type_resolver);
let return_type = self.extract_return_type(&func.sig.output, type_resolver);
let is_async = func.sig.asyncness.is_some();
let line_number = func.sig.ident.span().start().line;
Some(CommandInfo {
name,
parameters,
return_type,
file_path: file_path.to_string_lossy().to_string(),
line_number,
is_async,
})
}
fn extract_parameters(
&self,
inputs: &syn::punctuated::Punctuated<FnArg, syn::token::Comma>,
type_resolver: &mut TypeResolver,
) -> Vec<ParameterInfo> {
inputs
.iter()
.filter_map(|input| {
if let FnArg::Typed(PatType { pat, ty, .. }) = input {
if let syn::Pat::Ident(pat_ident) = pat.as_ref() {
let name = pat_ident.ident.to_string();
let rust_type = self.type_to_string(ty);
if self.is_tauri_parameter(&name, &rust_type) {
return None;
}
let typescript_type = type_resolver.map_rust_type_to_typescript(&rust_type);
let is_optional = self.is_optional_type(ty);
return Some(ParameterInfo {
name,
rust_type,
typescript_type,
is_optional,
});
}
}
None
})
.collect()
}
fn is_tauri_parameter(&self, name: &str, rust_type: &str) -> bool {
if matches!(name, "app" | "window" | "state" | "handle") {
return true;
}
if rust_type.contains("AppHandle")
|| rust_type.contains("Window")
|| rust_type.contains("State")
|| rust_type.contains("Manager")
{
return true;
}
false
}
fn extract_return_type(&self, output: &ReturnType, type_resolver: &mut TypeResolver) -> String {
match output {
ReturnType::Default => "void".to_string(),
ReturnType::Type(_, ty) => {
let rust_type = self.type_to_string(ty);
type_resolver.map_rust_type_to_typescript(&rust_type)
}
}
}
fn type_to_string(&self, ty: &Type) -> String {
match ty {
Type::Path(type_path) => {
let segments: Vec<String> = type_path
.path
.segments
.iter()
.map(|segment| {
if segment.arguments.is_empty() {
segment.ident.to_string()
} else {
match &segment.arguments {
syn::PathArguments::AngleBracketed(args) => {
let inner_types: Vec<String> = args
.args
.iter()
.filter_map(|arg| {
if let syn::GenericArgument::Type(inner_ty) = arg {
Some(self.type_to_string(inner_ty))
} else {
None
}
})
.collect();
format!("{}<{}>", segment.ident, inner_types.join(", "))
}
_ => segment.ident.to_string(),
}
}
})
.collect();
segments.join("::")
}
Type::Reference(type_ref) => {
format!("&{}", self.type_to_string(&type_ref.elem))
}
Type::Tuple(type_tuple) => {
if type_tuple.elems.is_empty() {
"()".to_string()
} else {
let types: Vec<String> = type_tuple
.elems
.iter()
.map(|t| self.type_to_string(t))
.collect();
format!("({})", types.join(", "))
}
}
_ => "unknown".to_string(),
}
}
fn is_optional_type(&self, ty: &Type) -> bool {
if let Type::Path(type_path) = ty {
if let Some(segment) = type_path.path.segments.last() {
return segment.ident == "Option";
}
}
false
}
}
impl Default for CommandParser {
fn default() -> Self {
Self::new()
}
}