use std::cell::{Cell, RefCell}; use std::char; use std::str::Chars; use ast::OperationKind; use backend::ast; use backend::util::{ident_ty, ShortHash}; use backend::Diagnostic; use proc_macro2::{Ident, Span, TokenStream, TokenTree}; use quote::ToTokens; use syn::ext::IdentExt; use syn::parse::{Parse, ParseStream, Result as SynResult}; use syn::spanned::Spanned; use syn::{ItemFn, Lit, MacroDelimiter, ReturnType}; use crate::ClassMarker; thread_local!(static ATTRS: AttributeParseState = Default::default()); /// Javascript keywords which are not keywords in Rust. const JS_KEYWORDS: [&str; 20] = [ "class", "case", "catch", "debugger", "default", "delete", "export", "extends", "finally", "function", "import", "instanceof", "new", "null", "switch", "this", "throw", "var", "void", "with", ]; #[derive(Default)] struct AttributeParseState { parsed: Cell, checks: Cell, unused_attrs: RefCell>, } /// Parsed attributes from a `#[wasm_bindgen(..)]`. #[cfg_attr(feature = "extra-traits", derive(Debug))] pub struct BindgenAttrs { /// List of parsed attributes pub attrs: Vec<(Cell, BindgenAttr)>, } macro_rules! attrgen { ($mac:ident) => { $mac! { (catch, Catch(Span)), (constructor, Constructor(Span)), (method, Method(Span)), (static_method_of, StaticMethodOf(Span, Ident)), (js_namespace, JsNamespace(Span, Vec, Vec)), (module, Module(Span, String, Span)), (raw_module, RawModule(Span, String, Span)), (inline_js, InlineJs(Span, String, Span)), (getter, Getter(Span, Option)), (setter, Setter(Span, Option)), (indexing_getter, IndexingGetter(Span)), (indexing_setter, IndexingSetter(Span)), (indexing_deleter, IndexingDeleter(Span)), (structural, Structural(Span)), (r#final, Final(Span)), (readonly, Readonly(Span)), (js_name, JsName(Span, String, Span)), (js_class, JsClass(Span, String, Span)), (inspectable, Inspectable(Span)), (is_type_of, IsTypeOf(Span, syn::Expr)), (extends, Extends(Span, syn::Path)), (no_deref, NoDeref(Span)), (vendor_prefix, VendorPrefix(Span, Ident)), (variadic, Variadic(Span)), (typescript_custom_section, TypescriptCustomSection(Span)), (skip_typescript, SkipTypescript(Span)), (skip_jsdoc, SkipJsDoc(Span)), (main, Main(Span)), (start, Start(Span)), (wasm_bindgen, WasmBindgen(Span, syn::Path)), (wasm_bindgen_futures, WasmBindgenFutures(Span, syn::Path)), (skip, Skip(Span)), (typescript_type, TypeScriptType(Span, String, Span)), (getter_with_clone, GetterWithClone(Span)), // For testing purposes only. (assert_no_shim, AssertNoShim(Span)), } }; } macro_rules! methods { ($(($name:ident, $variant:ident($($contents:tt)*)),)*) => { $(methods!(@method $name, $variant($($contents)*));)* fn enforce_used(self) -> Result<(), Diagnostic> { // Account for the fact this method was called ATTRS.with(|state| state.checks.set(state.checks.get() + 1)); let mut errors = Vec::new(); for (used, attr) in self.attrs.iter() { if used.get() { continue } let span = match attr { $(BindgenAttr::$variant(span, ..) => span,)* }; errors.push(Diagnostic::span_error(*span, "unused wasm_bindgen attribute")); } Diagnostic::from_vec(errors) } fn check_used(self) { // Account for the fact this method was called ATTRS.with(|state| { state.checks.set(state.checks.get() + 1); state.unused_attrs.borrow_mut().extend( self.attrs .iter() .filter_map(|(used, attr)| if used.get() { None } else { Some(attr) }) .map(|attr| { match attr { $(BindgenAttr::$variant(span, ..) => { syn::parse_quote_spanned!(*span => $name) })* } }) ); }); } }; (@method $name:ident, $variant:ident(Span, String, Span)) => { fn $name(&self) -> Option<(&str, Span)> { self.attrs .iter() .find_map(|a| match &a.1 { BindgenAttr::$variant(_, s, span) => { a.0.set(true); Some((&s[..], *span)) } _ => None, }) } }; (@method $name:ident, $variant:ident(Span, Vec, Vec)) => { fn $name(&self) -> Option<(&[String], &[Span])> { self.attrs .iter() .find_map(|a| match &a.1 { BindgenAttr::$variant(_, ss, spans) => { a.0.set(true); Some((&ss[..], &spans[..])) } _ => None, }) } }; (@method $name:ident, $variant:ident(Span, $($other:tt)*)) => { #[allow(unused)] fn $name(&self) -> Option<&$($other)*> { self.attrs .iter() .find_map(|a| match &a.1 { BindgenAttr::$variant(_, s) => { a.0.set(true); Some(s) } _ => None, }) } }; (@method $name:ident, $variant:ident($($other:tt)*)) => { #[allow(unused)] fn $name(&self) -> Option<&$($other)*> { self.attrs .iter() .find_map(|a| match &a.1 { BindgenAttr::$variant(s) => { a.0.set(true); Some(s) } _ => None, }) } }; } impl BindgenAttrs { /// Find and parse the wasm_bindgen attributes. fn find(attrs: &mut Vec) -> Result { let mut ret = BindgenAttrs::default(); loop { let pos = attrs .iter() .enumerate() .find(|&(_, m)| m.path().segments[0].ident == "wasm_bindgen") .map(|a| a.0); let pos = match pos { Some(i) => i, None => return Ok(ret), }; let attr = attrs.remove(pos); let tokens = match attr.meta { syn::Meta::Path(_) => continue, syn::Meta::List(syn::MetaList { delimiter: MacroDelimiter::Paren(_), tokens, .. }) => tokens, syn::Meta::List(_) | syn::Meta::NameValue(_) => { bail_span!(attr, "malformed #[wasm_bindgen] attribute") } }; let mut attrs: BindgenAttrs = syn::parse2(tokens)?; ret.attrs.append(&mut attrs.attrs); attrs.check_used(); } } attrgen!(methods); } impl Default for BindgenAttrs { fn default() -> BindgenAttrs { // Add 1 to the list of parsed attribute sets. We'll use this counter to // sanity check that we call `check_used` an appropriate number of // times. ATTRS.with(|state| state.parsed.set(state.parsed.get() + 1)); BindgenAttrs { attrs: Vec::new() } } } impl Parse for BindgenAttrs { fn parse(input: ParseStream) -> SynResult { let mut attrs = BindgenAttrs::default(); if input.is_empty() { return Ok(attrs); } let opts = syn::punctuated::Punctuated::<_, syn::token::Comma>::parse_terminated(input)?; attrs.attrs = opts.into_iter().map(|c| (Cell::new(false), c)).collect(); Ok(attrs) } } macro_rules! gen_bindgen_attr { ($(($method:ident, $($variants:tt)*),)*) => { /// The possible attributes in the `#[wasm_bindgen]`. #[cfg_attr(feature = "extra-traits", derive(Debug))] pub enum BindgenAttr { $($($variants)*,)* } } } attrgen!(gen_bindgen_attr); impl Parse for BindgenAttr { fn parse(input: ParseStream) -> SynResult { let original = input.fork(); let attr: AnyIdent = input.parse()?; let attr = attr.0; let attr_span = attr.span(); let attr_string = attr.to_string(); let raw_attr_string = format!("r#{}", attr_string); macro_rules! parsers { ($(($name:ident, $($contents:tt)*),)*) => { $( if attr_string == stringify!($name) || raw_attr_string == stringify!($name) { parsers!( @parser $($contents)* ); } )* }; (@parser $variant:ident(Span)) => ({ return Ok(BindgenAttr::$variant(attr_span)); }); (@parser $variant:ident(Span, Ident)) => ({ input.parse::()?; let ident = input.parse::()?.0; return Ok(BindgenAttr::$variant(attr_span, ident)) }); (@parser $variant:ident(Span, Option)) => ({ if input.parse::().is_ok() { let ident = input.parse::()?.0; return Ok(BindgenAttr::$variant(attr_span, Some(ident))) } else { return Ok(BindgenAttr::$variant(attr_span, None)); } }); (@parser $variant:ident(Span, syn::Path)) => ({ input.parse::()?; return Ok(BindgenAttr::$variant(attr_span, input.parse()?)); }); (@parser $variant:ident(Span, syn::Expr)) => ({ input.parse::()?; return Ok(BindgenAttr::$variant(attr_span, input.parse()?)); }); (@parser $variant:ident(Span, String, Span)) => ({ input.parse::()?; let (val, span) = match input.parse::() { Ok(str) => (str.value(), str.span()), Err(_) => { let ident = input.parse::()?.0; (ident.to_string(), ident.span()) } }; return Ok(BindgenAttr::$variant(attr_span, val, span)) }); (@parser $variant:ident(Span, Vec, Vec)) => ({ input.parse::()?; let (vals, spans) = match input.parse::() { Ok(exprs) => { let mut vals = vec![]; let mut spans = vec![]; for expr in exprs.elems.iter() { if let syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(ref str), .. }) = expr { vals.push(str.value()); spans.push(str.span()); } else { return Err(syn::Error::new(expr.span(), "expected string literals")); } } (vals, spans) }, Err(_) => { let ident = input.parse::()?.0; (vec![ident.to_string()], vec![ident.span()]) } }; return Ok(BindgenAttr::$variant(attr_span, vals, spans)) }); } attrgen!(parsers); Err(original.error(if attr_string.starts_with('_') { "unknown attribute: it's safe to remove unused attributes entirely." } else { "unknown attribute" })) } } struct AnyIdent(Ident); impl Parse for AnyIdent { fn parse(input: ParseStream) -> SynResult { input.step(|cursor| match cursor.ident() { Some((ident, remaining)) => Ok((AnyIdent(ident), remaining)), None => Err(cursor.error("expected an identifier")), }) } } /// Conversion trait with context. /// /// Used to convert syn tokens into an AST, that we can then use to generate glue code. The context /// (`Ctx`) is used to pass in the attributes from the `#[wasm_bindgen]`, if needed. trait ConvertToAst { /// What we are converting to. type Target; /// Convert into our target. /// /// Since this is used in a procedural macro, use panic to fail. fn convert(self, context: Ctx) -> Result; } impl<'a> ConvertToAst<(&ast::Program, BindgenAttrs)> for &'a mut syn::ItemStruct { type Target = ast::Struct; fn convert( self, (program, attrs): (&ast::Program, BindgenAttrs), ) -> Result { if !self.generics.params.is_empty() { bail_span!( self.generics, "structs with #[wasm_bindgen] cannot have lifetime or \ type parameters currently" ); } let mut fields = Vec::new(); let js_name = attrs .js_name() .map(|s| s.0.to_string()) .unwrap_or(self.ident.to_string()); let is_inspectable = attrs.inspectable().is_some(); let getter_with_clone = attrs.getter_with_clone(); for (i, field) in self.fields.iter_mut().enumerate() { match field.vis { syn::Visibility::Public(..) => {} _ => continue, } let (js_field_name, member) = match &field.ident { Some(ident) => (ident.unraw().to_string(), syn::Member::Named(ident.clone())), None => (i.to_string(), syn::Member::Unnamed(i.into())), }; let attrs = BindgenAttrs::find(&mut field.attrs)?; if attrs.skip().is_some() { attrs.check_used(); continue; } let js_field_name = match attrs.js_name() { Some((name, _)) => name.to_string(), None => js_field_name, }; let comments = extract_doc_comments(&field.attrs); let getter = shared::struct_field_get(&js_name, &js_field_name); let setter = shared::struct_field_set(&js_name, &js_field_name); fields.push(ast::StructField { rust_name: member, js_name: js_field_name, struct_name: self.ident.clone(), readonly: attrs.readonly().is_some(), ty: field.ty.clone(), getter: Ident::new(&getter, Span::call_site()), setter: Ident::new(&setter, Span::call_site()), comments, generate_typescript: attrs.skip_typescript().is_none(), generate_jsdoc: attrs.skip_jsdoc().is_none(), getter_with_clone: attrs.getter_with_clone().or(getter_with_clone).copied(), wasm_bindgen: program.wasm_bindgen.clone(), }); attrs.check_used(); } let generate_typescript = attrs.skip_typescript().is_none(); let comments: Vec = extract_doc_comments(&self.attrs); attrs.check_used(); Ok(ast::Struct { rust_name: self.ident.clone(), js_name, fields, comments, is_inspectable, generate_typescript, wasm_bindgen: program.wasm_bindgen.clone(), }) } } fn get_ty(mut ty: &syn::Type) -> &syn::Type { while let syn::Type::Group(g) = ty { ty = &g.elem; } ty } fn get_expr(mut expr: &syn::Expr) -> &syn::Expr { while let syn::Expr::Group(g) = expr { expr = &g.expr; } expr } impl<'a> ConvertToAst<(&ast::Program, BindgenAttrs, &'a Option)> for syn::ForeignItemFn { type Target = ast::ImportKind; fn convert( self, (program, opts, module): (&ast::Program, BindgenAttrs, &'a Option), ) -> Result { let mut wasm = function_from_decl( &self.sig.ident, &opts, self.sig.clone(), self.attrs.clone(), self.vis.clone(), false, None, false, Some(&["default"]), )? .0; let catch = opts.catch().is_some(); let variadic = opts.variadic().is_some(); let js_ret = if catch { // TODO: this assumes a whole bunch: // // * The outer type is actually a `Result` // * The error type is a `JsValue` // * The actual type is the first type parameter // // should probably fix this one day... extract_first_ty_param(wasm.ret.as_ref())? } else { wasm.ret.clone() }; let operation_kind = operation_kind(&opts); let kind = if opts.method().is_some() { let class = wasm.arguments.first().ok_or_else(|| { err_span!(self, "imported methods must have at least one argument") })?; let class = match get_ty(&class.ty) { syn::Type::Reference(syn::TypeReference { mutability: None, elem, .. }) => &**elem, _ => bail_span!( class.ty, "first argument of method must be a shared reference" ), }; let class_name = match get_ty(class) { syn::Type::Path(syn::TypePath { qself: None, ref path, }) => path, _ => bail_span!(class, "first argument of method must be a path"), }; let class_name = extract_path_ident(class_name)?; let class_name = opts .js_class() .map(|p| p.0.into()) .unwrap_or_else(|| class_name.to_string()); let kind = ast::MethodKind::Operation(ast::Operation { is_static: false, kind: operation_kind, }); ast::ImportFunctionKind::Method { class: class_name, ty: class.clone(), kind, } } else if let Some(cls) = opts.static_method_of() { let class = opts .js_class() .map(|p| p.0.into()) .unwrap_or_else(|| cls.to_string()); let ty = ident_ty(cls.clone()); let kind = ast::MethodKind::Operation(ast::Operation { is_static: true, kind: operation_kind, }); ast::ImportFunctionKind::Method { class, ty, kind } } else if opts.constructor().is_some() { let class = match js_ret { Some(ref ty) => ty, _ => bail_span!(self, "constructor returns must be bare types"), }; let class_name = match get_ty(class) { syn::Type::Path(syn::TypePath { qself: None, ref path, }) => path, _ => bail_span!(self, "return value of constructor must be a bare path"), }; let class_name = extract_path_ident(class_name)?; let class_name = opts .js_class() .map(|p| p.0.into()) .unwrap_or_else(|| class_name.to_string()); ast::ImportFunctionKind::Method { class: class_name, ty: class.clone(), kind: ast::MethodKind::Constructor, } } else { ast::ImportFunctionKind::Normal }; let shim = { let ns = match kind { ast::ImportFunctionKind::Normal => (0, "n"), ast::ImportFunctionKind::Method { ref class, .. } => (1, &class[..]), }; let data = (ns, &self.sig.ident, module); format!( "__wbg_{}_{}", wasm.name .chars() .filter(|c| c.is_ascii_alphanumeric()) .collect::(), ShortHash(data) ) }; if let Some(span) = opts.r#final() { if opts.structural().is_some() { let msg = "cannot specify both `structural` and `final`"; return Err(Diagnostic::span_error(*span, msg)); } } let assert_no_shim = opts.assert_no_shim().is_some(); let mut doc_comment = String::new(); // Extract the doc comments from our list of attributes. wasm.rust_attrs.retain(|attr| { /// Returns the contents of the passed `#[doc = "..."]` attribute, /// or `None` if it isn't one. fn get_docs(attr: &syn::Attribute) -> Option { if attr.path().is_ident("doc") { if let syn::Meta::NameValue(syn::MetaNameValue { value: syn::Expr::Lit(syn::ExprLit { lit: Lit::Str(str), .. }), .. }) = &attr.meta { Some(str.value()) } else { None } } else { None } } if let Some(docs) = get_docs(attr) { if !doc_comment.is_empty() { // Add newlines between the doc comments doc_comment.push('\n'); } // Add this doc comment to the complete docs doc_comment.push_str(&docs); // Remove it from the list of regular attributes false } else { true } }); let ret = ast::ImportKind::Function(ast::ImportFunction { function: wasm, assert_no_shim, kind, js_ret, catch, variadic, structural: opts.structural().is_some() || opts.r#final().is_none(), rust_name: self.sig.ident, shim: Ident::new(&shim, Span::call_site()), doc_comment, wasm_bindgen: program.wasm_bindgen.clone(), wasm_bindgen_futures: program.wasm_bindgen_futures.clone(), }); opts.check_used(); Ok(ret) } } impl ConvertToAst<(&ast::Program, BindgenAttrs)> for syn::ForeignItemType { type Target = ast::ImportKind; fn convert( self, (program, attrs): (&ast::Program, BindgenAttrs), ) -> Result { let js_name = attrs .js_name() .map(|s| s.0) .map_or_else(|| self.ident.to_string(), |s| s.to_string()); let typescript_type = attrs.typescript_type().map(|s| s.0.to_string()); let is_type_of = attrs.is_type_of().cloned(); let shim = format!("__wbg_instanceof_{}_{}", self.ident, ShortHash(&self.ident)); let mut extends = Vec::new(); let mut vendor_prefixes = Vec::new(); let no_deref = attrs.no_deref().is_some(); for (used, attr) in attrs.attrs.iter() { match attr { BindgenAttr::Extends(_, e) => { extends.push(e.clone()); used.set(true); } BindgenAttr::VendorPrefix(_, e) => { vendor_prefixes.push(e.clone()); used.set(true); } _ => {} } } attrs.check_used(); Ok(ast::ImportKind::Type(ast::ImportType { vis: self.vis, attrs: self.attrs, doc_comment: None, instanceof_shim: shim, is_type_of, rust_name: self.ident, typescript_type, js_name, extends, vendor_prefixes, no_deref, wasm_bindgen: program.wasm_bindgen.clone(), })) } } impl<'a> ConvertToAst<(&ast::Program, BindgenAttrs, &'a Option)> for syn::ForeignItemStatic { type Target = ast::ImportKind; fn convert( self, (program, opts, module): (&ast::Program, BindgenAttrs, &'a Option), ) -> Result { if let syn::StaticMutability::Mut(_) = self.mutability { bail_span!(self.mutability, "cannot import mutable globals yet") } let default_name = self.ident.to_string(); let js_name = opts .js_name() .map(|p| p.0) .unwrap_or(&default_name) .to_string(); let shim = format!( "__wbg_static_accessor_{}_{}", self.ident, ShortHash((&js_name, module, &self.ident)), ); opts.check_used(); Ok(ast::ImportKind::Static(ast::ImportStatic { ty: *self.ty, vis: self.vis, rust_name: self.ident.clone(), js_name, shim: Ident::new(&shim, Span::call_site()), wasm_bindgen: program.wasm_bindgen.clone(), })) } } impl ConvertToAst for syn::ItemFn { type Target = ast::Function; fn convert(self, attrs: BindgenAttrs) -> Result { match self.vis { syn::Visibility::Public(_) => {} _ if attrs.start().is_some() => {} _ => bail_span!(self, "can only #[wasm_bindgen] public functions"), } if self.sig.constness.is_some() { bail_span!( self.sig.constness, "can only #[wasm_bindgen] non-const functions" ); } let ret = function_from_decl( &self.sig.ident, &attrs, self.sig.clone(), self.attrs, self.vis, false, None, false, None, )?; attrs.check_used(); Ok(ret.0) } } pub(crate) fn is_js_keyword(keyword: &str, skip: Option<&[&str]>) -> bool { JS_KEYWORDS .iter() .filter(|keyword| skip.filter(|skip| skip.contains(keyword)).is_none()) .any(|this| *this == keyword) } /// Construct a function (and gets the self type if appropriate) for our AST from a syn function. #[allow(clippy::too_many_arguments)] fn function_from_decl( decl_name: &syn::Ident, opts: &BindgenAttrs, sig: syn::Signature, attrs: Vec, vis: syn::Visibility, allow_self: bool, self_ty: Option<&Ident>, is_from_impl: bool, skip_keywords: Option<&[&str]>, ) -> Result<(ast::Function, Option), Diagnostic> { if sig.variadic.is_some() { bail_span!(sig.variadic, "can't #[wasm_bindgen] variadic functions"); } if !sig.generics.params.is_empty() { bail_span!( sig.generics, "can't #[wasm_bindgen] functions with lifetime or type parameters", ); } assert_no_lifetimes(&sig)?; let syn::Signature { inputs, output, .. } = sig; let replace_self = |t: syn::Type| { let self_ty = match self_ty { Some(i) => i, None => return t, }; let path = match get_ty(&t) { 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, }) }; let replace_colliding_arg = |i: &mut syn::PatType| { if let syn::Pat::Ident(ref mut i) = *i.pat { let ident = i.ident.to_string(); if is_js_keyword(ident.as_str(), skip_keywords) { i.ident = Ident::new(format!("_{}", ident).as_str(), i.ident.span()); } } }; let mut method_self = None; let arguments = inputs .into_iter() .filter_map(|arg| match arg { syn::FnArg::Typed(mut c) => { replace_colliding_arg(&mut c); c.ty = Box::new(replace_self(*c.ty)); Some(c) } syn::FnArg::Receiver(r) => { if !allow_self { panic!("arguments cannot be `self`") } assert!(method_self.is_none()); if r.reference.is_none() { method_self = Some(ast::MethodSelf::ByValue); } else if r.mutability.is_some() { method_self = Some(ast::MethodSelf::RefMutable); } else { method_self = Some(ast::MethodSelf::RefShared); } None } }) .collect::>(); let ret = match output { syn::ReturnType::Default => None, syn::ReturnType::Type(_, ty) => Some(replace_self(*ty)), }; let (name, name_span, renamed_via_js_name) = if let Some((js_name, js_name_span)) = opts.js_name() { let kind = operation_kind(opts); let prefix = match kind { OperationKind::Setter(_) => "set_", _ => "", }; let name = if prefix.is_empty() && opts.method().is_none() && is_js_keyword(js_name, skip_keywords) { format!("_{}", js_name) } else { format!("{}{}", prefix, js_name) }; (name, js_name_span, true) } else { let name = if !is_from_impl && opts.method().is_none() && is_js_keyword(&decl_name.to_string(), skip_keywords) { format!("_{}", decl_name) } else { decl_name.to_string() }; (name, decl_name.span(), false) }; Ok(( ast::Function { arguments, name_span, name, renamed_via_js_name, ret, rust_attrs: attrs, rust_vis: vis, r#unsafe: sig.unsafety.is_some(), r#async: sig.asyncness.is_some(), generate_typescript: opts.skip_typescript().is_none(), generate_jsdoc: opts.skip_jsdoc().is_none(), variadic: opts.variadic().is_some(), }, method_self, )) } pub(crate) trait MacroParse { /// Parse the contents of an object into our AST, with a context if necessary. /// /// The context is used to have access to the attributes on `#[wasm_bindgen]`, and to allow /// writing to the output `TokenStream`. fn macro_parse(self, program: &mut ast::Program, context: Ctx) -> Result<(), Diagnostic>; } impl<'a> MacroParse<(Option, &'a mut TokenStream)> for syn::Item { fn macro_parse( self, program: &mut ast::Program, (opts, tokens): (Option, &'a mut TokenStream), ) -> Result<(), Diagnostic> { match self { syn::Item::Fn(mut f) => { let opts = opts.unwrap_or_default(); if let Some(path) = opts.wasm_bindgen() { program.wasm_bindgen = path.clone(); } if let Some(path) = opts.wasm_bindgen_futures() { program.wasm_bindgen_futures = path.clone(); } if opts.main().is_some() { opts.check_used(); return main(program, f, tokens); } let no_mangle = f .attrs .iter() .enumerate() .find(|(_, m)| m.path().is_ident("no_mangle")); if let Some((i, _)) = no_mangle { f.attrs.remove(i); } let comments = extract_doc_comments(&f.attrs); // If the function isn't used for anything other than being exported to JS, // it'll be unused when not building for the wasm target and produce a // `dead_code` warning. So, add `#[allow(dead_code)]` before it to avoid that. tokens.extend(quote::quote! { #[allow(dead_code)] }); f.to_tokens(tokens); if opts.start().is_some() { if !f.sig.generics.params.is_empty() { bail_span!(&f.sig.generics, "the start function cannot have generics",); } if !f.sig.inputs.is_empty() { bail_span!(&f.sig.inputs, "the start function cannot have arguments",); } } let method_kind = ast::MethodKind::Operation(ast::Operation { is_static: true, kind: operation_kind(&opts), }); let rust_name = f.sig.ident.clone(); let start = opts.start().is_some(); program.exports.push(ast::Export { comments, function: f.convert(opts)?, js_class: None, method_kind, method_self: None, rust_class: None, rust_name, start, wasm_bindgen: program.wasm_bindgen.clone(), wasm_bindgen_futures: program.wasm_bindgen_futures.clone(), }); } syn::Item::Struct(mut s) => { let opts = opts.unwrap_or_default(); program.structs.push((&mut s).convert((program, opts))?); s.to_tokens(tokens); } syn::Item::Impl(mut i) => { let opts = opts.unwrap_or_default(); (&mut i).macro_parse(program, opts)?; i.to_tokens(tokens); } syn::Item::ForeignMod(mut f) => { let opts = match opts { Some(opts) => opts, None => BindgenAttrs::find(&mut f.attrs)?, }; f.macro_parse(program, opts)?; } syn::Item::Enum(mut e) => { let opts = match opts { Some(opts) => opts, None => BindgenAttrs::find(&mut e.attrs)?, }; e.macro_parse(program, (tokens, opts))?; } syn::Item::Const(mut c) => { let opts = match opts { Some(opts) => opts, None => BindgenAttrs::find(&mut c.attrs)?, }; c.macro_parse(program, opts)?; } _ => { bail_span!( self, "#[wasm_bindgen] can only be applied to a function, \ struct, enum, impl, or extern block", ); } } Ok(()) } } impl<'a> MacroParse for &'a mut syn::ItemImpl { fn macro_parse(self, program: &mut ast::Program, opts: BindgenAttrs) -> Result<(), Diagnostic> { if self.defaultness.is_some() { bail_span!( self.defaultness, "#[wasm_bindgen] default impls are not supported" ); } if self.unsafety.is_some() { bail_span!( self.unsafety, "#[wasm_bindgen] unsafe impls are not supported" ); } if let Some((_, path, _)) = &self.trait_ { bail_span!(path, "#[wasm_bindgen] trait impls are not supported"); } if !self.generics.params.is_empty() { bail_span!( self.generics, "#[wasm_bindgen] generic impls aren't supported" ); } let name = match get_ty(&self.self_ty) { syn::Type::Path(syn::TypePath { qself: None, ref path, }) => path, _ => bail_span!( self.self_ty, "unsupported self type in #[wasm_bindgen] impl" ), }; let mut errors = Vec::new(); for item in self.items.iter_mut() { if let Err(e) = prepare_for_impl_recursion(item, name, program, &opts) { errors.push(e); } } Diagnostic::from_vec(errors)?; opts.check_used(); Ok(()) } } // Prepare for recursion into an `impl` block. Here we want to attach an // internal attribute, `__wasm_bindgen_class_marker`, with any metadata we need // to pass from the impl to the impl item. Recursive macro expansion will then // expand the `__wasm_bindgen_class_marker` attribute. // // Note that we currently do this because inner items may have things like cfgs // on them, so we want to expand the impl first, let the insides get cfg'd, and // then go for the rest. fn prepare_for_impl_recursion( item: &mut syn::ImplItem, class: &syn::Path, program: &ast::Program, impl_opts: &BindgenAttrs, ) -> Result<(), Diagnostic> { let method = match item { syn::ImplItem::Fn(m) => m, syn::ImplItem::Const(_) => { bail_span!( &*item, "const definitions aren't supported with #[wasm_bindgen]" ); } syn::ImplItem::Type(_) => bail_span!( &*item, "type definitions in impls aren't supported with #[wasm_bindgen]" ), syn::ImplItem::Macro(_) => { // In theory we want to allow this, but we have no way of expanding // the macro and then placing our magical attributes on the expanded // functions. As a result, just disallow it for now to hopefully // ward off buggy results from this macro. bail_span!(&*item, "macros in impls aren't supported"); } syn::ImplItem::Verbatim(_) => panic!("unparsed impl item?"), other => bail_span!(other, "failed to parse this item as a known item"), }; let ident = extract_path_ident(class)?; let js_class = impl_opts .js_class() .map(|s| s.0.to_string()) .unwrap_or(ident.to_string()); let wasm_bindgen = &program.wasm_bindgen; let wasm_bindgen_futures = &program.wasm_bindgen_futures; method.attrs.insert( 0, syn::Attribute { pound_token: Default::default(), style: syn::AttrStyle::Outer, bracket_token: Default::default(), meta: syn::parse_quote! { #wasm_bindgen::prelude::__wasm_bindgen_class_marker(#class = #js_class, wasm_bindgen = #wasm_bindgen, wasm_bindgen_futures = #wasm_bindgen_futures) }, }, ); Ok(()) } impl<'a> MacroParse<&ClassMarker> for &'a mut syn::ImplItemFn { fn macro_parse( self, program: &mut ast::Program, ClassMarker { class, js_class, wasm_bindgen, wasm_bindgen_futures, }: &ClassMarker, ) -> Result<(), Diagnostic> { program.wasm_bindgen = wasm_bindgen.clone(); program.wasm_bindgen_futures = wasm_bindgen_futures.clone(); match self.vis { syn::Visibility::Public(_) => {} _ => return Ok(()), } if self.defaultness.is_some() { panic!("default methods are not supported"); } if self.sig.constness.is_some() { bail_span!( self.sig.constness, "can only #[wasm_bindgen] non-const functions", ); } let opts = BindgenAttrs::find(&mut self.attrs)?; let comments = extract_doc_comments(&self.attrs); let (function, method_self) = function_from_decl( &self.sig.ident, &opts, self.sig.clone(), self.attrs.clone(), self.vis.clone(), true, Some(class), true, None, )?; let method_kind = if opts.constructor().is_some() { ast::MethodKind::Constructor } else { let is_static = method_self.is_none(); let kind = operation_kind(&opts); ast::MethodKind::Operation(ast::Operation { is_static, kind }) }; program.exports.push(ast::Export { comments, function, js_class: Some(js_class.to_string()), method_kind, method_self, rust_class: Some(class.clone()), rust_name: self.sig.ident.clone(), start: false, wasm_bindgen: program.wasm_bindgen.clone(), wasm_bindgen_futures: program.wasm_bindgen_futures.clone(), }); opts.check_used(); Ok(()) } } fn import_enum(enum_: syn::ItemEnum, program: &mut ast::Program) -> Result<(), Diagnostic> { let mut variants = vec![]; let mut variant_values = vec![]; for v in enum_.variants.iter() { match v.fields { syn::Fields::Unit => (), _ => bail_span!(v.fields, "only C-Style enums allowed with #[wasm_bindgen]"), } let (_, expr) = match &v.discriminant { Some(pair) => pair, None => { bail_span!(v, "all variants must have a value"); } }; match get_expr(expr) { syn::Expr::Lit(syn::ExprLit { attrs: _, lit: syn::Lit::Str(str_lit), }) => { variants.push(v.ident.clone()); variant_values.push(str_lit.value()); } expr => bail_span!( expr, "enums with #[wasm_bindgen] cannot mix string and non-string values", ), } } program.imports.push(ast::Import { module: None, js_namespace: None, kind: ast::ImportKind::Enum(ast::ImportEnum { vis: enum_.vis, name: enum_.ident, variants, variant_values, rust_attrs: enum_.attrs, wasm_bindgen: program.wasm_bindgen.clone(), }), }); Ok(()) } impl<'a> MacroParse<(&'a mut TokenStream, BindgenAttrs)> for syn::ItemEnum { fn macro_parse( self, program: &mut ast::Program, (tokens, opts): (&'a mut TokenStream, BindgenAttrs), ) -> Result<(), Diagnostic> { if self.variants.is_empty() { bail_span!(self, "cannot export empty enums to JS"); } let generate_typescript = opts.skip_typescript().is_none(); // Check if the first value is a string literal if let Some((_, expr)) = &self.variants[0].discriminant { if let syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(_), .. }) = get_expr(expr) { opts.check_used(); return import_enum(self, program); } } let js_name = opts .js_name() .map(|s| s.0) .map_or_else(|| self.ident.to_string(), |s| s.to_string()); opts.check_used(); let has_discriminant = self.variants[0].discriminant.is_some(); match self.vis { syn::Visibility::Public(_) => {} _ => bail_span!(self, "only public enums are allowed with #[wasm_bindgen]"), } let variants = self .variants .iter() .enumerate() .map(|(i, v)| { match v.fields { syn::Fields::Unit => (), _ => bail_span!(v.fields, "only C-Style enums allowed with #[wasm_bindgen]"), } // Require that everything either has a discriminant or doesn't. // We don't really want to get in the business of emulating how // rustc assigns values to enums. if v.discriminant.is_some() != has_discriminant { bail_span!( v, "must either annotate discriminant of all variants or none" ); } let value = match &v.discriminant { Some((_, expr)) => match get_expr(expr) { syn::Expr::Lit(syn::ExprLit { attrs: _, lit: syn::Lit::Int(int_lit), }) => match int_lit.base10_digits().parse::() { Ok(v) => v, Err(_) => { bail_span!( int_lit, "enums with #[wasm_bindgen] can only support \ numbers that can be represented as u32" ); } }, expr => bail_span!( expr, "enums with #[wasm_bindgen] may only have \ number literal values", ), }, None => i as u32, }; let comments = extract_doc_comments(&v.attrs); Ok(ast::Variant { name: v.ident.clone(), value, comments, }) }) .collect::, Diagnostic>>()?; let mut values = variants.iter().map(|v| v.value).collect::>(); values.sort(); let hole = values .windows(2) .find_map(|window| { if window[0] + 1 != window[1] { Some(window[0] + 1) } else { None } }) .unwrap_or(*values.last().unwrap() + 1); for value in values { assert!(hole != value); } let comments = extract_doc_comments(&self.attrs); self.to_tokens(tokens); program.enums.push(ast::Enum { rust_name: self.ident, js_name, variants, comments, hole, generate_typescript, wasm_bindgen: program.wasm_bindgen.clone(), }); Ok(()) } } impl MacroParse for syn::ItemConst { fn macro_parse(self, program: &mut ast::Program, opts: BindgenAttrs) -> Result<(), Diagnostic> { // Shortcut if opts.typescript_custom_section().is_none() { bail_span!(self, "#[wasm_bindgen] will not work on constants unless you are defining a #[wasm_bindgen(typescript_custom_section)]."); } match get_expr(&self.expr) { syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(litstr), .. }) => { program.typescript_custom_sections.push(litstr.value()); } expr => { bail_span!(expr, "Expected a string literal to be used with #[wasm_bindgen(typescript_custom_section)]."); } } opts.check_used(); Ok(()) } } impl MacroParse for syn::ItemForeignMod { fn macro_parse(self, program: &mut ast::Program, opts: BindgenAttrs) -> Result<(), Diagnostic> { let mut errors = Vec::new(); if let Some(other) = self.abi.name.filter(|l| l.value() != "C") { errors.push(err_span!( other, "only foreign mods with the `C` ABI are allowed" )); } let js_namespace = opts.js_namespace().map(|(s, _)| s.to_owned()); let module = module_from_opts(program, &opts) .map_err(|e| errors.push(e)) .unwrap_or_default(); for item in self.items.into_iter() { let ctx = ForeignItemCtx { module: module.clone(), js_namespace: js_namespace.clone(), }; if let Err(e) = item.macro_parse(program, ctx) { errors.push(e); } } Diagnostic::from_vec(errors)?; opts.check_used(); Ok(()) } } struct ForeignItemCtx { module: Option, js_namespace: Option>, } impl MacroParse for syn::ForeignItem { fn macro_parse( mut self, program: &mut ast::Program, ctx: ForeignItemCtx, ) -> Result<(), Diagnostic> { let item_opts = { let attrs = match self { syn::ForeignItem::Fn(ref mut f) => &mut f.attrs, syn::ForeignItem::Type(ref mut t) => &mut t.attrs, syn::ForeignItem::Static(ref mut s) => &mut s.attrs, _ => panic!("only foreign functions/types allowed for now"), }; BindgenAttrs::find(attrs)? }; let js_namespace = item_opts .js_namespace() .map(|(s, _)| s.to_owned()) .or(ctx.js_namespace); let module = ctx.module; let kind = match self { syn::ForeignItem::Fn(f) => f.convert((program, item_opts, &module))?, syn::ForeignItem::Type(t) => t.convert((program, item_opts))?, syn::ForeignItem::Static(s) => s.convert((program, item_opts, &module))?, _ => panic!("only foreign functions/types allowed for now"), }; program.imports.push(ast::Import { module, js_namespace, kind, }); Ok(()) } } pub fn module_from_opts( program: &mut ast::Program, opts: &BindgenAttrs, ) -> Result, Diagnostic> { if let Some(path) = opts.wasm_bindgen() { program.wasm_bindgen = path.clone(); } if let Some(path) = opts.wasm_bindgen_futures() { program.wasm_bindgen_futures = path.clone(); } let mut errors = Vec::new(); let module = if let Some((name, span)) = opts.module() { if opts.inline_js().is_some() { let msg = "cannot specify both `module` and `inline_js`"; errors.push(Diagnostic::span_error(span, msg)); } if opts.raw_module().is_some() { let msg = "cannot specify both `module` and `raw_module`"; errors.push(Diagnostic::span_error(span, msg)); } Some(ast::ImportModule::Named(name.to_string(), span)) } else if let Some((name, span)) = opts.raw_module() { if opts.inline_js().is_some() { let msg = "cannot specify both `raw_module` and `inline_js`"; errors.push(Diagnostic::span_error(span, msg)); } Some(ast::ImportModule::RawNamed(name.to_string(), span)) } else if let Some((js, span)) = opts.inline_js() { let i = program.inline_js.len(); program.inline_js.push(js.to_string()); Some(ast::ImportModule::Inline(i, span)) } else { None }; Diagnostic::from_vec(errors)?; Ok(module) } /// Get the first type parameter of a generic type, errors on incorrect input. fn extract_first_ty_param(ty: Option<&syn::Type>) -> Result, Diagnostic> { let t = match ty { Some(t) => t, None => return Ok(None), }; let path = match *get_ty(t) { syn::Type::Path(syn::TypePath { qself: None, ref path, }) => path, _ => bail_span!(t, "must be Result<...>"), }; let seg = path .segments .last() .ok_or_else(|| err_span!(t, "must have at least one segment"))?; let generics = match seg.arguments { syn::PathArguments::AngleBracketed(ref t) => t, _ => bail_span!(t, "must be Result<...>"), }; let generic = generics .args .first() .ok_or_else(|| err_span!(t, "must have at least one generic parameter"))?; let ty = match generic { syn::GenericArgument::Type(t) => t, other => bail_span!(other, "must be a type parameter"), }; match get_ty(ty) { syn::Type::Tuple(t) if t.elems.is_empty() => return Ok(None), _ => {} } Ok(Some(ty.clone())) } /// Extract the documentation comments from a Vec of attributes fn extract_doc_comments(attrs: &[syn::Attribute]) -> Vec { attrs .iter() .filter_map(|a| { // if the path segments include an ident of "doc" we know this // this is a doc comment if a.path().segments.iter().any(|s| s.ident == "doc") { let tokens = match &a.meta { syn::Meta::Path(_) => None, syn::Meta::List(list) => Some(list.tokens.clone()), syn::Meta::NameValue(name_value) => Some(name_value.value.to_token_stream()), }; Some( // We want to filter out any Puncts so just grab the Literals tokens.into_iter().flatten().filter_map(|t| match t { TokenTree::Literal(lit) => { let quoted = lit.to_string(); Some(try_unescape("ed).unwrap_or(quoted)) } _ => None, }), ) } else { None } }) //Fold up the [[String]] iter we created into Vec .fold(vec![], |mut acc, a| { acc.extend(a); acc }) } // Unescapes a quoted string. char::escape_debug() was used to escape the text. fn try_unescape(mut s: &str) -> Option { s = s.strip_prefix('"').unwrap_or(s); s = s.strip_suffix('"').unwrap_or(s); let mut result = String::with_capacity(s.len()); let mut chars = s.chars(); while let Some(c) = chars.next() { 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); } } Some(result) } fn unescape_unicode(chars: &mut Chars) -> Option<(char, char)> { let mut value = 0; for (i, c) in chars.enumerate() { match (i, c.to_digit(16)) { (0..=5, Some(num)) => value = (value << 4) | num, (1.., None) => return Some((char::from_u32(value)?, c)), _ => break, } } None } /// Check there are no lifetimes on the function. fn assert_no_lifetimes(sig: &syn::Signature) -> Result<(), Diagnostic> { struct Walk { diagnostics: Vec, } impl<'ast> syn::visit::Visit<'ast> for Walk { fn visit_lifetime(&mut self, i: &'ast syn::Lifetime) { self.diagnostics.push(err_span!( i, "it is currently not sound to use lifetimes in function \ signatures" )); } } let mut walk = Walk { diagnostics: Vec::new(), }; syn::visit::Visit::visit_signature(&mut walk, sig); Diagnostic::from_vec(walk.diagnostics) } /// Extracts the last ident from the path fn extract_path_ident(path: &syn::Path) -> Result { for segment in path.segments.iter() { match segment.arguments { syn::PathArguments::None => {} _ => bail_span!(path, "paths with type parameters are not supported yet"), } } match path.segments.last() { Some(value) => Ok(value.ident.clone()), None => { bail_span!(path, "empty idents are not supported"); } } } pub fn reset_attrs_used() { ATTRS.with(|state| { state.parsed.set(0); state.checks.set(0); state.unused_attrs.borrow_mut().clear(); }) } pub fn check_unused_attrs(tokens: &mut TokenStream) { ATTRS.with(|state| { assert_eq!(state.parsed.get(), state.checks.get()); let unused_attrs = &*state.unused_attrs.borrow(); if !unused_attrs.is_empty() { tokens.extend(quote::quote! { // Anonymous scope to prevent name clashes. const _: () = { #(let #unused_attrs: ();)* }; }); } }) } fn operation_kind(opts: &BindgenAttrs) -> ast::OperationKind { let mut operation_kind = ast::OperationKind::Regular; if let Some(g) = opts.getter() { operation_kind = ast::OperationKind::Getter(g.clone()); } if let Some(s) = opts.setter() { operation_kind = ast::OperationKind::Setter(s.clone()); } if opts.indexing_getter().is_some() { operation_kind = ast::OperationKind::IndexingGetter; } if opts.indexing_setter().is_some() { operation_kind = ast::OperationKind::IndexingSetter; } if opts.indexing_deleter().is_some() { operation_kind = ast::OperationKind::IndexingDeleter; } operation_kind } pub fn link_to(opts: BindgenAttrs) -> Result { let mut program = ast::Program::default(); let module = module_from_opts(&mut program, &opts)?.ok_or_else(|| { Diagnostic::span_error(Span::call_site(), "`link_to!` requires a module.") })?; if let ast::ImportModule::Named(p, s) | ast::ImportModule::RawNamed(p, s) = &module { if !p.starts_with("./") && !p.starts_with("../") && !p.starts_with('/') { return Err(Diagnostic::span_error( *s, "`link_to!` does not support module paths.", )); } } opts.enforce_used()?; program.linked_modules.push(module); Ok(ast::LinkToModule(program)) } fn main(program: &ast::Program, mut f: ItemFn, tokens: &mut TokenStream) -> Result<(), Diagnostic> { if f.sig.ident != "main" { bail_span!(&f.sig.ident, "the main function has to be called main"); } if let Some(constness) = f.sig.constness { bail_span!(&constness, "the main function cannot be const"); } if !f.sig.generics.params.is_empty() { bail_span!(&f.sig.generics, "the main function cannot have generics"); } if !f.sig.inputs.is_empty() { bail_span!(&f.sig.inputs, "the main function cannot have arguments"); } let r#return = f.sig.output; f.sig.output = ReturnType::Default; let body = f.block; let wasm_bindgen = &program.wasm_bindgen; let wasm_bindgen_futures = &program.wasm_bindgen_futures; if f.sig.asyncness.take().is_some() { f.block = Box::new( syn::parse2(quote::quote! { { async fn __wasm_bindgen_generated_main() #r#return #body #wasm_bindgen_futures::spawn_local( async move { use #wasm_bindgen::__rt::Main; let __ret = __wasm_bindgen_generated_main(); (&mut &mut &mut #wasm_bindgen::__rt::MainWrapper(Some(__ret.await))).__wasm_bindgen_main() }, ) } }) .unwrap(), ); } else { f.block = Box::new( syn::parse2(quote::quote! { { fn __wasm_bindgen_generated_main() #r#return #body use #wasm_bindgen::__rt::Main; let __ret = __wasm_bindgen_generated_main(); (&mut &mut &mut #wasm_bindgen::__rt::MainWrapper(Some(__ret))).__wasm_bindgen_main() } }) .unwrap(), ); } f.to_tokens(tokens); Ok(()) } #[cfg(test)] mod tests { #[test] fn test_try_unescape() { use super::try_unescape; assert_eq!(try_unescape("hello").unwrap(), "hello"); assert_eq!(try_unescape("\"hello").unwrap(), "hello"); assert_eq!(try_unescape("hello\"").unwrap(), "hello"); assert_eq!(try_unescape("\"hello\"").unwrap(), "hello"); assert_eq!(try_unescape("hello\\\\").unwrap(), "hello\\"); assert_eq!(try_unescape("hello\\n").unwrap(), "hello\n"); assert_eq!(try_unescape("hello\\u"), None); assert_eq!(try_unescape("hello\\u{"), None); assert_eq!(try_unescape("hello\\u{}"), None); assert_eq!(try_unescape("hello\\u{0}").unwrap(), "hello\0"); assert_eq!(try_unescape("hello\\u{000000}").unwrap(), "hello\0"); assert_eq!(try_unescape("hello\\u{0000000}"), None); } }