feat(napi-derive-backend, napi-derive): add support for string enums (#1551)

This commit is contained in:
Francesco Benedetto 2023-04-03 08:10:58 +02:00 committed by GitHub
parent 71e44be73d
commit 7c4dc2a2bd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 132 additions and 72 deletions

View file

@ -1,4 +1,4 @@
use proc_macro2::Ident; use proc_macro2::{Ident, Literal};
use syn::{Attribute, Expr, Type}; use syn::{Attribute, Expr, Type};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -133,10 +133,25 @@ pub struct NapiEnum {
pub skip_typescript: bool, pub skip_typescript: bool,
} }
#[derive(Debug, Clone)]
pub enum NapiEnumValue {
String(String),
Number(i32),
}
impl Into<Literal> for &NapiEnumValue {
fn into(self) -> Literal {
match self {
NapiEnumValue::String(string) => Literal::string(string),
NapiEnumValue::Number(number) => Literal::i32_unsuffixed(number.to_owned()),
}
}
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct NapiEnumVariant { pub struct NapiEnumVariant {
pub name: Ident, pub name: Ident,
pub val: i32, pub val: NapiEnumValue,
pub comments: Vec<String>, pub comments: Vec<String>,
} }

View file

@ -29,7 +29,7 @@ impl NapiEnum {
let mut to_napi_branches = vec![]; let mut to_napi_branches = vec![];
self.variants.iter().for_each(|v| { self.variants.iter().for_each(|v| {
let val = Literal::i32_unsuffixed(v.val); let val: Literal = (&v.val).into();
let v_name = &v.name; let v_name = &v.name;
from_napi_branches.push(quote! { #val => Ok(#name::#v_name) }); from_napi_branches.push(quote! { #val => Ok(#name::#v_name) });
@ -62,7 +62,7 @@ impl NapiEnum {
env: napi::bindgen_prelude::sys::napi_env, env: napi::bindgen_prelude::sys::napi_env,
napi_val: napi::bindgen_prelude::sys::napi_value napi_val: napi::bindgen_prelude::sys::napi_value
) -> napi::bindgen_prelude::Result<Self> { ) -> napi::bindgen_prelude::Result<Self> {
let val = i32::from_napi_value(env, napi_val).map_err(|e| { let val = FromNapiValue::from_napi_value(env, napi_val).map_err(|e| {
napi::bindgen_prelude::error!( napi::bindgen_prelude::error!(
e.status, e.status,
"Failed to convert napi value into enum `{}`. {}", "Failed to convert napi value into enum `{}`. {}",
@ -76,7 +76,7 @@ impl NapiEnum {
_ => { _ => {
Err(napi::bindgen_prelude::error!( Err(napi::bindgen_prelude::error!(
napi::bindgen_prelude::Status::InvalidArg, napi::bindgen_prelude::Status::InvalidArg,
"value `{}` does not match any variant of enum `{}`", "value `{:?}` does not match any variant of enum `{}`",
val, val,
#name_str #name_str
)) ))
@ -94,7 +94,7 @@ impl NapiEnum {
#(#to_napi_branches,)* #(#to_napi_branches,)*
}; };
i32::to_napi_value(env, val) ToNapiValue::to_napi_value(env, val)
} }
} }
} }
@ -109,13 +109,17 @@ impl NapiEnum {
for variant in self.variants.iter() { for variant in self.variants.iter() {
let name_lit = Literal::string(&format!("{}\0", variant.name)); let name_lit = Literal::string(&format!("{}\0", variant.name));
let val_lit = Literal::i32_unsuffixed(variant.val); let val_lit: Literal = (&variant.val).into();
define_properties.push(quote! { define_properties.push(quote! {
{ {
let name = std::ffi::CStr::from_bytes_with_nul_unchecked(#name_lit.as_bytes()); let name = std::ffi::CStr::from_bytes_with_nul_unchecked(#name_lit.as_bytes());
napi::bindgen_prelude::check_status!( napi::bindgen_prelude::check_status!(
napi::bindgen_prelude::sys::napi_set_named_property(env, obj_ptr, name.as_ptr(), i32::to_napi_value(env, #val_lit)?), napi::bindgen_prelude::sys::napi_set_named_property(
env,
obj_ptr, name.as_ptr(),
ToNapiValue::to_napi_value(env, #val_lit)?
),
"Failed to defined enum `{}`", "Failed to defined enum `{}`",
#js_name_lit #js_name_lit
)?; )?;

View file

@ -1,5 +1,5 @@
use super::{add_alias, ToTypeDef, TypeDef}; use super::{add_alias, ToTypeDef, TypeDef};
use crate::{js_doc_from_comments, NapiEnum}; use crate::{js_doc_from_comments, NapiEnum, NapiEnumValue};
impl ToTypeDef for NapiEnum { impl ToTypeDef for NapiEnum {
fn to_type_def(&self) -> Option<TypeDef> { fn to_type_def(&self) -> Option<TypeDef> {
@ -26,12 +26,11 @@ impl NapiEnum {
.variants .variants
.iter() .iter()
.map(|v| { .map(|v| {
format!( let val = match &v.val {
"{}{} = {}", NapiEnumValue::Number(num) => format!("{}", num),
js_doc_from_comments(&v.comments), NapiEnumValue::String(string) => format!("'{}'", string),
v.name, };
v.val, format!("{}{} = {}", js_doc_from_comments(&v.comments), v.name, val)
)
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(",\n ") .join(",\n ")

View file

@ -38,7 +38,7 @@ pub struct BindgenAttrs {
} }
// NOTE: borrowed from wasm-bindgen // NOTE: borrowed from wasm-bindgen
// some of them may useless is #[napi] macro // some of them may useless in #[napi] macro
macro_rules! attrgen { macro_rules! attrgen {
($mac:ident) => { ($mac:ident) => {
$mac! { $mac! {
@ -65,6 +65,7 @@ macro_rules! attrgen {
(ts_return_type, TsReturnType(Span, String, Span)), (ts_return_type, TsReturnType(Span, String, Span)),
(ts_type, TsType(Span, String, Span)), (ts_type, TsType(Span, String, Span)),
(ts_generic_types, TsGenericTypes(Span, String, Span)), (ts_generic_types, TsGenericTypes(Span, String, Span)),
(string_enum, StringEnum(Span)),
// impl later // impl later
// (inspectable, Inspectable(Span)), // (inspectable, Inspectable(Span)),

View file

@ -9,7 +9,7 @@ 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, NapiEnumValue,
NapiEnumVariant, NapiFn, NapiFnArg, NapiFnArgKind, NapiImpl, NapiItem, NapiStruct, NapiEnumVariant, NapiFn, NapiFnArg, NapiFnArgKind, NapiImpl, NapiItem, NapiStruct,
NapiStructField, NapiStructKind, NapiStructField, NapiStructKind,
}; };
@ -1124,63 +1124,87 @@ impl ConvertToAST for syn::ItemEnum {
.js_name() .js_name()
.map_or_else(|| self.ident.to_string(), |(s, _)| s.to_string()); .map_or_else(|| self.ident.to_string(), |(s, _)| s.to_string());
let mut last_variant_val: i32 = -1; let variants = match opts.string_enum() {
let variants = self Some(_) => self
.variants .variants
.iter() .iter()
.map(|v| { .map(|v| {
match v.fields { if !matches!(v.fields, syn::Fields::Unit) {
syn::Fields::Unit => {} bail_span!(v.fields, "Structured enum is not supported in #[napi]")
_ => bail_span!(v.fields, "Structured enum is not supported in #[napi]"),
};
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, if matches!(&v.discriminant, Some((_, _))) {
}; bail_span!(
v.fields,
last_variant_val = val; "Literal values are not supported with string enum in #[napi]"
)
Ok(NapiEnumVariant { }
name: v.ident.clone(), Ok(NapiEnumVariant {
val, name: v.ident.clone(),
comments: extract_doc_comments(&v.attrs), val: NapiEnumValue::String(v.ident.to_string()),
comments: extract_doc_comments(&v.attrs),
})
}) })
}) .collect::<BindgenResult<Vec<NapiEnumVariant>>>()?,
.collect::<BindgenResult<Vec<NapiEnumVariant>>>()?; None => {
let mut last_variant_val: i32 = -1;
self
.variants
.iter()
.map(|v| {
if !matches!(v.fields, syn::Fields::Unit) {
bail_span!(v.fields, "Structured enum is not supported in #[napi]")
}
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 { Ok(Napi {
item: NapiItem::Enum(NapiEnum { item: NapiItem::Enum(NapiEnum {

View file

@ -98,6 +98,11 @@ Generated by [AVA](https://avajs.dev).
export const enum Empty {␊ export const enum Empty {␊
}␊ }␊
export const enum Status {␊
Pristine = 'Pristine',␊
Loading = 'Loading',␊
Ready = 'Ready'␊
}␊
/** You could break the step and for an new continuous value. */␊ /** You could break the step and for an new continuous value. */␊
export const enum CustomNumEnum {␊ export const enum CustomNumEnum {␊
One = 1,␊ One = 1,␊

View file

@ -87,6 +87,11 @@ export const enum Kind {
} }
export const enum Empty { export const enum Empty {
}
export const enum Status {
Pristine = 'Pristine',
Loading = 'Loading',
Ready = 'Ready'
} }
/** You could break the step and for an new continuous value. */ /** You could break the step and for an new continuous value. */
export const enum CustomNumEnum { export const enum CustomNumEnum {

View file

@ -14,6 +14,13 @@ pub enum Kind {
#[napi] #[napi]
pub enum Empty {} pub enum Empty {}
#[napi(string_enum)]
pub enum Status {
Pristine,
Loading,
Ready,
}
/// You could break the step and for an new continuous value. /// You could break the step and for an new continuous value.
#[napi] #[napi]
pub enum CustomNumEnum { pub enum CustomNumEnum {