From 5a1f229dba73501ba09902a3d6dcb1e6062062a4 Mon Sep 17 00:00:00 2001 From: liuyi Date: Tue, 18 Apr 2023 10:15:29 +0800 Subject: [PATCH] feat: clean napi-derive noop feature code path (#1571) --- crates/backend/src/ast.rs | 6 +- crates/macro/Cargo.toml | 1 + crates/macro/src/expand.rs | 9 + crates/macro/src/expand/napi.rs | 242 ++++++++++++++++++++++++ crates/macro/src/expand/noop.rs | 36 ++++ crates/macro/src/lib.rs | 316 +------------------------------- 6 files changed, 300 insertions(+), 310 deletions(-) create mode 100644 crates/macro/src/expand.rs create mode 100644 crates/macro/src/expand/napi.rs create mode 100644 crates/macro/src/expand/noop.rs diff --git a/crates/backend/src/ast.rs b/crates/backend/src/ast.rs index a1ee4424..8fb384e6 100644 --- a/crates/backend/src/ast.rs +++ b/crates/backend/src/ast.rs @@ -139,9 +139,9 @@ pub enum NapiEnumValue { Number(i32), } -impl Into for &NapiEnumValue { - fn into(self) -> Literal { - match self { +impl From<&NapiEnumValue> for Literal { + fn from(val: &NapiEnumValue) -> Self { + match val { NapiEnumValue::String(string) => Literal::string(string), NapiEnumValue::Number(number) => Literal::i32_unsuffixed(number.to_owned()), } diff --git a/crates/macro/Cargo.toml b/crates/macro/Cargo.toml index 752306d0..c500951c 100644 --- a/crates/macro/Cargo.toml +++ b/crates/macro/Cargo.toml @@ -27,6 +27,7 @@ napi-derive-backend = { version = "1.0.49", path = "../backend" } proc-macro2 = "1.0" quote = "1.0" syn = { version = "1.0.61", features = ["fold", "full", "extra-traits"] } +cfg-if = "1.0" [lib] proc-macro = true diff --git a/crates/macro/src/expand.rs b/crates/macro/src/expand.rs new file mode 100644 index 00000000..ee6d8ac8 --- /dev/null +++ b/crates/macro/src/expand.rs @@ -0,0 +1,9 @@ +cfg_if::cfg_if! { + if #[cfg(feature = "noop")] { + mod noop; + pub use self::noop::*; + } else { + mod napi; + pub use self::napi::*; + } +} diff --git a/crates/macro/src/expand/napi.rs b/crates/macro/src/expand/napi.rs new file mode 100644 index 00000000..6b5d1b74 --- /dev/null +++ b/crates/macro/src/expand/napi.rs @@ -0,0 +1,242 @@ +use std::env; +use std::fs; +#[cfg(feature = "type-def")] +use std::io::BufWriter; +use std::io::Write; +use std::sync::atomic::{AtomicBool, Ordering}; + +use crate::parser::{attrs::BindgenAttrs, ParseNapi}; +#[cfg(feature = "type-def")] +use napi_derive_backend::ToTypeDef; +use napi_derive_backend::{BindgenResult, Napi, TryToTokens, REGISTER_IDENTS}; +use proc_macro2::{TokenStream, TokenTree}; +use quote::ToTokens; +use syn::{Attribute, Item}; + +/// a flag indicate whether or never at least one `napi` macro has been expanded. +/// ```ignore +/// if BUILT_FLAG +/// .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed) +/// .is_ok() { +/// // logic on first macro expansion +/// } +/// +/// ``` +static BUILT_FLAG: AtomicBool = AtomicBool::new(false); + +pub fn expand(attr: TokenStream, input: TokenStream) -> BindgenResult { + if BUILT_FLAG + .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed) + .is_ok() + { + // logic on first macro expansion + #[cfg(feature = "type-def")] + prepare_type_def_file(); + + if let Ok(wasi_register_file) = env::var("WASI_REGISTER_TMP_PATH") { + if let Err(_e) = fs::remove_file(wasi_register_file) { + #[cfg(debug_assertions)] + { + println!("Failed to manipulate wasi register file: {:?}", _e); + } + } + } + } + + let mut item = syn::parse2::(input)?; + let opts: BindgenAttrs = syn::parse2(attr)?; + let mut tokens = proc_macro2::TokenStream::new(); + if let Item::Mod(mut js_mod) = item { + let js_name = opts.js_name().map_or_else( + || js_mod.ident.to_string(), + |(js_name, _)| js_name.to_owned(), + ); + if let Some((_, mut items)) = js_mod.content.clone() { + for item in items.iter_mut() { + let mut empty_attrs = vec![]; + if let Some(item_opts) = replace_napi_attr_in_mod( + js_name.clone(), + match item { + Item::Fn(ref mut function) => &mut function.attrs, + Item::Struct(ref mut struct_) => &mut struct_.attrs, + Item::Enum(ref mut enum_) => &mut enum_.attrs, + Item::Const(ref mut const_) => &mut const_.attrs, + Item::Impl(ref mut impl_) => &mut impl_.attrs, + Item::Mod(mod_) => { + let mod_in_mod = mod_ + .attrs + .iter() + .enumerate() + .find(|(_, m)| m.path.segments[0].ident == "napi"); + if mod_in_mod.is_some() { + bail_span!( + mod_, + "napi module cannot be nested under another napi module" + ); + } else { + &mut empty_attrs + } + } + _ => &mut empty_attrs, + }, + ) { + let napi = item.parse_napi(&mut tokens, item_opts)?; + napi.try_to_tokens(&mut tokens)?; + + #[cfg(feature = "type-def")] + output_type_def(&napi); + } else { + item.to_tokens(&mut tokens); + }; + } + js_mod.content = None; + }; + + let js_mod_attrs: Vec = js_mod + .attrs + .clone() + .into_iter() + .filter(|attr| attr.path.segments[0].ident != "napi") + .collect(); + let mod_name = js_mod.ident; + let visible = js_mod.vis; + let mod_tokens = quote! { #(#js_mod_attrs)* #visible mod #mod_name { #tokens } }; + Ok(mod_tokens) + } else { + let napi = item.parse_napi(&mut tokens, opts)?; + napi.try_to_tokens(&mut tokens)?; + + #[cfg(feature = "type-def")] + output_type_def(&napi); + + REGISTER_IDENTS.with(|idents| { + if let Ok(wasi_register_file) = env::var("WASI_REGISTER_TMP_PATH") { + let mut file = + fs::File::create(wasi_register_file).expect("Create wasi register file failed"); + file + .write_all(format!("{:?}", idents.borrow()).as_bytes()) + .expect("Write wasi register file failed"); + } + }); + + Ok(tokens) + } +} + +#[cfg(feature = "type-def")] +fn output_type_def(napi: &Napi) { + if let Ok(type_def_file) = env::var("TYPE_DEF_TMP_PATH") { + if let Some(type_def) = napi.to_type_def() { + fs::OpenOptions::new() + .append(true) + .create(true) + .open(type_def_file) + .and_then(|file| { + let mut writer = BufWriter::::new(file); + writer.write_all(type_def.to_string().as_bytes())?; + writer.write_all("\n".as_bytes()) + }) + .unwrap_or_else(|e| { + println!("Failed to write type def file: {:?}", e); + }); + } + } +} + +fn replace_napi_attr_in_mod( + js_namespace: String, + attrs: &mut Vec, +) -> Option { + let napi_attr = attrs.clone(); + let napi_attr = napi_attr + .iter() + .enumerate() + .find(|(_, m)| m.path.segments[0].ident == "napi"); + if let Some((index, napi_attr)) = napi_attr { + let attr_token_stream = napi_attr.tokens.clone(); + let raw_attr_stream = attr_token_stream.to_string(); + let raw_attr_stream = if !raw_attr_stream.is_empty() { + raw_attr_stream + .strip_prefix('(') + .unwrap() + .strip_suffix(')') + .unwrap() + .to_string() + } else { + raw_attr_stream + }; + let raw_attr_token_stream = syn::parse_str::(raw_attr_stream.as_str()).unwrap(); + + let new_attr: syn::Attribute = if !raw_attr_stream.is_empty() { + syn::parse_quote!( + #[napi(#raw_attr_token_stream, namespace = #js_namespace)] + ) + } else { + syn::parse_quote!( + #[napi(namespace = #js_namespace)] + ) + }; + let struct_opts: BindgenAttrs = + if let Some(TokenTree::Group(g)) = new_attr.tokens.into_iter().next() { + syn::parse2(g.stream()).ok()? + } else { + syn::parse2(quote! {}).ok()? + }; + attrs.remove(index); + Some(struct_opts) + } else { + None + } +} + +#[cfg(feature = "type-def")] +fn prepare_type_def_file() { + if let Ok(ref type_def_file) = env::var("TYPE_DEF_TMP_PATH") { + use napi_derive_backend::{NAPI_RS_CLI_VERSION, NAPI_RS_CLI_VERSION_WITH_SHARED_CRATES_FIX}; + if let Err(_e) = if *NAPI_RS_CLI_VERSION >= *NAPI_RS_CLI_VERSION_WITH_SHARED_CRATES_FIX { + remove_existed_type_def(type_def_file) + } else { + fs::remove_file(type_def_file) + } { + #[cfg(debug_assertions)] + { + println!("Failed to manipulate type def file: {:?}", _e); + } + } + } +} + +#[cfg(feature = "type-def")] +fn remove_existed_type_def(type_def_file: &str) -> std::io::Result<()> { + use std::io::{BufRead, BufReader}; + + let pkg_name = std::env::var("CARGO_PKG_NAME").expect("CARGO_PKG_NAME is not set"); + if let Ok(content) = std::fs::File::open(type_def_file) { + let reader = BufReader::new(content); + let cleaned_content = reader + .lines() + .filter_map(|line| { + if let Ok(line) = line { + if let Some((package_name, _)) = line.split_once(':') { + if pkg_name == package_name { + return None; + } + } + Some(line) + } else { + None + } + }) + .collect::>() + .join("\n"); + let mut content = std::fs::OpenOptions::new() + .read(true) + .write(true) + .truncate(true) + .open(type_def_file)?; + + content.write_all(cleaned_content.as_bytes())?; + content.write_all(b"\n")?; + } + Ok(()) +} diff --git a/crates/macro/src/expand/noop.rs b/crates/macro/src/expand/noop.rs new file mode 100644 index 00000000..f11d3284 --- /dev/null +++ b/crates/macro/src/expand/noop.rs @@ -0,0 +1,36 @@ +use napi_derive_backend::BindgenResult; +use proc_macro2::TokenStream; +use quote::ToTokens; +use syn::Attribute; + +pub fn expand(_attr: TokenStream, input: TokenStream) -> BindgenResult { + let mut item = syn::parse2::(input)?; + let mut tokens = TokenStream::new(); + + if let syn::Item::Struct(ref mut struct_) = item { + struct_ + .fields + .iter_mut() + .for_each(|field| find_and_remove_napi_attr(&mut field.attrs)) + } + + item.to_tokens(&mut tokens); + + Ok(tokens) +} + +fn find_and_remove_napi_attr(attrs: &mut Vec) { + loop { + let napi_attr = attrs + .iter() + .enumerate() + .find(|&(_, m)| m.path.segments[0].ident == "napi"); + + let pos = match napi_attr { + Some((pos, _raw_attr)) => pos, + None => break, + }; + + attrs.remove(pos); + } +} diff --git a/crates/macro/src/lib.rs b/crates/macro/src/lib.rs index c81ed0ca..d72a8e6e 100644 --- a/crates/macro/src/lib.rs +++ b/crates/macro/src/lib.rs @@ -1,5 +1,6 @@ #[cfg(feature = "compat-mode")] mod compat_macro; +mod expand; mod parser; #[macro_use] @@ -9,101 +10,25 @@ extern crate napi_derive_backend; #[macro_use] extern crate quote; -#[cfg(not(feature = "noop"))] use std::env; -#[cfg(not(feature = "noop"))] -use std::fs; -#[cfg(not(feature = "noop"))] -use std::io::Write; -#[cfg(all(feature = "type-def", not(feature = "noop")))] -use std::io::{BufWriter, Result as IOResult}; -#[cfg(not(feature = "noop"))] -use std::sync::atomic::{AtomicBool, Ordering}; -use napi_derive_backend::BindgenResult; -#[cfg(not(feature = "noop"))] -use napi_derive_backend::TryToTokens; -#[cfg(not(feature = "noop"))] -use napi_derive_backend::REGISTER_IDENTS; -#[cfg(all(feature = "type-def", not(feature = "noop")))] -use napi_derive_backend::{ToTypeDef, TypeDef}; -#[cfg(not(feature = "noop"))] -use parser::{attrs::BindgenAttrs, ParseNapi}; -use proc_macro::TokenStream as RawStream; -use proc_macro2::TokenStream; -#[cfg(not(feature = "noop"))] -use proc_macro2::TokenTree; -use quote::ToTokens; -use syn::Attribute; -#[cfg(not(feature = "noop"))] -use syn::Item; +use proc_macro::TokenStream; #[cfg(feature = "compat-mode")] use syn::{fold::Fold, parse_macro_input, ItemFn}; -#[cfg(not(feature = "noop"))] -/// a flag indicate whether or never at least one `napi` macro has been expanded. -/// ```ignore -/// if BUILT_FLAG -/// .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed) -/// .is_ok() { -/// // logic on first macro expansion -/// } -/// -/// ``` -static BUILT_FLAG: AtomicBool = AtomicBool::new(false); - /// ```ignore /// #[napi] /// fn test(ctx: CallContext, name: String) { /// "hello" + name /// } /// ``` -#[cfg(not(feature = "noop"))] #[proc_macro_attribute] -pub fn napi(attr: RawStream, input: RawStream) -> RawStream { - if BUILT_FLAG - .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed) - .is_ok() - { - // logic on first macro expansion - #[cfg(feature = "type-def")] - if let Ok(ref type_def_file) = env::var("TYPE_DEF_TMP_PATH") { - use napi_derive_backend::{NAPI_RS_CLI_VERSION, NAPI_RS_CLI_VERSION_WITH_SHARED_CRATES_FIX}; - if let Err(_e) = if *NAPI_RS_CLI_VERSION >= *NAPI_RS_CLI_VERSION_WITH_SHARED_CRATES_FIX { - remove_existed_type_def(type_def_file) - } else { - fs::remove_file(type_def_file) - } { - #[cfg(debug_assertions)] - { - println!("Failed to manipulate type def file: {:?}", _e); - } - } - } - if let Ok(wasi_register_file) = env::var("WASI_REGISTER_TMP_PATH") { - if let Err(_e) = fs::remove_file(wasi_register_file) { - #[cfg(debug_assertions)] - { - println!("Failed to manipulate wasi register file: {:?}", _e); - } - } - } - } - - match expand(attr.into(), input.into()) { +pub fn napi(attr: TokenStream, input: TokenStream) -> TokenStream { + match expand::expand(attr.into(), input.into()) { Ok(tokens) => { if env::var("DEBUG_GENERATED_CODE").is_ok() { println!("{}", tokens); } - REGISTER_IDENTS.with(|idents| { - if let Ok(wasi_register_file) = env::var("WASI_REGISTER_TMP_PATH") { - let mut file = - fs::File::create(wasi_register_file).expect("Create wasi register file failed"); - file - .write_all(format!("{:?}", idents.borrow()).as_bytes()) - .expect("Write wasi register file failed"); - } - }); tokens.into() } Err(diagnostic) => { @@ -114,150 +39,9 @@ pub fn napi(attr: RawStream, input: RawStream) -> RawStream { } } -#[cfg(feature = "noop")] -#[proc_macro_attribute] -pub fn napi(attr: RawStream, input: RawStream) -> RawStream { - match expand(attr.into(), input.into()) { - Ok(tokens) => tokens.into(), - Err(diagnostic) => { - println!("`napi` macro expand failed."); - - (quote! { #diagnostic }).into() - } - } -} - -#[cfg(feature = "noop")] -fn expand(_attr: TokenStream, input: TokenStream) -> BindgenResult { - let mut item = syn::parse2::(input.into())?; - let mut tokens = proc_macro2::TokenStream::new(); - - match item { - syn::Item::Struct(ref mut struct_) => struct_ - .fields - .iter_mut() - .for_each(|field| find_and_remove_napi_attr(&mut field.attrs)), - _ => {} - } - - item.to_tokens(&mut tokens); - - Ok(tokens) -} - -#[cfg(feature = "noop")] -fn find_and_remove_napi_attr(attrs: &mut Vec) { - loop { - let napi_attr = attrs - .iter() - .enumerate() - .find(|&(_, m)| m.path.segments[0].ident == "napi"); - - let pos = match napi_attr { - Some((pos, _raw_attr)) => pos, - None => break, - }; - - attrs.remove(pos); - } -} - -#[cfg(not(feature = "noop"))] -fn expand(attr: TokenStream, input: TokenStream) -> BindgenResult { - let mut item = syn::parse2::(input)?; - let opts: BindgenAttrs = syn::parse2(attr)?; - let mut tokens = proc_macro2::TokenStream::new(); - if let Item::Mod(mut js_mod) = item { - let js_name = opts.js_name().map_or_else( - || js_mod.ident.to_string(), - |(js_name, _)| js_name.to_owned(), - ); - if let Some((_, mut items)) = js_mod.content.clone() { - for item in items.iter_mut() { - let mut empty_attrs = vec![]; - if let Some(item_opts) = replace_napi_attr_in_mod( - js_name.clone(), - match item { - syn::Item::Fn(ref mut function) => &mut function.attrs, - syn::Item::Struct(ref mut struct_) => &mut struct_.attrs, - syn::Item::Enum(ref mut enum_) => &mut enum_.attrs, - syn::Item::Const(ref mut const_) => &mut const_.attrs, - syn::Item::Impl(ref mut impl_) => &mut impl_.attrs, - syn::Item::Mod(mod_) => { - let mod_in_mod = mod_ - .attrs - .iter() - .enumerate() - .find(|(_, m)| m.path.segments[0].ident == "napi"); - if mod_in_mod.is_some() { - bail_span!( - mod_, - "napi module cannot be nested under another napi module" - ); - } else { - &mut empty_attrs - } - } - _ => &mut empty_attrs, - }, - ) { - let napi = item.parse_napi(&mut tokens, item_opts)?; - napi.try_to_tokens(&mut tokens)?; - #[cfg(feature = "type-def")] - if let Ok(type_def_file) = env::var("TYPE_DEF_TMP_PATH") { - if let Err(e) = output_type_def(type_def_file, napi.to_type_def()) { - println!("Failed to write type def file: {:?}", e); - }; - } - } else { - item.to_tokens(&mut tokens); - }; - } - js_mod.content = None; - }; - let js_mod_attrs: Vec = js_mod - .attrs - .clone() - .into_iter() - .filter(|attr| attr.path.segments[0].ident != "napi") - .collect(); - let mod_name = js_mod.ident; - let visible = js_mod.vis; - let mod_tokens = quote! { #(#js_mod_attrs)* #visible mod #mod_name { #tokens } }; - Ok(mod_tokens) - } else { - let napi = item.parse_napi(&mut tokens, opts)?; - napi.try_to_tokens(&mut tokens)?; - - #[cfg(feature = "type-def")] - if let Ok(type_def_file) = env::var("TYPE_DEF_TMP_PATH") { - if let Err(e) = output_type_def(type_def_file, napi.to_type_def()) { - println!("Failed to write type def file: {:?}", e); - }; - } - Ok(tokens) - } -} - -#[cfg(all(feature = "type-def", not(feature = "noop")))] -fn output_type_def(type_def_file: String, type_def: Option) -> IOResult<()> { - if type_def.is_some() { - let file = fs::OpenOptions::new() - .append(true) - .create(true) - .open(type_def_file)?; - - let mut writer = BufWriter::::new(file); - writer.write_all(type_def.unwrap().to_string().as_bytes())?; - writer.write_all("\n".as_bytes()) - } else { - IOResult::Ok(()) - } -} - #[cfg(feature = "compat-mode")] #[proc_macro_attribute] -pub fn contextless_function(_attr: RawStream, input: RawStream) -> RawStream { +pub fn contextless_function(_attr: TokenStream, input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as ItemFn); let mut js_fn = compat_macro::JsFunction::new(); js_fn.fold_item_fn(input); @@ -287,12 +71,12 @@ pub fn contextless_function(_attr: RawStream, input: RawStream) -> RawStream { } }; // Hand the output tokens back to the compiler - RawStream::from(expanded) + TokenStream::from(expanded) } #[cfg(feature = "compat-mode")] #[proc_macro_attribute] -pub fn js_function(attr: RawStream, input: RawStream) -> RawStream { +pub fn js_function(attr: TokenStream, input: TokenStream) -> TokenStream { let arg_len = parse_macro_input!(attr as compat_macro::ArgLength); let arg_len_span = arg_len.length; let input = parse_macro_input!(input as ItemFn); @@ -345,12 +129,12 @@ pub fn js_function(attr: RawStream, input: RawStream) -> RawStream { } }; // Hand the output tokens back to the compiler - RawStream::from(expanded) + TokenStream::from(expanded) } #[cfg(feature = "compat-mode")] #[proc_macro_attribute] -pub fn module_exports(_attr: RawStream, input: RawStream) -> RawStream { +pub fn module_exports(_attr: TokenStream, input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as ItemFn); let mut js_fn = compat_macro::JsFunction::new(); js_fn.fold_item_fn(input); @@ -390,85 +174,3 @@ pub fn module_exports(_attr: RawStream, input: RawStream) -> RawStream { }) .into() } - -#[cfg(not(feature = "noop"))] -fn replace_napi_attr_in_mod( - js_namespace: String, - attrs: &mut Vec, -) -> Option { - let napi_attr = attrs.clone(); - let napi_attr = napi_attr - .iter() - .enumerate() - .find(|(_, m)| m.path.segments[0].ident == "napi"); - if let Some((index, napi_attr)) = napi_attr { - let attr_token_stream = napi_attr.tokens.clone(); - let raw_attr_stream = attr_token_stream.to_string(); - let raw_attr_stream = if !raw_attr_stream.is_empty() { - raw_attr_stream - .strip_prefix('(') - .unwrap() - .strip_suffix(')') - .unwrap() - .to_string() - } else { - raw_attr_stream - }; - let raw_attr_token_stream = syn::parse_str::(raw_attr_stream.as_str()).unwrap(); - - let new_attr: syn::Attribute = if !raw_attr_stream.is_empty() { - syn::parse_quote!( - #[napi(#raw_attr_token_stream, namespace = #js_namespace)] - ) - } else { - syn::parse_quote!( - #[napi(namespace = #js_namespace)] - ) - }; - let struct_opts: BindgenAttrs = - if let Some(TokenTree::Group(g)) = new_attr.tokens.into_iter().next() { - syn::parse2(g.stream()).ok()? - } else { - syn::parse2(quote! {}).ok()? - }; - attrs.remove(index); - Some(struct_opts) - } else { - None - } -} - -#[cfg(all(feature = "type-def", not(feature = "noop")))] -fn remove_existed_type_def(type_def_file: &str) -> std::io::Result<()> { - use std::io::{BufRead, BufReader}; - - let pkg_name = std::env::var("CARGO_PKG_NAME").expect("CARGO_PKG_NAME is not set"); - if let Ok(content) = std::fs::File::open(type_def_file) { - let reader = BufReader::new(content); - let cleaned_content = reader - .lines() - .filter_map(|line| { - if let Ok(line) = line { - if let Some((package_name, _)) = line.split_once(':') { - if pkg_name == package_name { - return None; - } - } - Some(line) - } else { - None - } - }) - .collect::>() - .join("\n"); - let mut content = std::fs::OpenOptions::new() - .read(true) - .write(true) - .truncate(true) - .open(type_def_file)?; - - content.write_all(cleaned_content.as_bytes())?; - content.write_all(b"\n")?; - } - Ok(()) -}