Copy more doc comments to JS/TS files, unescape comments (#2070)

* Copy more doc comments to JS/TS files, unescape comments

* Move unescape code to macro-support
This commit is contained in:
0xd4d 2020-04-13 20:51:32 +02:00 committed by GitHub
parent fc86589715
commit 826538922f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 179 additions and 28 deletions

View File

@ -264,6 +264,7 @@ pub struct Enum {
pub struct Variant { pub struct Variant {
pub name: Ident, pub name: Ident,
pub value: u32, pub value: u32,
pub comments: Vec<String>,
} }
#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[derive(Copy, Clone, Debug, PartialEq, Eq)]

View File

@ -227,6 +227,7 @@ fn shared_variant<'a>(v: &'a ast::Variant, intern: &'a Interner) -> EnumVariant<
EnumVariant { EnumVariant {
name: intern.intern(&v.name), name: intern.intern(&v.name),
value: v.value, value: v.value,
comments: v.comments.iter().map(|s| &**s).collect(),
} }
} }

View File

@ -66,9 +66,9 @@ pub struct ExportedClass {
is_inspectable: bool, is_inspectable: bool,
/// All readable properties of the class /// All readable properties of the class
readable_properties: Vec<String>, readable_properties: Vec<String>,
/// 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 /// and it is optional
typescript_fields: HashMap<String, (String, bool, bool)>, typescript_fields: HashMap<String, (String, String, bool, bool)>,
} }
const INITIAL_HEAP_VALUES: &[&str] = &["undefined", "null", "true", "false"]; 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::<Vec<_>>(); let mut fields = class.typescript_fields.keys().collect::<Vec<_>>();
fields.sort(); // make sure we have deterministic output fields.sort(); // make sure we have deterministic output
for name in fields { 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(" "); ts_dst.push_str(" ");
if !has_setter { if !has_setter {
ts_dst.push_str("readonly "); ts_dst.push_str("readonly ");
@ -816,6 +817,7 @@ impl<'a> Context<'a> {
ts_dst.push_str("}\n"); ts_dst.push_str("}\n");
self.export(&name, &dst, Some(&class.comments))?; self.export(&name, &dst, Some(&class.comments))?;
self.typescript.push_str(&class.comments);
self.typescript.push_str(&ts_dst); self.typescript.push_str(&ts_dst);
Ok(()) Ok(())
@ -2901,10 +2903,23 @@ impl<'a> Context<'a> {
self.typescript self.typescript
.push_str(&format!("export enum {} {{", enum_.name)); .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)); variants.push_str(&format!("{}:{},", name, value));
if enum_.generate_typescript { 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 { 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>) { fn push_getter(&mut self, docs: &str, field: &str, js: &str, ret_ty: Option<&str>) {
self.push_accessor(docs, field, js, "get "); self.push_accessor(docs, field, js, "get ");
if let Some(ret_ty) = ret_ty { 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()); self.readable_properties.push(field.to_string());
} }
@ -3244,20 +3259,30 @@ impl ExportedClass {
) { ) {
self.push_accessor(docs, field, js, "set "); self.push_accessor(docs, field, js, "set ");
if let Some(ret_ty) = ret_ty { if let Some(ret_ty) = ret_ty {
let (has_setter, is_optional) = self.push_accessor_ts(field, ret_ty); let is_optional = self.push_accessor_ts(docs, field, ret_ty, true);
*has_setter = true;
*is_optional = might_be_optional_field; *is_optional = might_be_optional_field;
} }
} }
fn push_accessor_ts(&mut self, field: &str, ret_ty: &str) -> (&mut bool, &mut bool) { fn push_accessor_ts(
let (ty, has_setter, is_optional) = self &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 .typescript_fields
.entry(field.to_string()) .entry(field.to_string())
.or_insert_with(Default::default); .or_insert_with(Default::default);
*ty = ret_ty.to_string(); *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) { fn push_accessor(&mut self, docs: &str, field: &str, js: &str, prefix: &str) {

View File

@ -477,7 +477,8 @@ fn reset_indentation(s: &str) -> String {
dst.push_str(line); dst.push_str(line);
} }
dst.push_str("\n"); 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; indent += 1;
} }
} }

View File

@ -766,7 +766,13 @@ impl<'a> Context<'a> {
variants: enum_ variants: enum_
.variants .variants
.iter() .iter()
.map(|v| (v.name.to_string(), v.value)) .map(|v| {
(
v.name.to_string(),
v.value,
concatenate_comments(&v.comments),
)
})
.collect(), .collect(),
generate_typescript: enum_.generate_typescript, generate_typescript: enum_.generate_typescript,
}; };
@ -1511,9 +1517,5 @@ fn verify_schema_matches<'a>(data: &'a [u8]) -> Result<Option<&'a str>, Error> {
} }
fn concatenate_comments(comments: &[&str]) -> String { fn concatenate_comments(comments: &[&str]) -> String {
comments comments.iter().map(|&s| s).collect::<Vec<_>>().join("\n")
.iter()
.map(|s| s.trim_matches('"'))
.collect::<Vec<_>>()
.join("\n")
} }

View File

@ -132,9 +132,9 @@ pub struct AuxEnum {
pub name: String, pub name: String,
/// The copied Rust comments to forward to JS /// The copied Rust comments to forward to JS
pub comments: String, 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 /// 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. /// Whether typescript bindings should be generated for this enum.
pub generate_typescript: bool, pub generate_typescript: bool,
} }

View File

@ -1,4 +1,6 @@
use std::cell::Cell; use std::cell::Cell;
use std::char;
use std::str::Chars;
use ast::OperationKind; use ast::OperationKind;
use backend::ast; 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 { Ok(ast::Variant {
name: v.ident.clone(), name: v.ident.clone(),
value, value,
comments,
}) })
}) })
.collect::<Result<Vec<_>, Diagnostic>>()?; .collect::<Result<Vec<_>, Diagnostic>>()?;
@ -1324,9 +1328,8 @@ fn extract_doc_comments(attrs: &[syn::Attribute]) -> Vec<String> {
// We want to filter out any Puncts so just grab the Literals // We want to filter out any Puncts so just grab the Literals
a.tokens.clone().into_iter().filter_map(|t| match t { a.tokens.clone().into_iter().filter_map(|t| match t {
TokenTree::Literal(lit) => { TokenTree::Literal(lit) => {
// this will always return the quoted string, we deal with let quoted = lit.to_string();
// that in the cli when we read in the comments Some(try_unescape(&quoted).unwrap_or_else(|| quoted))
Some(lit.to_string())
} }
_ => None, _ => None,
}), }),
@ -1342,6 +1345,76 @@ fn extract_doc_comments(attrs: &[syn::Attribute]) -> Vec<String> {
}) })
} }
// Unescapes a quoted string. char::escape_debug() was used to escape the text.
fn try_unescape(s: &str) -> Option<String> {
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. /// Check there are no lifetimes on the function.
fn assert_no_lifetimes(sig: &syn::Signature) -> Result<(), Diagnostic> { fn assert_no_lifetimes(sig: &syn::Signature) -> Result<(), Diagnostic> {
struct Walk { struct Walk {

View File

@ -106,6 +106,7 @@ macro_rules! shared_api {
struct EnumVariant<'a> { struct EnumVariant<'a> {
name: &'a str, name: &'a str,
value: u32, value: u32,
comments: Vec<&'a str>,
} }
struct Function<'a> { struct Function<'a> {

View File

@ -4,8 +4,16 @@ const assert = require('assert');
exports.assert_comments_exist = function() { exports.assert_comments_exist = function() {
const bindings_file = require.resolve('wasm-bindgen-test'); const bindings_file = require.resolve('wasm-bindgen-test');
const contents = fs.readFileSync(bindings_file); 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 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 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"));
}; };

View File

@ -6,26 +6,65 @@ extern "C" {
fn assert_comments_exist(); fn assert_comments_exist();
} }
/// annotated function ✔️ " \ ' {
#[wasm_bindgen] #[wasm_bindgen]
/// annotated function
pub fn annotated() -> String { pub fn annotated() -> String {
String::new() String::new()
} }
#[wasm_bindgen]
/// annotated struct type /// annotated struct type
#[wasm_bindgen]
pub struct Annotated { pub struct Annotated {
a: String, a: String,
/// annotated struct field /// annotated struct field b
pub b: u32, pub b: u32,
/// annotated struct field c
#[wasm_bindgen(readonly)]
pub c: u32,
d: u32,
} }
#[wasm_bindgen] #[wasm_bindgen]
impl Annotated { 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 /// annotated struct method
pub fn get_a(&self) -> String { pub fn get_a(&self) -> String {
self.a.clone() 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] #[wasm_bindgen_test]