Mikaël Francoeur 88fca29e3b
feat: make generator an iterator (#2784)
* make the generator an iterator

* add JSDoc for generators

* prevent naming conflicts

* add JSDoc struct

* update snapshot

* clippy

* Update typedef

* update fmt

* Apply code review

* test compatible

---------

Co-authored-by: LongYinan <lynweklm@gmail.com>
2025-08-07 08:03:37 +00:00

1761 lines
53 KiB
Rust

#[macro_use]
pub mod attrs;
use std::collections::{HashMap, HashSet};
use std::str::Chars;
use std::sync::{
atomic::{AtomicBool, AtomicUsize, Ordering},
LazyLock, Mutex, OnceLock,
};
use attrs::BindgenAttrs;
use convert_case::{Case, Casing};
use napi_derive_backend::{
rm_raw_prefix, BindgenResult, CallbackArg, Diagnostic, FnKind, FnSelf, Napi, NapiArray,
NapiClass, NapiConst, NapiEnum, NapiEnumValue, NapiEnumVariant, NapiFn, NapiFnArg, NapiFnArgKind,
NapiImpl, NapiItem, NapiObject, NapiStruct, NapiStructField, NapiStructKind, NapiStructuredEnum,
NapiStructuredEnumVariant, NapiTransparent, NapiType,
};
use proc_macro2::{Ident, Span, TokenStream};
use quote::ToTokens;
use syn::ext::IdentExt;
use syn::parse::{Parse, ParseStream, Result as SynResult};
use syn::spanned::Spanned;
use syn::{
AngleBracketedGenericArguments, Attribute, ExprLit, GenericArgument, Meta, PatType, Path,
PathArguments, PathSegment, Signature, Type, Visibility,
};
use crate::parser::attrs::{check_recorded_struct_for_impl, record_struct};
static GENERATOR_STRUCT: OnceLock<Mutex<HashMap<String, bool>>> = OnceLock::new();
static REGISTER_INDEX: AtomicUsize = AtomicUsize::new(0);
static HAS_MODULE_EXPORTS: AtomicBool = AtomicBool::new(false);
static KNOWN_JS_VALUE_TYPES_WITH_LIFETIME: LazyLock<HashSet<&str>> = LazyLock::new(|| {
[
"Array",
"Function",
"JsDate",
"JsGlobal",
"JsNumber",
"JsString",
"JsSymbol",
"JsTimeout",
"JSON",
"Object",
"PromiseRaw",
"ReadableStream",
"This",
"Unknown",
"WriteableStream",
]
.into()
});
fn get_register_ident(name: &str) -> Ident {
let new_name = format!(
"__napi_register__{}_{}",
rm_raw_prefix(name),
REGISTER_INDEX.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
);
Ident::new(&new_name, Span::call_site())
}
struct AnyIdent(Ident);
impl Parse for AnyIdent {
fn parse(input: ParseStream) -> SynResult<Self> {
input.step(|cursor| match cursor.ident() {
Some((ident, remaining)) => Ok((AnyIdent(ident), remaining)),
None => Err(cursor.error("expected an identifier")),
})
}
}
pub trait ConvertToAST {
fn convert_to_ast(&mut self, opts: &BindgenAttrs) -> BindgenResult<Napi>;
}
pub trait ParseNapi {
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>> {
let mut ts_type_attr: Option<(usize, String)> = None;
for (idx, attr) in p.attrs.iter().enumerate() {
if attr.path().is_ident("napi") {
if let Some((ts_args_type, _)) = ts_args_type {
bail_span!(
attr,
"Found a 'ts_args_type'=\"{}\" override. Cannot use 'ts_arg_type' at the same time since they are mutually exclusive.",
ts_args_type
);
}
match &attr.meta {
syn::Meta::Path(_) | syn::Meta::NameValue(_) => {
bail_span!(
attr,
"Expects an assignment #[napi(ts_arg_type = \"MyType\")]"
)
}
syn::Meta::List(list) => {
let mut found = false;
list
.parse_args_with(|tokens: &syn::parse::ParseBuffer<'_>| {
// tokens:
// #[napi(xxx, xxx=xxx)]
// ^^^^^^^^^^^^
let list = tokens.parse_terminated(Meta::parse, Token![,])?;
for meta in list {
if meta.path().is_ident("ts_arg_type") {
match meta {
Meta::Path(_) | Meta::List(_) => {
return Err(syn::Error::new(
meta.path().span(),
"Expects an assignment (ts_arg_type = \"MyType\")",
))
}
Meta::NameValue(name_value) => match name_value.value {
syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(str),
..
}) => {
let value = str.value();
found = true;
ts_type_attr = Some((idx, value));
}
_ => {
return Err(syn::Error::new(
name_value.value.span(),
"Expects a string literal",
))
}
},
}
}
}
Ok(())
})
.map_err(Diagnostic::from)?;
if !found {
bail_span!(attr, "Expects a 'ts_arg_type'");
}
}
}
}
}
if let Some((idx, value)) = ts_type_attr {
p.attrs.remove(idx);
Ok(Some(value))
} else {
Ok(None)
}
}
fn find_enum_value_and_remove_attribute(v: &mut syn::Variant) -> BindgenResult<Option<String>> {
let mut name_attr: Option<(usize, String)> = None;
for (idx, attr) in v.attrs.iter().enumerate() {
if attr.path().is_ident("napi") {
match &attr.meta {
syn::Meta::Path(_) | syn::Meta::NameValue(_) => {
bail_span!(
attr,
"Expects an assignment #[napi(value = \"enum-variant-value\")]"
)
}
syn::Meta::List(list) => {
let mut found = false;
list
.parse_args_with(|tokens: &syn::parse::ParseBuffer<'_>| {
// tokens:
// #[napi(xxx, xxx=xxx)]
// ^^^^^^^^^^^^
let list = tokens.parse_terminated(Meta::parse, Token![,])?;
for meta in list {
if meta.path().is_ident("value") {
match meta {
Meta::Path(_) | Meta::List(_) => {
return Err(syn::Error::new(
meta.path().span(),
"Expects an assignment (value = \"enum-variant-value\")",
))
}
Meta::NameValue(name_value) => match name_value.value {
syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(str),
..
}) => {
let value = str.value();
found = true;
name_attr = Some((idx, value));
}
_ => {
return Err(syn::Error::new(
name_value.value.span(),
"Expects a string literal",
))
}
},
}
}
}
Ok(())
})
.map_err(Diagnostic::from)?;
if !found {
bail_span!(attr, "Expects a 'value'");
}
}
}
}
}
if let Some((idx, value)) = name_attr {
v.attrs.remove(idx);
Ok(Some(value))
} else {
Ok(None)
}
}
fn get_ty(mut ty: &mut syn::Type) -> &mut syn::Type {
while let syn::Type::Group(g) = ty {
ty = &mut g.elem;
}
ty
}
fn replace_self(mut ty: syn::Type, self_ty: Option<&Ident>) -> syn::Type {
let self_ty = match self_ty {
Some(i) => i,
None => return ty,
};
let path = match get_ty(&mut ty) {
syn::Type::Path(syn::TypePath { qself: None, path }) => path.clone(),
other => return other.clone(),
};
let new_path = if path.segments.len() == 1 && path.segments[0].ident == "Self" {
self_ty.clone().into()
} else {
path
};
syn::Type::Path(syn::TypePath {
qself: None,
path: new_path,
})
}
/// Extracts the last ident from the path
fn extract_path_ident(path: &mut syn::Path) -> BindgenResult<(Ident, bool)> {
let mut has_lifetime = false;
for segment in path.segments.iter_mut() {
match &segment.arguments {
syn::PathArguments::None => {}
syn::PathArguments::AngleBracketed(generic) => {
if let Some(GenericArgument::Lifetime(_)) = generic.args.first() {
has_lifetime = true;
} else {
bail_span!(path, "Only 1 lifetime is supported for now");
}
}
_ => bail_span!(path, "paths with type parameters are not supported yet"),
}
}
match path.segments.last() {
Some(value) => Ok((value.ident.clone(), has_lifetime)),
None => {
bail_span!(path, "empty idents are not supported");
}
}
}
fn extract_callback_trait_types(
arguments: &syn::PathArguments,
) -> BindgenResult<(Vec<syn::Type>, Option<syn::Type>)> {
match arguments {
// <T: Fn>
syn::PathArguments::None => Ok((vec![], None)),
syn::PathArguments::AngleBracketed(_) => {
bail_span!(arguments, "use parentheses for napi callback trait")
}
syn::PathArguments::Parenthesized(arguments) => {
let args = arguments.inputs.iter().cloned().collect::<Vec<_>>();
let ret = match &arguments.output {
syn::ReturnType::Type(_, ret_ty) => {
let ret_ty = &**ret_ty;
if let Some(ty_of_result) = extract_result_ty(ret_ty)? {
if ty_of_result.to_token_stream().to_string() == "()" {
None
} else {
Some(ty_of_result)
}
} else {
bail_span!(ret_ty, "The return type of callback can only be `Result`");
}
}
_ => {
bail_span!(
arguments,
"The return type of callback can only be `Result`. Try with `Result<()>`"
);
}
};
Ok((args, ret))
}
}
}
fn extract_result_ty(ty: &syn::Type) -> BindgenResult<Option<syn::Type>> {
match ty {
syn::Type::Path(syn::TypePath { qself: None, path }) => {
let segment = path.segments.last().unwrap();
if segment.ident != "Result" {
Ok(None)
} else {
match &segment.arguments {
syn::PathArguments::AngleBracketed(syn::AngleBracketedGenericArguments {
args, ..
}) => {
let ok_arg = args.first().unwrap();
match ok_arg {
syn::GenericArgument::Type(ty) => Ok(Some(ty.clone())),
_ => bail_span!(ok_arg, "unsupported generic type"),
}
}
_ => {
bail_span!(segment, "unsupported generic type")
}
}
}
}
_ => Ok(None),
}
}
fn get_expr(mut expr: &syn::Expr) -> &syn::Expr {
while let syn::Expr::Group(g) = expr {
expr = &g.expr;
}
expr
}
/// Extract the documentation comments from a Vec of attributes
fn extract_doc_comments(attrs: &[syn::Attribute]) -> Vec<String> {
attrs
.iter()
.filter_map(|a| {
// if the path segments include an ident of "doc" we know this
// this is a doc comment
let name_value = a.meta.require_name_value();
if let Ok(name) = name_value {
if a.path().is_ident("doc") {
Some(
// We want to filter out any Puncts so just grab the Literals
match &name.value {
syn::Expr::Lit(ExprLit {
lit: syn::Lit::Str(str),
..
}) => {
let quoted = str.token().to_string();
Some(try_unescape(&quoted).unwrap_or(quoted))
}
_ => None,
},
)
} else {
None
}
} else {
None
}
})
//Fold up the [[String]] iter we created into Vec<String>
.fold(vec![], |mut acc, a| {
acc.extend(a);
acc
})
}
// Unescaped a quoted string. char::escape_debug() was used to escape the text.
fn try_unescape(s: &str) -> Option<String> {
if s.is_empty() {
return Some(String::new());
}
let mut result = String::with_capacity(s.len());
let mut chars = s.chars();
for i in 0.. {
let c = match chars.next() {
Some(c) => c,
None => {
if result.ends_with('"') {
result.pop();
}
return Some(result);
}
};
if i == 0 && c == '"' {
// ignore it
} else if c == '\\' {
let c = chars.next()?;
match c {
't' => result.push('\t'),
'r' => result.push('\r'),
'n' => result.push('\n'),
'\\' | '\'' | '"' => result.push(c),
'u' => {
if chars.next() != Some('{') {
return None;
}
let (c, next) = unescape_unicode(&mut chars)?;
result.push(c);
if next != '}' {
return None;
}
}
_ => return None,
}
} else {
result.push(c);
}
}
None
}
fn unescape_unicode(chars: &mut Chars) -> Option<(char, char)> {
let mut value = 0;
for i in 0..7 {
let c = chars.next()?;
let num = match c {
'0'..='9' => c as u32 - '0' as u32,
'a'..='f' => c as u32 - 'a' as u32,
'A'..='F' => c as u32 - 'A' as u32,
_ => {
if i == 0 {
return None;
}
if i == 0 {
return None;
}
let decoded = char::from_u32(value)?;
return Some((decoded, c));
}
};
if i >= 6 {
return None;
}
value = (value << 4) | num;
}
None
}
fn extract_fn_closure_generics(
generics: &syn::Generics,
) -> BindgenResult<HashMap<String, syn::PathArguments>> {
let mut errors = vec![];
let mut map = HashMap::default();
if generics.params.is_empty() {
return Ok(map);
}
if let Some(where_clause) = &generics.where_clause {
for prediction in where_clause.predicates.iter() {
match prediction {
syn::WherePredicate::Type(syn::PredicateType {
bounded_ty, bounds, ..
}) => {
for bound in bounds {
match bound {
syn::TypeParamBound::Trait(t) => {
for segment in t.path.segments.iter() {
match segment.ident.to_string().as_str() {
"Fn" | "FnOnce" | "FnMut" => {
map.insert(
bounded_ty.to_token_stream().to_string(),
segment.arguments.clone(),
);
}
_ => {}
};
}
}
syn::TypeParamBound::Lifetime(lifetime) => {
if lifetime.ident != "static" {
errors.push(err_span!(
bound,
"only 'static is supported in lifetime bound for fn arguments"
));
}
}
_ => errors.push(err_span! {
bound,
"unsupported bound in napi"
}),
}
}
}
_ => errors.push(err_span! {
prediction,
"unsupported where clause prediction in napi"
}),
};
}
}
for param in generics.params.iter() {
match param {
syn::GenericParam::Type(syn::TypeParam { ident, bounds, .. }) => {
for bound in bounds {
match bound {
syn::TypeParamBound::Trait(t) => {
for segment in t.path.segments.iter() {
match segment.ident.to_string().as_str() {
"Fn" | "FnOnce" | "FnMut" => {
map.insert(ident.to_string(), segment.arguments.clone());
}
_ => {}
};
}
}
syn::TypeParamBound::Lifetime(lifetime) => {
if lifetime.ident != "static" {
errors.push(err_span!(
bound,
"only 'static is supported in lifetime bound for fn arguments"
));
}
}
_ => errors.push(err_span! {
bound,
"unsupported bound in napi"
}),
}
}
}
syn::GenericParam::Lifetime(_) => {}
_ => {
errors.push(err_span!(param, "unsupported napi generic param for fn"));
}
}
}
Diagnostic::from_vec(errors).and(Ok(map))
}
fn napi_fn_from_decl(
sig: &mut Signature,
opts: &BindgenAttrs,
attrs: Vec<Attribute>,
vis: Visibility,
parent: Option<&Ident>,
) -> BindgenResult<NapiFn> {
let mut errors = vec![];
let syn::Signature {
ident,
asyncness,
output,
generics,
..
} = sig.clone();
let mut fn_self = None;
let callback_traits = extract_fn_closure_generics(&generics)?;
let args = sig
.inputs
.iter_mut()
.filter_map(|arg| match arg {
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();
if let Some(path_arguments) = callback_traits.get(&ty_str) {
match extract_callback_trait_types(path_arguments) {
Ok((fn_args, fn_ret)) => Some(NapiFnArg {
kind: NapiFnArgKind::Callback(Box::new(CallbackArg {
pat: p.pat.clone(),
args: fn_args,
ret: fn_ret,
})),
ts_arg_type,
}),
Err(e) => {
errors.push(e);
None
}
}
} else {
let ty = replace_self(p.ty.as_ref().clone(), parent);
p.ty = Box::new(ty);
Some(NapiFnArg {
kind: NapiFnArgKind::PatType(Box::new(p.clone())),
ts_arg_type,
})
}
}
syn::FnArg::Receiver(r) => {
if parent.is_some() {
assert!(fn_self.is_none());
if r.reference.is_none() {
errors.push(err_span!(
r,
"The native methods can't move values from napi. Try `&self` or `&mut self` instead."
));
} else if r.mutability.is_some() {
fn_self = Some(FnSelf::MutRef);
} else {
fn_self = Some(FnSelf::Ref);
}
} else {
errors.push(err_span!(r, "arguments cannot be `self`"));
}
None
}
})
.collect::<Vec<_>>();
let (ret, is_ret_result) = match output {
syn::ReturnType::Default => (None, false),
syn::ReturnType::Type(_, ty) => {
let result_ty = extract_result_ty(&ty)?;
if let Some(result_ty) = result_ty {
(Some(replace_self(result_ty, parent)), true)
} else {
(Some(replace_self(*ty, parent)), false)
}
}
};
Diagnostic::from_vec(errors).and_then(|_| {
let js_name = if let Some(prop_name) = opts.getter() {
opts.js_name().map_or_else(
|| {
if let Some(ident) = prop_name {
ident.to_string()
} else {
ident
.to_string()
.trim_start_matches("get_")
.to_case(Case::Camel)
}
},
|(js_name, _)| js_name.to_owned(),
)
} else if let Some(prop_name) = opts.setter() {
opts.js_name().map_or_else(
|| {
if let Some(ident) = prop_name {
ident.to_string()
} else {
ident
.to_string()
.trim_start_matches("set_")
.to_case(Case::Camel)
}
},
|(js_name, _)| js_name.to_owned(),
)
} else if opts.constructor().is_some() {
"constructor".to_owned()
} else if opts.module_exports().is_some() {
if HAS_MODULE_EXPORTS.load(Ordering::Relaxed) {
bail_span!(sig.ident, "module_exports can only be used once");
}
HAS_MODULE_EXPORTS.store(true, Ordering::Relaxed);
if opts.js_name().is_some() {
bail_span!(sig.ident, "module_exports fn can't have js_name");
}
if opts.getter().is_some() || opts.setter().is_some() {
bail_span!(sig.ident, "module_exports fn can't have getter or setter");
}
if opts.factory().is_some() || opts.constructor().is_some() {
bail_span!(
sig.ident,
"module_exports fn can't have factory or constructor"
);
}
if opts.strict().is_some() {
bail_span!(sig.ident, "module_exports fn can't have strict");
}
if opts.return_if_invalid().is_some() {
bail_span!(sig.ident, "module_exports fn can't have return_if_invalid");
}
if parent.is_some() {
bail_span!(sig.ident, "module_exports fn can't inside impl block");
}
if !generics.params.is_empty() {
bail_span!(sig.ident, "module_exports fn can't have generic parameters");
}
if opts.no_export().is_some() {
bail_span!(
sig.ident,
"#[napi(no_export)] can not be used with module_exports attribute"
);
}
for arg in args.iter() {
match &arg.kind {
NapiFnArgKind::Callback(_) => {
bail_span!(sig.ident, "module_exports fn can't have callback arguments");
}
NapiFnArgKind::PatType(pat) => {
if arg.ts_arg_type.is_some() {
bail_span!(sig.ident, "module_exports fn can't have ts_arg_type");
}
if let syn::Type::Path(syn::TypePath {
path: syn::Path { segments, .. },
..
}) = &*pat.ty
{
if let Some(segment) = segments.last() {
if segment.ident != "Env" && segment.ident != "Object" {
bail_span!(
sig.ident,
"module_exports fn can only accept Env or Object as argument"
);
}
continue;
}
}
if let syn::Type::Reference(syn::TypeReference { elem, .. }) = &*pat.ty {
if let syn::Type::Path(syn::TypePath {
path: syn::Path { segments, .. },
..
}) = &**elem
{
if let Some(segment) = segments.last() {
if segment.ident != "Env" && segment.ident != "Object" {
bail_span!(
sig.ident,
"module_exports fn can only accept Env or Object as argument"
);
}
continue;
}
}
}
}
}
bail_span!(
sig.ident,
"module_exports fn can only accept Env or Object as argument"
);
}
if let syn::ReturnType::Type(_, ty) = &sig.output {
if let syn::Type::Path(syn::TypePath {
path: syn::Path { segments, .. },
..
}) = &**ty
{
if let Some(segment) = segments.last() {
if segment.ident != "Result" && segment.ident != "()" {
bail_span!(
sig.ident,
"module_exports fn can only return Result<()> or (), got {}",
segment.ident
);
}
if segment.ident == "Result" {
if let syn::PathArguments::AngleBracketed(syn::AngleBracketedGenericArguments {
args,
..
}) = &segment.arguments
{
if args.len() != 1 {
bail_span!(
segment.ident,
"module_exports fn can only return Result<()> or ()"
);
}
if let syn::GenericArgument::Type(syn::Type::Tuple(syn::TypeTuple {
elems, ..
})) = &args[0]
{
if !elems.empty_or_trailing() {
bail_span!(
segment.ident,
"module_exports fn can only return Result<()> or ()"
);
}
} else {
bail_span!(
segment.ident,
"module_exports fn can only return Result<()> or ()"
);
}
} else {
bail_span!(
segment.ident,
"module_exports fn can only return Result<()> or ()"
);
}
}
}
}
}
ident.to_string().to_case(Case::Camel)
} else {
opts.js_name().map_or_else(
|| ident.to_string().to_case(Case::Camel),
|(js_name, _)| js_name.to_owned(),
)
};
let namespace = opts.namespace().map(|(m, _)| m.to_owned());
let parent_is_generator = if let Some(p) = parent {
let generator_struct = GENERATOR_STRUCT.get_or_init(|| Mutex::new(HashMap::new()));
let generator_struct = generator_struct
.lock()
.expect("Lock generator struct failed");
let key = namespace
.as_ref()
.map(|n| format!("{n}::{p}"))
.unwrap_or_else(|| p.to_string());
*generator_struct.get(&key).unwrap_or(&false)
} else {
false
};
let kind = fn_kind(opts);
if !matches!(kind, FnKind::Normal) && parent.is_none() {
bail_span!(
sig.ident,
"Only fn in impl block can be marked as factory, constructor, getter or setter"
);
}
if matches!(kind, FnKind::Constructor) && asyncness.is_some() {
bail_span!(sig.ident, "Constructor don't support asynchronous function");
}
Ok(NapiFn {
name: ident.clone(),
js_name,
module_exports: opts.module_exports().is_some(),
args,
ret,
is_ret_result,
is_async: asyncness.is_some(),
within_async_runtime: opts.async_runtime().is_some(),
vis,
kind,
fn_self,
parent: parent.cloned(),
comments: extract_doc_comments(&attrs),
attrs,
strict: opts.strict().is_some(),
return_if_invalid: opts.return_if_invalid().is_some(),
js_mod: opts.namespace().map(|(m, _)| m.to_owned()),
ts_type: opts.ts_type().map(|(m, _)| m.to_owned()),
ts_generic_types: opts.ts_generic_types().map(|(m, _)| m.to_owned()),
ts_args_type: opts.ts_args_type().map(|(m, _)| m.to_owned()),
ts_return_type: opts.ts_return_type().map(|(m, _)| m.to_owned()),
skip_typescript: opts.skip_typescript().is_some(),
parent_is_generator,
writable: opts.writable(),
enumerable: opts.enumerable(),
configurable: opts.configurable(),
catch_unwind: opts.catch_unwind().is_some(),
unsafe_: sig.unsafety.is_some(),
register_name: get_register_ident(ident.to_string().as_str()),
no_export: opts.no_export().is_some(),
})
})
}
impl ParseNapi for syn::Item {
fn parse_napi(&mut self, tokens: &mut TokenStream, opts: &BindgenAttrs) -> BindgenResult<Napi> {
match self {
syn::Item::Fn(f) => f.parse_napi(tokens, opts),
syn::Item::Struct(s) => s.parse_napi(tokens, opts),
syn::Item::Impl(i) => i.parse_napi(tokens, opts),
syn::Item::Enum(e) => e.parse_napi(tokens, opts),
syn::Item::Const(c) => c.parse_napi(tokens, opts),
syn::Item::Type(c) => c.parse_napi(tokens, opts),
_ => bail_span!(
self,
"#[napi] can only be applied to a function, struct, enum, const, mod or impl."
),
}
}
}
impl ParseNapi for syn::ItemFn {
fn parse_napi(&mut self, tokens: &mut TokenStream, opts: &BindgenAttrs) -> BindgenResult<Napi> {
if opts.ts_type().is_some()
&& (opts.ts_args_type().is_some() || opts.ts_return_type().is_some())
{
bail_span!(
self,
"#[napi] with ts_type cannot be combined with ts_args_type, ts_return_type in function"
);
}
if opts.return_if_invalid().is_some() && opts.strict().is_some() {
bail_span!(
self,
"#[napi(return_if_invalid)] can't be used with #[napi(strict)]"
);
}
let napi = self.convert_to_ast(opts);
self.to_tokens(tokens);
napi
}
}
impl ParseNapi for syn::ItemStruct {
fn parse_napi(&mut self, tokens: &mut TokenStream, opts: &BindgenAttrs) -> BindgenResult<Napi> {
if opts.ts_args_type().is_some()
|| opts.ts_return_type().is_some()
|| opts.skip_typescript().is_some()
|| opts.ts_type().is_some()
{
bail_span!(
self,
"#[napi] can't be applied to a struct with #[napi(ts_args_type)], #[napi(ts_return_type)], #[napi(skip_typescript)] or #[napi(ts_type)]"
);
}
if opts.return_if_invalid().is_some() {
bail_span!(
self,
"#[napi(return_if_invalid)] can only be applied to a function or method."
);
}
if opts.catch_unwind().is_some() {
bail_span!(
self,
"#[napi(catch_unwind)] can only be applied to a function or method."
);
}
if opts.no_export().is_some() {
bail_span!(
self,
"#[napi(no_export)] can only be applied to a function."
);
}
if opts.object().is_some() && opts.custom_finalize().is_some() {
bail_span!(self, "Custom finalize is not supported for #[napi(object)]");
}
let napi = self.convert_to_ast(opts);
self.to_tokens(tokens);
napi
}
}
impl ParseNapi for syn::ItemImpl {
fn parse_napi(&mut self, tokens: &mut TokenStream, opts: &BindgenAttrs) -> BindgenResult<Napi> {
if opts.ts_args_type().is_some()
|| opts.ts_return_type().is_some()
|| opts.skip_typescript().is_some()
|| opts.ts_type().is_some()
|| opts.custom_finalize().is_some()
{
bail_span!(
self,
"#[napi] can't be applied to impl with #[napi(ts_args_type)], #[napi(ts_return_type)], #[napi(skip_typescript)] or #[napi(ts_type)] or #[napi(custom_finalize)]"
);
}
if opts.return_if_invalid().is_some() {
bail_span!(
self,
"#[napi(return_if_invalid)] can only be applied to a function or method."
);
}
if opts.catch_unwind().is_some() {
bail_span!(
self,
"#[napi(catch_unwind)] can only be applied to a function or method."
);
}
if opts.no_export().is_some() {
bail_span!(
self,
"#[napi(no_export)] can only be applied to a function."
);
}
// #[napi] macro will be remove from impl items after converted to ast
let napi = self.convert_to_ast(opts);
self.to_tokens(tokens);
napi
}
}
impl ParseNapi for syn::ItemEnum {
fn parse_napi(&mut self, tokens: &mut TokenStream, opts: &BindgenAttrs) -> BindgenResult<Napi> {
if opts.ts_args_type().is_some()
|| opts.ts_return_type().is_some()
|| opts.ts_type().is_some()
|| opts.custom_finalize().is_some()
{
bail_span!(
self,
"#[napi] can't be applied to a enum with #[napi(ts_args_type)], #[napi(ts_return_type)] or #[napi(ts_type)] or #[napi(custom_finalize)]"
);
}
if opts.return_if_invalid().is_some() {
bail_span!(
self,
"#[napi(return_if_invalid)] can only be applied to a function or method."
);
}
if opts.catch_unwind().is_some() {
bail_span!(
self,
"#[napi(catch_unwind)] can only be applied to a function or method."
);
}
if opts.no_export().is_some() {
bail_span!(
self,
"#[napi(no_export)] can only be applied to a function."
);
}
let napi = self.convert_to_ast(opts);
self.to_tokens(tokens);
napi
}
}
impl ParseNapi for syn::ItemConst {
fn parse_napi(&mut self, tokens: &mut TokenStream, opts: &BindgenAttrs) -> BindgenResult<Napi> {
if opts.ts_args_type().is_some()
|| opts.ts_return_type().is_some()
|| opts.ts_type().is_some()
|| opts.custom_finalize().is_some()
{
bail_span!(
self,
"#[napi] can't be applied to a const with #[napi(ts_args_type)], #[napi(ts_return_type)] or #[napi(ts_type)] or #[napi(custom_finalize)]"
);
}
if opts.return_if_invalid().is_some() {
bail_span!(
self,
"#[napi(return_if_invalid)] can only be applied to a function or method."
);
}
if opts.catch_unwind().is_some() {
bail_span!(
self,
"#[napi(catch_unwind)] can only be applied to a function or method."
);
}
if opts.no_export().is_some() {
bail_span!(
self,
"#[napi(no_export)] can only be applied to a function."
);
}
let napi = self.convert_to_ast(opts);
self.to_tokens(tokens);
napi
}
}
impl ParseNapi for syn::ItemType {
fn parse_napi(&mut self, tokens: &mut TokenStream, opts: &BindgenAttrs) -> BindgenResult<Napi> {
if opts.ts_args_type().is_some()
|| opts.ts_return_type().is_some()
|| opts.custom_finalize().is_some()
{
bail_span!(
self,
"#[napi] can't be applied to a type with #[napi(ts_args_type)], #[napi(ts_return_type)] or #[napi(custom_finalize)]"
);
}
if opts.return_if_invalid().is_some() {
bail_span!(
self,
"#[napi(return_if_invalid)] can only be applied to a function or method."
);
}
if opts.catch_unwind().is_some() {
bail_span!(
self,
"#[napi(catch_unwind)] can only be applied to a function or method."
);
}
if opts.no_export().is_some() {
bail_span!(
self,
"#[napi(no_export)] can only be applied to a function."
);
}
let napi = self.convert_to_ast(opts);
self.to_tokens(tokens);
napi
}
}
fn fn_kind(opts: &BindgenAttrs) -> FnKind {
let mut kind = FnKind::Normal;
if opts.getter().is_some() {
kind = FnKind::Getter;
}
if opts.setter().is_some() {
kind = FnKind::Setter;
}
if opts.constructor().is_some() {
kind = FnKind::Constructor;
}
if opts.factory().is_some() {
kind = FnKind::Factory;
}
kind
}
impl ConvertToAST for syn::ItemFn {
fn convert_to_ast(&mut self, opts: &BindgenAttrs) -> BindgenResult<Napi> {
let func = napi_fn_from_decl(
&mut self.sig,
opts,
self.attrs.clone(),
self.vis.clone(),
None,
)?;
Ok(Napi {
item: NapiItem::Fn(func),
})
}
}
fn convert_fields(
fields: &mut syn::Fields,
check_vis: bool,
) -> BindgenResult<(Vec<NapiStructField>, bool)> {
let mut napi_fields = vec![];
let is_tuple = matches!(fields, syn::Fields::Unnamed(_));
for (i, field) in fields.iter_mut().enumerate() {
if check_vis && !matches!(field.vis, syn::Visibility::Public(_)) {
continue;
}
let field_opts = BindgenAttrs::find(&mut field.attrs)?;
let (js_name, name) = match &field.ident {
Some(ident) => (
field_opts.js_name().map_or_else(
|| ident.unraw().to_string().to_case(Case::Camel),
|(js_name, _)| js_name.to_owned(),
),
syn::Member::Named(ident.clone()),
),
None => (
field_opts
.js_name()
.map_or_else(|| format!("field{i}"), |(js_name, _)| js_name.to_owned()),
syn::Member::Unnamed(i.into()),
),
};
let ignored = field_opts.skip().is_some();
let readonly = field_opts.readonly().is_some();
let writable = field_opts.writable();
let enumerable = field_opts.enumerable();
let configurable = field_opts.configurable();
let skip_typescript = field_opts.skip_typescript().is_some();
let ts_type = field_opts.ts_type().map(|e| e.0.to_string());
let mut ty = field.ty.clone();
let has_lifetime = if let Type::Path(syn::TypePath {
path: Path { segments, .. },
..
}) = &mut ty
{
if let Some(PathSegment {
arguments: PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }),
..
}) = segments.last_mut()
{
args.iter_mut().any(|arg| {
if let GenericArgument::Lifetime(lifetime) = arg {
*lifetime = syn::Lifetime::new("'static", Span::call_site());
true
} else {
false
}
})
} else {
false
}
} else {
false
};
napi_fields.push(NapiStructField {
name,
js_name,
ty,
getter: !ignored,
setter: !(ignored || readonly),
writable,
enumerable,
configurable,
comments: extract_doc_comments(&field.attrs),
skip_typescript,
ts_type,
has_lifetime,
})
}
Ok((napi_fields, is_tuple))
}
impl ConvertToAST for syn::ItemStruct {
fn convert_to_ast(&mut self, opts: &BindgenAttrs) -> BindgenResult<Napi> {
let mut errors = vec![];
let rust_struct_ident: Ident = self.ident.clone();
let final_js_name_for_struct = opts.js_name().map_or_else(
|| self.ident.to_string().to_case(Case::Pascal),
|(attr_js_name, _span)| attr_js_name.to_owned(),
);
let use_nullable = opts.use_nullable();
let (fields, is_tuple) = convert_fields(&mut self.fields, true)?;
record_struct(&rust_struct_ident, final_js_name_for_struct.clone(), opts);
let namespace = opts.namespace().map(|(m, _)| m.to_owned());
let implement_iterator = opts.iterator().is_some();
if implement_iterator
&& self
.fields
.iter()
.filter(|f| matches!(f.vis, Visibility::Public(_)))
.filter_map(|f| f.ident.clone())
.map(|ident| ident.to_string())
.any(|field_name| field_name == "next" || field_name == "throw" || field_name == "return")
{
bail_span!(
self,
"Generator structs cannot have public fields named `next`, `throw`, or `return`."
);
}
let generator_struct = GENERATOR_STRUCT.get_or_init(|| Mutex::new(HashMap::new()));
let mut generator_struct = generator_struct
.lock()
.expect("Lock generator struct failed");
let key = namespace
.as_ref()
.map(|n| format!("{n}::{rust_struct_ident}"))
.unwrap_or_else(|| rust_struct_ident.to_string());
generator_struct.insert(key, implement_iterator);
drop(generator_struct);
let transparent = opts
.transparent()
.is_some()
.then(|| -> Result<_, Diagnostic> {
if !is_tuple || self.fields.len() != 1 {
bail_span!(
self,
"#[napi(transparent)] can only be applied to a struct with a single field tuple",
)
}
let first_field = self.fields.iter().next().unwrap();
Ok(first_field.ty.clone())
})
.transpose()?;
let struct_kind = if let Some(transparent) = transparent {
NapiStructKind::Transparent(NapiTransparent {
ty: transparent,
object_from_js: opts.object_from_js(),
object_to_js: opts.object_to_js(),
})
} else if opts.array().is_some() {
if !is_tuple {
bail_span!(self, "#[napi(array)] can only be applied to a tuple struct",)
}
NapiStructKind::Array(NapiArray {
fields,
object_from_js: opts.object_from_js(),
object_to_js: opts.object_to_js(),
})
} else if opts.object().is_some() {
NapiStructKind::Object(NapiObject {
fields,
object_from_js: opts.object_from_js(),
object_to_js: opts.object_to_js(),
is_tuple,
})
} else {
// field lifetime check, JsValue types with lifetime can't be assigned to a field of napi class struct
for syn::Field { ty, .. } in self.fields.iter() {
if let syn::Type::Path(syn::TypePath { path, .. }) = ty {
if let Some(PathSegment {
ident,
arguments:
syn::PathArguments::AngleBracketed(syn::AngleBracketedGenericArguments { args, .. }),
..
}) = path.segments.last()
{
if let Some(GenericArgument::Lifetime(syn::Lifetime { ident: _, .. })) = args.first() {
// has lifetime and type name matched with known js value types
if KNOWN_JS_VALUE_TYPES_WITH_LIFETIME.contains(ident.to_string().as_str()) {
// TODO: add link for more information
errors.push(err_span!(
ty,
"Can't assign {} to a field of napi class struct",
ident
));
}
}
}
}
}
NapiStructKind::Class(NapiClass {
fields,
ctor: opts.constructor().is_some(),
implement_iterator,
is_tuple,
use_custom_finalize: opts.custom_finalize().is_some(),
})
};
match &struct_kind {
NapiStructKind::Transparent(_) => {}
NapiStructKind::Class(class) if !class.ctor => {}
_ => {
for field in self.fields.iter() {
if !matches!(field.vis, syn::Visibility::Public(_)) {
errors.push(err_span!(
field,
"#[napi] requires all struct fields to be public to mark struct as constructor or object shape\nthis field is not public."
));
}
}
}
};
if self.generics.lifetimes().size_hint().0 > 1 {
errors.push(err_span!(
self,
"struct with multiple generic parameters is not supported"
));
}
let lifetime = if let Some(lifetime) = self.generics.lifetimes().next() {
if !lifetime.bounds.is_empty() {
bail_span!(lifetime.bounds, "unsupported self type in #[napi] impl")
}
Some(lifetime.lifetime.to_string())
} else {
None
};
Diagnostic::from_vec(errors).map(|()| Napi {
item: NapiItem::Struct(NapiStruct {
js_name: final_js_name_for_struct,
name: rust_struct_ident.clone(),
kind: struct_kind,
js_mod: namespace,
use_nullable,
register_name: get_register_ident(format!("{rust_struct_ident}_struct").as_str()),
comments: extract_doc_comments(&self.attrs),
has_lifetime: lifetime.is_some(),
is_generator: implement_iterator,
}),
})
}
}
impl ConvertToAST for syn::ItemImpl {
fn convert_to_ast(&mut self, impl_opts: &BindgenAttrs) -> BindgenResult<Napi> {
let struct_name = match get_ty(&mut self.self_ty) {
syn::Type::Path(syn::TypePath {
ref mut path,
qself: None,
}) => path,
_ => {
bail_span!(self.self_ty, "unsupported self type in #[napi] impl")
}
};
let (struct_name, has_lifetime) = extract_path_ident(struct_name)?;
// Check if this struct was recorded with a custom js_name, fallback to default if not found
let mut struct_js_name =
match check_recorded_struct_for_impl(&struct_name, &BindgenAttrs::default()) {
Ok(recorded_js_name) => recorded_js_name,
Err(_) => struct_name.to_string().to_case(Case::UpperCamel),
};
let mut items = vec![];
let mut task_output_type = None;
let mut iterator_yield_type = None;
let mut iterator_next_type = None;
let mut iterator_return_type = None;
for item in self.items.iter_mut() {
if let Some(method) = match item {
syn::ImplItem::Fn(m) => Some(m),
syn::ImplItem::Type(m) => {
if let Some((_, t, _)) = &self.trait_ {
if let Some(PathSegment { ident, .. }) = t.segments.last() {
if (ident == "Task" || ident == "ScopedTask") && m.ident == "JsValue" {
task_output_type = Some(m.ty.clone());
} else if ident == "Generator" {
if let Type::Path(_) = &m.ty {
if m.ident == "Yield" {
iterator_yield_type = Some(m.ty.clone());
} else if m.ident == "Next" {
iterator_next_type = Some(m.ty.clone());
} else if m.ident == "Return" {
iterator_return_type = Some(m.ty.clone());
}
}
}
}
}
None
}
_ => {
bail_span!(item, "unsupported impl item in #[napi]")
}
} {
let opts = BindgenAttrs::find(&mut method.attrs)?;
// it'd better only care methods decorated with `#[napi]` attribute
if !opts.exists {
continue;
}
if opts.constructor().is_some() || opts.factory().is_some() {
struct_js_name = check_recorded_struct_for_impl(&struct_name, &opts)?;
}
let vis = method.vis.clone();
match &vis {
Visibility::Public(_) => {}
_ => {
bail_span!(method.sig.ident, "only pub method supported by #[napi].",);
}
}
let func = napi_fn_from_decl(
&mut method.sig,
&opts,
method.attrs.clone(),
vis,
Some(&struct_name),
)?;
items.push(func);
}
}
let namespace = impl_opts.namespace().map(|(m, _)| m.to_owned());
Ok(Napi {
item: NapiItem::Impl(NapiImpl {
name: struct_name.clone(),
js_name: struct_js_name,
items,
task_output_type,
iterator_yield_type,
iterator_next_type,
iterator_return_type,
has_lifetime,
js_mod: namespace,
comments: extract_doc_comments(&self.attrs),
register_name: get_register_ident(format!("{struct_name}_impl").as_str()),
}),
})
}
}
impl ConvertToAST for syn::ItemEnum {
fn convert_to_ast(&mut self, opts: &BindgenAttrs) -> BindgenResult<Napi> {
match self.vis {
Visibility::Public(_) => {}
_ => bail_span!(self, "only public enum allowed"),
}
let js_name = opts
.js_name()
.map_or_else(|| self.ident.to_string(), |(s, _)| s.to_string());
let is_string_enum = opts.string_enum().is_some();
if self
.variants
.iter()
.any(|v| !matches!(v.fields, syn::Fields::Unit))
{
let discriminant = opts.discriminant().map_or("type", |(s, _)| s);
let mut errors = vec![];
let mut variants = vec![];
for variant in self.variants.iter_mut() {
let (fields, is_tuple) = convert_fields(&mut variant.fields, false)?;
for field in fields.iter() {
if field.js_name == discriminant {
errors.push(err_span!(
field.name,
r#"field's js_name("{}") and discriminator("{}") conflict"#,
field.js_name,
discriminant,
));
}
}
variants.push(NapiStructuredEnumVariant {
name: variant.ident.clone(),
fields,
is_tuple,
});
}
let rust_struct_ident = self.ident.clone();
return Diagnostic::from_vec(errors).map(|()| Napi {
item: NapiItem::Struct(NapiStruct {
name: rust_struct_ident.clone(),
js_name,
comments: extract_doc_comments(&self.attrs),
js_mod: opts.namespace().map(|(m, _)| m.to_owned()),
use_nullable: opts.use_nullable(),
register_name: get_register_ident(format!("{rust_struct_ident}_struct").as_str()),
kind: NapiStructKind::StructuredEnum(NapiStructuredEnum {
variants,
discriminant: discriminant.to_owned(),
object_from_js: opts.object_from_js(),
object_to_js: opts.object_to_js(),
}),
has_lifetime: false,
is_generator: false,
}),
});
}
let variants = match opts.string_enum() {
Some(case) => {
let case = case.map(|c| Ok::<Case, Diagnostic>(match c.0.as_str() {
"lowercase" => Case::Flat,
"UPPERCASE" => Case::UpperFlat,
"PascalCase" => Case::Pascal,
"camelCase" => Case::Camel,
"snake_case" => Case::Snake,
"SCREAMING_SNAKE_CASE" => Case::UpperSnake,
"kebab-case" => Case::Kebab,
"SCREAMING-KEBAB-CASE" => Case::UpperKebab,
_ => {
bail_span!(self, "Unknown string enum case. Possible values are \"lowercase\", \"UPPERCASE\", \"PascalCase\", \"camelCase\", \"snake_case\", \"SCREAMING_SNAKE_CASE\", \"kebab-case\", or \"SCREAMING-KEBAB-CASE\"")
}
})).transpose()?;
self
.variants
.iter_mut()
.map(|v| {
if !matches!(v.fields, syn::Fields::Unit) {
bail_span!(
v.fields,
"Structured enum is not supported with string enum in #[napi]"
)
}
if matches!(&v.discriminant, Some((_, _))) {
bail_span!(
v.fields,
"Literal values are not supported with string enum in #[napi]"
)
}
let val = find_enum_value_and_remove_attribute(v)?.unwrap_or_else(|| {
let mut val = v.ident.to_string();
if let Some(case) = case {
val = val.to_case(case)
}
val
});
Ok(NapiEnumVariant {
name: v.ident.clone(),
val: NapiEnumValue::String(val),
comments: extract_doc_comments(&v.attrs),
})
})
.collect::<BindgenResult<Vec<NapiEnumVariant>>>()?
}
None => {
let mut last_variant_val: i32 = -1;
self
.variants
.iter()
.map(|v| {
let val = match &v.discriminant {
Some((_, expr)) => {
let mut symbol = 1;
let mut inner_expr = get_expr(expr);
if let syn::Expr::Unary(syn::ExprUnary {
attrs: _,
op: syn::UnOp::Neg(_),
expr,
}) = inner_expr
{
symbol = -1;
inner_expr = expr;
}
match inner_expr {
syn::Expr::Lit(syn::ExprLit {
attrs: _,
lit: syn::Lit::Int(int_lit),
}) => match int_lit.base10_digits().parse::<i32>() {
Ok(v) => symbol * v,
Err(_) => {
bail_span!(
int_lit,
"enums with #[wasm_bindgen] can only support \
numbers that can be represented as i32",
);
}
},
_ => bail_span!(
expr,
"enums with #[wasm_bindgen] may only have \
number literal values",
),
}
}
None => last_variant_val + 1,
};
last_variant_val = val;
Ok(NapiEnumVariant {
name: v.ident.clone(),
val: NapiEnumValue::Number(val),
comments: extract_doc_comments(&v.attrs),
})
})
.collect::<BindgenResult<Vec<NapiEnumVariant>>>()?
}
};
Ok(Napi {
item: NapiItem::Enum(NapiEnum {
name: self.ident.clone(),
js_name,
variants,
js_mod: opts.namespace().map(|(m, _)| m.to_owned()),
comments: extract_doc_comments(&self.attrs),
skip_typescript: opts.skip_typescript().is_some(),
register_name: get_register_ident(self.ident.to_string().as_str()),
is_string_enum,
object_from_js: opts.object_from_js(),
object_to_js: opts.object_to_js(),
}),
})
}
}
impl ConvertToAST for syn::ItemConst {
fn convert_to_ast(&mut self, opts: &BindgenAttrs) -> BindgenResult<Napi> {
match self.vis {
Visibility::Public(_) => Ok(Napi {
item: NapiItem::Const(NapiConst {
name: self.ident.clone(),
js_name: opts
.js_name()
.map_or_else(|| self.ident.to_string(), |(s, _)| s.to_string()),
type_name: *self.ty.clone(),
value: *self.expr.clone(),
js_mod: opts.namespace().map(|(m, _)| m.to_owned()),
comments: extract_doc_comments(&self.attrs),
skip_typescript: opts.skip_typescript().is_some(),
register_name: get_register_ident(self.ident.to_string().as_str()),
}),
}),
_ => bail_span!(self, "only public const allowed"),
}
}
}
impl ConvertToAST for syn::ItemType {
fn convert_to_ast(&mut self, opts: &BindgenAttrs) -> BindgenResult<Napi> {
let js_name = match opts.js_name() {
Some((name, _)) => name.to_string(),
_ => {
if !self.generics.params.is_empty() {
let types = self
.generics
.type_params()
.map(|param| param.ident.to_string())
.collect::<Vec<String>>()
.join(", ");
format!("{}<{}>", self.ident, types)
} else {
self.ident.to_string()
}
}
};
match self.vis {
Visibility::Public(_) => Ok(Napi {
item: NapiItem::Type(NapiType {
name: self.ident.clone(),
js_name,
value: *self.ty.clone(),
js_mod: opts.namespace().map(|(m, _)| m.to_owned()),
comments: extract_doc_comments(&self.attrs),
skip_typescript: opts.skip_typescript().is_some(),
register_name: get_register_ident(self.ident.to_string().as_str()),
}),
}),
_ => bail_span!(self, "only public type allowed"),
}
}
}