| 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<usize>, |
| checks: Cell<usize>, |
| unused_attrs: RefCell<Vec<Ident>>, |
| } |
| |
| /// Parsed attributes from a `#[wasm_bindgen(..)]`. |
| #[cfg_attr(feature = "extra-traits", derive(Debug))] |
| pub struct BindgenAttrs { |
| /// List of parsed attributes |
| pub attrs: Vec<(Cell<bool>, 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<String>, Vec<Span>)), |
| (module, Module(Span, String, Span)), |
| (raw_module, RawModule(Span, String, Span)), |
| (inline_js, InlineJs(Span, String, Span)), |
| (getter, Getter(Span, Option<Ident>)), |
| (setter, Setter(Span, Option<Ident>)), |
| (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<String>, Vec<Span>)) => { |
| 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<syn::Attribute>) -> Result<BindgenAttrs, Diagnostic> { |
| 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<Self> { |
| 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<Self> { |
| 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::<Token![=]>()?; |
| let ident = input.parse::<AnyIdent>()?.0; |
| return Ok(BindgenAttr::$variant(attr_span, ident)) |
| }); |
| |
| (@parser $variant:ident(Span, Option<Ident>)) => ({ |
| if input.parse::<Token![=]>().is_ok() { |
| let ident = input.parse::<AnyIdent>()?.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::<Token![=]>()?; |
| return Ok(BindgenAttr::$variant(attr_span, input.parse()?)); |
| }); |
| |
| (@parser $variant:ident(Span, syn::Expr)) => ({ |
| input.parse::<Token![=]>()?; |
| return Ok(BindgenAttr::$variant(attr_span, input.parse()?)); |
| }); |
| |
| (@parser $variant:ident(Span, String, Span)) => ({ |
| input.parse::<Token![=]>()?; |
| let (val, span) = match input.parse::<syn::LitStr>() { |
| Ok(str) => (str.value(), str.span()), |
| Err(_) => { |
| let ident = input.parse::<AnyIdent>()?.0; |
| (ident.to_string(), ident.span()) |
| } |
| }; |
| return Ok(BindgenAttr::$variant(attr_span, val, span)) |
| }); |
| |
| (@parser $variant:ident(Span, Vec<String>, Vec<Span>)) => ({ |
| input.parse::<Token![=]>()?; |
| let (vals, spans) = match input.parse::<syn::ExprArray>() { |
| 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::<AnyIdent>()?.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<Self> { |
| 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<Ctx> { |
| /// 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<Self::Target, Diagnostic>; |
| } |
| |
| impl<'a> ConvertToAst<(&ast::Program, BindgenAttrs)> for &'a mut syn::ItemStruct { |
| type Target = ast::Struct; |
| |
| fn convert( |
| self, |
| (program, attrs): (&ast::Program, BindgenAttrs), |
| ) -> Result<Self::Target, Diagnostic> { |
| 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<String> = 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<ast::ImportModule>)> |
| for syn::ForeignItemFn |
| { |
| type Target = ast::ImportKind; |
| |
| fn convert( |
| self, |
| (program, opts, module): (&ast::Program, BindgenAttrs, &'a Option<ast::ImportModule>), |
| ) -> Result<Self::Target, Diagnostic> { |
| 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::<String>(), |
| 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<String> { |
| 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<Self::Target, Diagnostic> { |
| 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<ast::ImportModule>)> |
| for syn::ForeignItemStatic |
| { |
| type Target = ast::ImportKind; |
| |
| fn convert( |
| self, |
| (program, opts, module): (&ast::Program, BindgenAttrs, &'a Option<ast::ImportModule>), |
| ) -> Result<Self::Target, Diagnostic> { |
| 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<BindgenAttrs> for syn::ItemFn { |
| type Target = ast::Function; |
| |
| fn convert(self, attrs: BindgenAttrs) -> Result<Self::Target, Diagnostic> { |
| 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<syn::Attribute>, |
| vis: syn::Visibility, |
| allow_self: bool, |
| self_ty: Option<&Ident>, |
| is_from_impl: bool, |
| skip_keywords: Option<&[&str]>, |
| ) -> Result<(ast::Function, Option<ast::MethodSelf>), 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::<Vec<_>>(); |
| |
| 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<Ctx> { |
| /// 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<BindgenAttrs>, &'a mut TokenStream)> for syn::Item { |
| fn macro_parse( |
| self, |
| program: &mut ast::Program, |
| (opts, tokens): (Option<BindgenAttrs>, &'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<BindgenAttrs> 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::<u32>() { |
| 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::<Result<Vec<_>, Diagnostic>>()?; |
| |
| let mut values = variants.iter().map(|v| v.value).collect::<Vec<_>>(); |
| 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<BindgenAttrs> 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<BindgenAttrs> 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<ast::ImportModule>, |
| js_namespace: Option<Vec<String>>, |
| } |
| |
| impl MacroParse<ForeignItemCtx> 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<Option<ast::ImportModule>, 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<Option<syn::Type>, 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<String> { |
| 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<String> |
| .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<String> { |
| 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<Diagnostic>, |
| } |
| |
| 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<Ident, Diagnostic> { |
| 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<ast::LinkToModule, Diagnostic> { |
| 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); |
| } |
| } |