use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use std::str::Chars;
use std::{char, iter};
use ast::OperationKind;
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::visit_mut::VisitMut;
use syn::Token;
use syn::{ItemFn, Lit, MacroDelimiter, ReturnType};
use wasm_bindgen_shared::identifier::{is_js_keyword, is_non_value_js_keyword, is_valid_ident};
use crate::ast::{self, ThreadLocal};
use crate::hash::ShortHash;
use crate::ClassMarker;
use crate::Diagnostic;
thread_local!(static ATTRS: AttributeParseState = Default::default());
fn check_js_comment_close(str: &str, span: Span) -> Result<(), Diagnostic> {
if str.contains("*/") {
Err(Diagnostic::span_error(
span,
"contains comment close syntax",
))
} else {
Ok(())
}
}
fn check_invalid_type(str: &str, span: Span) -> Result<(), Diagnostic> {
if is_js_keyword(str) {
return Err(Diagnostic::span_error(span, "collides with JS keyword"));
}
check_js_comment_close(str, span)?;
Ok(())
}
#[derive(Default)]
struct AttributeParseState {
parsed: Cell<usize>,
checks: Cell<usize>,
unused_attrs: RefCell<Vec<UnusedState>>,
conflicting_attrs: RefCell<Vec<ConflictState>>,
}
struct UnusedState {
error: bool,
ident: Ident,
}
struct ConflictState {
ignored: Ident,
used: &'static str,
}
#[cfg_attr(feature = "extra-traits", derive(Debug))]
pub struct BindgenAttrs {
pub attrs: Vec<(Cell<bool>, BindgenAttr)>,
}
#[cfg_attr(feature = "extra-traits", derive(Debug))]
#[derive(Clone)]
pub struct JsNamespace(Vec<String>);
macro_rules! attrgen {
($mac:ident) => {
$mac! {
(catch, false, Catch(Span)),
(constructor, false, Constructor(Span)),
(method, false, Method(Span)),
(r#this, false, This(Span)),
(static_method_of, false, StaticMethodOf(Span, syn::TypePath)),
(js_namespace, false, JsNamespace(Span, JsNamespace, Vec<Span>)),
(module, true, Module(Span, String, Span)),
(raw_module, true, RawModule(Span, String, Span)),
(inline_js, true, InlineJs(Span, String, Span)),
(getter, false, Getter(Span, Option<String>)),
(setter, false, Setter(Span, Option<String>)),
(indexing_getter, false, IndexingGetter(Span)),
(indexing_setter, false, IndexingSetter(Span)),
(indexing_deleter, false, IndexingDeleter(Span)),
(structural, false, Structural(Span)),
(r#final, false, Final(Span)),
(readonly, false, Readonly(Span)),
(js_name, false, JsName(Span, String, Span)),
(js_class, false, JsClass(Span, String, Span)),
(reexport, false, Reexport(Span, Option<String>)),
(inspectable, false, Inspectable(Span)),
(is_type_of, false, IsTypeOf(Span, syn::Expr)),
(extends, false, Extends(Span, syn::Path)),
(extends_js_class, false, ExtendsJsClass(Span, String, Span)),
(extends_js_namespace, false, ExtendsJsNamespace(Span, JsNamespace, Vec<Span>)),
(no_deref, false, NoDeref(Span)),
(no_upcast, false, NoUpcast(Span)),
(no_promising, false, NoPromising(Span)),
(no_into_js_generic, false, NoIntoJsGeneric(Span)),
(vendor_prefix, false, VendorPrefix(Span, Ident)),
(variadic, false, Variadic(Span)),
(typescript_custom_section, false, TypescriptCustomSection(Span)),
(skip_typescript, false, SkipTypescript(Span)),
(skip_jsdoc, false, SkipJsDoc(Span)),
(private, false, Hide(Span)),
(fallback, false, Fallback(Span)),
(main, false, Main(Span)),
(start, false, Start(Span)),
(experimental_tokio, false, ExperimentalTokio(Span, Option<String>)),
(wasm_bindgen, false, WasmBindgen(Span, syn::Path)),
(js_sys, false, JsSys(Span, syn::Path)),
(wasm_bindgen_futures, false, WasmBindgenFutures(Span, syn::Path)),
(skip, false, Skip(Span)),
(slice_to_array, false, SliceToArray(Span)),
(slice, false, Slice(Span, Ident)),
(vec, false, Vec(Span, Ident)),
(typescript_type, false, TypeScriptType(Span, String, Span)),
(getter_with_clone, false, GetterWithClone(Span)),
(static_string, false, StaticString(Span)),
(thread_local, false, ThreadLocal(Span)),
(thread_local_v2, false, ThreadLocalV2(Span)),
(unchecked_return_type, true, ReturnType(Span, String, Span)),
(return_description, true, ReturnDesc(Span, String, Span)),
(unchecked_param_type, true, ParamType(Span, String, Span)),
(unchecked_optional_param_type, true, OptionalParamType(Span, String, Span)),
(param_description, true, ParamDesc(Span, String, Span)),
(experimental_generic_mono, true, GenericPerMono(Span)),
(assert_no_shim, false, AssertNoShim(Span)),
(jspi, false, Jspi(Span)),
(suspending, false, Suspending(Span)),
}
};
}
macro_rules! methods {
($(($name:ident, $invalid_unused:literal, $variant:ident($($contents:tt)*)),)*) => {
$(methods!(@method $name, $variant($($contents)*));)*
fn enforce_used(self) -> Result<(), Diagnostic> {
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) {
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, ..) => {
UnusedState {
error: $invalid_unused,
ident: syn::parse_quote_spanned!(*span => $name)
}
})*
}
})
);
});
}
};
(@method $name:ident, $variant:ident(Span, String, Span)) => {
pub(crate) 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, JsNamespace, Vec<Span>)) => {
pub(crate) fn $name(&self) -> Option<(JsNamespace, &[Span])> {
self.attrs
.iter()
.find_map(|a| match &a.1 {
BindgenAttr::$variant(_, ss, spans) => {
a.0.set(true);
Some((ss.clone(), &spans[..]))
}
_ => None,
})
}
};
(@method $name:ident, $variant:ident(Span, $($other:tt)*)) => {
#[allow(unused)]
pub(crate) 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)]
pub(crate) 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 {
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 => break,
};
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();
}
for (_, attr) in &ret.attrs {
if let BindgenAttr::JsName(_, value, span) = attr {
validate_computed_key("js_name", value, *span)?;
}
}
Ok(ret)
}
fn get_thread_local(&self) -> Result<Option<ThreadLocal>, Diagnostic> {
let mut thread_local = self.thread_local_v2().map(|_| ThreadLocal::V2);
if let Some(span) = self.thread_local() {
if thread_local.is_some() {
return Err(Diagnostic::span_error(
*span,
"`thread_local` can't be used with `thread_local_v2`",
));
} else {
thread_local = Some(ThreadLocal::V1)
}
}
Ok(thread_local)
}
fn js_name_no_symbol(&self, position: &str) -> Result<Option<&str>, Diagnostic> {
match self.js_name() {
Some((value, span)) if is_computed_key(value) => Err(Diagnostic::span_error(
span,
format!("{position} do not support symbols in js_name"),
)),
Some((value, _)) => Ok(Some(value)),
None => Ok(None),
}
}
attrgen!(methods);
}
impl Default for BindgenAttrs {
fn default() -> BindgenAttrs {
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, $_:literal, $($variants:tt)*),)*) => {
#[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, $_:literal, $($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 = if input.peek(syn::LitStr) {
let litstr = input.parse::<syn::LitStr>()?;
syn::parse_str::<Ident>(&litstr.value()).map_err(|e| {
syn::Error::new(litstr.span(), format!("expected an identifier: {e}"))
})?
} else {
input.parse::<AnyIdent>()?.0
};
return Ok(BindgenAttr::$variant(attr_span, ident))
});
(@parser $variant:ident(Span, syn::TypePath)) => ({
input.parse::<Token![=]>()?;
let path = if input.peek(syn::LitStr) {
let litstr = input.parse::<syn::LitStr>()?;
syn::parse_str::<syn::TypePath>(&litstr.value()).map_err(|e| {
syn::Error::new(litstr.span(), format!("expected a type path: {e}"))
})?
} else {
input.parse()?
};
return Ok(BindgenAttr::$variant(attr_span, path));
});
(@parser $variant:ident(Span, Option<String>)) => ({
if input.parse::<Token![=]>().is_ok() {
if input.peek(syn::LitStr) {
let litstr = input.parse::<syn::LitStr>()?;
return Ok(BindgenAttr::$variant(attr_span, Some(litstr.value())))
}
let ident = input.parse::<AnyIdent>()?.0;
return Ok(BindgenAttr::$variant(attr_span, Some(ident.to_string())))
} else {
return Ok(BindgenAttr::$variant(attr_span, None));
}
});
(@parser $variant:ident(Span, syn::Path)) => ({
input.parse::<Token![=]>()?;
let path = if input.peek(syn::LitStr) {
let litstr = input.parse::<syn::LitStr>()?;
syn::parse_str::<syn::Path>(&litstr.value()).map_err(|e| {
syn::Error::new(litstr.span(), format!("expected a path: {e}"))
})?
} else {
input.parse()?
};
return Ok(BindgenAttr::$variant(attr_span, path));
});
(@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, JsNamespace, 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"));
}
}
if vals.is_empty() {
return Err(syn::Error::new(exprs.span(), "Empty namespace lists are not allowed."));
}
(vals, spans)
},
Err(_) => match input.parse::<syn::LitStr>() {
Ok(str) => (vec![str.value()], vec![str.span()]),
Err(_) => {
let ident = input.parse::<AnyIdent>()?.0;
(vec![ident.to_string()], vec![ident.span()])
}
}
};
let first = &vals[0];
if is_non_value_js_keyword(first) && first != "default" {
let msg = format!("Namespace cannot start with the JS keyword `{}`", first);
return Err(syn::Error::new(spans[0], msg));
}
return Ok(BindgenAttr::$variant(attr_span, JsNamespace(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"
}))
}
}
pub(crate) fn is_computed_key(name: &str) -> bool {
let bytes = name.as_bytes();
bytes.len() >= 2 && bytes[0] == b'[' && bytes[bytes.len() - 1] == b']'
}
fn validate_computed_key(attr: &str, value: &str, span: Span) -> Result<(), Diagnostic> {
if !is_computed_key(value) {
return Ok(());
}
let inner = &value[1..value.len() - 1];
let symbol_name = inner.strip_prefix("Symbol.");
let ok = symbol_name
.map(|n| !n.is_empty() && n.chars().all(|c| c.is_ascii_alphanumeric() || c == '_'))
.unwrap_or(false);
if ok {
Ok(())
} else {
Err(Diagnostic::span_error(
span,
format!("the only computed-key form supported in `{attr}` is `\"[Symbol.<ident>]\"`"),
))
}
}
struct AnyIdent(Ident);
impl Parse for AnyIdent {
fn parse(input: ParseStream) -> SynResult<Self> {
input.step(|cursor| match cursor.ident() {
Some((ident, remaining)) => Ok((AnyIdent(ident), remaining)),
None => Err(cursor.error("expected an identifier")),
})
}
}
pub(crate) trait ConvertToAst<Ctx> {
type Target;
fn convert(self, context: Ctx) -> Result<Self::Target, Diagnostic>;
}
impl ConvertToAst<&ast::Program> for &mut syn::ItemStruct {
type Target = ast::Struct;
fn convert(self, program: &ast::Program) -> 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 attrs = BindgenAttrs::find(&mut self.attrs)?;
let _ = attrs.wasm_bindgen();
let mut extends: Option<syn::Path> = None;
for (used, attr) in attrs.attrs.iter() {
if let BindgenAttr::Extends(span, path) = attr {
if extends.is_some() {
return Err(Diagnostic::span_error(
*span,
"`extends` may only be specified once on an exported struct",
));
}
if path.is_ident(&self.ident) || path.is_ident("Self") {
return Err(Diagnostic::span_error(
*span,
"`extends = Self` (or naming the same struct) is not allowed; \
a class cannot inherit from itself",
));
}
extends = Some(path.clone());
used.set(true);
}
}
let extends_js_class: Option<String> = attrs
.extends_js_class()
.map(|(s, _)| s.to_string())
.or_else(|| {
extends
.as_ref()
.and_then(|p| p.segments.last().map(|seg| seg.ident.unraw().to_string()))
});
let extends_js_namespace: Option<Vec<String>> =
attrs.extends_js_namespace().map(|(ns, _)| ns.0);
if extends_js_class.is_some() && extends.is_none() {
return Err(Diagnostic::span_error(
self.ident.span(),
"`extends_js_class` requires `extends = ...` to be set too \
(extends_js_class declares the parent's JS identity; \
extends declares the parent's Rust type)",
));
}
if extends_js_namespace.is_some() && extends.is_none() {
return Err(Diagnostic::span_error(
self.ident.span(),
"`extends_js_namespace` requires `extends = ...` to be set too \
(extends_js_namespace declares the parent's JS namespace; \
extends declares the parent's Rust type)",
));
}
let mut fields = Vec::new();
let js_name = attrs
.js_name_no_symbol("structs with #[wasm_bindgen]")?
.map(|s| s.to_string())
.unwrap_or(self.ident.unraw().to_string());
if is_js_keyword(&js_name) && js_name != "default" {
bail_span!(
self.ident,
"struct cannot use the JS keyword `{}` as its name",
js_name
);
}
let is_inspectable = attrs.inspectable().is_some();
let getter_with_clone = attrs.getter_with_clone();
let js_namespace = attrs.js_namespace().map(|(ns, _)| ns.0);
let qualified_name = wasm_bindgen_shared::qualified_name(js_namespace.as_deref(), &js_name);
let mut parent_count = 0usize;
for (i, field) in self.fields.iter_mut().enumerate() {
let field_attrs = BindgenAttrs::find(&mut field.attrs)?;
let is_parent = is_parent_wrapper_type(&field.ty);
if is_parent {
parent_count += 1;
}
let is_public = matches!(field.vis, syn::Visibility::Public(..));
if !is_public && !is_parent {
field_attrs.check_used();
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())),
};
if field_attrs.skip().is_some() && !is_parent {
field_attrs.check_used();
continue;
}
let js_field_name = match field_attrs.js_name() {
Some((name, _)) => name.to_string(),
None => js_field_name,
};
let comments = extract_doc_comments(&field.attrs);
let getter = wasm_bindgen_shared::struct_field_get(&qualified_name, &js_field_name);
let setter = wasm_bindgen_shared::struct_field_set(&qualified_name, &js_field_name);
fields.push(ast::StructField {
rust_name: member,
js_name: js_field_name,
struct_name: self.ident.clone(),
readonly: field_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: field_attrs.skip_typescript().is_none(),
generate_jsdoc: field_attrs.skip_jsdoc().is_none(),
getter_with_clone: field_attrs
.getter_with_clone()
.or(getter_with_clone)
.copied(),
is_parent,
wasm_bindgen: program.wasm_bindgen.clone(),
});
field_attrs.check_used();
}
match (&extends, parent_count) {
(Some(_), 1) => {}
(None, 0) => {}
_ => {
return Err(Diagnostic::span_error(
self.ident.span(),
"internal error: inconsistent Parent<T> field state; \
this should have been caught by inject_parent_field",
));
}
}
let generate_typescript = attrs.skip_typescript().is_none();
let private = attrs.private().is_some();
let comments: Vec<String> = extract_doc_comments(&self.attrs);
attrs.check_used();
Ok(ast::Struct {
rust_name: self.ident.clone(),
js_name,
qualified_name,
fields,
comments,
is_inspectable,
generate_typescript,
private,
js_namespace,
extends,
extends_js_class,
extends_js_namespace,
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
}
pub fn inject_parent_field(
s: &mut syn::ItemStruct,
extends_path: Option<&syn::Path>,
wasm_bindgen: &syn::Path,
) -> Result<(), Diagnostic> {
for field in s.fields.iter() {
if is_parent_wrapper_type(&field.ty) {
let path_str = path_to_string(field_type_path(&field.ty));
let msg = if extends_path.is_some() {
format!(
"do not declare a `{path_str}` field; the \
`#[wasm_bindgen(extends = ...)]` macro injects \
`parent: wasm_bindgen::Parent<...>` automatically"
)
} else {
format!(
"`{path_str}` looks like `wasm_bindgen::Parent<T>`, \
which is reserved for the \
`#[wasm_bindgen(extends = ...)]` macro. If you intended \
a different `Parent` type, qualify it (e.g. \
`my_crate::tree::Parent`) so this check skips it."
)
};
bail_span!(&field.ty, "{}", msg);
}
}
let Some(parent_path) = extends_path else {
return Ok(());
};
for field in s.fields.iter() {
if let Some(ident) = &field.ident {
if ident == "parent" {
bail_span!(
ident,
"struct with `#[wasm_bindgen(extends = ...)]` cannot \
have a field named `parent`; the macro injects one"
);
}
}
}
let injected: syn::Field = syn::parse_quote! {
parent: #wasm_bindgen::Parent<#parent_path>
};
match &mut s.fields {
syn::Fields::Named(named) => {
named.named.insert(0, injected);
}
syn::Fields::Unit => {
let mut named = syn::FieldsNamed {
brace_token: Default::default(),
named: syn::punctuated::Punctuated::new(),
};
named.named.push(injected);
s.fields = syn::Fields::Named(named);
s.semi_token = None;
}
syn::Fields::Unnamed(_) => {
bail_span!(
&s.ident,
"tuple structs cannot use `#[wasm_bindgen(extends = ...)]`; \
use a named-field struct"
);
}
}
Ok(())
}
fn is_parent_wrapper_type(ty: &syn::Type) -> bool {
let path = match field_type_path(ty) {
Some(p) => p,
None => return false,
};
let last = match path.segments.last() {
Some(seg) => seg,
None => return false,
};
if last.ident != "Parent" {
return false;
}
let one_type_arg = match &last.arguments {
syn::PathArguments::AngleBracketed(args) => {
args.args
.iter()
.filter(|a| matches!(a, syn::GenericArgument::Type(_)))
.count()
== 1
}
_ => false,
};
if !one_type_arg {
return false;
}
let segs: Vec<String> = path.segments.iter().map(|s| s.ident.to_string()).collect();
let leading_colon = path.leading_colon.is_some();
match (leading_colon, segs.as_slice()) {
(false, [only]) if only == "Parent" => true,
(false, [first, last]) if first == "wasm_bindgen" && last == "Parent" => true,
(true, [first, last]) if first == "wasm_bindgen" && last == "Parent" => true,
(false, [first, last]) if first == "crate" && last == "Parent" => true,
_ => false,
}
}
fn field_type_path(ty: &syn::Type) -> Option<&syn::Path> {
match get_ty(ty) {
syn::Type::Path(p) => Some(&p.path),
_ => None,
}
}
fn path_to_string(path: Option<&syn::Path>) -> String {
let Some(path) = path else {
return String::from("<unknown>");
};
let mut out = String::new();
if path.leading_colon.is_some() {
out.push_str("::");
}
let segs: Vec<String> = path.segments.iter().map(|s| s.ident.to_string()).collect();
out.push_str(&segs.join("::"));
out
}
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>,
SeqValues,
bool,
&'a mut Option<Vec<String>>,
&'a BlockTypes,
)> for syn::ForeignItemFn
{
type Target = ast::ImportKind;
fn convert(
mut self,
(
program,
opts,
module,
block_seq_values,
block_generic_per_mono,
js_namespace,
block_types,
): (
&ast::Program,
BindgenAttrs,
&'a Option<ast::ImportModule>,
SeqValues,
bool,
&'a mut Option<Vec<String>>,
&'a BlockTypes,
),
) -> Result<Self::Target, Diagnostic> {
let fn_generic_per_mono = opts.experimental_generic_mono().is_some();
let is_generic = self.sig.generics.type_params().next().is_some()
|| self.sig.inputs.iter().any(|arg| match arg {
syn::FnArg::Typed(pat_type) => crate::generics::has_impl_trait(&pat_type.ty),
syn::FnArg::Receiver(_) => false,
});
let generic_per_mono = fn_generic_per_mono || (block_generic_per_mono && is_generic);
let mut inherited = seq_values(&opts)?;
for (value, block) in inherited.iter_mut().zip(block_seq_values) {
*value = value.or(block);
}
let args_attrs = extract_args_attrs(&mut self.sig, Some(inherited))?;
let (mut wasm, _) = function_from_decl(
&self.sig.ident,
&opts,
self.sig.clone(),
self.attrs.clone(),
self.vis.clone(),
FunctionPosition::Extern,
Some(args_attrs),
)?;
let catch = opts.catch().is_some();
let variadic = opts.variadic().is_some();
let js_ret = if catch {
extract_first_ty_param(wasm.ret.as_ref().map(|ret| &ret.r#type))?
} else {
wasm.ret.as_ref().map(|ret| ret.r#type.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.pat_type.ty) {
syn::Type::Reference(syn::TypeReference {
mutability: None,
elem,
..
}) => &**elem,
_ => bail_span!(
class.pat_type.ty,
"first argument of method must be a shared reference"
),
};
let class_ty = get_ty(class);
let js_class = opts.js_class().map(|p| p.0.to_string());
let kind = ast::MethodKind::Operation(ast::Operation {
is_static: false,
kind: operation_kind,
});
let class_name = match class_ty {
syn::Type::Path(syn::TypePath {
attrs: _,
qself: None,
ref path,
}) => path,
_ => bail_span!(class_ty, "first argument of method must be a path"),
};
let class_name_str = match js_class {
Some(class) => class,
None => match block_type_class(block_types, class_name, js_namespace) {
Some(class) => class,
None => extract_path_ident(class_name, true)?.unraw().to_string(),
},
};
ast::ImportFunctionKind::Method {
class: class_name_str,
ty: class_ty.clone(),
kind,
}
} else if let Some(cls) = opts.static_method_of() {
if cls.qself.is_some() {
bail_span!(
cls,
"static_method_of does not support qualified-self projections"
);
}
let class = match opts.js_class() {
Some(class) => class.0.into(),
None => block_type_class(block_types, &cls.path, js_namespace)
.unwrap_or_else(|| cls.path.segments.last().unwrap().ident.unraw().to_string()),
};
let ty = syn::Type::Path(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 {
attrs: _,
qself: None,
ref path,
}) => path,
_ => bail_span!(self, "return value of constructor must be a bare path"),
};
let class_ident = extract_path_ident(class_name, true)?;
let class_name = match opts.js_class() {
Some(class) => class.0.into(),
None => block_type_class(block_types, class_name, js_namespace)
.unwrap_or_else(|| class_ident.unraw().to_string()),
};
ast::ImportFunctionKind::Method {
class: class_name,
ty: class.clone(),
kind: ast::MethodKind::Constructor,
}
} else {
ast::ImportFunctionKind::Normal
};
if opts.reexport().is_some() && matches!(kind, ast::ImportFunctionKind::Method { .. }) {
return Err(Diagnostic::span_error(
self.sig.ident.span(),
"`reexport` cannot be used on methods, constructors, or static methods. \
Use `reexport` on the type import instead.",
));
}
let assert_no_shim = opts.assert_no_shim();
if generic_per_mono {
if let Some(span) = assert_no_shim {
return Err(Diagnostic::span_error(
*span,
"`assert_no_shim` cannot be used with `experimental_generic_mono`, which always \
generates one shim per monomorphisation",
));
}
if let Some(span) = opts.suspending() {
return Err(Diagnostic::span_error(
*span,
"`suspending` cannot be used with `experimental_generic_mono`; use the \
type-erasure generic path for suspending imports",
));
}
if opts.reexport().is_some() {
return Err(Diagnostic::span_error(
self.sig.ident.span(),
"`reexport` cannot be used with `experimental_generic_mono`, because one binding is \
manufactured per monomorphisation and there is no single shim to reexport; \
remove the reexport or use the type-erasure generic path",
));
}
}
let shim = {
let ns = match kind {
ast::ImportFunctionKind::Normal => (0, "n"),
ast::ImportFunctionKind::Method { ref class, .. } => (1, &class[..]),
};
let cfg_attrs: String = crate::cfg_gate_attrs(&self.attrs)
.iter()
.map(|attr| attr.to_token_stream().to_string())
.collect();
let data = (
ns,
self.sig.to_token_stream().to_string(),
module,
cfg_attrs,
);
let name = wasm
.name
.chars()
.filter(|&c| c.is_ascii_alphanumeric() || c == '_')
.collect::<String>();
let call_style = ImportCallStyle {
catch,
variadic,
r#final: opts.r#final().is_some(),
suspending: opts.suspending().is_some(),
generic_per_mono,
method_kind: match &kind {
ast::ImportFunctionKind::Method {
kind:
ast::MethodKind::Operation(ast::Operation {
is_static: false,
kind: ast::OperationKind::Regular,
}),
..
}
| ast::ImportFunctionKind::Normal => None,
ast::ImportFunctionKind::Method { kind, .. } => Some(kind),
},
slice_to_array: wasm
.arguments
.iter()
.enumerate()
.filter(|(_, arg)| {
(arg.seq_modes.iter()).any(|m| m.value == ast::SeqValue::SliceToArray)
})
.map(|(i, _)| i)
.collect(),
seq_modes: wasm
.arguments
.iter()
.enumerate()
.flat_map(|(i, arg)| {
(arg.seq_modes.iter())
.filter(|m| m.value != ast::SeqValue::SliceToArray)
.map(move |m| (i, *m))
})
.collect(),
js_name: (name != wasm.name).then_some(&wasm.name),
};
let hash = match (
js_namespace.as_deref(),
inline_js_snippet(program, module),
(!call_style.is_plain()).then_some(call_style),
) {
(None, None, None) => ShortHash(data).to_string(),
(Some(ns), None, None) => ShortHash((data, ns)).to_string(),
(ns, snippet, call_style) => ShortHash((data, ns, snippet, call_style)).to_string(),
};
format!("__wbg_{name}_{hash}")
};
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 = assert_no_shim.is_some();
let suspending = opts.suspending().is_some();
if suspending && wasm.r#async {
if let Some(span) = opts.suspending() {
return Err(Diagnostic::span_error(
*span,
"`suspending` cannot be combined with `async`: a suspending \
import returns the settled value directly, so declare a \
plain `fn` with the resolved type as its return type",
));
}
}
if wasm.jspi {
if let Some(span) = opts.jspi() {
return Err(Diagnostic::span_error(
*span,
"`jspi` can only be used on exported functions, not imports",
));
}
}
let mut doc_comment = String::new();
wasm.rust_attrs.retain(|attr| {
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() {
doc_comment.push('\n');
}
doc_comment.push_str(&docs);
false
} else {
true
}
});
validate_generics(&self.sig.generics)?;
let ret = ast::ImportKind::Function(ast::ImportFunction {
function: wasm,
assert_no_shim,
suspending,
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(),
js_sys: program.js_sys.clone(),
generics: self.sig.generics,
generic_per_mono,
});
opts.check_used();
Ok(ret)
}
}
impl<'a>
ConvertToAst<(
&ast::Program,
BindgenAttrs,
&'a Option<ast::ImportModule>,
Option<&'a [String]>,
)> for syn::ForeignItemType
{
type Target = ast::ImportKind;
fn convert(
self,
(program, attrs, module, js_namespace): (
&ast::Program,
BindgenAttrs,
&'a Option<ast::ImportModule>,
Option<&'a [String]>,
),
) -> Result<Self::Target, Diagnostic> {
let js_name = attrs
.js_name_no_symbol("extern types with #[wasm_bindgen]")?
.map_or_else(|| self.ident.unraw().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 unraw_ident = self.ident.unraw();
let cfg_attrs: String = crate::cfg_gate_attrs(&self.attrs)
.iter()
.map(|attr| attr.to_token_stream().to_string())
.collect();
let hash = ShortHash((
&js_name,
module,
&unraw_ident,
js_namespace,
inline_js_snippet(program, module),
cfg_attrs,
));
let shim = format!("__wbg_instanceof_{unraw_ident}_{hash}");
let mut extends = Vec::new();
let mut vendor_prefixes = Vec::new();
let no_deref = attrs.no_deref().is_some();
let no_upcast = attrs.no_upcast().is_some();
let no_promising = attrs.no_promising().is_some();
let no_into_js_generic = attrs.no_into_js_generic().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();
validate_generics(&self.generics)?;
let mut generics = None;
for (n, param) in self.generics.type_params().enumerate() {
if param.default.is_none() {
let generics = generics.get_or_insert_with(|| self.generics.clone());
let type_param_mut = generics.type_params_mut().nth(n).unwrap();
type_param_mut.default = Some((Default::default(), syn::parse_quote! { JsValue }));
}
}
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,
no_upcast,
no_promising,
no_into_js_generic,
wasm_bindgen: program.wasm_bindgen.clone(),
generics: generics.unwrap_or(self.generics),
}))
}
}
impl<'a>
ConvertToAst<(
&ast::Program,
BindgenAttrs,
&'a Option<ast::ImportModule>,
Option<&'a [String]>,
)> for syn::ForeignItemStatic
{
type Target = ast::ImportKind;
fn convert(
self,
(program, opts, module, js_namespace): (
&ast::Program,
BindgenAttrs,
&'a Option<ast::ImportModule>,
Option<&'a [String]>,
),
) -> Result<Self::Target, Diagnostic> {
if let syn::StaticMutability::Mut(_) = self.mutability {
bail_span!(self.mutability, "cannot import mutable globals yet")
}
if let Some(span) = opts.static_string() {
return Err(Diagnostic::span_error(
*span,
"static strings require a string literal",
));
}
let default_name = self.ident.unraw().to_string();
let js_name = opts
.js_name_no_symbol("statics with #[wasm_bindgen]")?
.unwrap_or(&default_name)
.to_string();
let unraw_ident = self.ident.unraw();
let hash = match (js_namespace, inline_js_snippet(program, module)) {
(None, None) => ShortHash((&js_name, module, &unraw_ident)).to_string(),
(Some(ns), None) => ShortHash((&js_name, module, &unraw_ident, ns)).to_string(),
(ns, Some(snippet)) => {
ShortHash((&js_name, module, &unraw_ident, ns, snippet)).to_string()
}
};
let shim = format!("__wbg_static_accessor_{unraw_ident}_{hash}");
let thread_local = opts.get_thread_local()?;
opts.check_used();
Ok(ast::ImportKind::Static(ast::ImportStatic {
ty: *self.ty,
vis: self.vis,
rust_name: self.ident.clone(),
js_name,
attrs: self.attrs,
shim: Ident::new(&shim, Span::call_site()),
wasm_bindgen: program.wasm_bindgen.clone(),
thread_local,
}))
}
}
impl<'a> ConvertToAst<(&ast::Program, BindgenAttrs, &'a Option<ast::ImportModule>)>
for syn::ItemStatic
{
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 string = if let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(string),
..
}) = *self.expr.clone()
{
string.value()
} else {
bail_span!(
self.expr,
"statics with a value can only be string literals"
)
};
if opts.static_string().is_none() {
bail_span!(
self,
"static strings require `#[wasm_bindgen(static_string)]`"
)
}
let thread_local = if let Some(thread_local) = opts.get_thread_local()? {
thread_local
} else {
bail_span!(
self,
"static strings require `#[wasm_bindgen(thread_local_v2)]`"
)
};
let unraw_ident = self.ident.unraw();
let hash = ShortHash((&module, &unraw_ident, &string));
let shim = format!("__wbg_string_{unraw_ident}_{hash}");
opts.check_used();
Ok(ast::ImportKind::String(ast::ImportString {
ty: *self.ty,
vis: self.vis,
rust_name: self.ident.clone(),
attrs: self.attrs,
shim: Ident::new(&shim, Span::call_site()),
wasm_bindgen: program.wasm_bindgen.clone(),
js_sys: program.js_sys.clone(),
string,
thread_local,
}))
}
}
impl ConvertToAst<(BindgenAttrs, Vec<FnArgAttrs>)> for syn::ItemFn {
type Target = ast::Function;
fn convert(
self,
(attrs, args_attrs): (BindgenAttrs, Vec<FnArgAttrs>),
) -> 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 (mut ret, _) = function_from_decl(
&self.sig.ident,
&attrs,
self.sig.clone(),
self.attrs,
self.vis,
FunctionPosition::Free,
Some(args_attrs),
)?;
attrs.check_used();
if is_js_keyword(&ret.name) && ret.name != "default" {
ret.name = format!("_{}", ret.name);
}
Ok(ret)
}
}
fn get_self_method(r: syn::Receiver) -> ast::MethodSelf {
match &r.kind {
syn::ReceiverKind::Reference(_, _, mutability) => {
if mutability.is_some() {
ast::MethodSelf::RefMutable
} else {
ast::MethodSelf::RefShared
}
}
syn::ReceiverKind::Typed(_, ty) => match &**ty {
syn::Type::Reference(ty) => {
if ty.mutability.is_some() {
ast::MethodSelf::RefMutable
} else {
ast::MethodSelf::RefShared
}
}
_ => ast::MethodSelf::ByValue,
},
_ => ast::MethodSelf::ByValue,
}
}
enum FunctionPosition<'a> {
Extern,
Free,
Impl { self_ty: &'a Ident },
}
#[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,
position: FunctionPosition,
args_attrs: Option<Vec<FnArgAttrs>>,
) -> Result<(ast::Function, Option<ast::MethodSelf>), Diagnostic> {
if sig.variadic.is_some() {
bail_span!(sig.variadic, "can't #[wasm_bindgen] variadic functions");
}
if !matches!(position, FunctionPosition::Extern) && !sig.generics.params.is_empty() {
bail_span!(
sig.generics,
"can't #[wasm_bindgen] functions with lifetime or type parameters"
);
}
let paren_span = sig.paren_token.span.join();
let syn::Signature { inputs, output, .. } = sig;
let replace_self = |mut t: syn::Type| {
if let FunctionPosition::Impl { self_ty } = position {
struct SelfReplace(Ident);
impl VisitMut for SelfReplace {
fn visit_ident_mut(&mut self, i: &mut proc_macro2::Ident) {
if i == "Self" {
*i = self.0.clone();
}
}
}
let mut replace = SelfReplace(self_ty.clone());
replace.visit_type_mut(&mut t);
}
t
};
let replace_colliding_arg = |i: &mut syn::PatType| {
if let syn::Pat::Ident(ref mut i) = *i.pat {
let ident = i.ident.unraw().to_string();
if is_js_keyword(&ident) {
i.ident = Ident::new(format!("_{ident}").as_str(), i.ident.span());
}
}
};
let mut method_self = None;
let mut arguments = Vec::new();
for arg in inputs.into_iter() {
match arg {
syn::FnArg::Typed(mut c) => {
replace_colliding_arg(&mut c);
*c.ty = replace_self(*c.ty);
arguments.push(c);
}
syn::FnArg::Receiver(r) => {
match position {
FunctionPosition::Free => {
bail_span!(
r.self_token,
"the `self` argument is only allowed for functions in `impl` blocks.\n\n\
If the function is already in an `impl` block, did you perhaps forget to add `#[wasm_bindgen]` to the `impl` block?"
);
}
FunctionPosition::Extern => {
bail_span!(
r.self_token,
"the `self` argument is not allowed for `extern` functions.\n\n\
Did you perhaps mean `this`? For more information on importing JavaScript functions, see:\n\
https://wasm-bindgen.github.io/wasm-bindgen/examples/import-js.html"
);
}
FunctionPosition::Impl { .. } => {}
}
assert!(method_self.is_none());
method_self = Some(get_self_method(r));
}
}
}
let ret_ty_override = opts.unchecked_return_type();
let ret_desc = opts.return_description();
let ret = match output {
syn::ReturnType::Default => None,
syn::ReturnType::Type(_, ty) => Some(ast::FunctionReturnData {
r#type: replace_self(*ty),
js_type: ret_ty_override
.as_ref()
.map_or::<Result<_, Diagnostic>, _>(Ok(None), |(ty, span)| {
check_invalid_type(ty, *span)?;
Ok(Some(ty.to_string()))
})?,
desc: ret_desc.as_ref().map_or::<Result<_, Diagnostic>, _>(
Ok(None),
|(desc, span)| {
check_js_comment_close(desc, *span)?;
Ok(Some(desc.to_string()))
},
)?,
}),
};
if ret.is_none() && (ret_ty_override.is_some() || ret_desc.is_some()) {
if let Some((_, span)) = ret_ty_override {
return Err(Diagnostic::span_error(
span,
"cannot specify return type for a function that doesn't return",
));
}
if let Some((_, span)) = ret_desc {
return Err(Diagnostic::span_error(
span,
"cannot specify return description for a function that doesn't return",
));
}
}
let (name, name_span) = if let Some((js_name, js_name_span)) = opts.js_name() {
if is_computed_key(js_name) && matches!(position, FunctionPosition::Free) {
return Err(Diagnostic::span_error(
js_name_span,
"free functions with #[wasm_bindgen] do not support symbols in js_name",
));
}
let (kind, _) = pick_operation_kind(opts);
let prefix = match kind {
OperationKind::Setter(_) => "set_",
_ => "",
};
(format!("{prefix}{js_name}"), js_name_span)
} else {
(decl_name.unraw().to_string(), decl_name.span())
};
Ok((
ast::Function {
name_span,
paren_span,
name,
rust_attrs: attrs,
rust_vis: vis,
r#unsafe: matches!(sig.safety, syn::Safety::Unsafe(_)),
r#async: sig.asyncness.is_some(),
jspi: opts.jspi().is_some(),
tokio: match opts.experimental_tokio() {
Some(mode) => {
let span = opts
.attrs
.iter()
.find_map(|(_, attr)| match attr {
BindgenAttr::ExperimentalTokio(span, _) => Some(*span),
_ => None,
})
.unwrap();
if sig.asyncness.is_none() {
return Err(Diagnostic::span_error(
span,
"#[wasm_bindgen(experimental_tokio)] can only be applied to `async` functions",
));
}
match mode.as_deref() {
None => Some(ast::TokioMode::Ambient),
Some("isolated") => Some(ast::TokioMode::Isolated),
Some(other) => {
return Err(Diagnostic::span_error(
span,
format!(
"unknown tokio mode `{other}`; expected `experimental_tokio` or `experimental_tokio = \"isolated\"`"
),
))
}
}
}
None => None,
},
generate_typescript: opts.skip_typescript().is_none(),
generate_jsdoc: opts.skip_jsdoc().is_none(),
variadic: opts.variadic().is_some(),
ret,
arguments: arguments
.into_iter()
.zip(
args_attrs
.into_iter()
.flatten()
.chain(iter::repeat(FnArgAttrs::default())),
)
.map(|(pat_type, attrs)| ast::FunctionArgumentData {
pat_type,
js_name: attrs.js_name,
js_type: attrs.js_type,
optional: attrs.optional,
desc: attrs.desc,
seq_modes: attrs.seq_modes,
})
.collect(),
},
method_self,
))
}
#[derive(Default, Clone)]
struct FnArgAttrs {
js_name: Option<String>,
js_type: Option<String>,
optional: bool,
desc: Option<String>,
seq_modes: Vec<ast::SeqMode>,
}
type SeqValues = [Option<ast::SeqValue>; 2];
const SEQ_KEYS: [ast::SeqKey; 2] = [ast::SeqKey::Slice, ast::SeqKey::Vec];
fn export_seq_mode_errors(attrs: &BindgenAttrs) -> Vec<Diagnostic> {
attrs
.attrs
.iter()
.filter_map(|(used, attr)| {
let (span, key) = match attr {
BindgenAttr::Slice(span, _) => (span, "slice"),
BindgenAttr::Vec(span, _) => (span, "vec"),
_ => return None,
};
used.set(true);
Some(Diagnostic::span_error(
*span,
format!("`{key}` only applies to arguments of imported JS functions"),
))
})
.collect()
}
fn seq_values(attrs: &BindgenAttrs) -> Result<SeqValues, Diagnostic> {
let mut values = [None; 2];
for key in SEQ_KEYS {
let ident = match key {
ast::SeqKey::Slice => attrs.slice(),
ast::SeqKey::Vec => attrs.vec(),
};
let Some(ident) = ident else { continue };
values[key as usize] = Some(match (key, ident.to_string().as_str()) {
(ast::SeqKey::Slice, "view") => ast::SeqValue::View,
(_, "copy") => ast::SeqValue::Copy,
(_, "array") => ast::SeqValue::Array,
(ast::SeqKey::Slice, "transfer") => {
bail_span!(
ident,
"`slice = transfer` is reserved and not yet supported"
)
}
(ast::SeqKey::Vec, "view") => bail_span!(
ident,
"`vec = view` is not supported: owned data is freed after the call, so JS \
always receives a copy; use `vec = copy` or `vec = array`"
),
(ast::SeqKey::Slice, other) => bail_span!(
ident,
"unknown slice mode `{other}`; expected `slice = view`, `slice = array` or \
`slice = copy`"
),
(ast::SeqKey::Vec, other) => bail_span!(
ident,
"unknown vec mode `{other}`; expected `vec = copy` or `vec = array`"
),
});
}
if let Some(alias_span) = attrs.slice_to_array() {
if let Some(slice) = attrs.slice() {
let msg = "cannot specify both `slice_to_array` and `slice`; use `slice = array`";
Diagnostic::from_vec(vec![
Diagnostic::span_error(*alias_span, msg),
Diagnostic::span_error(slice.span(), msg),
])?;
}
values[ast::SeqKey::Slice as usize] = Some(ast::SeqValue::SliceToArray);
}
Ok(values)
}
fn check_mut_slice(ty: &syn::Type, mode: ast::SeqMode) -> Result<(), Diagnostic> {
let is_mut = crate::codegen::detect_slice_or_option_slice(ty).is_some_and(|s| s.is_mut);
let (attr, shape) = match mode.value {
_ if !is_mut || mode.key != ast::SeqKey::Slice => return Ok(()),
ast::SeqValue::View => return Ok(()),
ast::SeqValue::Array => ("slice = array", "plain `Array`"),
ast::SeqValue::SliceToArray => ("slice_to_array", "plain `Array`"),
ast::SeqValue::Copy => ("slice = copy", "typed array"),
};
Err(Diagnostic::spanned_error(
ty,
format!(
"`{attr}` cannot be applied to a `&mut` slice: JS receives an owned \
{shape}, so its writes are never written back into the caller's slice — use a \
shared slice if JS only needs to read the elements, or set `slice = view` on \
this argument (the mode may be inherited from the function or the enclosing \
`extern \"C\"` block) to keep the writable typed-array view"
),
))
}
fn extract_args_attrs(
sig: &mut syn::Signature,
import: Option<SeqValues>,
) -> Result<Vec<FnArgAttrs>, Diagnostic> {
let mut args_attrs = vec![];
let mut seen_optional: Option<Span> = None;
let mut seq_mode_errors = vec![];
for input in sig.inputs.iter_mut() {
if let syn::FnArg::Typed(pat_type) = input {
let attrs = BindgenAttrs::find(&mut pat_type.attrs)?;
let param_type = attrs.unchecked_param_type();
let optional_param_type = attrs.unchecked_optional_param_type();
if param_type.is_some() && optional_param_type.is_some() {
let mut param_pos_and_span: Option<(usize, Span)> = None;
let mut optional_pos_and_span: Option<(usize, Span)> = None;
for (pos, (_, attr)) in attrs.attrs.iter().enumerate() {
match attr {
BindgenAttr::ParamType(span, _, _) => {
param_pos_and_span = Some((pos, *span));
}
BindgenAttr::OptionalParamType(span, _, _) => {
optional_pos_and_span = Some((pos, *span));
}
_ => {}
}
}
let error_span = match (param_pos_and_span, optional_pos_and_span) {
(Some((p_pos, p_span)), Some((o_pos, o_span))) => {
if p_pos > o_pos {
p_span
} else {
o_span
}
}
(Some((_, p_span)), None) => p_span,
(None, Some((_, o_span))) => o_span,
(None, None) => unreachable!(
"both param_type and optional_param_type are Some, but attrs not found"
),
};
return Err(Diagnostic::span_error(
error_span,
"cannot use both `unchecked_param_type` and `unchecked_optional_param_type` on the same parameter",
));
}
let js_type = param_type
.or(optional_param_type)
.map_or::<Result<_, Diagnostic>, _>(Ok(None), |(ty, span)| {
check_invalid_type(ty, span)?;
Ok(Some(ty.to_string()))
})?;
let is_optional = optional_param_type.is_some();
if let Some(optional_span) = seen_optional {
if !is_optional {
return Err(Diagnostic::span_error(
optional_span,
"a required parameter cannot follow an optional parameter",
));
}
}
if is_optional {
if let Some((_, span)) = optional_param_type {
seen_optional = Some(span);
}
}
let seq_modes = match &import {
Some(inherited) => {
let own = seq_values(&attrs).unwrap_or_else(|diagnostic| {
seq_mode_errors.push(diagnostic);
[None; 2]
});
SEQ_KEYS
.into_iter()
.filter_map(|key| {
let own = own[key as usize];
let value = own.or(inherited[key as usize])?;
let mode = ast::SeqMode {
key,
value,
explicit: own.is_some() && value != ast::SeqValue::SliceToArray,
};
let default = match key {
ast::SeqKey::Slice => ast::SeqValue::View,
ast::SeqKey::Vec => ast::SeqValue::Copy,
};
if !mode.explicit && value == default {
return None;
}
check_mut_slice(&pat_type.ty, mode)
.map_err(|diagnostic| seq_mode_errors.push(diagnostic))
.ok()?;
Some(mode)
})
.collect()
}
None => {
attrs.slice_to_array();
seq_mode_errors.extend(export_seq_mode_errors(&attrs));
vec![]
}
};
let arg_attrs = FnArgAttrs {
js_name: attrs
.js_name()
.map_or(Ok(None), |(js_name_override, span)| {
if is_computed_key(js_name_override) {
return Err(Diagnostic::span_error(
span,
"function arguments do not support symbols in js_name",
));
}
if is_js_keyword(js_name_override) || !is_valid_ident(js_name_override) {
return Err(Diagnostic::span_error(span, "invalid JS identifier"));
}
Ok(Some(js_name_override.to_string()))
})?,
js_type,
optional: is_optional,
desc: attrs
.param_description()
.map_or::<Result<_, Diagnostic>, _>(Ok(None), |(description, span)| {
check_js_comment_close(description, span)?;
Ok(Some(description.to_string()))
})?,
seq_modes,
};
attrs.enforce_used()?;
args_attrs.push(arg_attrs);
}
}
Diagnostic::from_vec(seq_mode_errors)?;
Ok(args_attrs)
}
pub(crate) trait MacroParse<Ctx> {
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.js_sys() {
program.js_sys = 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);
}
Diagnostic::from_vec(export_seq_mode_errors(&opts))?;
let args_attrs = extract_args_attrs(&mut f.sig, None)?;
let comments = extract_doc_comments(&f.attrs);
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 = if opts.start().is_some() {
if opts.private().is_some() {
ast::StartKind::Private
} else {
ast::StartKind::Public
}
} else {
ast::StartKind::None
};
if opts.this().is_some() && f.sig.inputs.is_empty() {
bail_span!(
&f.sig.inputs,
"functions taking a 'this' argument must have at least one parameter"
);
}
let js_namespace = opts.js_namespace().map(|(ns, _)| ns.0);
program.exports.push(ast::Export {
comments,
function: f.convert((opts, args_attrs))?,
js_class: None,
js_namespace,
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(),
js_sys: program.js_sys.clone(),
});
}
syn::Item::Impl(mut i) => {
let opts = opts.unwrap_or_default();
if let Some(path) = opts.wasm_bindgen() {
program.wasm_bindgen = path.clone();
}
if let Some(path) = opts.js_sys() {
program.js_sys = path.clone();
}
if let Some(path) = opts.wasm_bindgen_futures() {
program.wasm_bindgen_futures = path.clone();
}
Diagnostic::from_vec(export_seq_mode_errors(&opts))?;
(&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 MacroParse<BindgenAttrs> for &mut syn::ItemImpl {
fn macro_parse(self, program: &mut ast::Program, opts: BindgenAttrs) -> Result<(), Diagnostic> {
if self.modifiers.defaultness.is_some() {
bail_span!(
self.modifiers.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 {
attrs: _,
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(())
}
}
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(_) => {
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, false)?;
let js_class = impl_opts
.js_class()
.map(|s| s.0.to_string())
.unwrap_or(ident.unraw().to_string());
let js_namespace_segments: Vec<String> = impl_opts
.js_namespace()
.map(|(ns, _)| ns.0)
.unwrap_or_default();
let js_namespace_lits = js_namespace_segments
.iter()
.map(|s| syn::LitStr::new(s, proc_macro2::Span::call_site()))
.collect::<Vec<_>>();
let js_namespace_tokens = if js_namespace_lits.is_empty() {
quote::quote! {}
} else {
quote::quote! { , js_namespace = [ #(#js_namespace_lits),* ] }
};
let wasm_bindgen = &program.wasm_bindgen;
let wasm_bindgen_futures = &program.wasm_bindgen_futures;
let js_sys = &program.js_sys;
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 #js_namespace_tokens, wasm_bindgen = #wasm_bindgen, wasm_bindgen_futures = #wasm_bindgen_futures, js_sys = #js_sys) },
},
);
Ok(())
}
impl MacroParse<&ClassMarker> for &mut syn::ImplItemFn {
fn macro_parse(
self,
program: &mut ast::Program,
ClassMarker {
class,
js_class,
js_namespace,
wasm_bindgen,
wasm_bindgen_futures,
js_sys,
}: &ClassMarker,
) -> Result<(), Diagnostic> {
program.wasm_bindgen = wasm_bindgen.clone();
program.wasm_bindgen_futures = wasm_bindgen_futures.clone();
program.js_sys = js_sys.clone();
match self.vis {
syn::Visibility::Public(_) => {}
_ => return Ok(()),
}
if self.modifiers.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)?;
Diagnostic::from_vec(export_seq_mode_errors(&opts))?;
if opts.this().is_some() {
bail_span!(
&self.sig.ident,
"#[wasm_bindgen(this)] cannot be used on impl block methods; \
it is only valid on free functions"
);
}
let comments = extract_doc_comments(&self.attrs);
let args_attrs: Vec<FnArgAttrs> = extract_args_attrs(&mut self.sig, None)?;
let (function, method_self) = function_from_decl(
&self.sig.ident,
&opts,
self.sig.clone(),
self.attrs.clone(),
self.vis.clone(),
FunctionPosition::Impl { self_ty: class },
Some(args_attrs),
)?;
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 })
};
if function.jspi {
match &method_kind {
ast::MethodKind::Constructor => {
if let Some(span) = opts.jspi() {
return Err(Diagnostic::span_error(
*span,
"`jspi` cannot be used on constructors",
));
}
}
ast::MethodKind::Operation(ast::Operation { kind, .. }) => match kind {
ast::OperationKind::Getter(_)
| ast::OperationKind::Setter(_)
| ast::OperationKind::IndexingGetter
| ast::OperationKind::IndexingSetter
| ast::OperationKind::IndexingDeleter => {
if let Some(span) = opts.jspi() {
return Err(Diagnostic::span_error(
*span,
"`jspi` cannot be used on getters or setters",
));
}
}
_ => {}
},
}
}
if let Some((_, span)) = opts.js_namespace() {
return Err(Diagnostic::span_error(
span[0],
"`js_namespace` cannot be used on methods, getters, setters, or static methods. \
Use `js_namespace` on the exported struct definition instead to put the entire class in a namespace.",
));
}
program.exports.push(ast::Export {
comments,
function,
js_class: Some(js_class.to_string()),
js_namespace: js_namespace.clone(),
method_kind,
method_self,
rust_class: Some(class.clone()),
rust_name: self.sig.ident.clone(),
start: ast::StartKind::None,
wasm_bindgen: program.wasm_bindgen.clone(),
wasm_bindgen_futures: program.wasm_bindgen_futures.clone(),
js_sys: program.js_sys.clone(),
});
opts.check_used();
Ok(())
}
}
fn string_enum(
enum_: syn::ItemEnum,
program: &mut ast::Program,
js_name: String,
generate_typescript: bool,
private: bool,
comments: Vec<String>,
js_namespace: Option<Vec<String>>,
) -> Result<(), Diagnostic> {
let mut variants = vec![];
let mut variant_values = vec![];
for v in enum_.variants.iter() {
let (_, expr) = match &v.discriminant {
Some(pair) => pair,
None => {
bail_span!(v, "all variants of a string enum must have a string 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,
reexport: None,
kind: ast::ImportKind::Enum(ast::StringEnum {
vis: enum_.vis,
name: enum_.ident,
export_name: js_name,
variants,
variant_values,
comments,
rust_attrs: enum_.attrs,
generate_typescript,
private,
js_namespace,
wasm_bindgen: program.wasm_bindgen.clone(),
}),
});
Ok(())
}
fn dynamic_union(
enum_: syn::ItemEnum,
program: &mut ast::Program,
js_name: String,
generate_typescript: bool,
private: bool,
fallback: bool,
comments: Vec<String>,
) -> Result<(), Diagnostic> {
let mut variants = vec![];
let mut variant_values = vec![];
let mut variant_fields = vec![];
for v in enum_.variants.iter() {
match &v.fields {
syn::Fields::Unit => {
let (_, expr) = match &v.discriminant {
Some(pair) => pair,
None => {
bail_span!(
v,
"all unit variants of a string enum must have a string 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());
variant_fields.push(Vec::new());
}
expr => bail_span!(
expr,
"enums with #[wasm_bindgen] cannot mix string and non-string values",
),
}
}
syn::Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
variants.push(v.ident.clone());
variant_values.push(String::new());
variant_fields.push(vec![fields.unnamed.first().unwrap().ty.clone()]);
}
_ => {
bail_span!(
v.fields,
"string enum variants with fields must have exactly one unnamed field"
);
}
}
}
program.imports.push(ast::Import {
module: None,
js_namespace: None,
reexport: None,
kind: ast::ImportKind::DynamicUnion(ast::DynamicUnion {
vis: enum_.vis,
name: enum_.ident,
js_name,
variants,
variant_values,
variant_fields,
comments,
rust_attrs: enum_.attrs,
generate_typescript,
private,
fallback,
wasm_bindgen: program.wasm_bindgen.clone(),
}),
});
Ok(())
}
struct NumericValue<'a> {
negative: bool,
base10_digits: &'a str,
}
impl<'a> NumericValue<'a> {
fn from_expr(expr: &'a syn::Expr) -> Option<Self> {
match get_expr(expr) {
syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Int(int_lit),
..
}) => Some(Self {
negative: false,
base10_digits: int_lit.base10_digits(),
}),
syn::Expr::Unary(syn::ExprUnary {
op: syn::UnOp::Neg(_),
expr,
..
}) => Self::from_expr(expr).map(|n| n.neg()),
_ => None,
}
}
fn parse(&self) -> Option<i64> {
let mut value = self.base10_digits.parse::<i64>().ok()?;
if self.negative {
value = -value;
}
Some(value)
}
fn neg(self) -> Self {
Self {
negative: !self.negative,
base10_digits: self.base10_digits,
}
}
}
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();
let private = opts.private().is_some();
let fallback = opts.fallback().is_some();
let comments = extract_doc_comments(&self.attrs);
let js_name = opts
.js_name_no_symbol("enums with #[wasm_bindgen]")?
.map_or_else(|| self.ident.unraw().to_string(), |s| s.to_string());
if is_js_keyword(&js_name) && js_name != "default" {
bail_span!(
self.ident,
"enum cannot use the JS keyword `{}` as its name",
js_name
);
}
let js_namespace = opts.js_namespace().map(|(ns, _)| ns.0);
opts.check_used();
let mut has_unnamed_fields = false;
for variant in self.variants.iter() {
match &variant.fields {
syn::Fields::Unit => (), syn::Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
has_unnamed_fields = true;
}
syn::Fields::Unnamed(_) => bail_span!(
variant.fields,
"enum variants with fields must have exactly one unnamed field"
),
syn::Fields::Named(_) => {
bail_span!(variant.fields, "enum variants cannot have named fields")
}
}
}
if has_unnamed_fields {
return dynamic_union(
self,
program,
js_name,
generate_typescript,
private,
fallback,
comments,
);
}
if fallback {
bail_span!(
self,
"the `fallback` attribute is only supported on dynamic unions \
(enums with at least one tuple variant)"
);
}
let is_string_enum = self.variants.iter().any(|v| {
if let Some((_, expr)) = &v.discriminant {
if let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(_),
..
}) = get_expr(expr)
{
return true;
}
}
false
});
if is_string_enum {
return string_enum(
self,
program,
js_name,
generate_typescript,
private,
comments,
js_namespace,
);
}
match self.vis {
syn::Visibility::Public(_) => {}
_ => bail_span!(self, "only public enums are allowed with #[wasm_bindgen]"),
}
let signed = self.variants.iter().any(|v| match &v.discriminant {
Some((_, expr)) => NumericValue::from_expr(expr).is_some_and(|n| n.negative),
None => false,
});
let underlying_min = if signed { i32::MIN as i64 } else { 0 };
let underlying_max = if signed {
i32::MAX as i64
} else {
u32::MAX as i64
};
let mut last_discriminant: Option<i64> = None;
let mut discriminant_map: HashMap<i64, &syn::Variant> = HashMap::new();
let variants = self
.variants
.iter()
.map(|v| {
let value: i64 = match &v.discriminant {
Some((_, expr)) => match NumericValue::from_expr(expr).and_then(|n| n.parse()) {
Some(value) => value,
_ => bail_span!(
expr,
"C-style enums with #[wasm_bindgen] may only have \
numeric literal values that fit in a 32-bit integer as discriminants. \
Expressions or variables are not supported.",
),
},
None => {
last_discriminant.map_or(0, |last| last + 1)
}
};
last_discriminant = Some(value);
let underlying = if signed { "i32" } else { "u32" };
let numbers = if signed { "signed numbers" } else { "unsigned numbers" };
if value < underlying_min {
bail_span!(
v,
"C-style enums with #[wasm_bindgen] can only support {0} that can be represented by `{2}`, \
but `{1}` is too small for `{2}`",
numbers,
value,
underlying
);
}
if value > underlying_max {
bail_span!(
v,
"C-style enums with #[wasm_bindgen] can only support {0} that can be represented by `{2}`, \
but `{1}` is too large for `{2}`",
numbers,
value,
underlying
);
}
if let Some(old) = discriminant_map.insert(value, v) {
bail_span!(
v,
"discriminant value `{}` is already used by {} in this enum",
value,
old.ident
);
}
let comments = extract_doc_comments(&v.attrs);
Ok(ast::Variant {
rust_name: v.ident.clone(),
js_name: v.ident.unraw().to_string(),
value: value as u32,
comments,
})
})
.collect::<Result<Vec<_>, Diagnostic>>()?;
let hole = (0..=underlying_max)
.find(|v| !discriminant_map.contains_key(v))
.unwrap() as u32;
self.to_tokens(tokens);
program.enums.push(ast::Enum {
rust_name: self.ident,
js_name,
signed,
variants,
comments,
hole,
generate_typescript,
private,
js_namespace,
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> {
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)].");
}
let typescript_custom_section = match get_expr(&self.expr) {
syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(litstr),
..
}) => ast::LitOrExpr::Lit(litstr.value()),
expr => ast::LitOrExpr::Expr(expr.clone()),
};
program
.typescript_custom_sections
.push(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();
let import_start = program.imports.len();
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);
let module = module_from_opts(program, &opts)
.map_err(|e| errors.push(e))
.unwrap_or_default();
let seq_values = seq_values(&opts).unwrap_or_else(|diagnostic| {
errors.push(diagnostic);
[None; 2]
});
let generic_per_mono = opts.experimental_generic_mono().is_some();
let block_types = block_types(&self.items);
for item in self.items.into_iter() {
let ctx = ForeignItemCtx {
module: module.clone(),
js_namespace: js_namespace.clone(),
seq_values,
generic_per_mono,
block_types: &block_types,
};
if let Err(e) = item.macro_parse(program, ctx) {
errors.push(e);
}
}
add_class_cfg_to_function_shims(&mut program.imports[import_start..]);
Diagnostic::from_vec(errors)?;
opts.check_used();
Ok(())
}
}
fn add_class_cfg_to_function_shims(imports: &mut [ast::Import]) {
let mut type_cfgs = HashMap::<String, Vec<Vec<String>>>::new();
for import in imports.iter() {
let ast::ImportKind::Type(type_) = &import.kind else {
continue;
};
let mut cfg = crate::cfg_gate_attrs(&type_.attrs)
.iter()
.map(ToTokens::to_token_stream)
.map(|tokens| tokens.to_string())
.collect::<Vec<_>>();
cfg.sort();
type_cfgs
.entry(type_.rust_name.unraw().to_string())
.or_default()
.push(cfg);
}
for import in imports {
let ast::ImportKind::Function(function) = &mut import.kind else {
continue;
};
let ast::ImportFunctionKind::Method { ty, kind, .. } = &function.kind else {
continue;
};
let syn::Type::Path(syn::TypePath {
attrs: _,
qself: None,
path,
}) = get_ty(ty)
else {
continue;
};
let is_constructor = matches!(kind, ast::MethodKind::Constructor);
let is_local_path = path.leading_colon.is_none()
&& (path.segments.len() == 1
|| path
.segments
.first()
.is_some_and(|segment| segment.ident == "self" || segment.ident == "crate"));
if !is_constructor && !is_local_path {
continue;
}
let Some(name) = path
.segments
.last()
.map(|segment| segment.ident.unraw().to_string())
else {
continue;
};
let Some(candidates) = type_cfgs.get_mut(&name) else {
continue;
};
if candidates.iter().any(Vec::is_empty) {
continue;
}
candidates.sort();
let old_shim = function.shim.to_string();
let hash = ShortHash((&old_shim, &*candidates));
function.shim = Ident::new(&format!("{old_shim}_{hash}"), function.shim.span());
}
}
type BlockTypes = HashMap<String, Option<(String, Option<Vec<String>>)>>;
fn block_types(items: &[syn::ForeignItem]) -> BlockTypes {
let mut types = BlockTypes::new();
for item in items {
let syn::ForeignItem::Type(t) = item else {
continue;
};
let Ok(opts) = BindgenAttrs::find(&mut t.attrs.clone()) else {
continue;
};
let class = match opts.js_name() {
Some((js_name, _)) if is_computed_key(js_name) => None,
Some((js_name, _)) => Some(js_name.to_string()),
None => Some(t.ident.unraw().to_string()),
}
.map(|js_name| (js_name, opts.js_namespace().map(|(ns, _)| ns.0)));
for (used, _) in opts.attrs.iter() {
used.set(true);
}
opts.check_used();
let Some(class) = class else {
continue;
};
types
.entry(t.ident.unraw().to_string())
.and_modify(|existing| {
if existing.as_ref() != Some(&class) {
*existing = None;
}
})
.or_insert(Some(class));
}
types
}
fn block_type_class(
block_types: &BlockTypes,
path: &syn::Path,
js_namespace: &mut Option<Vec<String>>,
) -> Option<String> {
if path.leading_colon.is_some() || path.segments.len() != 1 {
return None;
}
let (js_name, type_namespace) = block_types
.get(&path.segments[0].ident.unraw().to_string())?
.as_ref()?;
if js_namespace.is_none() {
js_namespace.clone_from(type_namespace);
}
Some(js_name.clone())
}
struct ForeignItemCtx<'a> {
module: Option<ast::ImportModule>,
js_namespace: Option<JsNamespace>,
seq_values: SeqValues,
generic_per_mono: bool,
block_types: &'a BlockTypes,
}
impl<'a> MacroParse<ForeignItemCtx<'a>> for syn::ForeignItem {
fn macro_parse(
mut self,
program: &mut ast::Program,
ctx: ForeignItemCtx<'a>,
) -> 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,
syn::ForeignItem::Verbatim(v) => {
let mut item: syn::ItemStatic =
syn::parse(v.into()).expect("only foreign functions/types allowed for now");
let item_opts = BindgenAttrs::find(&mut item.attrs)?;
let reexport = item_opts.reexport().cloned();
let kind = item.convert((program, item_opts, &ctx.module))?;
program.imports.push(ast::Import {
module: None,
js_namespace: None,
reexport,
kind,
});
return Ok(());
}
_ => panic!("only foreign functions/types allowed for now"),
};
BindgenAttrs::find(attrs)?
};
if ctx.js_namespace.is_some() {
if let Some((_, spans)) = item_opts.js_namespace() {
return Err(Diagnostic::span_error(
spans[0],
"`js_namespace` cannot be set on both an `extern` block and an item \
inside it; write the full path on one of them, e.g. \
`js_namespace = [\"a\", \"b\"]`",
));
}
}
let mut js_namespace = item_opts
.js_namespace()
.map(|(s, _)| s)
.or(ctx.js_namespace)
.map(|s| s.0);
let module = ctx.module;
let block_seq_values = ctx.seq_values;
let block_generic_per_mono = ctx.generic_per_mono;
let reexport = item_opts.reexport().cloned();
if let syn::ForeignItem::Fn(_) = &self {
let is_method_like = item_opts.method().is_some()
|| item_opts.constructor().is_some()
|| item_opts.static_method_of().is_some()
|| item_opts.getter().is_some()
|| item_opts.setter().is_some();
if !is_method_like && js_namespace.is_none() {
if let Some((value, span)) = item_opts.js_name() {
if is_computed_key(value) {
return Err(Diagnostic::span_error(
span,
"free imported functions do not support symbols in js_name unless a js_namespace is specified",
));
}
}
}
}
let kind = match self {
syn::ForeignItem::Fn(f) => f.convert((
program,
item_opts,
&module,
block_seq_values,
block_generic_per_mono,
&mut js_namespace,
ctx.block_types,
))?,
syn::ForeignItem::Type(t) => {
t.convert((program, item_opts, &module, js_namespace.as_deref()))?
}
syn::ForeignItem::Static(s) => {
s.convert((program, item_opts, &module, js_namespace.as_deref()))?
}
_ => panic!("only foreign functions/types allowed for now"),
};
let needs_check = js_namespace.is_none() && module.is_none();
if needs_check {
match &kind {
ast::ImportKind::Function(import_function) => {
if matches!(import_function.kind, ast::ImportFunctionKind::Normal)
&& is_non_value_js_keyword(&import_function.function.name)
{
bail_span!(
import_function.rust_name,
"Imported function cannot use the JS keyword `{}` as its name.",
import_function.function.name
);
}
}
ast::ImportKind::Static(import_static) => {
if is_non_value_js_keyword(&import_static.js_name) {
bail_span!(
import_static.rust_name,
"Imported static cannot use the JS keyword `{}` as its name.",
import_static.js_name
);
}
}
ast::ImportKind::String(_) => {
}
ast::ImportKind::Type(import_type) => {
if is_non_value_js_keyword(&import_type.js_name) {
bail_span!(
import_type.rust_name,
"Imported type cannot use the JS keyword `{}` as its name.",
import_type.js_name
);
}
}
ast::ImportKind::Enum(_) => {
}
ast::ImportKind::DynamicUnion(_) => {
}
}
}
program.imports.push(ast::Import {
module,
js_namespace,
reexport,
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.js_sys() {
program.js_sys = 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))
} else {
None
};
Diagnostic::from_vec(errors)?;
Ok(module)
}
fn inline_js_snippet<'a>(
program: &'a ast::Program,
module: &Option<ast::ImportModule>,
) -> Option<&'a str> {
match module {
Some(ast::ImportModule::Inline(idx)) => Some(&program.inline_js[*idx]),
_ => None,
}
}
struct ImportCallStyle<'a> {
catch: bool,
variadic: bool,
r#final: bool,
suspending: bool,
generic_per_mono: bool,
method_kind: Option<&'a ast::MethodKind>,
slice_to_array: Vec<usize>,
seq_modes: Vec<(usize, ast::SeqMode)>,
js_name: Option<&'a String>,
}
impl Hash for ImportCallStyle<'_> {
fn hash<H: Hasher>(&self, state: &mut H) {
let Self {
catch,
variadic,
r#final,
suspending,
generic_per_mono,
method_kind,
slice_to_array,
seq_modes,
js_name,
} = self;
(
catch,
variadic,
r#final,
suspending,
generic_per_mono,
method_kind,
slice_to_array,
js_name,
)
.hash(state);
if !seq_modes.is_empty() {
seq_modes.hash(state);
}
}
}
impl ImportCallStyle<'_> {
fn is_plain(&self) -> bool {
let Self {
catch,
variadic,
r#final,
suspending,
generic_per_mono,
method_kind,
slice_to_array,
seq_modes,
js_name,
} = self;
!(*catch || *variadic || *r#final || *suspending || *generic_per_mono)
&& method_kind.is_none()
&& slice_to_array.is_empty()
&& seq_modes.is_empty()
&& js_name.is_none()
}
}
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 {
attrs: _,
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()))
}
fn extract_doc_comments(attrs: &[syn::Attribute]) -> Vec<String> {
attrs
.iter()
.filter_map(|a| {
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(
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(vec![], |mut acc, a| {
acc.extend(a);
acc
})
}
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
}
fn extract_path_ident(path: &syn::Path, allow_generics: bool) -> Result<Ident, Diagnostic> {
for segment in path.segments.iter() {
match &segment.arguments {
syn::PathArguments::None => {}
syn::PathArguments::AngleBracketed(_) => {
if !allow_generics {
bail_span!(
path,
"paths with type parameters are not supported in this position"
)
}
}
syn::PathArguments::Parenthesized(_) => {
bail_span!(path, "parenthesized paths are not supported yet")
}
}
}
match path.segments.last() {
Some(value) => Ok(value.ident.clone()),
None => {
bail_span!(path, "empty idents are not supported");
}
}
}
fn bail_generic_unsupported(span: impl Spanned + ToTokens) -> Result<(), Diagnostic> {
bail_span!(span, "unsupported in wasm-bindgen generics");
}
fn validate_generic_type_param_bound(bound: &syn::TypeParamBound) -> Result<(), Diagnostic> {
match bound {
syn::TypeParamBound::Trait(trait_bound) => {
if let Some(question) = &trait_bound.maybe {
bail_generic_unsupported(question)?;
}
}
syn::TypeParamBound::Lifetime(_) => {
}
syn::TypeParamBound::Verbatim(_) => {}
_ => {}
}
Ok(())
}
fn validate_generics(generics: &syn::Generics) -> Result<(), Diagnostic> {
if let Some(where_clause) = &generics.where_clause {
for predicate in &where_clause.predicates {
match predicate {
syn::WherePredicate::Type(predicate_type) => {
predicate_type
.bounds
.iter()
.try_for_each(validate_generic_type_param_bound)?;
}
syn::WherePredicate::Lifetime(_) => {
}
_ => bail_generic_unsupported(predicate)?,
}
}
}
for param in &generics.params {
match param {
syn::GenericParam::Lifetime(_) => {
}
syn::GenericParam::Type(type_param) => {
type_param
.bounds
.iter()
.try_for_each(validate_generic_type_param_bound)?;
}
syn::GenericParam::Const(const_param) => bail_generic_unsupported(const_param)?,
}
}
Ok(())
}
pub fn reset_attrs_used() {
ATTRS.with(|state| {
state.parsed.set(0);
state.checks.set(0);
state.unused_attrs.borrow_mut().clear();
state.conflicting_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() {
let unused_attrs = unused_attrs.iter().map(|UnusedState { error, ident }| {
if *error {
let text = format!("invalid attribute {ident} in this position");
quote::quote_spanned! { ident.span() => ::core::compile_error!(#text); }
} else {
quote::quote! { let #ident: (); }
}
});
tokens.extend(quote::quote! {
const _: () = {
#(#unused_attrs)*
};
});
}
for ConflictState { ignored, used } in &*state.conflicting_attrs.borrow() {
let note = format!(
"`{ignored}` conflicts with `{used}` and is ignored; \
conflicting options will be an error in the next major version"
);
tokens.extend(quote::quote_spanned! { ignored.span() =>
const _: () = {
#[deprecated(note = #note)]
const fn #ignored() {}
#ignored();
};
});
}
})
}
fn operation_kind(opts: &BindgenAttrs) -> ast::OperationKind {
let (kind, conflicts) = pick_operation_kind(opts);
ATTRS.with(|state| state.conflicting_attrs.borrow_mut().extend(conflicts));
kind
}
fn pick_operation_kind(opts: &BindgenAttrs) -> (ast::OperationKind, Vec<ConflictState>) {
let mut found = Vec::new();
let mut winner = None;
for (used, attr) in &opts.attrs {
let (rank, name, span, kind) = match attr {
BindgenAttr::This(span) => (0, "this", span, ast::OperationKind::RegularThis),
BindgenAttr::Getter(span, property) => (
1,
"getter",
span,
ast::OperationKind::Getter(property.clone()),
),
BindgenAttr::Setter(span, property) => (
2,
"setter",
span,
ast::OperationKind::Setter(property.clone()),
),
BindgenAttr::IndexingGetter(span) => (
3,
"indexing_getter",
span,
ast::OperationKind::IndexingGetter,
),
BindgenAttr::IndexingSetter(span) => (
4,
"indexing_setter",
span,
ast::OperationKind::IndexingSetter,
),
BindgenAttr::IndexingDeleter(span) => (
5,
"indexing_deleter",
span,
ast::OperationKind::IndexingDeleter,
),
_ => continue,
};
if !found.iter().any(|&(r, _, _)| r == rank) {
used.set(true);
if winner.as_ref().map_or(true, |&(r, _, _)| rank > r) {
winner = Some((rank, name, kind));
}
found.push((rank, name, *span));
}
}
let Some((rank, used, kind)) = winner else {
return (ast::OperationKind::Regular, Vec::new());
};
let conflicts = found
.into_iter()
.filter(|&(r, _, _)| r != rank)
.map(|(_, name, span)| ConflictState {
ignored: Ident::new(name, span),
used,
})
.collect();
(kind, conflicts)
}
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.as_ref();
let wasm_bindgen = &program.wasm_bindgen;
let wasm_bindgen_futures = &program.wasm_bindgen_futures;
let js_sys = &program.js_sys;
let futures = if ast::use_js_sys_futures() {
quote::quote! { #js_sys::futures }
} else {
quote::quote! { #wasm_bindgen_futures }
};
if f.sig.asyncness.take().is_some() {
*f.block = syn::parse2(quote::quote! {
{
async fn __wasm_bindgen_generated_main() #r#return #body
#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 = 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);
}
fn import_namespaces(
block_opts: proc_macro2::TokenStream,
block: syn::ItemForeignMod,
) -> Result<Vec<Option<Vec<String>>>, super::Diagnostic> {
use super::{ast, BindgenAttrs, MacroParse};
let opts: BindgenAttrs = syn::parse2(block_opts).unwrap();
let mut program = ast::Program::default();
block.macro_parse(&mut program, opts)?;
Ok(program
.imports
.into_iter()
.map(|import| import.js_namespace)
.collect())
}
fn ns(segments: &[&str]) -> Option<Vec<String>> {
Some(segments.iter().map(|s| s.to_string()).collect())
}
#[test]
fn js_namespace_only_on_block() {
let namespaces = import_namespaces(
quote::quote! { js_namespace = ["a"] },
syn::parse_quote! {
extern "C" {
fn my_function();
}
},
)
.unwrap();
assert_eq!(namespaces, vec![ns(&["a"])]);
}
#[test]
fn js_namespace_only_on_item() {
let namespaces = import_namespaces(
quote::quote! {},
syn::parse_quote! {
extern "C" {
#[wasm_bindgen(js_namespace = ["b"])]
fn my_function();
}
},
)
.unwrap();
assert_eq!(namespaces, vec![ns(&["b"])]);
}
#[test]
fn js_namespace_absent_everywhere() {
let namespaces = import_namespaces(
quote::quote! {},
syn::parse_quote! {
extern "C" {
fn my_function();
}
},
)
.unwrap();
assert_eq!(namespaces, vec![None]);
}
fn conflict_warnings(attr: proc_macro2::TokenStream, item: proc_macro2::TokenStream) -> usize {
let tokens = crate::expand(attr, item).unwrap();
tokens.to_string().matches("conflicts with").count()
}
#[test]
fn conflicting_options_warn_once_with_js_name() {
let import = conflict_warnings(
quote::quote! {},
quote::quote! {
extern "C" {
type Foo;
#[wasm_bindgen(method, getter, setter, js_name = renamed)]
fn set_renamed(this: &Foo, value: u32);
}
},
);
assert_eq!(import, 1);
let export = conflict_warnings(
quote::quote! { getter, setter, js_name = renamed },
quote::quote! { pub fn set_renamed(value: u32) {} },
);
assert_eq!(export, 1);
}
}