diff --git a/crates/cli-support/src/js/binding.rs b/crates/cli-support/src/js/binding.rs index d5ed01363ad..907845c1128 100644 --- a/crates/cli-support/src/js/binding.rs +++ b/crates/cli-support/src/js/binding.rs @@ -4,6 +4,7 @@ //! exported functions, table elements, imports, etc. All function shims //! generated by `wasm-bindgen` run through this type. +use crate::js::jsdoc::{JsDocTag, Optionality, ParamTag, ReturnsTag}; use crate::js::Context; use crate::wit::InstructionData; use crate::wit::{Adapter, AdapterId, AdapterKind, AdapterType, Instruction}; @@ -69,7 +70,7 @@ pub struct JsBuilder<'a, 'b> { pub struct JsFunction { pub code: String, pub ts_sig: String, - pub js_doc: String, + pub js_doc: Vec, pub ts_arg_tys: Vec, pub ts_ret_ty: Option, pub ts_refs: HashSet, @@ -276,7 +277,7 @@ impl<'a, 'b> Builder<'a, 'b> { let js_doc = if generate_jsdoc { self.js_doc_comments(&function_args, &arg_tys, &ts_ret_ty, variadic) } else { - String::new() + Vec::new() }; Ok(JsFunction { @@ -383,50 +384,55 @@ impl<'a, 'b> Builder<'a, 'b> { arg_tys: &[&AdapterType], ts_ret: &Option, variadic: bool, - ) -> String { + ) -> Vec { let (variadic_arg, fn_arg_names) = match arg_names.split_last() { Some((last, args)) if variadic => (Some(last), args), _ => (None, arg_names), }; - let mut omittable = true; - let mut js_doc_args = Vec::new(); + fn to_ts_type(ty: &AdapterType) -> String { + let mut ret = String::new(); + adapter2ts(ty, &mut ret, None); + ret + } - for (name, ty) in fn_arg_names.iter().zip(arg_tys).rev() { - let mut arg = "@param {".to_string(); + let mut tags = Vec::new(); - adapter2ts(ty, &mut arg, None); - arg.push_str("} "); - match ty { - AdapterType::Option(..) if omittable => { - arg.push('['); - arg.push_str(name); - arg.push(']'); - } - _ => { - omittable = false; - arg.push_str(name); - } - } - arg.push('\n'); - js_doc_args.push(arg); + let mut omittable = true; + for (name, ty) in fn_arg_names.iter().zip(arg_tys).rev() { + tags.push(JsDocTag::Param(ParamTag { + ty: Some(to_ts_type(ty)), + name: name.to_string(), + optional: match ty { + AdapterType::Option(..) if omittable => Optionality::Optional, + _ => { + omittable = false; + Optionality::Required + } + }, + description: String::new(), + })); } - - let mut ret: String = js_doc_args.into_iter().rev().collect(); + tags.reverse(); if let (Some(name), Some(ty)) = (variadic_arg, arg_tys.last()) { - ret.push_str("@param {..."); - adapter2ts(ty, &mut ret, None); - ret.push_str("} "); - ret.push_str(name); - ret.push('\n'); + tags.push(JsDocTag::Param(ParamTag { + ty: Some(format!("...{}", to_ts_type(ty))), + name: name.to_string(), + optional: Optionality::Required, + description: String::new(), + })); } if let Some(ts) = ts_ret { if ts != "void" { - ret.push_str(&format!("@returns {{{}}}", ts)); + tags.push(JsDocTag::Returns(ReturnsTag { + ty: Some(ts.to_string()), + description: String::new(), + })); } } - ret + + tags } } diff --git a/crates/cli-support/src/js/ident.rs b/crates/cli-support/src/js/ident.rs new file mode 100644 index 00000000000..f4dae057db6 --- /dev/null +++ b/crates/cli-support/src/js/ident.rs @@ -0,0 +1,48 @@ +/// Returns whether a character has the Unicode `ID_Start` properly. +/// +/// This is only ever-so-slightly different from `XID_Start` in a few edge +/// cases, so we handle those edge cases manually and delegate everything else +/// to `unicode-ident`. +fn is_unicode_id_start(c: char) -> bool { + match c { + '\u{037A}' | '\u{0E33}' | '\u{0EB3}' | '\u{309B}' | '\u{309C}' | '\u{FC5E}' + | '\u{FC5F}' | '\u{FC60}' | '\u{FC61}' | '\u{FC62}' | '\u{FC63}' | '\u{FDFA}' + | '\u{FDFB}' | '\u{FE70}' | '\u{FE72}' | '\u{FE74}' | '\u{FE76}' | '\u{FE78}' + | '\u{FE7A}' | '\u{FE7C}' | '\u{FE7E}' | '\u{FF9E}' | '\u{FF9F}' => true, + _ => unicode_ident::is_xid_start(c), + } +} + +/// Returns whether a character has the Unicode `ID_Continue` properly. +/// +/// This is only ever-so-slightly different from `XID_Continue` in a few edge +/// cases, so we handle those edge cases manually and delegate everything else +/// to `unicode-ident`. +fn is_unicode_id_continue(c: char) -> bool { + match c { + '\u{037A}' | '\u{309B}' | '\u{309C}' | '\u{FC5E}' | '\u{FC5F}' | '\u{FC60}' + | '\u{FC61}' | '\u{FC62}' | '\u{FC63}' | '\u{FDFA}' | '\u{FDFB}' | '\u{FE70}' + | '\u{FE72}' | '\u{FE74}' | '\u{FE76}' | '\u{FE78}' | '\u{FE7A}' | '\u{FE7C}' + | '\u{FE7E}' => true, + _ => unicode_ident::is_xid_continue(c), + } +} + +pub fn is_ident_start(char: char) -> bool { + is_unicode_id_start(char) || char == '$' || char == '_' +} +pub fn is_ident_continue(char: char) -> bool { + is_unicode_id_continue(char) || char == '$' || char == '\u{200C}' || char == '\u{200D}' +} + +/// Returns whether a string is a valid JavaScript identifier. +/// Defined at https://tc39.es/ecma262/#prod-IdentifierName. +pub fn is_valid_ident(name: &str) -> bool { + name.chars().enumerate().all(|(i, char)| { + if i == 0 { + is_ident_start(char) + } else { + is_ident_continue(char) + } + }) +} diff --git a/crates/cli-support/src/js/jsdoc.rs b/crates/cli-support/src/js/jsdoc.rs new file mode 100644 index 00000000000..51b79ff3036 --- /dev/null +++ b/crates/cli-support/src/js/jsdoc.rs @@ -0,0 +1,970 @@ +//! BEWARE: JSDoc does not have a formal specification, so this parser is based +//! on common conventions generally tries to mimic the behavior of the +//! TypeScript's JSDoc parser when it comes to edge cases. +//! +//! Well formatted JSDoc comments will be handled correctly, but edge cases +//! (e.g. weird tags, missing/too many spaces) may be handled differently +//! compared to other parsers. See the below test cases for examples. + +use super::ident::is_ident_start; + +#[derive(Debug, Clone)] +pub struct JsDoc { + /// Optional description at the start of a comment. + pub description: String, + pub tags: Vec, +} + +#[derive(Debug, Clone)] +pub enum JsDocTag { + Param(ParamTag), + Returns(ReturnsTag), + Unknown(UnknownTag), +} + +#[derive(Debug, Clone)] +pub struct ParamTag { + pub ty: Option, + pub name: String, + pub optional: Optionality, + /// Description of the parameter. Might be empty. + pub description: String, +} + +#[derive(Debug, Clone)] +pub enum Optionality { + /// E.g. `@param {number} foo` + Required, + /// E.g. `@param {number} [foo]` + Optional, + /// E.g. `@param {number} [foo=123]`. In this case, the `String` value is `123`. + OptionalWithDefault(String), +} + +#[derive(Debug, Clone)] +pub struct ReturnsTag { + pub ty: Option, + /// Description of the return value. Might be empty. + pub description: String, +} + +#[derive(Debug, Clone)] +pub struct UnknownTag { + pub tag: String, + /// The text right after the tag name. + /// + /// E.g. for `@foo bar`, the text is `" bar"` (note that the space is included). + pub text: String, +} + +impl JsDoc { + /// Parses a JSDoc comment. + /// + /// Any string is valid JSDoc, but not all strings are equally valid. This + /// parser only supports a subset of the JSDoc syntax. All tags that are + /// not supported as parsed as `JsDocTag::Unknown`, which represents an + /// arbitrary block tag. This allows us to parse all comments, even if we + /// don't understand all tags. + /// + /// Note that supported tags that are not well-formed (=not following the + /// usual syntax) are also parsed as `JsDocTag::Unknown`. Examples of this + /// include `@param` tags with wildly incorrect syntax. E.g. + /// `@param { name`. + pub fn parse(comment: &str) -> Self { + let comment = remove_leading_space(comment); + let comment = trim_right(comment.as_str()); + + let mut tags = Vec::new(); + + let Some(mut block) = find_next_tag(comment) else { + // there are no tags, the entire comment is the description + return Self { + description: comment.to_string(), + tags, + }; + }; + + let mut description = String::new(); + let description_text = &comment[..block.0]; + if !description_text.trim().is_empty() { + description = trim_right(description_text).to_string(); + + // preserve final new line + if description_text.ends_with("\n\n") { + description.push('\n'); + } + } + + loop { + let mut rest = &comment[block.0 + block.1.len()..]; + + // find the next tag, so we know where this block ends + let next_line_index = get_line_length(rest.as_bytes()); + let next_block = find_next_tag(&rest[next_line_index..]); + + if let Some(next_block) = next_block { + rest = trim_right(&rest[..(next_block.0 + next_line_index)]); + } + + tags.push(Self::parse_tag(block.1, rest)); + + if let Some(mut next_block) = next_block { + // change the index of the next block to be relative to the entire comment + next_block.0 += block.0 + block.1.len() + next_line_index; + block = next_block; + } else { + // no more tags + break; + } + } + + Self { description, tags } + } + + fn parse_tag(tag_name: &str, rest: &str) -> JsDocTag { + match tag_name { + "@param" | "@arg" | "@argument" => { + if let Some(tag) = ParamTag::parse(rest) { + return JsDocTag::Param(tag); + } + } + "@returns" | "@return" => { + if let Some(tag) = ReturnsTag::parse(rest) { + return JsDocTag::Returns(tag); + } + } + _ => {} + } + + JsDocTag::Unknown(UnknownTag { + tag: tag_name.to_string(), + text: rest.to_string(), + }) + } + + pub fn enhance(&mut self, tags: Vec) { + for tag in tags { + match tag { + JsDocTag::Param(tag) => { + if let Some(param_tag) = self.get_or_add_param(&tag.name) { + if param_tag.ty.is_none() { + param_tag.ty = tag.ty; + } + if matches!(param_tag.optional, Optionality::Required) { + param_tag.optional = tag.optional; + } + } + } + JsDocTag::Returns(tag) => { + if let Some(returns_tag) = self.get_or_add_returns() { + if returns_tag.ty.is_none() { + returns_tag.ty = tag.ty; + } + } + } + _ => {} + } + } + } + + /// If there is a single `@returns` tag, return it. Otherwise, add a new + /// `@returns` tag and return it. + /// + /// If there are multiple `@returns` tags, return `None`. + pub fn get_or_add_param<'a>(&'a mut self, name: &str) -> Option<&'a mut ParamTag> { + // check that there is exactly one returns tag + let returns_count = self + .tags + .iter() + .filter(|tag| match tag { + JsDocTag::Param(tag) => { + if tag.name == name { + return true; + } + if tag.name.starts_with(name) { + // account for paths + let after = tag.name[name.len()..].chars().next(); + return after == Some('.') || after == Some('['); + } + false + } + _ => false, + }) + .count(); + + if returns_count > 1 { + // multiple return tags, we don't know which one to update + return None; + } + if returns_count == 0 { + // add a new returns tag + // try to insert it before a returns tag + let pos = self + .tags + .iter() + .position(|tag| matches!(tag, JsDocTag::Returns(_))) + .unwrap_or(self.tags.len()); + + self.tags.insert( + pos, + JsDocTag::Param(ParamTag { + ty: None, + name: name.to_string(), + optional: Optionality::Required, + description: String::new(), + }), + ); + } + + for tag in &mut self.tags { + if let JsDocTag::Param(tag) = tag { + if tag.name == name { + // return the existing tag + return Some(tag); + } + } + } + + None + } + + /// If there is a single `@returns` tag, return it. Otherwise, add a new + /// `@returns` tag and return it. + /// + /// If there are multiple `@returns` tags, return `None`. + pub fn get_or_add_returns(&mut self) -> Option<&mut ReturnsTag> { + // check that there is exactly one returns tag + let count = self + .tags + .iter() + .filter(|tag| matches!(tag, JsDocTag::Returns(_))) + .count(); + + if count > 1 { + // multiple return tags, we don't know which one to update + return None; + } + if count == 0 { + // add a new returns tag + self.tags.push(JsDocTag::Returns(ReturnsTag { + ty: None, + description: String::new(), + })); + } + + for tag in &mut self.tags { + if let JsDocTag::Returns(tag) = tag { + // return the existing tag + return Some(tag); + } + } + + unreachable!() + } + + /// Same as `to_string`, but indents the output with 1 space. + pub fn to_string_indented(&self) -> String { + let mut out = String::new(); + for (index, line) in self.to_string().lines().enumerate() { + if index > 0 { + out.push('\n'); + } + if !line.is_empty() { + out.push(' '); + } + out.push_str(line); + } + out + } +} + +impl std::fmt::Display for JsDoc { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + if !self.description.trim().is_empty() { + writeln!(f, "{}", self.description)?; + } + + for tag in &self.tags { + writeln!(f, "{}", tag)?; + } + + Ok(()) + } +} + +impl std::fmt::Display for JsDocTag { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + JsDocTag::Param(tag) => { + write!(f, "@param")?; + if let Some(ty) = &tag.ty { + write!(f, " {{{}}}", ty)? + } + match &tag.optional { + Optionality::Required => write!(f, " {}", tag.name)?, + Optionality::Optional => write!(f, " [{}]", tag.name)?, + Optionality::OptionalWithDefault(default) => { + write!(f, " [{}={}]", tag.name, default)? + } + } + if tag.description.starts_with(['\r', '\n']) { + write!(f, "{}", tag.description)?; + } else if !tag.description.is_empty() { + write!(f, " {}", tag.description)?; + } + } + JsDocTag::Returns(tag) => { + write!(f, "@returns")?; + if let Some(ty) = &tag.ty { + write!(f, " {{{}}}", ty)? + } + if tag.description.starts_with(['\r', '\n']) { + write!(f, "{}", tag.description)?; + } else if !tag.description.is_empty() { + write!(f, " {}", tag.description)?; + } + } + JsDocTag::Unknown(tag) => write!(f, "{}{}", tag.tag, tag.text)?, + } + + Ok(()) + } +} + +impl ParamTag { + fn parse(rest: &str) -> Option { + let mut text = trim_left(rest); + + let mut optional_by_type = false; + + let mut ty = None; + if text.starts_with('{') { + let mut type_len = 0; + ty = consume_type_script_expression(&text[1..]).map(|mut t| { + type_len = t.len() + 2; + t = t.trim_matches(' '); + if t.ends_with('=') { + optional_by_type = true; + t = t[..t.len() - 1].trim_matches(' '); + } + t.to_string() + }); + + if ty.is_some() { + text = trim_left(&text[type_len..]); + } else { + // the type expression is not terminated, so the tag is not well-formed + return None; + } + } + ty = post_process_typescript_expression(ty); + + let (optional, name) = if text.starts_with('[') { + // skip the `[` + text = trim_left_space(&text[1..]); + + let Some(name) = consume_parameter_name_path(text) else { + // the name is not well-formed + return None; + }; + text = trim_left_space(&text[name.len()..]); + + let mut default = None; + if text.starts_with('=') { + text = trim_left_space(&text[1..]); + // the default value doesn't have to be a valid JS expression, + // so we just search for ']', '\n', or end of string + let end = text.find([']', '\n']).unwrap_or(text.len()); + let default_text = text[..end].trim(); + if !default_text.is_empty() { + default = Some(default_text.to_string()); + } + + text = &text[end..]; + if !text.is_empty() { + text = trim_left_space(&text[1..]); + } + } else if text.starts_with(']') { + text = trim_left_space(&text[1..]); + } + + ( + default + .map(Optionality::OptionalWithDefault) + .unwrap_or(Optionality::Optional), + name.to_string(), + ) + } else { + let Some(name) = consume_parameter_name_path(text) else { + // the name is not well-formed + return None; + }; + text = trim_left_space(&text[name.len()..]); + ( + if optional_by_type { + Optionality::Optional + } else { + Optionality::Required + }, + name.to_string(), + ) + }; + + Some(Self { + ty, + optional, + name, + description: text.to_string(), + }) + } +} + +impl ReturnsTag { + fn parse(rest: &str) -> Option { + // A bit careful now, because we want to keep the initial new lines of + // the description. + let mut text = { + let trimmed = trim_left(rest); + if trimmed.starts_with('{') { + trimmed + } else { + trim_left_space(rest) + } + }; + + let mut ty = None; + if text.starts_with('{') { + ty = consume_type_script_expression(&text[1..]).map(|t| t.to_string()); + + if let Some(ty) = &ty { + text = trim_left_space(&text[(ty.len() + 2)..]); + } else { + // the type expression is not terminated, so the tag is not well-formed + return None; + } + } + ty = post_process_typescript_expression(ty); + + Some(Self { + ty, + description: text.to_string(), + }) + } +} + +fn post_process_typescript_expression(expr: Option) -> Option { + expr.and_then(|e| if e.trim().is_empty() { None } else { Some(e) }) +} + +/// A version trim_start that ignores text direction. +fn trim_left(s: &str) -> &str { + let mut first_non_space = None; + for (index, c) in s.char_indices() { + if !c.is_whitespace() { + first_non_space = Some(index); + break; + } + } + if let Some(first_non_space) = first_non_space { + &s[first_non_space..] + } else { + "" + } +} + +/// Trims all space and tab characters from the left side of the string. +fn trim_left_space(s: &str) -> &str { + let mut first_non_space = None; + for (index, c) in s.char_indices() { + if c != ' ' && c != '\t' { + first_non_space = Some(index); + break; + } + } + if let Some(first_non_space) = first_non_space { + &s[first_non_space..] + } else { + "" + } +} + +/// A version trim_end that ignores text direction. +fn trim_right(s: &str) -> &str { + let mut last_space = s.len(); + for (index, c) in s.char_indices().rev() { + if c.is_whitespace() { + last_space = index; + } else { + break; + } + } + &s[..last_space] +} + +// returns the byte index of the `@` symbol of the next tag as well as the tag name. +fn find_next_tag(comment: &str) -> Option<(usize, &str)> { + // This function essentially implement this regex: `/^[ ]*@/m.exec(comment)` + + let mut index = 0; + while index < comment.len() { + // we are at the start of a line + + // skip leading spaces + while let Some(b' ') = comment.as_bytes().get(index) { + index += 1; + } + + if let Some(tag_name) = parse_tag_name(&comment[index..]) { + return Some((index, tag_name)); + } + + // skip to the next line + while index < comment.len() { + if comment.as_bytes()[index] == b'\n' { + index += 1; + break; + } + index += 1; + } + } + + None +} + +/// If the given string starts with a syntactically valid tag, it will returns +/// the string slice with the tag. +/// +/// E.g. `@param {string} foo` will return `Some("@param")`. +fn parse_tag_name(comment: &str) -> Option<&str> { + if comment.starts_with('@') && comment.len() > 1 { + let stop = comment[1..].find(|c: char| c.is_whitespace() || c == '{'); + if let Some(stop) = stop { + if stop == 0 { + None + } else { + Some(&comment[..stop + 1]) + } + } else { + // the entire string is the tag + Some(comment) + } + } else { + None + } +} + +/// Returns the length in bytes of the current line including the trailing new +/// line character. +fn get_line_length(comment: &[u8]) -> usize { + comment + .iter() + .position(|&c| c == b'\n') + .map(|p| p + 1) + .unwrap_or(comment.len()) +} + +/// Consumes a TypeScript expression from the beginning of the given string +/// until a `}` character is found. +/// +/// The return string will be the TypeScript expression without the braces. +/// However, if `Some` is returned, the next character after the string will be +/// the closing `}` character. +fn consume_type_script_expression(comment: &str) -> Option<&str> { + // Okay, so the main challenge here is that TypeScript expressions can + // contain nested `{}` pairs, strings, comments, and string template + // literals. So we have to handle those 4 things, but that's it. + // + // We will also assume that the expression is valid TypeScript. If it's not, + // the results may be unexpected. + + /// Returns the number of bytes consumed by the string, including the + /// opening and closing quotes. + /// + /// If the string is not terminated, `None` is returned. + fn consume_string(bytes: &[u8]) -> Option { + debug_assert!(bytes[0] == b'"' || bytes[0] == b'\''); + + let kind = bytes[0]; + + // string can't contain certain characters (e.g. new lines), but that's + // not a problem, because we assume valid strings. + + let mut index = 1; + while index < bytes.len() { + let c = bytes[index]; + if c == b'\\' { + // skip the next character + index += 1; + } else if c == kind { + return Some(index + 1); + } + index += 1; + } + + // the string is not terminated + None + } + /// Returns the number of bytes consumed by the single line comment, + /// including the trailing new line. + fn consume_single_line_comment(bytes: &[u8]) -> usize { + debug_assert!(bytes[0] == b'/'); + debug_assert!(bytes[1] == b'/'); + + get_line_length(bytes) + } + /// Returns the number of bytes consumed by braced (`{}`) expression, + /// including the closing `}`. + fn consume_string_template(bytes: &[u8]) -> Option { + debug_assert!(bytes[0] == b'`'); + + let mut index = 1; + while index < bytes.len() { + let c = bytes[index]; + if c == b'\\' { + // skip the next character + index += 1; + } else if c == b'`' { + return Some(index + 1); + } else if c == b'$' { + if let Some(b'{') = bytes.get(index + 1) { + // interpolated expression + index = consume_brace_expression(&bytes[index + 2..])?; + } + } + index += 1; + } + + // the string is not terminated + None + } + /// Returns the number of bytes consumed by braced (`{}`) expression, + /// including the closing `}`. + fn consume_brace_expression(bytes: &[u8]) -> Option { + let mut brace_depth = 0; + + let mut index = 0; + while index < bytes.len() { + let c = bytes[index]; + match c { + b'{' => { + brace_depth += 1; + index += 1; + } + b'}' => { + if brace_depth == 0 { + return Some(index + 1); + } + brace_depth -= 1; + index += 1; + } + b'"' | b'\'' => { + index += consume_string(&bytes[index..])?; + } + b'`' => { + index += consume_string_template(&bytes[index..])?; + } + b'/' => { + // might be a comment + if let Some(b'/') = bytes.get(index + 1) { + index += consume_single_line_comment(&bytes[index..]); + } else { + index += 1; + } + } + _ => { + index += 1; + } + } + } + + // not terminated + None + } + + let braced_len = consume_brace_expression(comment.as_bytes())?; + debug_assert!(braced_len > 0); + + // There is no closing brace + Some(&comment[..braced_len - 1]) +} + +/// `@param` tags support not just simple Js identifiers for the parameter +/// name, but also paths (e.g. `foo.bar.baz`) and array items +/// (e.g. `foo[].bar`). +/// +/// See https://jsdoc.app/tags-param +fn consume_parameter_name_path(text: &str) -> Option<&str> { + let mut chars = text.char_indices(); + + let mut valid_first = false; + if let Some((_, c)) = chars.next() { + if is_ident_start(c) { + valid_first = true; + } + } + if !valid_first { + return None; + } + + while let Some((index, c)) = chars.next() { + if c == '[' { + // this is only allowed if followed by a `].` + if let Some((_, ']')) = chars.next() { + if let Some((_, '.')) = chars.next() { + continue; + } + } + return None; + } + + if c == '.' { + // the next character must be a valid identifier start + if let Some((_, c)) = chars.next() { + if is_ident_start(c) { + continue; + } + } + return None; + } + + if c.is_whitespace() || matches!(c, ']' | '=') { + // found stop character + return Some(&text[..index]); + } + + if !is_ident_start(c) { + return None; + } + } + + Some(text) +} + +/// If all lines are empty or start with a leading space, remove the +/// leading space from all lines. +fn remove_leading_space(comment: &str) -> String { + let leading_space = comment.lines().all(|l| l.is_empty() || l.starts_with(' ')); + if leading_space { + let mut out = String::new(); + for (index, line) in comment.lines().enumerate() { + if index > 0 { + out.push('\n'); + } + if !line.is_empty() { + out.push_str(&line[1..]); + } + } + out + } else { + comment.to_string() + } +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use super::*; + + #[test] + fn weird_tag_names() { + fn test(comment: &str, expected: Option<&str>) { + assert_eq!( + parse_tag_name(comment), + expected, + "Expected comment {:?} to be parsed as {:?}", + comment, + expected + ); + } + + // doesn't start with @ + test("", None); + test("foo", None); + test(" @param", None); + + test("@", None); + test("@ foo", None); + test("@param", Some("@param")); + test("@param {type} name", Some("@param")); + test("@param{type}name", Some("@param")); + test("@type{type}", Some("@type")); + + // unicode + test("@üwü", Some("@üwü")); + test("@üwü äwoo", Some("@üwü")); + + // unicode spaces + // no-break space + test("@üwü\u{A0}", Some("@üwü")); + // line separator + test("@üwü\u{2028}", Some("@üwü")); + } + + #[test] + fn parser_snapshots() { + let mut suite = Suite::new(); + + // description + suite.test(r"This is a description."); + suite.test_lines(&["This is", " ", "", "a multi-line", "description."]); + suite.test_lines(&["This is a description with tag.", "", "@public"]); + suite.test_lines(&["This is a description with tag.", "@public"]); + + // @param + + // well-formed + suite.test_lines(&[ + "@param foo", + "@param foo description", + "@param foo\ndescription", + "@param [foo]", + "@param [foo] description", + "@param [foo]\ndescription", + "@param [foo=123]", + "@param [foo=123] description", + "@param [foo=123]\ndescription", + "@param {number} foo", + "@param {number} foo description", + "@param {number} [foo]", + "@param {number} [foo] description", + "@param {number} [foo=123]", + "@param {number} [foo=123] description", + // new objects + "@param {object} obj", + "@param {string} obj.name", + "@param {object[]} obj.locations", + "@param {string} obj.locations[].address", + "@param {string} [obj.locations[].address]", + "@param {} foo", + ]); + // weird + suite.test_lines(&[ + "@param {string} foo", + "@param{string}foo ", + "@param{string}[foo]", + "@param{string}[foo=]", + "@param { string } [ foo = 123 ]", + "@param { } [ foo = 123 ]", + ]); + // weird types + suite.test_lines(&[ + "@param {", + "string", + "} foo", + "@param {string // comment", + "| number} foo", + "@param { number = } foo", + "@param { = } foo", + "@param {{", + " name: { first: string, last: string };", + "}} foo", + "@param {'}' | \"}\" | `}${{'}': \"}\"}}}`} foo", + ]); + // alias + suite.test_lines(&[ + "@arg foo", + "@arg {string} foo", + "@argument foo", + "@argument {string} foo", + ]); + + // @returns + + // well-formed + suite.test_lines(&[ + "@returns", + "@returns description", + "@returns\ndescription", + "@returns {string}", + "@returns\n\n\n{number}", + "@returns {string} description", + "@returns {string}\ndescription", + ]); + // weird + suite.test_lines(&[ + "@returns ", + "@returns description", + "@returns {} ", + "@returns{void}", + "@returns{void} ", + "@returns{void}description", + "@returns{void} description", + "@returns\n\n\n{\n\nvoid\n\n}\n", + ]); + // invalid + suite.test_lines(&["@returns {"]); + // alias + suite.test("@return {string} description"); + + suite.assert(); + + struct Suite { + output: String, + } + impl Suite { + fn new() -> Self { + Self { + output: String::new(), + } + } + fn test_lines(&mut self, lines: &[&str]) { + self.test(&lines.join("\n")); + } + fn test(&mut self, comment: &str) { + fn indent(s: &str) -> String { + s.lines() + .map(|l| { + if l.is_empty() { + l.to_string() + } else { + format!(" {}", l) + } + }) + .collect::>() + .join("\n") + } + + let js_doc = JsDoc::parse(comment); + let output = js_doc.to_string(); + + self.output.push_str("\nInput: |\n"); + self.output.push_str(indent(comment).as_str()); + self.output.push_str("\n\nOutput: |\n"); + self.output.push_str(indent(&output).as_str()); + self.output.push_str("\n\nAst: |\n"); + self.output + .push_str(indent(&format!("{:#?}", js_doc)).as_str()); + self.output.push_str("\n\n---\n"); + } + fn assert(&self) { + let path = PathBuf::from("tests/snapshots/jsdoc.yml"); + + // read the file and remember the content/error for later + let expected = std::fs::read_to_string(&path); + + // update the snapshot file (and create parent dir if necessary) + std::fs::create_dir_all(path.parent().unwrap()).unwrap(); + std::fs::write(&path, self.output.as_bytes()).unwrap(); + + // compare the expected content with the actual content + let mut expected = expected.expect("Failed to read the snapshot file"); + // normalize line endings + expected = expected.replace("\r\n", "\n"); + let actual = self.output.replace("\r\n", "\n"); + + if actual != expected { + let first_diff_line = expected + .lines() + .zip(actual.lines()) + .position(|(e, a)| e != a) + .unwrap_or_else(|| expected.lines().count().min(actual.lines().count())); + + eprintln!("Snapshots differ!"); + eprintln!("First diff at line {}", first_diff_line + 1); + panic!("Snapshots differ"); + } + } + } + } +} diff --git a/crates/cli-support/src/js/mod.rs b/crates/cli-support/src/js/mod.rs index 72b0273d71f..701812c307c 100644 --- a/crates/cli-support/src/js/mod.rs +++ b/crates/cli-support/src/js/mod.rs @@ -10,6 +10,8 @@ use crate::wit::{JsImport, JsImportName, NonstandardWitSection, WasmBindgenAux}; use crate::{reset_indentation, Bindgen, EncodeInto, OutputMode, PLACEHOLDER_MODULE}; use anyhow::{anyhow, bail, Context as _, Error}; use binding::TsReference; +use ident::is_valid_ident; +use jsdoc::JsDoc; use std::borrow::Cow; use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; use std::fmt; @@ -19,6 +21,8 @@ use std::path::{Path, PathBuf}; use walrus::{FunctionId, ImportId, MemoryId, Module, TableId, ValType}; mod binding; +mod ident; +mod jsdoc; pub struct Context<'a> { globals: String, @@ -2795,7 +2799,7 @@ __wbg_set_wasm(wasm);" ts_arg_tys, ts_ret_ty, ts_refs, - js_doc, + js_doc: js_doc_tags, code, might_be_optional_field, catch, @@ -2823,8 +2827,14 @@ __wbg_set_wasm(wasm);" let ts_sig = export.generate_typescript.then_some(ts_sig.as_str()); - let js_docs = format_doc_comments(&export.comments, Some(js_doc)); - let ts_docs = format_doc_comments(&export.comments, None); + let ts_docs = format_doc_comments(&export.comments); + let js_docs = if export.generate_jsdoc { + let mut js_doc = JsDoc::parse(&export.comments); + js_doc.enhance(js_doc_tags); + format_doc_comments(&js_doc.to_string_indented()) + } else { + ts_docs.clone() + }; match &export.kind { AuxExportKind::Function(name) => { @@ -3917,7 +3927,7 @@ __wbg_set_wasm(wasm);" if enum_.generate_typescript { self.typescript - .push_str(&format_doc_comments(&enum_.comments, None)); + .push_str(&format_doc_comments(&enum_.comments)); self.typescript .push_str(&format!("export enum {} {{", enum_.name)); } @@ -3925,7 +3935,7 @@ __wbg_set_wasm(wasm);" let variant_docs = if comments.is_empty() { String::new() } else { - format_doc_comments(comments, None) + format_doc_comments(comments) }; variants.push_str(&variant_docs); variants.push_str(&format!("{}: {}, ", name, value)); @@ -3947,15 +3957,19 @@ __wbg_set_wasm(wasm);" } // add an `@enum {1 | 2 | 3}` to ensure that enums type-check even without .d.ts - let mut at_enum = "@enum {".to_string(); + let mut comment = enum_.comments.to_string(); + if !comment.is_empty() { + comment += "\n\n"; + } + comment += "@enum {"; for (i, (_, value, _)) in enum_.variants.iter().enumerate() { if i != 0 { - at_enum.push_str(" | "); + comment += " | "; } - at_enum.push_str(&value.to_string()); + comment += &value.to_string(); } - at_enum.push('}'); - let docs = format_doc_comments(&enum_.comments, Some(at_enum)); + comment += "}"; + let docs = format_doc_comments(&comment); self.export( &enum_.name, @@ -3978,7 +3992,7 @@ __wbg_set_wasm(wasm);" .typescript_refs .contains(&TsReference::StringEnum(string_enum.name.clone())) { - let docs = format_doc_comments(&string_enum.comments, None); + let docs = format_doc_comments(&string_enum.comments); let type_expr = if variants.is_empty() { "never".to_string() } else { @@ -4011,7 +4025,7 @@ __wbg_set_wasm(wasm);" fn generate_struct(&mut self, struct_: &AuxStruct) -> Result<(), Error> { let class = require_class(&mut self.exported_classes, &struct_.name); - class.comments = format_doc_comments(&struct_.comments, None); + class.comments = format_doc_comments(&struct_.comments); class.is_inspectable = struct_.is_inspectable; class.generate_typescript = struct_.generate_typescript; Ok(()) @@ -4440,7 +4454,12 @@ fn check_duplicated_getter_and_setter_names( Ok(()) } -fn format_doc_comments(comments: &str, js_doc_comments: Option) -> String { +fn format_doc_comments(comments: &str) -> String { + if comments.trim().is_empty() { + // don't emit empty doc comments + return String::new(); + } + let body: String = comments.lines().fold(String::new(), |mut output, c| { output.push_str(" *"); if !c.is_empty() && !c.starts_with(' ') { @@ -4450,20 +4469,8 @@ fn format_doc_comments(comments: &str, js_doc_comments: Option) -> Strin output.push('\n'); output }); - let doc = if let Some(docs) = js_doc_comments { - docs.lines().fold(String::new(), |mut output: String, l| { - let _ = writeln!(output, " * {}", l); - output - }) - } else { - String::new() - }; - if body.is_empty() && doc.is_empty() { - // don't emit empty doc comments - String::new() - } else { - format!("/**\n{}{} */\n", body, doc) - } + + format!("/**\n{} */\n", body) } fn require_class<'a>( @@ -4477,48 +4484,6 @@ fn require_class<'a>( .or_default() } -/// Returns whether a character has the Unicode `ID_Start` properly. -/// -/// This is only ever-so-slightly different from `XID_Start` in a few edge -/// cases, so we handle those edge cases manually and delegate everything else -/// to `unicode-ident`. -fn is_id_start(c: char) -> bool { - match c { - '\u{037A}' | '\u{0E33}' | '\u{0EB3}' | '\u{309B}' | '\u{309C}' | '\u{FC5E}' - | '\u{FC5F}' | '\u{FC60}' | '\u{FC61}' | '\u{FC62}' | '\u{FC63}' | '\u{FDFA}' - | '\u{FDFB}' | '\u{FE70}' | '\u{FE72}' | '\u{FE74}' | '\u{FE76}' | '\u{FE78}' - | '\u{FE7A}' | '\u{FE7C}' | '\u{FE7E}' | '\u{FF9E}' | '\u{FF9F}' => true, - _ => unicode_ident::is_xid_start(c), - } -} - -/// Returns whether a character has the Unicode `ID_Continue` properly. -/// -/// This is only ever-so-slightly different from `XID_Continue` in a few edge -/// cases, so we handle those edge cases manually and delegate everything else -/// to `unicode-ident`. -fn is_id_continue(c: char) -> bool { - match c { - '\u{037A}' | '\u{309B}' | '\u{309C}' | '\u{FC5E}' | '\u{FC5F}' | '\u{FC60}' - | '\u{FC61}' | '\u{FC62}' | '\u{FC63}' | '\u{FDFA}' | '\u{FDFB}' | '\u{FE70}' - | '\u{FE72}' | '\u{FE74}' | '\u{FE76}' | '\u{FE78}' | '\u{FE7A}' | '\u{FE7C}' - | '\u{FE7E}' => true, - _ => unicode_ident::is_xid_continue(c), - } -} - -/// Returns whether a string is a valid JavaScript identifier. -/// Defined at https://tc39.es/ecma262/#prod-IdentifierName. -fn is_valid_ident(name: &str) -> bool { - name.chars().enumerate().all(|(i, char)| { - if i == 0 { - is_id_start(char) || char == '$' || char == '_' - } else { - is_id_continue(char) || char == '$' || char == '\u{200C}' || char == '\u{200D}' - } - }) -} - /// Returns a string to tack on to the end of an expression to access a /// property named `name` of the object that expression resolves to. /// diff --git a/crates/cli-support/tests/snapshots/jsdoc.yml b/crates/cli-support/tests/snapshots/jsdoc.yml new file mode 100644 index 00000000000..7ab2308a3d1 --- /dev/null +++ b/crates/cli-support/tests/snapshots/jsdoc.yml @@ -0,0 +1,808 @@ + +Input: | + This is a description. + +Output: | + This is a description. + +Ast: | + JsDoc { + description: "This is a description.", + tags: [], + } + +--- + +Input: | + This is + + + a multi-line + description. + +Output: | + This is + + + a multi-line + description. + +Ast: | + JsDoc { + description: "This is\n \n\na multi-line\ndescription.", + tags: [], + } + +--- + +Input: | + This is a description with tag. + + @public + +Output: | + This is a description with tag. + + @public + +Ast: | + JsDoc { + description: "This is a description with tag.\n", + tags: [ + Unknown( + UnknownTag { + tag: "@public", + text: "", + }, + ), + ], + } + +--- + +Input: | + This is a description with tag. + @public + +Output: | + This is a description with tag. + @public + +Ast: | + JsDoc { + description: "This is a description with tag.", + tags: [ + Unknown( + UnknownTag { + tag: "@public", + text: "", + }, + ), + ], + } + +--- + +Input: | + @param foo + @param foo description + @param foo + description + @param [foo] + @param [foo] description + @param [foo] + description + @param [foo=123] + @param [foo=123] description + @param [foo=123] + description + @param {number} foo + @param {number} foo description + @param {number} [foo] + @param {number} [foo] description + @param {number} [foo=123] + @param {number} [foo=123] description + @param {object} obj + @param {string} obj.name + @param {object[]} obj.locations + @param {string} obj.locations[].address + @param {string} [obj.locations[].address] + @param {} foo + +Output: | + @param foo + @param foo description + @param foo + description + @param [foo] + @param [foo] description + @param [foo] + description + @param [foo=123] + @param [foo=123] description + @param [foo=123] + description + @param {number} foo + @param {number} foo description + @param {number} [foo] + @param {number} [foo] description + @param {number} [foo=123] + @param {number} [foo=123] description + @param {object} obj + @param {string} obj.name + @param {object[]} obj.locations + @param {string} obj.locations[].address + @param {string} [obj.locations[].address] + @param foo + +Ast: | + JsDoc { + description: "", + tags: [ + Param( + ParamTag { + ty: None, + name: "foo", + optional: Required, + description: "", + }, + ), + Param( + ParamTag { + ty: None, + name: "foo", + optional: Required, + description: "description", + }, + ), + Param( + ParamTag { + ty: None, + name: "foo", + optional: Required, + description: "\ndescription", + }, + ), + Param( + ParamTag { + ty: None, + name: "foo", + optional: Optional, + description: "", + }, + ), + Param( + ParamTag { + ty: None, + name: "foo", + optional: Optional, + description: "description", + }, + ), + Param( + ParamTag { + ty: None, + name: "foo", + optional: Optional, + description: "\ndescription", + }, + ), + Param( + ParamTag { + ty: None, + name: "foo", + optional: OptionalWithDefault( + "123", + ), + description: "", + }, + ), + Param( + ParamTag { + ty: None, + name: "foo", + optional: OptionalWithDefault( + "123", + ), + description: "description", + }, + ), + Param( + ParamTag { + ty: None, + name: "foo", + optional: OptionalWithDefault( + "123", + ), + description: "\ndescription", + }, + ), + Param( + ParamTag { + ty: Some( + "number", + ), + name: "foo", + optional: Required, + description: "", + }, + ), + Param( + ParamTag { + ty: Some( + "number", + ), + name: "foo", + optional: Required, + description: "description", + }, + ), + Param( + ParamTag { + ty: Some( + "number", + ), + name: "foo", + optional: Optional, + description: "", + }, + ), + Param( + ParamTag { + ty: Some( + "number", + ), + name: "foo", + optional: Optional, + description: "description", + }, + ), + Param( + ParamTag { + ty: Some( + "number", + ), + name: "foo", + optional: OptionalWithDefault( + "123", + ), + description: "", + }, + ), + Param( + ParamTag { + ty: Some( + "number", + ), + name: "foo", + optional: OptionalWithDefault( + "123", + ), + description: "description", + }, + ), + Param( + ParamTag { + ty: Some( + "object", + ), + name: "obj", + optional: Required, + description: "", + }, + ), + Param( + ParamTag { + ty: Some( + "string", + ), + name: "obj.name", + optional: Required, + description: "", + }, + ), + Param( + ParamTag { + ty: Some( + "object[]", + ), + name: "obj.locations", + optional: Required, + description: "", + }, + ), + Param( + ParamTag { + ty: Some( + "string", + ), + name: "obj.locations[].address", + optional: Required, + description: "", + }, + ), + Param( + ParamTag { + ty: Some( + "string", + ), + name: "obj.locations[].address", + optional: Optional, + description: "", + }, + ), + Param( + ParamTag { + ty: None, + name: "foo", + optional: Required, + description: "", + }, + ), + ], + } + +--- + +Input: | + @param {string} foo + @param{string}foo + @param{string}[foo] + @param{string}[foo=] + @param { string } [ foo = 123 ] + @param { } [ foo = 123 ] + +Output: | + @param {string} foo + @param {string} foo + @param {string} [foo] + @param {string} [foo] + @param {string} [foo=123] + @param [foo=123] + +Ast: | + JsDoc { + description: "", + tags: [ + Param( + ParamTag { + ty: Some( + "string", + ), + name: "foo", + optional: Required, + description: "", + }, + ), + Param( + ParamTag { + ty: Some( + "string", + ), + name: "foo", + optional: Required, + description: "", + }, + ), + Param( + ParamTag { + ty: Some( + "string", + ), + name: "foo", + optional: Optional, + description: "", + }, + ), + Param( + ParamTag { + ty: Some( + "string", + ), + name: "foo", + optional: Optional, + description: "", + }, + ), + Param( + ParamTag { + ty: Some( + "string", + ), + name: "foo", + optional: OptionalWithDefault( + "123", + ), + description: "", + }, + ), + Param( + ParamTag { + ty: None, + name: "foo", + optional: OptionalWithDefault( + "123", + ), + description: "", + }, + ), + ], + } + +--- + +Input: | + @param { + string + } foo + @param {string // comment + | number} foo + @param { number = } foo + @param { = } foo + @param {{ + name: { first: string, last: string }; + }} foo + @param {'}' | "}" | `}${{'}': "}"}}}`} foo + +Output: | + @param { + string + } foo + @param {string // comment + | number} foo + @param {number} [foo] + @param [foo] + @param {{ + name: { first: string, last: string }; + }} foo + @param {'}' | "}" | `}${{'}': "}"}}}`} foo + +Ast: | + JsDoc { + description: "", + tags: [ + Param( + ParamTag { + ty: Some( + "\nstring\n", + ), + name: "foo", + optional: Required, + description: "", + }, + ), + Param( + ParamTag { + ty: Some( + "string // comment\n| number", + ), + name: "foo", + optional: Required, + description: "", + }, + ), + Param( + ParamTag { + ty: Some( + "number", + ), + name: "foo", + optional: Optional, + description: "", + }, + ), + Param( + ParamTag { + ty: None, + name: "foo", + optional: Optional, + description: "", + }, + ), + Param( + ParamTag { + ty: Some( + "{\n name: { first: string, last: string };\n}", + ), + name: "foo", + optional: Required, + description: "", + }, + ), + Param( + ParamTag { + ty: Some( + "'}' | \"}\" | `}${{'}': \"}\"}}}`", + ), + name: "foo", + optional: Required, + description: "", + }, + ), + ], + } + +--- + +Input: | + @arg foo + @arg {string} foo + @argument foo + @argument {string} foo + +Output: | + @param foo + @param {string} foo + @param foo + @param {string} foo + +Ast: | + JsDoc { + description: "", + tags: [ + Param( + ParamTag { + ty: None, + name: "foo", + optional: Required, + description: "", + }, + ), + Param( + ParamTag { + ty: Some( + "string", + ), + name: "foo", + optional: Required, + description: "", + }, + ), + Param( + ParamTag { + ty: None, + name: "foo", + optional: Required, + description: "", + }, + ), + Param( + ParamTag { + ty: Some( + "string", + ), + name: "foo", + optional: Required, + description: "", + }, + ), + ], + } + +--- + +Input: | + @returns + @returns description + @returns + description + @returns {string} + @returns + + + {number} + @returns {string} description + @returns {string} + description + +Output: | + @returns + @returns description + @returns + description + @returns {string} + @returns {number} + @returns {string} description + @returns {string} + description + +Ast: | + JsDoc { + description: "", + tags: [ + Returns( + ReturnsTag { + ty: None, + description: "", + }, + ), + Returns( + ReturnsTag { + ty: None, + description: "description", + }, + ), + Returns( + ReturnsTag { + ty: None, + description: "\ndescription", + }, + ), + Returns( + ReturnsTag { + ty: Some( + "string", + ), + description: "", + }, + ), + Returns( + ReturnsTag { + ty: Some( + "number", + ), + description: "", + }, + ), + Returns( + ReturnsTag { + ty: Some( + "string", + ), + description: "description", + }, + ), + Returns( + ReturnsTag { + ty: Some( + "string", + ), + description: "\ndescription", + }, + ), + ], + } + +--- + +Input: | + @returns + @returns description + @returns {} + @returns{void} + @returns{void} + @returns{void}description + @returns{void} description + @returns + + + { + + void + + } + +Output: | + @returns + @returns description + @returns + @returns {void} + @returns {void} + @returns {void} description + @returns {void} description + @returns { + + void + + } + +Ast: | + JsDoc { + description: "", + tags: [ + Returns( + ReturnsTag { + ty: None, + description: "", + }, + ), + Returns( + ReturnsTag { + ty: None, + description: "description", + }, + ), + Returns( + ReturnsTag { + ty: None, + description: "", + }, + ), + Returns( + ReturnsTag { + ty: Some( + "void", + ), + description: "", + }, + ), + Returns( + ReturnsTag { + ty: Some( + "void", + ), + description: "", + }, + ), + Returns( + ReturnsTag { + ty: Some( + "void", + ), + description: "description", + }, + ), + Returns( + ReturnsTag { + ty: Some( + "void", + ), + description: "description", + }, + ), + Returns( + ReturnsTag { + ty: Some( + "\n\nvoid\n\n", + ), + description: "", + }, + ), + ], + } + +--- + +Input: | + @returns { + +Output: | + @returns { + +Ast: | + JsDoc { + description: "", + tags: [ + Unknown( + UnknownTag { + tag: "@returns", + text: " {", + }, + ), + ], + } + +--- + +Input: | + @return {string} description + +Output: | + @returns {string} description + +Ast: | + JsDoc { + description: "", + tags: [ + Returns( + ReturnsTag { + ty: Some( + "string", + ), + description: "description", + }, + ), + ], + } + +--- diff --git a/crates/cli/tests/reference/enums.js b/crates/cli/tests/reference/enums.js index 13459b9c77e..66756e30298 100644 --- a/crates/cli/tests/reference/enums.js +++ b/crates/cli/tests/reference/enums.js @@ -73,6 +73,7 @@ export function option_order(order) { /** * A color. + * * @enum {0 | 1 | 2} */ export const Color = Object.freeze({ @@ -100,6 +101,7 @@ export const ImplicitDiscriminant = Object.freeze({ }); /** * A C-style enum with negative discriminants. + * * @enum {-1 | 0 | 1} */ export const Ordering = Object.freeze({ diff --git a/crates/cli/tests/reference/jsdoc.d.ts b/crates/cli/tests/reference/jsdoc.d.ts new file mode 100644 index 00000000000..fb500ee5e76 --- /dev/null +++ b/crates/cli/tests/reference/jsdoc.d.ts @@ -0,0 +1,45 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Manually documented function + * + * @param {number} arg - This is my arg. It is mine. + * @returns to whence I came + */ +export function docme(arg: number): number; +/** + * Manually documented function + * + * @param {number} arg - This is my arg. It is mine. + * @returns to whence I came + */ +export function docme_skip(arg: number): number; +/** + * Regular documentation. + */ +export function i_has_docs(arg: number): number; +/** + * Regular documentation. + * + * @param [b=0] Description of `arg`. + * @param c Another description. + * @param {} d Another description. + * @returns + */ +export function add(a: number, b?: number, c?: number, d?: number): number; +/** + * ```js + * function foo() { + * return 1; + * } + * ``` + */ +export function indent_test1(arg: number): void; +/** + * ```js + * function foo() { + * return 1; + * } + * ``` + */ +export function indent_test2(arg: number): void; diff --git a/crates/cli/tests/reference/jsdoc.js b/crates/cli/tests/reference/jsdoc.js new file mode 100644 index 00000000000..151b843793e --- /dev/null +++ b/crates/cli/tests/reference/jsdoc.js @@ -0,0 +1,88 @@ +let wasm; +export function __wbg_set_wasm(val) { + wasm = val; +} + +/** + * Manually documented function + * + * @param {number} arg - This is my arg. It is mine. + * @returns {number} to whence I came + */ +export function docme(arg) { + const ret = wasm.docme(arg); + return ret >>> 0; +} + +/** + * Manually documented function + * + * @param {number} arg - This is my arg. It is mine. + * @returns to whence I came + */ +export function docme_skip(arg) { + const ret = wasm.docme_skip(arg); + return ret >>> 0; +} + +/** + * Regular documentation. + * @param {number} arg + * @returns {number} + */ +export function i_has_docs(arg) { + const ret = wasm.i_has_docs(arg); + return ret >>> 0; +} + +function isLikeNone(x) { + return x === undefined || x === null; +} +/** + * Regular documentation. + * + * @param {number | undefined} [b=0] Description of `arg`. + * @param {number | undefined} [c] Another description. + * @param {number | undefined} [d] Another description. + * @param {number} a + * @returns {number} + */ +export function add(a, b, c, d) { + const ret = wasm.add(a, isLikeNone(b) ? 0x100000001 : (b) >>> 0, isLikeNone(c) ? 0x100000001 : (c) >>> 0, isLikeNone(d) ? 0x100000001 : (d) >>> 0); + return ret >>> 0; +} + +/** + * ```js + * function foo() { + * return 1; + * } + * ``` + * @param {number} arg + */ +export function indent_test1(arg) { + wasm.indent_test1(arg); +} + +/** + * ```js + * function foo() { + * return 1; + * } + * ``` + */ +export function indent_test2(arg) { + wasm.indent_test2(arg); +} + +export function __wbindgen_init_externref_table() { + const table = wasm.__wbindgen_export_0; + const offset = table.grow(4); + table.set(0, undefined); + table.set(offset + 0, undefined); + table.set(offset + 1, null); + table.set(offset + 2, true); + table.set(offset + 3, false); + ; +}; + diff --git a/crates/cli/tests/reference/jsdoc.rs b/crates/cli/tests/reference/jsdoc.rs new file mode 100644 index 00000000000..89ce72aa247 --- /dev/null +++ b/crates/cli/tests/reference/jsdoc.rs @@ -0,0 +1,52 @@ +use wasm_bindgen::prelude::*; + +/// Manually documented function +/// +/// @param {number} arg - This is my arg. It is mine. +/// @returns to whence I came +#[wasm_bindgen] +pub fn docme(arg: u32) -> u32 { + arg + 1 +} + +/// Manually documented function +/// +/// @param {number} arg - This is my arg. It is mine. +/// @returns to whence I came +#[wasm_bindgen(skip_jsdoc)] +pub fn docme_skip(arg: u32) -> u32 { + arg + 1 +} + +/// Regular documentation. +#[wasm_bindgen] +pub fn i_has_docs(arg: u32) -> u32 { + arg + 1 +} + +/// Regular documentation. +/// +/// @param [b=0] Description of `arg`. +/// @param c Another description. +/// @param {} d Another description. +/// @returns +#[wasm_bindgen] +pub fn add(a: u32, b: Option, c: Option, d: Option) -> u32 { + a + b.unwrap_or(0) + c.unwrap_or(0) + d.unwrap_or(0) +} + +/// ```js +/// function foo() { +/// return 1; +/// } +/// ``` +#[wasm_bindgen] +pub fn indent_test1(arg: u32) {} + +/// ```js +/// function foo() { +/// return 1; +/// } +/// ``` +#[wasm_bindgen(skip_jsdoc)] +pub fn indent_test2(arg: u32) {} diff --git a/crates/cli/tests/reference/jsdoc.wat b/crates/cli/tests/reference/jsdoc.wat new file mode 100644 index 00000000000..5b9b6eacb66 --- /dev/null +++ b/crates/cli/tests/reference/jsdoc.wat @@ -0,0 +1,26 @@ +(module $reference_test.wasm + (type (;0;) (func)) + (type (;1;) (func (param i32))) + (type (;2;) (func (param i32) (result i32))) + (type (;3;) (func (param i32 f64 f64 f64) (result i32))) + (import "./reference_test_bg.js" "__wbindgen_init_externref_table" (func (;0;) (type 0))) + (func $add (;1;) (type 3) (param i32 f64 f64 f64) (result i32)) + (func $docme (;2;) (type 2) (param i32) (result i32)) + (func $docme_skip (;3;) (type 2) (param i32) (result i32)) + (func $i_has_docs (;4;) (type 2) (param i32) (result i32)) + (func $indent_test1 (;5;) (type 1) (param i32)) + (func $indent_test2 (;6;) (type 1) (param i32)) + (table (;0;) 128 externref) + (memory (;0;) 17) + (export "memory" (memory 0)) + (export "docme" (func $docme)) + (export "docme_skip" (func $docme_skip)) + (export "i_has_docs" (func $i_has_docs)) + (export "add" (func $add)) + (export "indent_test1" (func $indent_test1)) + (export "indent_test2" (func $indent_test2)) + (export "__wbindgen_export_0" (table 0)) + (export "__wbindgen_start" (func 0)) + (@custom "target_features" (after code) "\04+\0amultivalue+\0fmutable-globals+\0freference-types+\08sign-ext") +) + diff --git a/crates/cli/tests/reference/skip-jsdoc.d.ts b/crates/cli/tests/reference/skip-jsdoc.d.ts deleted file mode 100644 index 0d908f65acc..00000000000 --- a/crates/cli/tests/reference/skip-jsdoc.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * Manually documented function - * - * @param {number} arg - This is my arg. It is mine. - * @returns to whence I came - */ -export function docme(arg: number): number; -/** - * Regular documentation. - */ -export function i_has_docs(arg: number): number; diff --git a/crates/cli/tests/reference/skip-jsdoc.js b/crates/cli/tests/reference/skip-jsdoc.js deleted file mode 100644 index 1ba307bae00..00000000000 --- a/crates/cli/tests/reference/skip-jsdoc.js +++ /dev/null @@ -1,37 +0,0 @@ -let wasm; -export function __wbg_set_wasm(val) { - wasm = val; -} - -/** - * Manually documented function - * - * @param {number} arg - This is my arg. It is mine. - * @returns to whence I came - */ -export function docme(arg) { - const ret = wasm.docme(arg); - return ret >>> 0; -} - -/** - * Regular documentation. - * @param {number} arg - * @returns {number} - */ -export function i_has_docs(arg) { - const ret = wasm.i_has_docs(arg); - return ret >>> 0; -} - -export function __wbindgen_init_externref_table() { - const table = wasm.__wbindgen_export_0; - const offset = table.grow(4); - table.set(0, undefined); - table.set(offset + 0, undefined); - table.set(offset + 1, null); - table.set(offset + 2, true); - table.set(offset + 3, false); - ; -}; - diff --git a/crates/cli/tests/reference/skip-jsdoc.rs b/crates/cli/tests/reference/skip-jsdoc.rs deleted file mode 100644 index 2aa14eb1359..00000000000 --- a/crates/cli/tests/reference/skip-jsdoc.rs +++ /dev/null @@ -1,16 +0,0 @@ -use wasm_bindgen::prelude::*; - -/// Manually documented function -/// -/// @param {number} arg - This is my arg. It is mine. -/// @returns to whence I came -#[wasm_bindgen(skip_jsdoc)] -pub fn docme(arg: u32) -> u32 { - arg + 1 -} - -/// Regular documentation. -#[wasm_bindgen] -pub fn i_has_docs(arg: u32) -> u32 { - arg + 1 -} diff --git a/crates/cli/tests/reference/skip-jsdoc.wat b/crates/cli/tests/reference/skip-jsdoc.wat deleted file mode 100644 index 99c1443aca1..00000000000 --- a/crates/cli/tests/reference/skip-jsdoc.wat +++ /dev/null @@ -1,16 +0,0 @@ -(module $reference_test.wasm - (type (;0;) (func)) - (type (;1;) (func (param i32) (result i32))) - (import "./reference_test_bg.js" "__wbindgen_init_externref_table" (func (;0;) (type 0))) - (func $docme (;1;) (type 1) (param i32) (result i32)) - (func $i_has_docs (;2;) (type 1) (param i32) (result i32)) - (table (;0;) 128 externref) - (memory (;0;) 17) - (export "memory" (memory 0)) - (export "docme" (func $docme)) - (export "i_has_docs" (func $i_has_docs)) - (export "__wbindgen_export_0" (table 0)) - (export "__wbindgen_start" (func 0)) - (@custom "target_features" (after code) "\04+\0amultivalue+\0fmutable-globals+\0freference-types+\08sign-ext") -) -