Alex Crichton 9daa11592a Remove Module node from the backend AST
This is a roundabout way to say that this addresses the last comment on #23,
namely if you only use the `console` submodule from `web_sys` it doesn't
actually link correctly!

The problem here has to do with codegen units and the compiler. The compiler
will create a codegen unit for each `mod` in the source code. If a codegen unit
isn't actually used, then the codegen unit is removed from the final link step.
This causes problems for web-sys where the JSON description of our program was
part of the main CGU but not in each submodule, so when submodules were only
used the descriptor program in the main CGU was not included.

The fix in this commit is to instead generate a descriptor program in the
submodule itself instead of leaving it in the main CGU. By removing the `Module`
node in the AST this naturally happens as the descriptor is only generated in
the same module as all other associated items.
2018-09-17 13:50:26 -07:00

721 lines
24 KiB
Rust

/*!
# `wasm_bindgen_webidl`
Converts WebIDL into wasm-bindgen's internal AST form, so that bindings can be
emitted for the types and methods described in the WebIDL.
*/
#![deny(missing_docs)]
#![deny(missing_debug_implementations)]
#![doc(html_root_url = "https://docs.rs/wasm-bindgen-webidl/0.2")]
#[macro_use]
extern crate failure;
#[macro_use]
extern crate failure_derive;
extern crate heck;
#[macro_use]
extern crate log;
extern crate proc_macro2;
#[macro_use]
extern crate quote;
#[macro_use]
extern crate syn;
extern crate wasm_bindgen_backend as backend;
extern crate weedle;
mod first_pass;
mod idl_type;
mod util;
mod error;
use std::collections::{BTreeSet, HashSet};
use std::env;
use std::fs;
use std::iter::FromIterator;
use backend::TryToTokens;
use backend::ast;
use backend::defined::ImportedTypeReferences;
use backend::defined::{ImportedTypeDefinitions, RemoveUndefinedImports};
use backend::util::{ident_ty, rust_ident, raw_ident, wrap_import_function};
use proc_macro2::{Ident, Span};
use quote::ToTokens;
use weedle::attribute::{ExtendedAttributeList};
use weedle::dictionary::DictionaryMember;
use weedle::interface::InterfaceMember;
use first_pass::{FirstPass, FirstPassRecord, OperationId, InterfaceData};
use first_pass::{OperationData, CallbackInterfaceData};
use util::{public, webidl_const_v_to_backend_const_v, TypePosition, camel_case_ident, shouty_snake_case_ident, snake_case_ident, mdn_doc};
use idl_type::ToIdlType;
pub use error::{Error, ErrorKind, Result};
struct Program {
main: backend::ast::Program,
submodules: Vec<(String, backend::ast::Program)>,
}
/// Parse a string of WebIDL source text into a wasm-bindgen AST.
fn parse(webidl_source: &str, allowed_types: Option<&[&str]>)
-> Result<Program>
{
let definitions = match weedle::parse(webidl_source) {
Ok(def) => def,
Err(e) => {
return Err(match &e {
weedle::Err::Incomplete(needed) => {
format_err!("needed {:?} more bytes", needed)
.context(ErrorKind::ParsingWebIDLSource).into()
}
weedle::Err::Error(cx) |
weedle::Err::Failure(cx) => {
let remaining = match cx {
weedle::Context::Code(remaining, _) => remaining,
};
let pos = webidl_source.len() - remaining.len();
format_err!("failed to parse WebIDL")
.context(ErrorKind::ParsingWebIDLSourcePos(pos)).into()
}
});
}
};
let mut first_pass_record: FirstPassRecord = Default::default();
first_pass_record.builtin_idents = builtin_idents();
definitions.first_pass(&mut first_pass_record, ())?;
let mut program = Default::default();
let mut submodules = Vec::new();
let allowed_types = allowed_types.map(|list| {
list.iter().cloned().collect::<HashSet<_>>()
});
let filter = |name: &str| {
match &allowed_types {
Some(set) => set.contains(&camel_case_ident(name)[..]),
None => true,
}
};
for (name, e) in first_pass_record.enums.iter() {
if filter(name) {
first_pass_record.append_enum(&mut program, e);
}
}
for (name, d) in first_pass_record.dictionaries.iter() {
if filter(name) {
first_pass_record.append_dictionary(&mut program, d);
}
}
for (name, n) in first_pass_record.namespaces.iter() {
let prog = first_pass_record.append_ns(name, n);
submodules.push((snake_case_ident(name).to_string(), prog));
}
for (name, d) in first_pass_record.interfaces.iter() {
if filter(name) {
first_pass_record.append_interface(&mut program, name, d);
}
}
for (name, d) in first_pass_record.callback_interfaces.iter() {
if filter(name) {
first_pass_record.append_callback_interface(&mut program, d);
}
}
// Prune out `extends` annotations that aren't defined as these shouldn't
// prevent the type from being usable entirely. They're just there for
// `AsRef` and such implementations.
for import in program.imports.iter_mut() {
if let backend::ast::ImportKind::Type(t) = &mut import.kind {
t.extends.retain(|n| {
first_pass_record.builtin_idents.contains(n) ||
filter(&n.to_string())
});
}
}
Ok(Program {
main: program,
submodules: submodules,
})
}
/// Compile the given WebIDL source text into Rust source text containing
/// `wasm-bindgen` bindings to the things described in the WebIDL.
pub fn compile(
webidl_source: &str,
allowed_types: Option<&[&str]>,
) -> Result<String> {
let ast = parse(webidl_source, allowed_types)?;
Ok(compile_ast(ast))
}
fn builtin_idents() -> BTreeSet<Ident> {
BTreeSet::from_iter(
vec![
"str", "char", "bool", "JsValue", "u8", "i8", "u16", "i16", "u32", "i32", "u64", "i64",
"usize", "isize", "f32", "f64", "Result", "String", "Vec", "Option",
"Array", "ArrayBuffer", "Object", "Promise", "Function",
].into_iter()
.map(|id| proc_macro2::Ident::new(id, proc_macro2::Span::call_site())),
)
}
/// Run codegen on the AST to generate rust code.
fn compile_ast(mut ast: Program) -> String {
// Iteratively prune all entries from the AST which reference undefined
// fields. Each pass may remove definitions of types and so we need to
// reexecute this pass to see if we need to keep removing types until we
// reach a steady state.
let builtin = builtin_idents();
let mut all_definitions = BTreeSet::new();
let track = env::var_os("__WASM_BINDGEN_DUMP_FEATURES");
loop {
let mut defined = builtin.clone();
{
let mut cb = |id: &Ident| {
defined.insert(id.clone());
if track.is_some() {
all_definitions.insert(id.clone());
}
};
ast.main.imported_type_definitions(&mut cb);
for (_, m) in ast.submodules.iter() {
m.imported_type_references(&mut cb);
}
}
let changed =
ast.main.remove_undefined_imports(&|id| defined.contains(id)) ||
ast.submodules.iter_mut().any(|(_, m)| {
m.remove_undefined_imports(&|id| defined.contains(id))
});
if !changed {
break
}
}
if let Some(path) = track {
let contents = all_definitions.into_iter()
.map(|s| format!("{} = []", s))
.collect::<Vec<_>>()
.join("\n");
fs::write(path, contents).unwrap();
}
let mut tokens = proc_macro2::TokenStream::new();
if let Err(e) = ast.main.try_to_tokens(&mut tokens) {
e.panic();
}
for (name, m) in ast.submodules.iter() {
let mut m_tokens = proc_macro2::TokenStream::new();
if let Err(e) = m.try_to_tokens(&mut m_tokens) {
e.panic();
}
let name = Ident::new(name, Span::call_site());
(quote! {
pub mod #name { #m_tokens }
}).to_tokens(&mut tokens);
}
tokens.to_string()
}
impl<'src> FirstPassRecord<'src> {
fn append_enum(
&self,
program: &mut backend::ast::Program,
enum_: &'src weedle::EnumDefinition<'src>
) {
let variants = &enum_.values.body.list;
program.imports.push(backend::ast::Import {
module: None,
js_namespace: None,
kind: backend::ast::ImportKind::Enum(backend::ast::ImportEnum {
vis: public(),
name: rust_ident(camel_case_ident(enum_.identifier.0).as_str()),
variants: variants
.iter()
.map(|v| {
if !v.0.is_empty() {
rust_ident(camel_case_ident(&v.0).as_str())
} else {
rust_ident("None")
}
})
.collect(),
variant_values: variants.iter().map(|v| v.0.to_string()).collect(),
rust_attrs: vec![parse_quote!(#[derive(Copy, Clone, PartialEq, Debug)])],
}),
});
}
// tons more data for what's going on here at
// https://www.w3.org/TR/WebIDL-1/#idl-dictionaries
fn append_dictionary(
&self,
program: &mut backend::ast::Program,
data: &first_pass::DictionaryData<'src>,
) {
let def = match data.definition {
Some(def) => def,
None => return,
};
let mut fields = Vec::new();
if !self.append_dictionary_members(def.identifier.0, &mut fields) {
return
}
program.dictionaries.push(ast::Dictionary {
name: rust_ident(&camel_case_ident(def.identifier.0)),
fields,
});
}
fn append_dictionary_members(
&self,
dict: &'src str,
dst: &mut Vec<ast::DictionaryField>,
) -> bool {
let dict_data = &self.dictionaries[&dict];
let definition = dict_data.definition.unwrap();
// > The order of the dictionary members on a given dictionary is
// > such that inherited dictionary members are ordered before
// > non-inherited members ...
if let Some(parent) = &definition.inheritance {
if !self.append_dictionary_members(parent.identifier.0, dst) {
return false
}
}
// > ... and the dictionary members on the one dictionary
// > definition (including any partial dictionary definitions) are
// > ordered lexicographically by the Unicode codepoints that
// > comprise their identifiers.
let start = dst.len();
let members = definition.members.body.iter();
let partials = dict_data.partials.iter().flat_map(|d| &d.members.body);
for member in members.chain(partials) {
match self.dictionary_field(member) {
Some(f) => dst.push(f),
None => {
warn!(
"unsupported dictionary field {:?}",
(dict, member.identifier.0),
);
// If this is required then we can't support the
// dictionary at all, but if it's not required we can
// avoid generating bindings for the field and keep
// going otherwise.
if member.required.is_some() {
return false
}
}
}
}
// Note that this sort isn't *quite* right in that it is sorting
// based on snake case instead of the original casing which could
// produce inconsistent results, but should work well enough for
// now!
dst[start..].sort_by_key(|f| f.name.clone());
return true
}
fn dictionary_field(
&self,
field: &'src DictionaryMember<'src>,
) -> Option<ast::DictionaryField> {
// use argument position now as we're just binding setters
let ty = field.type_.to_idl_type(self)?.to_syn_type(TypePosition::Argument)?;
// Slice types aren't supported because they don't implement
// `Into<JsValue>`
match ty {
syn::Type::Reference(ref i) =>
match &*i.elem {
syn::Type::Slice(_) => return None,
_ => ()
}
syn::Type::Path(ref path, ..) =>
// check that our inner don't contains slices either
for seg in path.path.segments.iter() {
if let syn::PathArguments::AngleBracketed(ref arg) = seg.arguments {
for elem in &arg.args {
if let syn::GenericArgument::Type(syn::Type::Reference(ref i)) = elem {
match &*i.elem {
syn::Type::Slice(_) => return None,
_ => ()
}
}
}
}
}
_ => ()
};
// Similarly i64/u64 aren't supported because they don't
// implement `Into<JsValue>`
let mut any_64bit = false;
ty.imported_type_references(&mut |i| {
any_64bit = any_64bit || i == "u64" || i == "i64";
});
if any_64bit {
return None
}
Some(ast::DictionaryField {
required: field.required.is_some(),
name: rust_ident(&snake_case_ident(field.identifier.0)),
ty,
})
}
fn append_ns(
&'src self,
name: &'src str,
ns: &'src first_pass::NamespaceData<'src>,
) -> backend::ast::Program {
let mut ret = Default::default();
for (id, data) in ns.operations.iter() {
self.append_ns_member(&mut ret, name, id, data);
}
return ret
}
fn append_ns_member(
&self,
module: &mut backend::ast::Program,
self_name: &'src str,
id: &OperationId<'src>,
data: &OperationData<'src>,
) {
let name = match id {
OperationId::Operation(Some(name)) => name,
OperationId::Constructor(_) |
OperationId::Operation(None) |
OperationId::IndexingGetter |
OperationId::IndexingSetter |
OperationId::IndexingDeleter => {
warn!("Unsupported unnamed operation: on {:?}", self_name);
return
}
};
let doc_comment = format!(
"The `{}.{}()` function\n\n{}",
self_name,
name,
mdn_doc(self_name, Some(&name))
);
let kind = backend::ast::ImportFunctionKind::Normal;
for mut import_function in self.create_imports(None, kind, id, data) {
import_function.doc_comment = Some(doc_comment.clone());
module.imports.push(
backend::ast::Import {
module: None,
js_namespace: Some(raw_ident(self_name)),
kind: backend::ast::ImportKind::Function(import_function),
}
);
}
}
fn append_const(
&self,
program: &mut backend::ast::Program,
self_name: &'src str,
member: &'src weedle::interface::ConstMember<'src>,
) {
let idl_type = match member.const_type.to_idl_type(self) {
Some(idl_type) => idl_type,
None => return,
};
let ty = match idl_type.to_syn_type(TypePosition::Return) {
Some(ty) => ty,
None => {
warn!(
"Cannot convert const type to syn type: {:?} in {:?} on {:?}",
idl_type,
member,
self_name
);
return
},
};
program.consts.push(backend::ast::Const {
vis: public(),
name: rust_ident(shouty_snake_case_ident(member.identifier.0).as_str()),
class: Some(rust_ident(camel_case_ident(&self_name).as_str())),
ty,
value: webidl_const_v_to_backend_const_v(&member.const_value),
});
}
fn append_interface(
&self,
program: &mut backend::ast::Program,
name: &'src str,
data: &InterfaceData<'src>,
) {
let mut doc_comment = Some(format!(
"The `{}` object\n\n{}",
name,
mdn_doc(name, None),
));
let mut import_type = backend::ast::ImportType {
vis: public(),
rust_name: rust_ident(camel_case_ident(name).as_str()),
js_name: name.to_string(),
attrs: Vec::new(),
doc_comment: None,
instanceof_shim: format!("__widl_instanceof_{}", name),
extends: self.all_superclasses(name)
.map(|name| Ident::new(&name, Span::call_site()))
.chain(Some(Ident::new("Object", Span::call_site())))
.collect(),
};
self.append_required_features_doc(&import_type, &mut doc_comment);
import_type.doc_comment = doc_comment;
program.imports.push(backend::ast::Import {
module: None,
js_namespace: None,
kind: backend::ast::ImportKind::Type(import_type),
});
for (id, op_data) in data.operations.iter() {
self.member_operation(program, name, data, id, op_data);
}
for member in data.consts.iter() {
self.append_const(program, name, member);
}
for member in data.attributes.iter() {
self.member_attribute(
program,
name,
member.modifier,
member.readonly.is_some(),
&member.type_,
member.identifier.0,
&member.attributes,
data.definition_attributes,
);
}
for mixin_data in self.all_mixins(name) {
for (id, op_data) in mixin_data.operations.iter() {
self.member_operation(program, name, data, id, op_data);
}
for member in &mixin_data.consts {
self.append_const(program, name, member);
}
for member in &mixin_data.attributes {
self.member_attribute(
program,
name,
if let Some(s) = member.stringifier {
Some(weedle::interface::StringifierOrInheritOrStatic::Stringifier(s))
} else {
None
},
member.readonly.is_some(),
&member.type_,
member.identifier.0,
&member.attributes,
mixin_data.definition_attributes,
);
}
}
}
fn member_attribute(
&self,
program: &mut backend::ast::Program,
self_name: &'src str,
modifier: Option<weedle::interface::StringifierOrInheritOrStatic>,
readonly: bool,
type_: &'src weedle::types::AttributedType<'src>,
identifier: &'src str,
attrs: &'src Option<ExtendedAttributeList<'src>>,
container_attrs: Option<&'src ExtendedAttributeList<'src>>,
) {
use weedle::interface::StringifierOrInheritOrStatic::*;
let is_static = match modifier {
Some(Stringifier(_)) => unreachable!(), // filtered out earlier
Some(Inherit(_)) => false,
Some(Static(_)) => true,
None => false,
};
let global = self
.interfaces
.get(self_name)
.map(|interface_data| interface_data.global)
.unwrap_or(false);
for mut import_function in self.create_getter(
identifier,
&type_.type_,
self_name,
is_static,
global,
attrs,
container_attrs,
) {
let mut doc = import_function.doc_comment.take();
self.append_required_features_doc(&import_function, &mut doc);
import_function.doc_comment = doc;
program.imports.push(wrap_import_function(import_function));
}
if !readonly {
for mut import_function in self.create_setter(
identifier,
&type_.type_,
self_name,
is_static,
global,
attrs,
container_attrs,
) {
let mut doc = import_function.doc_comment.take();
self.append_required_features_doc(&import_function, &mut doc);
import_function.doc_comment = doc;
program.imports.push(wrap_import_function(import_function));
}
}
}
fn member_operation(
&self,
program: &mut backend::ast::Program,
self_name: &str,
data: &InterfaceData<'src>,
id: &OperationId<'src>,
op_data: &OperationData<'src>,
) {
let import_function_kind = |opkind| {
self.import_function_kind(self_name, data.global, op_data.is_static, opkind)
};
let kind = match id {
OperationId::Constructor(ctor_name) => {
let self_ty = ident_ty(rust_ident(&camel_case_ident(self_name)));
backend::ast::ImportFunctionKind::Method {
class: ctor_name.0.to_string(),
ty: self_ty.clone(),
kind: backend::ast::MethodKind::Constructor,
}
}
OperationId::Operation(_) => {
import_function_kind(backend::ast::OperationKind::Regular)
}
OperationId::IndexingGetter => {
import_function_kind(backend::ast::OperationKind::IndexingGetter)
}
OperationId::IndexingSetter => {
import_function_kind(backend::ast::OperationKind::IndexingSetter)
}
OperationId::IndexingDeleter => {
import_function_kind(backend::ast::OperationKind::IndexingDeleter)
}
};
let doc = match id {
OperationId::Constructor(_) |
OperationId::Operation(None) => Some(String::new()),
OperationId::Operation(Some(name)) => {
Some(format!(
"The `{}()` method\n\n{}",
name,
mdn_doc(self_name, Some(name))
))
}
OperationId::IndexingGetter => {
Some(format!("The indexing getter\n\n"))
}
OperationId::IndexingSetter => {
Some(format!("The indexing setter\n\n"))
}
OperationId::IndexingDeleter => {
Some(format!("The indexing deleter\n\n"))
}
};
let attrs = data.definition_attributes;
for mut method in self.create_imports(attrs, kind, id, op_data) {
let mut doc = doc.clone();
self.append_required_features_doc(&method, &mut doc);
method.doc_comment = doc;
program.imports.push(wrap_import_function(method));
}
}
fn append_required_features_doc(
&self,
item: impl ImportedTypeReferences,
doc: &mut Option<String>,
) {
let doc = match doc {
Some(doc) => doc,
None => return,
};
let mut required = BTreeSet::new();
item.imported_type_references(&mut |f| {
if !self.builtin_idents.contains(f) {
required.insert(f.clone());
}
});
if required.len() == 0 {
return
}
let list = required.iter()
.map(|ident| format!("`{}`", ident))
.collect::<Vec<_>>()
.join(", ");
doc.push_str(&format!(
"\n\n*This function requires the following crate features \
to be activated: {}*",
list,
));
}
fn append_callback_interface(
&self,
program: &mut backend::ast::Program,
item: &CallbackInterfaceData<'src>,
) {
let mut fields = Vec::new();
for member in item.definition.members.body.iter() {
match member {
InterfaceMember::Operation(op) => {
let identifier = match op.identifier {
Some(i) => i.0,
None => continue,
};
let pos = TypePosition::Argument;
fields.push(ast::DictionaryField {
required: false,
name: rust_ident(&snake_case_ident(identifier)),
ty: idl_type::IdlType::Callback.to_syn_type(pos)
.unwrap(),
});
}
_ => {
warn!("skipping callback interface member on {}",
item.definition.identifier.0);
}
}
}
program.dictionaries.push(ast::Dictionary {
name: rust_ident(&camel_case_ident(item.definition.identifier.0)),
fields,
});
}
}