feat(napi-derive): add ts_arg_type attribute to override individual args on functions (#1192)

This commit is contained in:
Jose L 2022-05-22 01:43:11 -04:00 committed by GitHub
parent 5cbeac59dc
commit 5be415d3d9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 180 additions and 25 deletions

View file

@ -6,7 +6,7 @@ pub struct NapiFn {
pub name: Ident, pub name: Ident,
pub js_name: String, pub js_name: String,
pub attrs: Vec<Attribute>, pub attrs: Vec<Attribute>,
pub args: Vec<NapiFnArgKind>, pub args: Vec<NapiFnArg>,
pub ret: Option<syn::Type>, pub ret: Option<syn::Type>,
pub is_ret_result: bool, pub is_ret_result: bool,
pub is_async: bool, pub is_async: bool,
@ -31,6 +31,23 @@ pub struct CallbackArg {
pub ret: Option<syn::Type>, pub ret: Option<syn::Type>,
} }
#[derive(Debug, Clone)]
pub struct NapiFnArg {
pub kind: NapiFnArgKind,
pub ts_arg_type: Option<String>,
}
impl NapiFnArg {
/// if type was overridden with `#[napi(ts_arg_type = "...")]` use that instead
pub fn use_overridden_type_or(&self, default: impl FnOnce() -> String) -> String {
self
.ts_arg_type
.as_ref()
.map(|ts| ts.clone())
.unwrap_or_else(default)
}
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum NapiFnArgKind { pub enum NapiFnArgKind {
PatType(Box<syn::PatType>), PatType(Box<syn::PatType>),

View file

@ -121,7 +121,7 @@ impl NapiFn {
let i = i - skipped_arg_count; let i = i - skipped_arg_count;
let ident = Ident::new(&format!("arg{}", i), Span::call_site()); let ident = Ident::new(&format!("arg{}", i), Span::call_site());
match arg { match &arg.kind {
NapiFnArgKind::PatType(path) => { NapiFnArgKind::PatType(path) => {
if &path.ty.to_token_stream().to_string() == "Env" { if &path.ty.to_token_stream().to_string() == "Env" {
args.push(quote! { napi::bindgen_prelude::Env::from(env) }); args.push(quote! { napi::bindgen_prelude::Env::from(env) });

View file

@ -119,7 +119,7 @@ impl NapiFn {
self self
.args .args
.iter() .iter()
.filter_map(|arg| match arg { .filter_map(|arg| match &arg.kind {
crate::NapiFnArgKind::PatType(path) => { crate::NapiFnArgKind::PatType(path) => {
let ty_string = path.ty.to_token_stream().to_string(); let ty_string = path.ty.to_token_stream().to_string();
if ty_string == "Env" { if ty_string == "Env" {
@ -142,8 +142,10 @@ impl NapiFn {
if let Pat::Ident(i) = path.pat.as_mut() { if let Pat::Ident(i) = path.pat.as_mut() {
i.mutability = None; i.mutability = None;
} }
let arg = path.pat.to_token_stream().to_string().to_case(Case::Camel);
let (ts_type, is_optional) = ty_to_ts_type(&path.ty, false, false); let (ts_type, is_optional) = ty_to_ts_type(&path.ty, false, false);
let ts_type = arg.use_overridden_type_or(|| ts_type);
let arg = path.pat.to_token_stream().to_string().to_case(Case::Camel);
Some(FnArg { Some(FnArg {
arg, arg,
@ -152,8 +154,8 @@ impl NapiFn {
}) })
} }
crate::NapiFnArgKind::Callback(cb) => { crate::NapiFnArgKind::Callback(cb) => {
let ts_type = arg.use_overridden_type_or(|| gen_callback_type(cb));
let arg = cb.pat.to_token_stream().to_string().to_case(Case::Camel); let arg = cb.pat.to_token_stream().to_string().to_case(Case::Camel);
let ts_type = gen_callback_type(cb);
Some(FnArg { Some(FnArg {
arg, arg,

View file

@ -10,14 +10,14 @@ use attrs::{BindgenAttr, BindgenAttrs};
use convert_case::{Case, Casing}; use convert_case::{Case, Casing};
use napi_derive_backend::{ use napi_derive_backend::{
BindgenResult, CallbackArg, Diagnostic, FnKind, FnSelf, Napi, NapiConst, NapiEnum, BindgenResult, CallbackArg, Diagnostic, FnKind, FnSelf, Napi, NapiConst, NapiEnum,
NapiEnumVariant, NapiFn, NapiFnArgKind, NapiImpl, NapiItem, NapiStruct, NapiStructField, NapiEnumVariant, NapiFn, NapiFnArg, NapiFnArgKind, NapiImpl, NapiItem, NapiStruct,
NapiStructKind, NapiStructField, NapiStructKind,
}; };
use proc_macro2::{Ident, TokenStream, TokenTree}; use proc_macro2::{Ident, Span, TokenStream, TokenTree};
use quote::ToTokens; use quote::ToTokens;
use syn::ext::IdentExt; use syn::ext::IdentExt;
use syn::parse::{Parse, ParseStream, Result as SynResult}; use syn::parse::{Parse, ParseStream, Result as SynResult};
use syn::{Attribute, PathSegment, Signature, Type, Visibility}; use syn::{Attribute, Meta, NestedMeta, PatType, PathSegment, Signature, Type, Visibility};
use crate::parser::attrs::{check_recorded_struct_for_impl, record_struct}; use crate::parser::attrs::{check_recorded_struct_for_impl, record_struct};
@ -155,6 +155,60 @@ pub trait ParseNapi {
fn parse_napi(&mut self, tokens: &mut TokenStream, opts: BindgenAttrs) -> BindgenResult<Napi>; fn parse_napi(&mut self, tokens: &mut TokenStream, opts: BindgenAttrs) -> BindgenResult<Napi>;
} }
/// This function does a few things:
/// - parses the tokens for the given argument `p` to find the `#[napi(ts_arg_type = "MyType")]`
/// attribute and return the manually overridden type.
/// - If both the `ts_args_type` override and the `ts_arg_type` override are present, bail
/// since it should only allow one at a time.
/// - Bails if it finds the `#[napi...]` attribute but it has the wrong data.
/// - Removes the attribute from the output token stream so this
/// `pub fn add(u: u32, #[napi(ts_arg_type = "MyType")] f: String)`
/// turns into
/// `pub fn add(u: u32, f: String)`
/// otherwise it won't compile
fn find_ts_arg_type_and_remove_attribute(
p: &mut PatType,
ts_args_type: Option<&(&str, Span)>,
) -> BindgenResult<Option<String>> {
for (idx, attr) in p.attrs.iter().enumerate() {
if let Ok(Meta::List(meta_list)) = attr.parse_meta() {
if meta_list.path.get_ident() != Some(&format_ident!("napi")) {
// If this attribute is not for `napi` ignore it.
continue;
}
if let Some((ts_args_type, _)) = ts_args_type {
bail_span!(
meta_list,
"Found a 'ts_args_type'=\"{}\" override. Cannot use 'ts_arg_type' at the same time since they are mutually exclusive.",
ts_args_type
);
}
let nested = meta_list.nested.first();
let nm = if let Some(NestedMeta::Meta(Meta::NameValue(nm))) = nested {
nm
} else {
bail_span!(meta_list.nested, "Expected Name Value");
};
if Some(&format_ident!("ts_arg_type")) != nm.path.get_ident() {
bail_span!(nm.path, "Did not find 'ts_arg_type'");
}
if let syn::Lit::Str(lit) = &nm.lit {
p.attrs.remove(idx);
return Ok(Some(lit.value()));
} else {
bail_span!(nm.lit, "Expected a string literal");
}
}
}
Ok(None)
}
fn get_ty(mut ty: &syn::Type) -> &syn::Type { fn get_ty(mut ty: &syn::Type) -> &syn::Type {
while let syn::Type::Group(g) = ty { while let syn::Type::Group(g) = ty {
ty = &g.elem; ty = &g.elem;
@ -462,7 +516,7 @@ fn extract_fn_closure_generics(
} }
fn napi_fn_from_decl( fn napi_fn_from_decl(
sig: Signature, sig: &mut Signature,
opts: &BindgenAttrs, opts: &BindgenAttrs,
attrs: Vec<Attribute>, attrs: Vec<Attribute>,
vis: Visibility, vis: Visibility,
@ -473,36 +527,48 @@ fn napi_fn_from_decl(
let syn::Signature { let syn::Signature {
ident, ident,
asyncness, asyncness,
inputs,
output, output,
generics, generics,
.. ..
} = sig; } = sig.clone();
let mut fn_self = None; let mut fn_self = None;
let callback_traits = extract_fn_closure_generics(&generics)?; let callback_traits = extract_fn_closure_generics(&generics)?;
let args = inputs let args = sig
.into_iter() .inputs
.iter_mut()
.filter_map(|arg| match arg { .filter_map(|arg| match arg {
syn::FnArg::Typed(mut p) => { syn::FnArg::Typed(ref mut p) => {
let ts_arg_type = find_ts_arg_type_and_remove_attribute(p, opts.ts_args_type().as_ref())
.unwrap_or_else(|e| {
errors.push(e);
None
});
let ty_str = p.ty.to_token_stream().to_string(); let ty_str = p.ty.to_token_stream().to_string();
if let Some(path_arguments) = callback_traits.get(&ty_str) { if let Some(path_arguments) = callback_traits.get(&ty_str) {
match extract_callback_trait_types(path_arguments) { match extract_callback_trait_types(path_arguments) {
Ok((fn_args, fn_ret)) => Some(NapiFnArgKind::Callback(Box::new(CallbackArg { Ok((fn_args, fn_ret)) => Some(NapiFnArg {
pat: p.pat, kind: NapiFnArgKind::Callback(Box::new(CallbackArg {
args: fn_args, pat: p.pat.clone(),
ret: fn_ret, args: fn_args,
}))), ret: fn_ret,
})),
ts_arg_type,
}),
Err(e) => { Err(e) => {
errors.push(e); errors.push(e);
None None
} }
} }
} else { } else {
let ty = replace_self(*p.ty, parent); let ty = replace_self(p.ty.as_ref().clone(), parent);
p.ty = Box::new(ty); p.ty = Box::new(ty);
Some(NapiFnArgKind::PatType(Box::new(p))) Some(NapiFnArg {
kind: NapiFnArgKind::PatType(Box::new(p.clone())),
ts_arg_type,
})
} }
} }
syn::FnArg::Receiver(r) => { syn::FnArg::Receiver(r) => {
@ -638,8 +704,10 @@ impl ParseNapi for syn::ItemFn {
"#[napi] can't be applied to a function with #[napi(ts_type)]" "#[napi] can't be applied to a function with #[napi(ts_type)]"
); );
} }
let napi = self.convert_to_ast(opts);
self.to_tokens(tokens); self.to_tokens(tokens);
self.convert_to_ast(opts)
napi
} }
} }
impl ParseNapi for syn::ItemStruct { impl ParseNapi for syn::ItemStruct {
@ -734,7 +802,7 @@ fn fn_kind(opts: &BindgenAttrs) -> FnKind {
impl ConvertToAST for syn::ItemFn { impl ConvertToAST for syn::ItemFn {
fn convert_to_ast(&mut self, opts: BindgenAttrs) -> BindgenResult<Napi> { fn convert_to_ast(&mut self, opts: BindgenAttrs) -> BindgenResult<Napi> {
let func = napi_fn_from_decl( let func = napi_fn_from_decl(
self.sig.clone(), &mut self.sig,
&opts, &opts,
self.attrs.clone(), self.attrs.clone(),
self.vis.clone(), self.vis.clone(),
@ -912,7 +980,7 @@ impl ConvertToAST for syn::ItemImpl {
} }
let func = napi_fn_from_decl( let func = napi_fn_from_decl(
method.sig.clone(), &mut method.sig,
&opts, &opts,
method.attrs.clone(), method.attrs.clone(),
vis, vis,

View file

@ -105,6 +105,8 @@ Generated by [AVA](https://avajs.dev).
export function validateString(s: string): string␊ export function validateString(s: string): string␊
export function validateSymbol(s: symbol): boolean␊ export function validateSymbol(s: symbol): boolean␊
export function tsRename(a: { foo: number }): string[]␊ export function tsRename(a: { foo: number }): string[]␊
export function overrideIndividualArgOnFunction(notOverridden: string, f: () => string, notOverridden2: number): string␊
export function overrideIndividualArgOnFunctionWithCbArg(callback: (town: string, name?: string | undefined | null) => string, notOverridden: number): object␊
export function xxh64Alias(input: Buffer): bigint␊ export function xxh64Alias(input: Buffer): bigint␊
export function getMapping(): Record<string, number> export function getMapping(): Record<string, number>
export function sumMapping(nums: Record<string, number>): number␊ export function sumMapping(nums: Record<string, number>): number␊
@ -208,6 +210,7 @@ Generated by [AVA](https://avajs.dev).
*/␊ */␊
returnOtherClass(): Dog␊ returnOtherClass(): Dog␊
returnOtherClassWithCustomConstructor(): Bird␊ returnOtherClassWithCustomConstructor(): Bird␊
overrideIndividualArgOnMethod(normalTy: string, overriddenTy: {n: string}): Bird␊
}␊ }␊
export class Dog {␊ export class Dog {␊
name: string␊ name: string␊

View file

@ -95,6 +95,8 @@ import {
callbackReturnPromise, callbackReturnPromise,
returnEitherClass, returnEitherClass,
eitherFromOption, eitherFromOption,
overrideIndividualArgOnFunction,
overrideIndividualArgOnFunctionWithCbArg,
} from '../' } from '../'
test('export const', (t) => { test('export const', (t) => {
@ -157,6 +159,10 @@ test('class', (t) => {
t.is(dog.name, '可乐') t.is(dog.name, '可乐')
t.deepEqual(dog.returnOtherClass(), new Dog('Doge')) t.deepEqual(dog.returnOtherClass(), new Dog('Doge'))
t.deepEqual(dog.returnOtherClassWithCustomConstructor(), new Bird('parrot')) t.deepEqual(dog.returnOtherClassWithCustomConstructor(), new Bird('parrot'))
t.is(
dog.overrideIndividualArgOnMethod('Jafar', { n: 'Iago' }).name,
'Jafar-Iago',
)
t.is(dog.returnOtherClassWithCustomConstructor().getCount(), 1234) t.is(dog.returnOtherClassWithCustomConstructor().getCount(), 1234)
t.is(dog.type, Kind.Dog) t.is(dog.type, Kind.Dog)
dog.type = Kind.Cat dog.type = Kind.Cat
@ -321,6 +327,20 @@ test('function ts type override', (t) => {
t.deepEqual(tsRename({ foo: 1, bar: 2, baz: 2 }), ['foo', 'bar', 'baz']) t.deepEqual(tsRename({ foo: 1, bar: 2, baz: 2 }), ['foo', 'bar', 'baz'])
}) })
test('function individual ts arg type override', (t) => {
t.is(
overrideIndividualArgOnFunction('someStr', () => 'anotherStr', 42),
'oia: someStr-42-anotherStr',
)
t.deepEqual(
overrideIndividualArgOnFunctionWithCbArg(
(town, opt) => `im: ${town}-${opt}`,
89,
),
'im: World(89)-null',
)
})
test('option object', (t) => { test('option object', (t) => {
t.notThrows(() => receiveAllOptionalObject()) t.notThrows(() => receiveAllOptionalObject())
t.notThrows(() => receiveAllOptionalObject({})) t.notThrows(() => receiveAllOptionalObject({}))

View file

@ -95,6 +95,8 @@ export function validatePromise(p: Promise<number>): Promise<number>
export function validateString(s: string): string export function validateString(s: string): string
export function validateSymbol(s: symbol): boolean export function validateSymbol(s: symbol): boolean
export function tsRename(a: { foo: number }): string[] export function tsRename(a: { foo: number }): string[]
export function overrideIndividualArgOnFunction(notOverridden: string, f: () => string, notOverridden2: number): string
export function overrideIndividualArgOnFunctionWithCbArg(callback: (town: string, name?: string | undefined | null) => string, notOverridden: number): object
export function xxh64Alias(input: Buffer): bigint export function xxh64Alias(input: Buffer): bigint
export function getMapping(): Record<string, number> export function getMapping(): Record<string, number>
export function sumMapping(nums: Record<string, number>): number export function sumMapping(nums: Record<string, number>): number
@ -198,6 +200,7 @@ export class Animal {
*/ */
returnOtherClass(): Dog returnOtherClass(): Dog
returnOtherClassWithCustomConstructor(): Bird returnOtherClassWithCustomConstructor(): Bird
overrideIndividualArgOnMethod(normalTy: string, overriddenTy: {n: string}): Bird
} }
export class Dog { export class Dog {
name: string name: string

View file

@ -85,6 +85,18 @@ impl Animal {
pub fn return_other_class_with_custom_constructor(&self) -> Bird { pub fn return_other_class_with_custom_constructor(&self) -> Bird {
Bird::new("parrot".to_owned()) Bird::new("parrot".to_owned())
} }
#[napi]
pub fn override_individual_arg_on_method(
&self,
normal_ty: String,
#[napi(ts_arg_type = "{n: string}")] overridden_ty: napi::JsObject,
) -> Bird {
let obj = overridden_ty.coerce_to_object().unwrap();
let the_n: Option<String> = obj.get("n").unwrap();
Bird::new(format!("{}-{}", normal_ty, the_n.unwrap()))
}
} }
#[napi(constructor)] #[napi(constructor)]

View file

@ -1,6 +1,36 @@
use napi::bindgen_prelude::{Object, Result}; use napi::bindgen_prelude::{Object, Result};
use napi::JsFunction;
#[napi(ts_args_type = "a: { foo: number }", ts_return_type = "string[]")] #[napi(ts_args_type = "a: { foo: number }", ts_return_type = "string[]")]
fn ts_rename(a: Object) -> Result<Object> { fn ts_rename(a: Object) -> Result<Object> {
a.get_property_names() a.get_property_names()
} }
#[napi]
fn override_individual_arg_on_function(
not_overridden: String,
#[napi(ts_arg_type = "() => string")] f: JsFunction,
not_overridden2: u32,
) -> String {
let u = f.call_without_args(None).unwrap();
let s = u
.coerce_to_string()
.unwrap()
.into_utf8()
.unwrap()
.as_str()
.unwrap()
.to_string();
format!("oia: {}-{}-{}", not_overridden, not_overridden2, s)
}
#[napi]
fn override_individual_arg_on_function_with_cb_arg<
T: Fn(String, Option<String>) -> Result<Object>,
>(
#[napi(ts_arg_type = "(town: string, name?: string | undefined | null) => string")] callback: T,
not_overridden: u32,
) -> Result<Object> {
callback(format!("World({})", not_overridden), None)
}