#![deny(missing_docs)]
#![deny(missing_debug_implementations)]
#![doc(html_root_url = "https://docs.rs/wasm-bindgen-webidl/0.2")]
mod constants;
mod first_pass;
mod generator;
mod idl_type;
mod traverse;
mod util;
use crate::first_pass::{CallbackInterfaceData, OperationData};
use crate::first_pass::{FirstPass, FirstPassRecord, InterfaceData, OperationId};
use crate::generator::{
Dictionary, DictionaryField, Enum, EnumVariant, Function, Interface, InterfaceAttribute,
InterfaceAttributeKind, InterfaceConst, InterfaceMethod, Namespace,
};
use crate::idl_type::ToIdlType;
use crate::traverse::TraverseType;
use crate::util::{
camel_case_ident, is_structural, shouty_snake_case_ident, snake_case_ident, throws,
webidl_const_v_to_backend_const_v, TypePosition,
};
use anyhow::Context;
use anyhow::Result;
use proc_macro2::{Ident, TokenStream};
use quote::ToTokens;
use sourcefile::SourceFile;
use std::collections::{BTreeMap, BTreeSet};
use std::ffi::OsStr;
use std::fmt;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use wasm_bindgen_backend::util::rust_ident;
use weedle::attribute::ExtendedAttributeList;
use weedle::dictionary::DictionaryMember;
use weedle::interface::InterfaceMember;
use weedle::Parse;
#[derive(Debug)]
pub struct Options {
pub features: bool,
}
#[derive(Default)]
struct Program {
tokens: TokenStream,
required_features: BTreeSet<String>,
}
impl Program {
fn to_string(&self) -> Option<String> {
if self.tokens.is_empty() {
None
} else {
Some(self.tokens.to_string())
}
}
}
#[derive(Debug)]
pub struct WebIDLParseError(pub usize);
impl fmt::Display for WebIDLParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "failed to parse webidl at byte position {}", self.0)
}
}
impl std::error::Error for WebIDLParseError {}
#[derive(Clone, Copy, PartialEq)]
pub(crate) enum ApiStability {
Stable,
Unstable,
}
impl ApiStability {
pub(crate) fn is_unstable(self) -> bool {
self == Self::Unstable
}
}
impl Default for ApiStability {
fn default() -> Self {
Self::Stable
}
}
fn parse_source(source: &str) -> Result<Vec<weedle::Definition>> {
match weedle::Definitions::parse(source) {
Ok(("", parsed)) => Ok(parsed),
Ok((remaining, _))
| Err(weedle::Err::Error((remaining, _)))
| Err(weedle::Err::Failure((remaining, _))) => {
Err(WebIDLParseError(source.len() - remaining.len()).into())
}
Err(weedle::Err::Incomplete(needed)) => {
Err(anyhow::anyhow!("needed {:?} more bytes", needed))
}
}
}
fn parse(
webidl_source: &str,
unstable_source: &str,
options: Options,
) -> Result<BTreeMap<String, Program>> {
let mut first_pass_record: FirstPassRecord = Default::default();
let definitions = parse_source(webidl_source)?;
definitions.first_pass(&mut first_pass_record, ApiStability::Stable)?;
let unstable_definitions = parse_source(unstable_source)?;
unstable_definitions.first_pass(&mut first_pass_record, ApiStability::Unstable)?;
let mut types: BTreeMap<String, Program> = BTreeMap::new();
for (js_name, e) in first_pass_record.enums.iter() {
let name = rust_ident(&camel_case_ident(js_name));
let program = types.entry(name.to_string()).or_default();
first_pass_record.append_enum(&options, program, name, js_name, e);
}
for (js_name, d) in first_pass_record.dictionaries.iter() {
let name = rust_ident(&camel_case_ident(js_name));
let program = types.entry(name.to_string()).or_default();
first_pass_record.append_dictionary(&options, program, name, js_name.to_string(), d);
}
for (js_name, n) in first_pass_record.namespaces.iter() {
let name = rust_ident(&snake_case_ident(js_name));
let program = types.entry(name.to_string()).or_default();
first_pass_record.append_ns(&options, program, name, js_name.to_string(), n);
}
for (js_name, d) in first_pass_record.interfaces.iter() {
let name = rust_ident(&camel_case_ident(js_name));
let program = types.entry(name.to_string()).or_default();
first_pass_record.append_interface(&options, program, name, js_name.to_string(), d);
}
for (js_name, d) in first_pass_record.callback_interfaces.iter() {
let name = rust_ident(&camel_case_ident(js_name));
let program = types.entry(name.to_string()).or_default();
first_pass_record.append_callback_interface(
&options,
program,
name,
js_name.to_string(),
d,
);
}
Ok(types)
}
#[derive(Debug)]
pub struct Feature {
pub code: String,
pub required_features: Vec<String>,
}
pub fn compile(
webidl_source: &str,
experimental_source: &str,
options: Options,
) -> Result<BTreeMap<String, Feature>> {
let ast = parse(webidl_source, experimental_source, options)?;
let features = ast
.into_iter()
.filter_map(|(name, program)| {
let code = program.to_string()?;
let required_features = program.required_features.into_iter().collect();
Some((
name,
Feature {
required_features,
code,
},
))
})
.collect();
Ok(features)
}
impl<'src> FirstPassRecord<'src> {
fn append_enum(
&self,
options: &Options,
program: &mut Program,
name: Ident,
js_name: &str,
data: &first_pass::EnumData<'src>,
) {
let enum_ = data.definition;
let unstable = data.stability.is_unstable();
assert_eq!(js_name, enum_.identifier.0);
let variants = enum_
.values
.body
.list
.iter()
.map(|v| {
let name = if !v.0.is_empty() {
rust_ident(camel_case_ident(&v.0).as_str())
} else {
rust_ident("None")
};
let value = v.0.to_string();
EnumVariant { name, value }
})
.collect::<Vec<_>>();
Enum {
name,
variants,
unstable,
}
.generate(options)
.to_tokens(&mut program.tokens);
}
fn append_dictionary(
&self,
options: &Options,
program: &mut Program,
name: Ident,
js_name: String,
data: &first_pass::DictionaryData<'src>,
) {
let def = match data.definition {
Some(def) => def,
None => return,
};
assert_eq!(js_name, def.identifier.0);
let unstable = data.stability.is_unstable();
let mut fields = Vec::new();
if !self.append_dictionary_members(&js_name, &mut fields) {
return;
}
Dictionary {
name,
js_name,
fields,
unstable,
}
.generate(options)
.to_tokens(&mut program.tokens);
}
fn append_dictionary_members(&self, dict: &'src str, dst: &mut Vec<DictionaryField>) -> bool {
let dict_data = &self.dictionaries[&dict];
let definition = dict_data.definition.unwrap();
if let Some(parent) = &definition.inheritance {
if !self.append_dictionary_members(parent.identifier.0, dst) {
return false;
}
}
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 => {
log::warn!(
"unsupported dictionary field {:?}",
(dict, member.identifier.0),
);
if member.required.is_some() {
return false;
}
}
}
}
dst[start..].sort_by_key(|f| f.js_name.clone());
return true;
}
fn dictionary_field(&self, field: &'src DictionaryMember<'src>) -> Option<DictionaryField> {
let ty = field
.type_
.to_idl_type(self)
.to_syn_type(TypePosition::Argument)
.unwrap_or(None)?;
match ty {
syn::Type::Reference(ref i) => match &*i.elem {
syn::Type::Slice(_) => return None,
_ => (),
},
syn::Type::Path(ref path, ..) =>
{
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,
_ => (),
}
}
}
}
}
}
_ => (),
};
let mut any_64bit = false;
ty.traverse_type(&mut |ident| {
if !any_64bit {
if ident == "u64" || ident == "i64" {
any_64bit = true;
}
}
});
if any_64bit {
return None;
}
Some(DictionaryField {
required: field.required.is_some(),
name: rust_ident(&snake_case_ident(field.identifier.0)),
js_name: field.identifier.0.to_string(),
ty,
})
}
fn append_ns(
&'src self,
options: &Options,
program: &mut Program,
name: Ident,
js_name: String,
ns: &'src first_pass::NamespaceData<'src>,
) {
let mut functions = vec![];
for (id, data) in ns.operations.iter() {
self.append_ns_member(&mut functions, &js_name, id, data);
}
if !functions.is_empty() {
Namespace {
name,
js_name,
functions,
}
.generate(options)
.to_tokens(&mut program.tokens);
}
}
fn append_ns_member(
&self,
functions: &mut Vec<Function>,
js_name: &'src str,
id: &OperationId<'src>,
data: &OperationData<'src>,
) {
match id {
OperationId::Operation(Some(_)) => {}
OperationId::Constructor
| OperationId::NamedConstructor(_)
| OperationId::Operation(None)
| OperationId::IndexingGetter
| OperationId::IndexingSetter
| OperationId::IndexingDeleter => {
log::warn!("Unsupported unnamed operation: on {:?}", js_name);
return;
}
}
for x in self.create_imports(None, id, data, false) {
functions.push(Function {
name: x.name,
js_name: x.js_name,
arguments: x.arguments,
ret_ty: x.ret_ty,
catch: x.catch,
variadic: x.variadic,
unstable: false,
});
}
}
fn append_const(
&self,
consts: &mut Vec<InterfaceConst>,
member: &'src weedle::interface::ConstMember<'src>,
unstable: bool,
) {
let idl_type = member.const_type.to_idl_type(self);
let ty = idl_type.to_syn_type(TypePosition::Return).unwrap().unwrap();
let js_name = member.identifier.0;
let name = rust_ident(shouty_snake_case_ident(js_name).as_str());
let value = webidl_const_v_to_backend_const_v(&member.const_value);
consts.push(InterfaceConst {
name,
js_name: js_name.to_string(),
ty,
value,
unstable,
});
}
fn append_interface(
&self,
options: &Options,
program: &mut Program,
name: Ident,
js_name: String,
data: &InterfaceData<'src>,
) {
let unstable = data.stability.is_unstable();
let has_interface = data.has_interface;
let deprecated = data.deprecated.clone();
let parents = self
.all_superclasses(&js_name)
.map(|parent| {
let ident = rust_ident(&camel_case_ident(&parent));
program.required_features.insert(parent);
ident
})
.collect::<Vec<_>>();
let mut consts = vec![];
let mut attributes = vec![];
let mut methods = vec![];
for member in data.consts.iter() {
self.append_const(&mut consts, member, unstable);
}
for member in data.attributes.iter() {
let unstable = unstable || member.stability.is_unstable();
let member = member.definition;
self.member_attribute(
&mut attributes,
member.modifier,
member.readonly.is_some(),
&member.type_,
member.identifier.0.to_string(),
&member.attributes,
data.definition_attributes,
unstable,
);
}
for (id, op_data) in data.operations.iter() {
self.member_operation(&mut methods, data, id, op_data);
}
for mixin_data in self.all_mixins(&js_name) {
for member in &mixin_data.consts {
self.append_const(&mut consts, member, unstable);
}
for member in &mixin_data.attributes {
let unstable = unstable || member.stability.is_unstable();
let member = member.definition;
self.member_attribute(
&mut attributes,
if let Some(s) = member.stringifier {
Some(weedle::interface::StringifierOrInheritOrStatic::Stringifier(s))
} else {
None
},
member.readonly.is_some(),
&member.type_,
member.identifier.0.to_string(),
&member.attributes,
data.definition_attributes,
unstable,
);
}
for (id, op_data) in mixin_data.operations.iter() {
self.member_operation(&mut methods, data, id, op_data);
}
}
Interface {
name,
js_name,
deprecated,
has_interface,
parents,
consts,
attributes,
methods,
unstable,
}
.generate(options)
.to_tokens(&mut program.tokens);
}
fn member_attribute(
&self,
attributes: &mut Vec<InterfaceAttribute>,
modifier: Option<weedle::interface::StringifierOrInheritOrStatic>,
readonly: bool,
type_: &'src weedle::types::AttributedType<'src>,
js_name: String,
attrs: &'src Option<ExtendedAttributeList<'src>>,
container_attrs: Option<&'src ExtendedAttributeList<'src>>,
unstable: bool,
) {
use weedle::interface::StringifierOrInheritOrStatic::*;
let is_static = match modifier {
Some(Stringifier(_)) => unreachable!(),
Some(Inherit(_)) => false,
Some(Static(_)) => true,
None => false,
};
let structural = is_structural(attrs.as_ref(), container_attrs);
let catch = throws(attrs);
let ty = type_
.type_
.to_idl_type(self)
.to_syn_type(TypePosition::Return)
.unwrap_or(None);
if let Some(ty) = ty {
let kind = InterfaceAttributeKind::Getter;
attributes.push(InterfaceAttribute {
is_static,
structural,
catch,
ty,
js_name: js_name.clone(),
kind,
unstable,
});
}
if !readonly {
let ty = type_
.type_
.to_idl_type(self)
.to_syn_type(TypePosition::Argument)
.unwrap_or(None);
if let Some(ty) = ty {
let kind = InterfaceAttributeKind::Setter;
attributes.push(InterfaceAttribute {
is_static,
structural,
catch,
ty,
js_name,
kind,
unstable,
});
}
}
}
fn member_operation(
&self,
methods: &mut Vec<InterfaceMethod>,
data: &InterfaceData<'src>,
id: &OperationId<'src>,
op_data: &OperationData<'src>,
) {
let attrs = data.definition_attributes;
let unstable = data.stability.is_unstable();
for method in self.create_imports(attrs, id, op_data, unstable) {
methods.push(method);
}
}
fn append_callback_interface(
&self,
options: &Options,
program: &mut Program,
name: Ident,
js_name: String,
item: &CallbackInterfaceData<'src>,
) {
assert_eq!(js_name, item.definition.identifier.0);
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(DictionaryField {
required: false,
name: rust_ident(&snake_case_ident(identifier)),
js_name: identifier.to_string(),
ty: idl_type::IdlType::Callback
.to_syn_type(pos)
.unwrap()
.unwrap(),
})
}
_ => {
log::warn!(
"skipping callback interface member on {}",
item.definition.identifier.0
);
}
}
}
Dictionary {
name,
js_name,
fields,
unstable: false,
}
.generate(options)
.to_tokens(&mut program.tokens);
}
}
pub fn generate(from: &Path, to: &Path, options: Options) -> Result<String> {
let generate_features = options.features;
let source = read_source_from_path(&from.join("enabled"))?;
let unstable_source = read_source_from_path(&from.join("unstable"))?;
let features = parse_webidl(generate_features, source, unstable_source)?;
if to.exists() {
fs::remove_dir_all(&to).context("Removing features directory")?;
}
fs::create_dir_all(&to).context("Creating features directory")?;
for (name, feature) in features.iter() {
let out_file_path = to.join(format!("gen_{}.rs", name));
fs::write(&out_file_path, &feature.code)?;
rustfmt(&out_file_path, name)?;
}
let binding_file = features.keys().map(|name| {
if generate_features {
format!("#[cfg(feature = \"{name}\")] #[allow(non_snake_case)] mod gen_{name};\n#[cfg(feature = \"{name}\")] pub use gen_{name}::*;", name = name)
} else {
format!("#[allow(non_snake_case)] mod gen_{name};\npub use gen_{name}::*;", name = name)
}
}).collect::<Vec<_>>().join("\n\n");
fs::write(to.join("mod.rs"), binding_file)?;
rustfmt(&to.join("mod.rs"), "mod")?;
return if generate_features {
let features = features
.iter()
.map(|(name, feature)| {
let features = feature
.required_features
.iter()
.map(|x| format!("\"{}\"", x))
.collect::<Vec<_>>()
.join(", ");
format!("{} = [{}]", name, features)
})
.collect::<Vec<_>>()
.join("\n");
Ok(features)
} else {
Ok(String::new())
};
fn read_source_from_path(dir: &Path) -> Result<SourceFile> {
let entries = fs::read_dir(dir).context("reading webidls directory")?;
let mut source = SourceFile::default();
for entry in entries {
let entry = entry.context(format!("getting {}/*.webidl entry", dir.display()))?;
let path = entry.path();
if path.extension() != Some(OsStr::new("webidl")) {
continue;
}
source = source
.add_file(&path)
.with_context(|| format!("reading contents of file \"{}\"", path.display()))?;
}
Ok(source)
}
fn rustfmt(path: &PathBuf, name: &str) -> Result<()> {
let result = Command::new("rustfmt")
.arg("--edition")
.arg("2018")
.arg(&path)
.status()
.context(format!("rustfmt on file {}", name))?;
assert!(result.success(), "rustfmt on file {}", name);
Ok(())
}
fn parse_webidl(
generate_features: bool,
enabled: SourceFile,
unstable: SourceFile,
) -> Result<BTreeMap<String, Feature>> {
let options = Options {
features: generate_features,
};
match compile(&enabled.contents, &unstable.contents, options) {
Ok(features) => Ok(features),
Err(e) => {
if let Some(err) = e.downcast_ref::<WebIDLParseError>() {
if let Some(pos) = enabled.resolve_offset(err.0) {
let ctx = format!(
"compiling WebIDL into wasm-bindgen bindings in file \
\"{}\", line {} column {}",
pos.filename,
pos.line + 1,
pos.col + 1
);
return Err(e.context(ctx));
} else {
return Err(e.context("compiling WebIDL into wasm-bindgen bindings"));
}
}
return Err(e.context("compiling WebIDL into wasm-bindgen bindings"));
}
}
}
}