diff --git a/crates/backend/src/ast.rs b/crates/backend/src/ast.rs index a75a85269cf..603253e663c 100644 --- a/crates/backend/src/ast.rs +++ b/crates/backend/src/ast.rs @@ -264,6 +264,7 @@ pub struct Enum { pub struct Variant { pub name: Ident, pub value: u32, + pub comments: Vec, } #[derive(Copy, Clone, Debug, PartialEq, Eq)] diff --git a/crates/backend/src/encode.rs b/crates/backend/src/encode.rs index 4e4faf45676..4728eb2088e 100644 --- a/crates/backend/src/encode.rs +++ b/crates/backend/src/encode.rs @@ -227,6 +227,7 @@ fn shared_variant<'a>(v: &'a ast::Variant, intern: &'a Interner) -> EnumVariant< EnumVariant { name: intern.intern(&v.name), value: v.value, + comments: v.comments.iter().map(|s| &**s).collect(), } } diff --git a/crates/cli-support/src/js/mod.rs b/crates/cli-support/src/js/mod.rs index fd0f1e11c55..18fd16494bf 100644 --- a/crates/cli-support/src/js/mod.rs +++ b/crates/cli-support/src/js/mod.rs @@ -66,9 +66,9 @@ pub struct ExportedClass { is_inspectable: bool, /// All readable properties of the class readable_properties: Vec, - /// Map from field name to type as a string plus whether it has a setter + /// Map from field name to type as a string, docs plus whether it has a setter /// and it is optional - typescript_fields: HashMap, + typescript_fields: HashMap, } const INITIAL_HEAP_VALUES: &[&str] = &["undefined", "null", "true", "false"]; @@ -798,7 +798,8 @@ impl<'a> Context<'a> { let mut fields = class.typescript_fields.keys().collect::>(); fields.sort(); // make sure we have deterministic output for name in fields { - let (ty, has_setter, is_optional) = &class.typescript_fields[name]; + let (ty, docs, has_setter, is_optional) = &class.typescript_fields[name]; + ts_dst.push_str(docs); ts_dst.push_str(" "); if !has_setter { ts_dst.push_str("readonly "); @@ -816,6 +817,7 @@ impl<'a> Context<'a> { ts_dst.push_str("}\n"); self.export(&name, &dst, Some(&class.comments))?; + self.typescript.push_str(&class.comments); self.typescript.push_str(&ts_dst); Ok(()) @@ -2901,10 +2903,23 @@ impl<'a> Context<'a> { self.typescript .push_str(&format!("export enum {} {{", enum_.name)); } - for (name, value) in enum_.variants.iter() { + for (name, value, comments) in enum_.variants.iter() { + let variant_docs = if comments.is_empty() { + String::new() + } else { + format_doc_comments(&comments, None) + }; + if !variant_docs.is_empty() { + variants.push_str("\n"); + variants.push_str(&variant_docs); + } variants.push_str(&format!("{}:{},", name, value)); if enum_.generate_typescript { - self.typescript.push_str(&format!("\n {},", name)); + self.typescript.push_str("\n"); + if !variant_docs.is_empty() { + self.typescript.push_str(&variant_docs); + } + self.typescript.push_str(&format!(" {},", name)); } } if enum_.generate_typescript { @@ -3227,7 +3242,7 @@ impl ExportedClass { fn push_getter(&mut self, docs: &str, field: &str, js: &str, ret_ty: Option<&str>) { self.push_accessor(docs, field, js, "get "); if let Some(ret_ty) = ret_ty { - self.push_accessor_ts(field, ret_ty); + self.push_accessor_ts(docs, field, ret_ty, false); } self.readable_properties.push(field.to_string()); } @@ -3244,20 +3259,30 @@ impl ExportedClass { ) { self.push_accessor(docs, field, js, "set "); if let Some(ret_ty) = ret_ty { - let (has_setter, is_optional) = self.push_accessor_ts(field, ret_ty); - *has_setter = true; + let is_optional = self.push_accessor_ts(docs, field, ret_ty, true); *is_optional = might_be_optional_field; } } - fn push_accessor_ts(&mut self, field: &str, ret_ty: &str) -> (&mut bool, &mut bool) { - let (ty, has_setter, is_optional) = self + fn push_accessor_ts( + &mut self, + docs: &str, + field: &str, + ret_ty: &str, + is_setter: bool, + ) -> &mut bool { + let (ty, accessor_docs, has_setter, is_optional) = self .typescript_fields .entry(field.to_string()) .or_insert_with(Default::default); *ty = ret_ty.to_string(); - (has_setter, is_optional) + // Deterministic output: always use the getter's docs if available + if !docs.is_empty() && (accessor_docs.is_empty() || !is_setter) { + *accessor_docs = docs.to_owned(); + } + *has_setter |= is_setter; + is_optional } fn push_accessor(&mut self, docs: &str, field: &str, js: &str, prefix: &str) { diff --git a/crates/cli-support/src/lib.rs b/crates/cli-support/src/lib.rs index 491602c96dd..dd47a620a79 100755 --- a/crates/cli-support/src/lib.rs +++ b/crates/cli-support/src/lib.rs @@ -477,7 +477,8 @@ fn reset_indentation(s: &str) -> String { dst.push_str(line); } dst.push_str("\n"); - if line.ends_with('{') { + // Ignore { inside of comments and if it's an exported enum + if line.ends_with('{') && !line.starts_with('*') && !line.ends_with("Object.freeze({") { indent += 1; } } diff --git a/crates/cli-support/src/wit/mod.rs b/crates/cli-support/src/wit/mod.rs index 878581dc29e..d7b4c930b41 100644 --- a/crates/cli-support/src/wit/mod.rs +++ b/crates/cli-support/src/wit/mod.rs @@ -766,7 +766,13 @@ impl<'a> Context<'a> { variants: enum_ .variants .iter() - .map(|v| (v.name.to_string(), v.value)) + .map(|v| { + ( + v.name.to_string(), + v.value, + concatenate_comments(&v.comments), + ) + }) .collect(), generate_typescript: enum_.generate_typescript, }; @@ -1511,9 +1517,5 @@ fn verify_schema_matches<'a>(data: &'a [u8]) -> Result, Error> { } fn concatenate_comments(comments: &[&str]) -> String { - comments - .iter() - .map(|s| s.trim_matches('"')) - .collect::>() - .join("\n") + comments.iter().map(|&s| s).collect::>().join("\n") } diff --git a/crates/cli-support/src/wit/nonstandard.rs b/crates/cli-support/src/wit/nonstandard.rs index be65fc4d9a7..d090f3385d4 100644 --- a/crates/cli-support/src/wit/nonstandard.rs +++ b/crates/cli-support/src/wit/nonstandard.rs @@ -132,9 +132,9 @@ pub struct AuxEnum { pub name: String, /// The copied Rust comments to forward to JS pub comments: String, - /// A list of variants with their name and value + /// A list of variants with their name, value and comments /// and whether typescript bindings should be generated for each variant - pub variants: Vec<(String, u32)>, + pub variants: Vec<(String, u32, String)>, /// Whether typescript bindings should be generated for this enum. pub generate_typescript: bool, } diff --git a/crates/macro-support/src/parser.rs b/crates/macro-support/src/parser.rs index a44eed847be..0838ad5b322 100644 --- a/crates/macro-support/src/parser.rs +++ b/crates/macro-support/src/parser.rs @@ -1,4 +1,6 @@ use std::cell::Cell; +use std::char; +use std::str::Chars; use ast::OperationKind; use backend::ast; @@ -1131,9 +1133,11 @@ impl<'a> MacroParse<(&'a mut TokenStream, BindgenAttrs)> for syn::ItemEnum { ), }; + let comments = extract_doc_comments(&v.attrs); Ok(ast::Variant { name: v.ident.clone(), value, + comments, }) }) .collect::, Diagnostic>>()?; @@ -1324,9 +1328,8 @@ fn extract_doc_comments(attrs: &[syn::Attribute]) -> Vec { // We want to filter out any Puncts so just grab the Literals a.tokens.clone().into_iter().filter_map(|t| match t { TokenTree::Literal(lit) => { - // this will always return the quoted string, we deal with - // that in the cli when we read in the comments - Some(lit.to_string()) + let quoted = lit.to_string(); + Some(try_unescape("ed).unwrap_or_else(|| quoted)) } _ => None, }), @@ -1342,6 +1345,76 @@ fn extract_doc_comments(attrs: &[syn::Attribute]) -> Vec { }) } +// Unescapes a quoted string. char::escape_debug() was used to escape the text. +fn try_unescape(s: &str) -> Option { + if s.is_empty() { + return Some(String::new()); + } + let mut result = String::with_capacity(s.len()); + let mut chars = s.chars(); + for i in 0.. { + let c = match chars.next() { + Some(c) => c, + None => { + if result.ends_with('"') { + result.pop(); + } + return Some(result); + } + }; + if i == 0 && c == '"' { + // ignore it + } else 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); + } + } + None +} + +fn unescape_unicode(chars: &mut Chars) -> Option<(char, char)> { + let mut value = 0; + for i in 0..7 { + let c = chars.next()?; + let num = if c >= '0' && c <= '9' { + c as u32 - '0' as u32 + } else if c >= 'a' && c <= 'f' { + c as u32 - 'a' as u32 + 10 + } else if c >= 'A' && c <= 'F' { + c as u32 - 'A' as u32 + 10 + } else { + if i == 0 { + return None; + } + let decoded = char::from_u32(value)?; + return Some((decoded, c)); + }; + if i >= 6 { + return None; + } + value = (value << 4) | num; + } + None +} + /// Check there are no lifetimes on the function. fn assert_no_lifetimes(sig: &syn::Signature) -> Result<(), Diagnostic> { struct Walk { diff --git a/crates/shared/src/lib.rs b/crates/shared/src/lib.rs index abc1901d917..caba298f1e0 100644 --- a/crates/shared/src/lib.rs +++ b/crates/shared/src/lib.rs @@ -106,6 +106,7 @@ macro_rules! shared_api { struct EnumVariant<'a> { name: &'a str, value: u32, + comments: Vec<&'a str>, } struct Function<'a> { diff --git a/tests/wasm/comments.js b/tests/wasm/comments.js index a2049921e22..1443281b54f 100644 --- a/tests/wasm/comments.js +++ b/tests/wasm/comments.js @@ -4,8 +4,16 @@ const assert = require('assert'); exports.assert_comments_exist = function() { const bindings_file = require.resolve('wasm-bindgen-test'); const contents = fs.readFileSync(bindings_file); - assert.ok(contents.includes("* annotated function")); + assert.ok(contents.includes("* annotated function ✔️ \" \\ ' {")); assert.ok(contents.includes("* annotated struct type")); - assert.ok(contents.includes("* annotated struct field")); + assert.ok(contents.includes("* annotated struct field b")); + assert.ok(contents.includes("* annotated struct field c")); + assert.ok(contents.includes("* annotated struct constructor")); assert.ok(contents.includes("* annotated struct method")); + assert.ok(contents.includes("* annotated struct getter")); + assert.ok(contents.includes("* annotated struct setter")); + assert.ok(contents.includes("* annotated struct static method")); + assert.ok(contents.includes("* annotated enum type")); + assert.ok(contents.includes("* annotated enum variant 1")); + assert.ok(contents.includes("* annotated enum variant 2")); }; diff --git a/tests/wasm/comments.rs b/tests/wasm/comments.rs index 65b9d7d43c5..2d44e7b4a76 100644 --- a/tests/wasm/comments.rs +++ b/tests/wasm/comments.rs @@ -6,26 +6,65 @@ extern "C" { fn assert_comments_exist(); } +/// annotated function ✔️ " \ ' { #[wasm_bindgen] -/// annotated function pub fn annotated() -> String { String::new() } -#[wasm_bindgen] /// annotated struct type +#[wasm_bindgen] pub struct Annotated { a: String, - /// annotated struct field + /// annotated struct field b pub b: u32, + /// annotated struct field c + #[wasm_bindgen(readonly)] + pub c: u32, + d: u32, } #[wasm_bindgen] impl Annotated { + /// annotated struct constructor + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + Self { + a: String::new(), + b: 0, + c: 0, + d: 0, + } + } + /// annotated struct method pub fn get_a(&self) -> String { self.a.clone() } + + /// annotated struct getter + #[wasm_bindgen(getter)] + pub fn d(&self) -> u32 { + self.d + } + + /// annotated struct setter + #[wasm_bindgen(setter)] + pub fn set_d(&mut self, value: u32) { + self.d = value + } + + /// annotated struct static method + pub fn static_method() {} +} + +/// annotated enum type +#[wasm_bindgen] +pub enum AnnotatedEnum { + /// annotated enum variant 1 + Variant1, + /// annotated enum variant 2 + Variant2, } #[wasm_bindgen_test]