use syn::{FnArg, ItemFn, Pat, ReturnType, Type};
fn tag_of(ty: &str) -> &'static str {
match ty {
"f32" => "Float:",
"bool" => "bool:",
_ => "",
}
}
fn type_name(ty: &Type) -> Option<String> {
match ty {
Type::Reference(r) => type_name(&r.elem),
Type::Path(p) => p.path.segments.last().map(|s| s.ident.to_string()),
_ => None,
}
}
fn first_generic(ty: &Type) -> Option<String> {
let Type::Path(p) = strip_ref(ty) else {
return None;
};
let last = p.path.segments.last()?;
let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
return None;
};
args.args.iter().find_map(|a| match a {
syn::GenericArgument::Type(t) => type_name(t),
_ => None,
})
}
fn strip_ref(ty: &Type) -> &Type {
match ty {
Type::Reference(r) => strip_ref(&r.elem),
other => other,
}
}
fn arg_decl(ty: &Type, name: &str) -> String {
match type_name(ty).as_deref() {
Some("AmxString") => format!("const {name}[]"),
Some("Ref") => {
let tag = first_generic(ty).map_or("", |g| tag_of(&g));
format!("&{tag}{name}")
}
Some("Buffer" | "UnsizedBuffer") => format!("{name}[]"),
Some(other) => format!("{}{name}", tag_of(other)),
None => name.to_string(),
}
}
fn unwrap_result(ty: &Type) -> Option<String> {
match type_name(ty).as_deref() {
Some("AmxResult" | "Result") => first_generic(ty),
other => other.map(str::to_string),
}
}
#[derive(Default)]
pub struct Shape {
pub defaults: Vec<(String, String)>,
pub sizeofs: Vec<(String, String)>,
pub varargs: bool,
pub arguments: Option<String>,
}
impl Shape {
fn apply(&self, name: &str, mut rendered: String) -> String {
if let Some((_, array)) = self.sizeofs.iter().find(|(arg, _)| arg == name) {
rendered.push_str(&format!(" = sizeof({array})"));
} else if let Some((_, value)) = self.defaults.iter().find(|(arg, _)| arg == name) {
rendered.push_str(&format!(" = {value}"));
}
rendered
}
pub fn unknown_arguments(&self, present: &[String]) -> Vec<String> {
self.defaults
.iter()
.chain(self.sizeofs.iter())
.map(|(arg, _)| arg)
.filter(|arg| !present.contains(arg))
.cloned()
.collect()
}
}
pub fn argument_names(origin_fn: &ItemFn, skip_count: usize) -> Vec<String> {
origin_fn
.sig
.inputs
.iter()
.skip(skip_count)
.filter_map(|arg| match arg {
FnArg::Typed(pat_type) => match &*pat_type.pat {
Pat::Ident(pat_ident) => Some(
pat_ident
.ident
.to_string()
.trim_start_matches('_')
.to_string(),
),
_ => None,
},
FnArg::Receiver(_) => None,
})
.collect()
}
pub fn native_decl(
origin_fn: &ItemFn,
amx_name: &str,
raw: bool,
skip_count: usize,
shape: &Shape,
) -> String {
let ret_tag = match &origin_fn.sig.output {
ReturnType::Default => "",
ReturnType::Type(_, ty) => unwrap_result(ty).map_or("", |t| tag_of(&t)),
};
if let Some(list) = &shape.arguments {
return format!("native {ret_tag}{amx_name}({list});");
}
if raw {
return format!(
"// native {ret_tag}{amx_name}(...); // raw native — fill in the arguments"
);
}
let mut args = Vec::new();
for arg in origin_fn.sig.inputs.iter().skip(skip_count) {
let FnArg::Typed(pat_type) = arg else {
continue;
};
let Pat::Ident(pat_ident) = &*pat_type.pat else {
continue;
};
let name = pat_ident.ident.to_string();
let name = name.trim_start_matches('_').to_string();
args.push(shape.apply(&name, arg_decl(&pat_type.ty, &name)));
}
if shape.varargs {
args.push("{Float,_}:...".to_string());
}
format!("native {ret_tag}{amx_name}({});", args.join(", "))
}
#[cfg(test)]
mod tests {
use super::*;
use syn::parse_quote;
fn decl(f: ItemFn, name: &str) -> String {
native_decl(&f, name, false, 2, &Shape::default())
}
fn shaped(f: ItemFn, name: &str, shape: &Shape) -> String {
native_decl(&f, name, false, 2, shape)
}
#[test]
fn primitives_and_tags() {
let f: ItemFn = parse_quote! {
fn f(&mut self, _amx: &Amx, count: i32, speed: f32, on: bool) -> AmxResult<i32> { }
};
assert_eq!(decl(f, "Foo"), "native Foo(count, Float:speed, bool:on);");
}
#[test]
fn string_is_a_const_array() {
let f: ItemFn = parse_quote! {
fn f(&mut self, _amx: &Amx, text: &AmxString) -> AmxResult<i32> { }
};
assert_eq!(decl(f, "Say"), "native Say(const text[]);");
}
#[test]
fn ref_becomes_by_reference_and_keeps_the_tag() {
let f: ItemFn = parse_quote! {
fn f(&mut self, _amx: &Amx, out: Ref<f32>, n: Ref<i32>) -> AmxResult<bool> { }
};
assert_eq!(decl(f, "Get"), "native bool:Get(&Float:out, &n);");
}
#[test]
fn buffers_are_arrays() {
let f: ItemFn = parse_quote! {
fn f(&mut self, _amx: &Amx, dest: UnsizedBuffer, size: usize) -> AmxResult<i32> { }
};
assert_eq!(decl(f, "Read"), "native Read(dest[], size);");
}
#[test]
fn leading_underscore_is_dropped_from_the_argument_name() {
let f: ItemFn = parse_quote! {
fn f(&mut self, _amx: &Amx, _unused: i32) -> AmxResult<i32> { }
};
assert_eq!(decl(f, "Foo"), "native Foo(unused);");
}
#[test]
fn associated_function_without_self() {
let f: ItemFn = parse_quote! {
fn f(_amx: &Amx, id: i32) -> AmxResult<i32> { }
};
assert_eq!(
native_decl(&f, "Ping", false, 1, &Shape::default()),
"native Ping(id);"
);
}
#[test]
fn a_default_lands_on_the_named_argument() {
let f: ItemFn = parse_quote! {
fn f(&mut self, _amx: &Amx, account: i32, name: &AmxString) -> AmxResult<bool> { }
};
let shape = Shape {
defaults: vec![
("account".into(), "0".into()),
("name".into(), "\"\"".into()),
],
..Shape::default()
};
assert_eq!(
shaped(f, "Status", &shape),
"native bool:Status(account = 0, const name[] = \"\");"
);
}
#[test]
fn a_sizeof_names_the_array_it_measures() {
let f: ItemFn = parse_quote! {
fn f(&mut self, _amx: &Amx, dest: UnsizedBuffer, dest_len: usize) -> AmxResult<bool> { }
};
let shape = Shape {
sizeofs: vec![("dest_len".into(), "dest".into())],
..Shape::default()
};
assert_eq!(
shaped(f, "Read", &shape),
"native bool:Read(dest[], dest_len = sizeof(dest));"
);
}
#[test]
fn a_sizeof_wins_over_a_default_for_the_same_argument() {
let f: ItemFn = parse_quote! {
fn f(&mut self, _amx: &Amx, dest: UnsizedBuffer, len: usize) -> AmxResult<bool> { }
};
let shape = Shape {
defaults: vec![("len".into(), "16".into())],
sizeofs: vec![("len".into(), "dest".into())],
..Shape::default()
};
assert_eq!(
shaped(f, "Read", &shape),
"native bool:Read(dest[], len = sizeof(dest));"
);
}
#[test]
fn varargs_close_the_argument_list() {
let f: ItemFn = parse_quote! {
fn f(&mut self, _amx: &Amx, format: &AmxString) -> AmxResult<bool> { }
};
let shape = Shape {
varargs: true,
..Shape::default()
};
assert_eq!(
shaped(f, "Printf", &shape),
"native bool:Printf(const format[], {Float,_}:...);"
);
}
#[test]
fn an_explicit_list_declares_a_raw_native() {
let f: ItemFn = parse_quote! {
fn f(&mut self, _amx: &Amx, args: Args) -> bool { }
};
let shape = Shape {
arguments: Some("account = 0, const to[], {Float,_}:...".into()),
..Shape::default()
};
assert_eq!(
native_decl(&f, "Send", true, 2, &shape),
"native bool:Send(account = 0, const to[], {Float,_}:...);"
);
}
#[test]
fn an_attribute_naming_an_argument_that_does_not_exist_is_reported() {
let f: ItemFn = parse_quote! {
fn f(&mut self, _amx: &Amx, account: i32) -> AmxResult<bool> { }
};
let shape = Shape {
defaults: vec![("acount".into(), "0".into())],
..Shape::default()
};
assert_eq!(
shape.unknown_arguments(&argument_names(&f, 2)),
vec!["acount".to_string()]
);
}
#[test]
fn plain_return_type_without_result() {
let f: ItemFn = parse_quote! {
fn f(&mut self, _amx: &Amx, a: i32) -> bool { }
};
assert_eq!(decl(f, "Is"), "native bool:Is(a);");
}
#[test]
fn fully_qualified_paths_resolve_by_last_segment() {
let f: ItemFn = parse_quote! {
fn f(&mut self, _amx: &Amx, s: &samp::cell::AmxString, r: samp::cell::Ref<f32>) -> AmxResult<i32> { }
};
assert_eq!(decl(f, "Q"), "native Q(const s[], &Float:r);");
}
#[test]
fn raw_native_comes_back_commented_out() {
let f: ItemFn = parse_quote! {
fn f(&mut self, _amx: &Amx, args: Args) -> AmxResult<i32> { }
};
assert!(
native_decl(&f, "Raw", true, 2, &Shape::default()).starts_with("// native Raw(...);")
);
}
#[test]
fn unknown_type_falls_back_to_a_plain_cell() {
let f: ItemFn = parse_quote! {
fn f(&mut self, _amx: &Amx, thing: MyCustomCell) -> AmxResult<i32> { }
};
assert_eq!(decl(f, "Foo"), "native Foo(thing);");
}
}