use quote::{format_ident, quote};
use syn::{
parse::{Parse, ParseBuffer, ParseStream},
Attribute, Ident, Path, Token,
};
struct CommandDef {
path: Path,
attrs: Vec<Attribute>,
}
impl Parse for CommandDef {
fn parse(input: ParseStream) -> syn::Result<Self> {
let attrs = input.call(Attribute::parse_outer)?;
let path = input.parse()?;
Ok(CommandDef { path, attrs })
}
}
pub struct Handler {
command_defs: Vec<CommandDef>,
commands: Vec<Ident>,
wrappers: Vec<Path>,
}
impl Parse for Handler {
fn parse(input: &ParseBuffer<'_>) -> syn::Result<Self> {
let plugin_name = try_get_plugin_name(input)?;
let mut command_defs = input
.parse_terminated(CommandDef::parse, Token![,])?
.into_iter()
.collect();
filter_unused_commands(plugin_name, &mut command_defs);
let mut commands = Vec::new();
let mut wrappers = Vec::new();
for command_def in &command_defs {
let mut wrapper = command_def.path.clone();
let last = super::path_to_command(&mut wrapper);
let command = last.ident.clone();
last.ident = super::format_command_wrapper(&command);
commands.push(command);
wrappers.push(wrapper);
}
Ok(Self {
command_defs,
commands,
wrappers,
})
}
}
fn try_get_plugin_name(input: &ParseBuffer<'_>) -> Result<Option<String>, syn::Error> {
if let Ok(attrs) = input.call(Attribute::parse_inner) {
for attr in attrs {
if attr.path().is_ident("plugin") {
let plugin_name = attr.parse_args::<Ident>()?.to_string();
return Ok(Some(if plugin_name == "__TAURI_CHANNEL__" {
plugin_name
} else {
plugin_name.replace("_", "-")
}));
}
}
}
Ok(
std::env::var("CARGO_PKG_NAME")
.ok()
.and_then(|var| var.strip_prefix("tauri-plugin-").map(String::from)),
)
}
fn filter_unused_commands(plugin_name: Option<String>, command_defs: &mut Vec<CommandDef>) {
let allowed_commands = tauri_utils::acl::read_allowed_commands();
let Some(allowed_commands) = allowed_commands else {
return;
};
if plugin_name.as_deref() == Some("__TAURI_CHANNEL__") {
return;
}
if plugin_name.is_none() && !allowed_commands.has_app_acl {
return;
}
let mut unused_commands = Vec::new();
let command_prefix = if let Some(plugin_name) = &plugin_name {
format!("plugin:{plugin_name}|")
} else {
"".into()
};
command_defs.retain(|command_def| {
let mut wrapper = command_def.path.clone();
let last = super::path_to_command(&mut wrapper);
let command_name = &last.ident;
let command = format!("{command_prefix}{command_name}");
let is_allowed = allowed_commands.commands.contains(&command);
if !is_allowed {
unused_commands.push(command_name.to_string());
}
is_allowed
});
if !unused_commands.is_empty() {
let plugin_display_name = plugin_name.as_deref().unwrap_or("application");
let unused_commands_display = unused_commands.join(", ");
println!("Removed unused commands from {plugin_display_name}: {unused_commands_display}",);
}
}
impl From<Handler> for proc_macro::TokenStream {
fn from(
Handler {
command_defs,
commands,
wrappers,
}: Handler,
) -> Self {
let cmd = format_ident!("__tauri_cmd__");
let invoke = format_ident!("__tauri_invoke__");
let mut paths: Vec<Path> = Vec::new();
let mut attrs: Vec<Vec<Attribute>> = Vec::new();
let mut command_name_macros: Vec<proc_macro2::TokenStream> = Vec::new();
for (def, command) in command_defs.into_iter().zip(commands) {
let path = def.path;
let attrs_vec = def.attrs;
let mut command_name_macro_path = path.clone();
let last = command_name_macro_path
.segments
.last_mut()
.expect("path has at least one segment");
last.ident = format_ident!("__tauri_command_name_{command}");
paths.push(path);
attrs.push(attrs_vec);
command_name_macros.push(quote!(#command_name_macro_path!()));
}
quote::quote!(move |#invoke| {
let #cmd = #invoke.message.command();
match #cmd {
#(#(#attrs)* #command_name_macros => #wrappers!(#paths, #invoke),)*
_ => {
return false;
},
}
})
.into()
}
}