diff options
author | Bartek IwaĆczuk <biwanczuk@gmail.com> | 2020-03-28 19:16:57 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-03-28 14:16:57 -0400 |
commit | 3fac487461abf055165fe0e2bb962573950277b8 (patch) | |
tree | c0ba09a2049975f9eb8625365e3ba395e67d8b50 | |
parent | bced52505f32d6cca4f944bb610a8a26767908a8 (diff) |
feat: Add "deno doc" subcommand (#4500)
-rw-r--r-- | Cargo.lock | 4 | ||||
-rw-r--r-- | cli/Cargo.toml | 7 | ||||
-rw-r--r-- | cli/doc/class.rs | 207 | ||||
-rw-r--r-- | cli/doc/enum.rs | 41 | ||||
-rw-r--r-- | cli/doc/function.rs | 70 | ||||
-rw-r--r-- | cli/doc/interface.rs | 243 | ||||
-rw-r--r-- | cli/doc/mod.rs | 63 | ||||
-rw-r--r-- | cli/doc/module.rs | 189 | ||||
-rw-r--r-- | cli/doc/namespace.rs | 81 | ||||
-rw-r--r-- | cli/doc/node.rs | 77 | ||||
-rw-r--r-- | cli/doc/parser.rs | 189 | ||||
-rw-r--r-- | cli/doc/printer.rs | 432 | ||||
-rw-r--r-- | cli/doc/tests.rs | 568 | ||||
-rw-r--r-- | cli/doc/ts_type.rs | 821 | ||||
-rw-r--r-- | cli/doc/type_alias.rs | 25 | ||||
-rw-r--r-- | cli/doc/variable.rs | 43 | ||||
-rw-r--r-- | cli/flags.rs | 97 | ||||
-rw-r--r-- | cli/lib.rs | 80 |
18 files changed, 3233 insertions, 4 deletions
diff --git a/Cargo.lock b/Cargo.lock index 6a178bf03..ca7a0a367 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -493,6 +493,10 @@ dependencies = [ "serde_derive", "serde_json", "sourcemap", + "swc_common", + "swc_ecma_ast", + "swc_ecma_parser", + "swc_ecma_parser_macros", "sys-info", "tempfile", "termcolor", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 1483324bf..acabcf7d0 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -63,9 +63,14 @@ webpki-roots = "0.19.0" walkdir = "2.3.1" warp = "0.2.2" semver-parser = "0.9.0" +# TODO(bartlomieju): make sure we're using exactly same versions +# of "swc_*" as dprint-plugin-typescript +swc_common = "=0.5.9" +swc_ecma_ast = "=0.18.1" +swc_ecma_parser = "=0.21.8" +swc_ecma_parser_macros = "=0.4.1" uuid = { version = "0.8", features = ["v4"] } - [target.'cfg(windows)'.dependencies] winapi = "0.3.8" fwdansi = "1.1.0" diff --git a/cli/doc/class.rs b/cli/doc/class.rs new file mode 100644 index 000000000..635fd585a --- /dev/null +++ b/cli/doc/class.rs @@ -0,0 +1,207 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +use serde::Serialize; +use swc_common; +use swc_common::SourceMap; +use swc_common::Spanned; +use swc_ecma_ast; + +use super::function::function_to_function_def; +use super::function::FunctionDef; +use super::parser::DocParser; +use super::ts_type::ts_type_ann_to_def; +use super::ts_type::TsTypeDef; +use super::Location; +use super::ParamDef; + +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ClassConstructorDef { + pub js_doc: Option<String>, + pub accessibility: Option<swc_ecma_ast::Accessibility>, + pub name: String, + pub params: Vec<ParamDef>, + pub location: Location, +} + +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ClassPropertyDef { + pub js_doc: Option<String>, + pub ts_type: Option<TsTypeDef>, + pub readonly: bool, + pub accessibility: Option<swc_ecma_ast::Accessibility>, + pub is_abstract: bool, + pub is_static: bool, + pub name: String, + pub location: Location, +} + +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ClassMethodDef { + pub js_doc: Option<String>, + // pub ts_type: Option<TsTypeDef>, + // pub readonly: bool, + pub accessibility: Option<swc_ecma_ast::Accessibility>, + pub is_abstract: bool, + pub is_static: bool, + pub name: String, + pub kind: swc_ecma_ast::MethodKind, + pub function_def: FunctionDef, + pub location: Location, +} + +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ClassDef { + // TODO: decorators, super_class, implements, + // type_params, super_type_params + pub is_abstract: bool, + pub constructors: Vec<ClassConstructorDef>, + pub properties: Vec<ClassPropertyDef>, + pub methods: Vec<ClassMethodDef>, +} + +fn prop_name_to_string( + source_map: &SourceMap, + prop_name: &swc_ecma_ast::PropName, +) -> String { + use swc_ecma_ast::PropName; + match prop_name { + PropName::Ident(ident) => ident.sym.to_string(), + PropName::Str(str_) => str_.value.to_string(), + PropName::Num(num) => num.value.to_string(), + PropName::Computed(comp_prop_name) => { + source_map.span_to_snippet(comp_prop_name.span).unwrap() + } + } +} + +pub fn get_doc_for_class_decl( + doc_parser: &DocParser, + class_decl: &swc_ecma_ast::ClassDecl, +) -> (String, ClassDef) { + let mut constructors = vec![]; + let mut methods = vec![]; + let mut properties = vec![]; + + for member in &class_decl.class.body { + use swc_ecma_ast::ClassMember::*; + + match member { + Constructor(ctor) => { + let ctor_js_doc = doc_parser.js_doc_for_span(ctor.span()); + let constructor_name = + prop_name_to_string(&doc_parser.source_map, &ctor.key); + + let mut params = vec![]; + + for param in &ctor.params { + use swc_ecma_ast::Pat; + use swc_ecma_ast::PatOrTsParamProp::*; + + let param_def = match param { + Pat(pat) => match pat { + Pat::Ident(ident) => { + let ts_type = ident + .type_ann + .as_ref() + .map(|rt| ts_type_ann_to_def(&doc_parser.source_map, rt)); + + ParamDef { + name: ident.sym.to_string(), + ts_type, + } + } + _ => ParamDef { + name: "<TODO>".to_string(), + ts_type: None, + }, + }, + TsParamProp(_) => ParamDef { + name: "<TODO>".to_string(), + ts_type: None, + }, + }; + params.push(param_def); + } + + let constructor_def = ClassConstructorDef { + js_doc: ctor_js_doc, + accessibility: ctor.accessibility, + name: constructor_name, + params, + location: doc_parser + .source_map + .lookup_char_pos(ctor.span.lo()) + .into(), + }; + constructors.push(constructor_def); + } + Method(class_method) => { + let method_js_doc = doc_parser.js_doc_for_span(class_method.span()); + let method_name = + prop_name_to_string(&doc_parser.source_map, &class_method.key); + let fn_def = + function_to_function_def(doc_parser, &class_method.function); + let method_def = ClassMethodDef { + js_doc: method_js_doc, + accessibility: class_method.accessibility, + is_abstract: class_method.is_abstract, + is_static: class_method.is_static, + name: method_name, + kind: class_method.kind, + function_def: fn_def, + location: doc_parser + .source_map + .lookup_char_pos(class_method.span.lo()) + .into(), + }; + methods.push(method_def); + } + ClassProp(class_prop) => { + let prop_js_doc = doc_parser.js_doc_for_span(class_prop.span()); + + let ts_type = class_prop + .type_ann + .as_ref() + .map(|rt| ts_type_ann_to_def(&doc_parser.source_map, rt)); + + use swc_ecma_ast::Expr; + let prop_name = match &*class_prop.key { + Expr::Ident(ident) => ident.sym.to_string(), + _ => "<TODO>".to_string(), + }; + + let prop_def = ClassPropertyDef { + js_doc: prop_js_doc, + ts_type, + readonly: class_prop.readonly, + is_abstract: class_prop.is_abstract, + is_static: class_prop.is_static, + accessibility: class_prop.accessibility, + name: prop_name, + location: doc_parser + .source_map + .lookup_char_pos(class_prop.span.lo()) + .into(), + }; + properties.push(prop_def); + } + // TODO: + TsIndexSignature(_) => {} + PrivateMethod(_) => {} + PrivateProp(_) => {} + } + } + + let class_name = class_decl.ident.sym.to_string(); + let class_def = ClassDef { + is_abstract: class_decl.class.is_abstract, + constructors, + properties, + methods, + }; + + (class_name, class_def) +} diff --git a/cli/doc/enum.rs b/cli/doc/enum.rs new file mode 100644 index 000000000..f71c15537 --- /dev/null +++ b/cli/doc/enum.rs @@ -0,0 +1,41 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +use serde::Serialize; +use swc_ecma_ast; + +use super::parser::DocParser; + +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct EnumMemberDef { + pub name: String, +} + +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct EnumDef { + pub members: Vec<EnumMemberDef>, +} + +pub fn get_doc_for_ts_enum_decl( + _doc_parser: &DocParser, + enum_decl: &swc_ecma_ast::TsEnumDecl, +) -> (String, EnumDef) { + let enum_name = enum_decl.id.sym.to_string(); + let mut members = vec![]; + + for enum_member in &enum_decl.members { + use swc_ecma_ast::TsEnumMemberId::*; + + let member_name = match &enum_member.id { + Ident(ident) => ident.sym.to_string(), + Str(str_) => str_.value.to_string(), + }; + + let member_def = EnumMemberDef { name: member_name }; + members.push(member_def); + } + + let enum_def = EnumDef { members }; + + (enum_name, enum_def) +} diff --git a/cli/doc/function.rs b/cli/doc/function.rs new file mode 100644 index 000000000..ec7f9bf38 --- /dev/null +++ b/cli/doc/function.rs @@ -0,0 +1,70 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +use serde::Serialize; +use swc_ecma_ast; + +use super::parser::DocParser; +use super::ts_type::ts_type_ann_to_def; +use super::ts_type::TsTypeDef; +use super::ParamDef; + +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct FunctionDef { + pub params: Vec<ParamDef>, + pub return_type: Option<TsTypeDef>, + pub is_async: bool, + pub is_generator: bool, + // TODO: type_params, decorators +} + +pub fn function_to_function_def( + doc_parser: &DocParser, + function: &swc_ecma_ast::Function, +) -> FunctionDef { + let mut params = vec![]; + + for param in &function.params { + use swc_ecma_ast::Pat; + + let param_def = match param { + Pat::Ident(ident) => { + let ts_type = ident + .type_ann + .as_ref() + .map(|rt| ts_type_ann_to_def(&doc_parser.source_map, rt)); + + ParamDef { + name: ident.sym.to_string(), + ts_type, + } + } + _ => ParamDef { + name: "<TODO>".to_string(), + ts_type: None, + }, + }; + + params.push(param_def); + } + + let maybe_return_type = function + .return_type + .as_ref() + .map(|rt| ts_type_ann_to_def(&doc_parser.source_map, rt)); + + FunctionDef { + params, + return_type: maybe_return_type, + is_async: function.is_async, + is_generator: function.is_generator, + } +} + +pub fn get_doc_for_fn_decl( + doc_parser: &DocParser, + fn_decl: &swc_ecma_ast::FnDecl, +) -> (String, FunctionDef) { + let name = fn_decl.ident.sym.to_string(); + let fn_def = function_to_function_def(doc_parser, &fn_decl.function); + (name, fn_def) +} diff --git a/cli/doc/interface.rs b/cli/doc/interface.rs new file mode 100644 index 000000000..b7e123773 --- /dev/null +++ b/cli/doc/interface.rs @@ -0,0 +1,243 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +use serde::Serialize; +use swc_ecma_ast; + +use super::parser::DocParser; +use super::ts_type::ts_type_ann_to_def; +use super::ts_type::TsTypeDef; +use super::Location; +use super::ParamDef; + +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct InterfaceMethodDef { + // TODO: type_params + pub name: String, + pub location: Location, + pub js_doc: Option<String>, + pub params: Vec<ParamDef>, + pub return_type: Option<TsTypeDef>, +} + +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct InterfacePropertyDef { + // TODO: type_params + pub name: String, + pub location: Location, + pub js_doc: Option<String>, + pub params: Vec<ParamDef>, + pub computed: bool, + pub optional: bool, + pub ts_type: Option<TsTypeDef>, +} + +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct InterfaceCallSignatureDef { + // TODO: type_params + pub location: Location, + pub js_doc: Option<String>, + pub params: Vec<ParamDef>, + pub ts_type: Option<TsTypeDef>, +} + +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct InterfaceDef { + // TODO: extends, type params + pub methods: Vec<InterfaceMethodDef>, + pub properties: Vec<InterfacePropertyDef>, + pub call_signatures: Vec<InterfaceCallSignatureDef>, +} + +fn expr_to_name(expr: &swc_ecma_ast::Expr) -> String { + use swc_ecma_ast::Expr::*; + use swc_ecma_ast::ExprOrSuper::*; + + match expr { + Ident(ident) => ident.sym.to_string(), + Member(member_expr) => { + let left = match &member_expr.obj { + Super(_) => "TODO".to_string(), + Expr(boxed_expr) => expr_to_name(&*boxed_expr), + }; + let right = expr_to_name(&*member_expr.prop); + format!("[{}.{}]", left, right) + } + _ => "<TODO>".to_string(), + } +} + +pub fn get_doc_for_ts_interface_decl( + doc_parser: &DocParser, + interface_decl: &swc_ecma_ast::TsInterfaceDecl, +) -> (String, InterfaceDef) { + let interface_name = interface_decl.id.sym.to_string(); + + let mut methods = vec![]; + let mut properties = vec![]; + let mut call_signatures = vec![]; + + for type_element in &interface_decl.body.body { + use swc_ecma_ast::TsTypeElement::*; + + match &type_element { + TsMethodSignature(ts_method_sig) => { + let method_js_doc = doc_parser.js_doc_for_span(ts_method_sig.span); + + let mut params = vec![]; + + for param in &ts_method_sig.params { + use swc_ecma_ast::TsFnParam::*; + + let param_def = match param { + Ident(ident) => { + let ts_type = ident + .type_ann + .as_ref() + .map(|rt| ts_type_ann_to_def(&doc_parser.source_map, rt)); + + ParamDef { + name: ident.sym.to_string(), + ts_type, + } + } + _ => ParamDef { + name: "<TODO>".to_string(), + ts_type: None, + }, + }; + + params.push(param_def); + } + + let name = expr_to_name(&*ts_method_sig.key); + + let maybe_return_type = ts_method_sig + .type_ann + .as_ref() + .map(|rt| ts_type_ann_to_def(&doc_parser.source_map, rt)); + + let method_def = InterfaceMethodDef { + name, + js_doc: method_js_doc, + location: doc_parser + .source_map + .lookup_char_pos(ts_method_sig.span.lo()) + .into(), + params, + return_type: maybe_return_type, + }; + methods.push(method_def); + } + TsPropertySignature(ts_prop_sig) => { + let prop_js_doc = doc_parser.js_doc_for_span(ts_prop_sig.span); + let name = match &*ts_prop_sig.key { + swc_ecma_ast::Expr::Ident(ident) => ident.sym.to_string(), + _ => "TODO".to_string(), + }; + + let mut params = vec![]; + + for param in &ts_prop_sig.params { + use swc_ecma_ast::TsFnParam::*; + + let param_def = match param { + Ident(ident) => { + let ts_type = ident + .type_ann + .as_ref() + .map(|rt| ts_type_ann_to_def(&doc_parser.source_map, rt)); + + ParamDef { + name: ident.sym.to_string(), + ts_type, + } + } + _ => ParamDef { + name: "<TODO>".to_string(), + ts_type: None, + }, + }; + + params.push(param_def); + } + + let ts_type = ts_prop_sig + .type_ann + .as_ref() + .map(|rt| ts_type_ann_to_def(&doc_parser.source_map, rt)); + + let prop_def = InterfacePropertyDef { + name, + js_doc: prop_js_doc, + location: doc_parser + .source_map + .lookup_char_pos(ts_prop_sig.span.lo()) + .into(), + params, + ts_type, + computed: ts_prop_sig.computed, + optional: ts_prop_sig.optional, + }; + properties.push(prop_def); + } + TsCallSignatureDecl(ts_call_sig) => { + let call_sig_js_doc = doc_parser.js_doc_for_span(ts_call_sig.span); + + let mut params = vec![]; + for param in &ts_call_sig.params { + use swc_ecma_ast::TsFnParam::*; + + let param_def = match param { + Ident(ident) => { + let ts_type = ident + .type_ann + .as_ref() + .map(|rt| ts_type_ann_to_def(&doc_parser.source_map, rt)); + + ParamDef { + name: ident.sym.to_string(), + ts_type, + } + } + _ => ParamDef { + name: "<TODO>".to_string(), + ts_type: None, + }, + }; + + params.push(param_def); + } + + let ts_type = ts_call_sig + .type_ann + .as_ref() + .map(|rt| ts_type_ann_to_def(&doc_parser.source_map, rt)); + + let call_sig_def = InterfaceCallSignatureDef { + js_doc: call_sig_js_doc, + location: doc_parser + .source_map + .lookup_char_pos(ts_call_sig.span.lo()) + .into(), + params, + ts_type, + }; + call_signatures.push(call_sig_def); + } + // TODO: + TsConstructSignatureDecl(_) => {} + TsIndexSignature(_) => {} + } + } + + let interface_def = InterfaceDef { + methods, + properties, + call_signatures, + }; + + (interface_name, interface_def) +} diff --git a/cli/doc/mod.rs b/cli/doc/mod.rs new file mode 100644 index 000000000..4926dccd7 --- /dev/null +++ b/cli/doc/mod.rs @@ -0,0 +1,63 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +pub mod class; +pub mod r#enum; +pub mod function; +pub mod interface; +pub mod module; +pub mod namespace; +mod node; +pub mod parser; +pub mod printer; +pub mod ts_type; +pub mod type_alias; +pub mod variable; + +pub use node::DocNode; +pub use node::DocNodeKind; +pub use node::Location; +pub use node::ParamDef; +pub use parser::DocParser; + +#[cfg(test)] +mod tests; + +pub fn find_node_by_name_recursively( + doc_nodes: Vec<DocNode>, + name: String, +) -> Option<DocNode> { + let mut parts = name.splitn(2, '.'); + let name = parts.next(); + let leftover = parts.next(); + name?; + let node = find_node_by_name(doc_nodes, name.unwrap().to_string()); + match node { + Some(node) => match node.kind { + DocNodeKind::Namespace => { + if let Some(leftover) = leftover { + find_node_by_name_recursively( + node.namespace_def.unwrap().elements, + leftover.to_string(), + ) + } else { + Some(node) + } + } + _ => { + if leftover.is_none() { + Some(node) + } else { + None + } + } + }, + _ => None, + } +} + +fn find_node_by_name(doc_nodes: Vec<DocNode>, name: String) -> Option<DocNode> { + let node = doc_nodes.iter().find(|node| node.name == name); + match node { + Some(node) => Some(node.clone()), + None => None, + } +} diff --git a/cli/doc/module.rs b/cli/doc/module.rs new file mode 100644 index 000000000..e6c97771a --- /dev/null +++ b/cli/doc/module.rs @@ -0,0 +1,189 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +use swc_common; +use swc_common::Spanned; +use swc_ecma_ast; + +use super::parser::DocParser; +use super::DocNode; +use super::DocNodeKind; + +pub fn get_doc_node_for_export_decl( + doc_parser: &DocParser, + export_decl: &swc_ecma_ast::ExportDecl, +) -> DocNode { + let export_span = export_decl.span(); + use swc_ecma_ast::Decl; + + let js_doc = doc_parser.js_doc_for_span(export_span); + let location = doc_parser + .source_map + .lookup_char_pos(export_span.lo()) + .into(); + + match &export_decl.decl { + Decl::Class(class_decl) => { + let (name, class_def) = + super::class::get_doc_for_class_decl(doc_parser, class_decl); + DocNode { + kind: DocNodeKind::Class, + name, + location, + js_doc, + class_def: Some(class_def), + function_def: None, + variable_def: None, + enum_def: None, + type_alias_def: None, + namespace_def: None, + interface_def: None, + } + } + Decl::Fn(fn_decl) => { + let (name, function_def) = + super::function::get_doc_for_fn_decl(doc_parser, fn_decl); + DocNode { + kind: DocNodeKind::Function, + name, + location, + js_doc, + function_def: Some(function_def), + class_def: None, + variable_def: None, + enum_def: None, + type_alias_def: None, + namespace_def: None, + interface_def: None, + } + } + Decl::Var(var_decl) => { + let (name, var_def) = + super::variable::get_doc_for_var_decl(doc_parser, var_decl); + DocNode { + kind: DocNodeKind::Variable, + name, + location, + js_doc, + variable_def: Some(var_def), + function_def: None, + class_def: None, + enum_def: None, + type_alias_def: None, + namespace_def: None, + interface_def: None, + } + } + Decl::TsInterface(ts_interface_decl) => { + let (name, interface_def) = + super::interface::get_doc_for_ts_interface_decl( + doc_parser, + ts_interface_decl, + ); + DocNode { + kind: DocNodeKind::Interface, + name, + location, + js_doc, + interface_def: Some(interface_def), + variable_def: None, + function_def: None, + class_def: None, + enum_def: None, + type_alias_def: None, + namespace_def: None, + } + } + Decl::TsTypeAlias(ts_type_alias) => { + let (name, type_alias_def) = + super::type_alias::get_doc_for_ts_type_alias_decl( + doc_parser, + ts_type_alias, + ); + DocNode { + kind: DocNodeKind::TypeAlias, + name, + location, + js_doc, + type_alias_def: Some(type_alias_def), + interface_def: None, + variable_def: None, + function_def: None, + class_def: None, + enum_def: None, + namespace_def: None, + } + } + Decl::TsEnum(ts_enum) => { + let (name, enum_def) = + super::r#enum::get_doc_for_ts_enum_decl(doc_parser, ts_enum); + DocNode { + kind: DocNodeKind::Enum, + name, + location, + js_doc, + enum_def: Some(enum_def), + type_alias_def: None, + interface_def: None, + variable_def: None, + function_def: None, + class_def: None, + namespace_def: None, + } + } + Decl::TsModule(ts_module) => { + let (name, namespace_def) = + super::namespace::get_doc_for_ts_module(doc_parser, ts_module); + DocNode { + kind: DocNodeKind::Namespace, + name, + location, + js_doc, + namespace_def: Some(namespace_def), + enum_def: None, + type_alias_def: None, + interface_def: None, + variable_def: None, + function_def: None, + class_def: None, + } + } + } +} + +#[allow(unused)] +pub fn get_doc_nodes_for_named_export( + doc_parser: &DocParser, + named_export: &swc_ecma_ast::NamedExport, +) -> Vec<DocNode> { + let file_name = named_export.src.as_ref().expect("").value.to_string(); + // TODO: resolve specifier + let source_code = + std::fs::read_to_string(&file_name).expect("Failed to read file"); + let doc_nodes = doc_parser + .parse(file_name, source_code) + .expect("Failed to print docs"); + let reexports: Vec<String> = named_export + .specifiers + .iter() + .map(|export_specifier| { + use swc_ecma_ast::ExportSpecifier::*; + + match export_specifier { + Named(named_export_specifier) => { + Some(named_export_specifier.orig.sym.to_string()) + } + // TODO: + Namespace(_) => None, + Default(_) => None, + } + }) + .filter(|s| s.is_some()) + .map(|s| s.unwrap()) + .collect(); + + let reexports_docs: Vec<DocNode> = doc_nodes + .into_iter() + .filter(|doc_node| reexports.contains(&doc_node.name)) + .collect(); + + reexports_docs +} diff --git a/cli/doc/namespace.rs b/cli/doc/namespace.rs new file mode 100644 index 000000000..ed6aac2f3 --- /dev/null +++ b/cli/doc/namespace.rs @@ -0,0 +1,81 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +use serde::Serialize; +use swc_ecma_ast; + +use super::parser::DocParser; +use super::DocNode; +use super::DocNodeKind; + +#[derive(Debug, Serialize, Clone)] +pub struct NamespaceDef { + pub elements: Vec<DocNode>, +} + +pub fn get_doc_for_ts_namespace_decl( + doc_parser: &DocParser, + ts_namespace_decl: &swc_ecma_ast::TsNamespaceDecl, +) -> DocNode { + let js_doc = doc_parser.js_doc_for_span(ts_namespace_decl.span); + let location = doc_parser + .source_map + .lookup_char_pos(ts_namespace_decl.span.lo()) + .into(); + let namespace_name = ts_namespace_decl.id.sym.to_string(); + + use swc_ecma_ast::TsNamespaceBody::*; + + let elements = match &*ts_namespace_decl.body { + TsModuleBlock(ts_module_block) => { + doc_parser.get_doc_nodes_for_module_body(ts_module_block.body.clone()) + } + TsNamespaceDecl(ts_namespace_decl) => { + vec![get_doc_for_ts_namespace_decl(doc_parser, ts_namespace_decl)] + } + }; + + let ns_def = NamespaceDef { elements }; + + DocNode { + kind: DocNodeKind::Namespace, + name: namespace_name, + location, + js_doc, + namespace_def: Some(ns_def), + function_def: None, + variable_def: None, + enum_def: None, + class_def: None, + type_alias_def: None, + interface_def: None, + } +} + +pub fn get_doc_for_ts_module( + doc_parser: &DocParser, + ts_module_decl: &swc_ecma_ast::TsModuleDecl, +) -> (String, NamespaceDef) { + use swc_ecma_ast::TsModuleName; + let namespace_name = match &ts_module_decl.id { + TsModuleName::Ident(ident) => ident.sym.to_string(), + TsModuleName::Str(str_) => str_.value.to_string(), + }; + + let elements = if let Some(body) = &ts_module_decl.body { + use swc_ecma_ast::TsNamespaceBody::*; + + match &body { + TsModuleBlock(ts_module_block) => { + doc_parser.get_doc_nodes_for_module_body(ts_module_block.body.clone()) + } + TsNamespaceDecl(ts_namespace_decl) => { + vec![get_doc_for_ts_namespace_decl(doc_parser, ts_namespace_decl)] + } + } + } else { + vec![] + }; + + let ns_def = NamespaceDef { elements }; + + (namespace_name, ns_def) +} diff --git a/cli/doc/node.rs b/cli/doc/node.rs new file mode 100644 index 000000000..da4b81c11 --- /dev/null +++ b/cli/doc/node.rs @@ -0,0 +1,77 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +use serde::Serialize; +use swc_common; + +#[derive(Debug, PartialEq, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub enum DocNodeKind { + Function, + Variable, + Class, + Enum, + Interface, + TypeAlias, + Namespace, +} + +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ParamDef { + pub name: String, + pub ts_type: Option<super::ts_type::TsTypeDef>, +} + +#[derive(Debug, Serialize, Clone)] +pub struct Location { + pub filename: String, + pub line: usize, + pub col: usize, +} + +impl Into<Location> for swc_common::Loc { + fn into(self) -> Location { + use swc_common::FileName::*; + + let filename = match &self.file.name { + Real(path_buf) => path_buf.to_string_lossy().to_string(), + Custom(str_) => str_.to_string(), + _ => panic!("invalid filename"), + }; + + Location { + filename, + line: self.line, + col: self.col_display, + } + } +} + +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct DocNode { + pub kind: DocNodeKind, + pub name: String, + pub location: Location, + pub js_doc: Option<String>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub function_def: Option<super::function::FunctionDef>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub variable_def: Option<super::variable::VariableDef>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub enum_def: Option<super::r#enum::EnumDef>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub class_def: Option<super::class::ClassDef>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub type_alias_def: Option<super::type_alias::TypeAliasDef>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub namespace_def: Option<super::namespace::NamespaceDef>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub interface_def: Option<super::interface::InterfaceDef>, +} diff --git a/cli/doc/parser.rs b/cli/doc/parser.rs new file mode 100644 index 000000000..bd3a64806 --- /dev/null +++ b/cli/doc/parser.rs @@ -0,0 +1,189 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +use regex::Regex; +use std::sync::Arc; +use std::sync::RwLock; +use swc_common; +use swc_common::comments::CommentKind; +use swc_common::comments::Comments; +use swc_common::errors::Diagnostic; +use swc_common::errors::DiagnosticBuilder; +use swc_common::errors::Emitter; +use swc_common::errors::Handler; +use swc_common::errors::HandlerFlags; +use swc_common::FileName; +use swc_common::Globals; +use swc_common::SourceMap; +use swc_common::Span; +use swc_ecma_parser::lexer::Lexer; +use swc_ecma_parser::JscTarget; +use swc_ecma_parser::Parser; +use swc_ecma_parser::Session; +use swc_ecma_parser::SourceFileInput; +use swc_ecma_parser::Syntax; +use swc_ecma_parser::TsConfig; + +use super::DocNode; + +pub type SwcDiagnostics = Vec<Diagnostic>; + +#[derive(Clone, Default)] +pub struct BufferedError(Arc<RwLock<SwcDiagnostics>>); + +impl Emitter for BufferedError { + fn emit(&mut self, db: &DiagnosticBuilder) { + self.0.write().unwrap().push((**db).clone()); + } +} + +impl From<BufferedError> for Vec<Diagnostic> { + fn from(buf: BufferedError) -> Self { + let s = buf.0.read().unwrap(); + s.clone() + } +} + +pub struct DocParser { + pub buffered_error: BufferedError, + pub source_map: Arc<SourceMap>, + pub handler: Handler, + pub comments: Comments, + pub globals: Globals, +} + +impl DocParser { + pub fn default() -> Self { + let buffered_error = BufferedError::default(); + + let handler = Handler::with_emitter_and_flags( + Box::new(buffered_error.clone()), + HandlerFlags { + dont_buffer_diagnostics: true, + can_emit_warnings: true, + ..Default::default() + }, + ); + + DocParser { + buffered_error, + source_map: Arc::new(SourceMap::default()), + handler, + comments: Comments::default(), + globals: Globals::new(), + } + } + + pub fn parse( + &self, + file_name: String, + source_code: String, + ) -> Result<Vec<DocNode>, SwcDiagnostics> { + swc_common::GLOBALS.set(&self.globals, || { + let swc_source_file = self + .source_map + .new_source_file(FileName::Custom(file_name), source_code); + + let buffered_err = self.buffered_error.clone(); + let session = Session { + handler: &self.handler, + }; + + let mut ts_config = TsConfig::default(); + ts_config.dynamic_import = true; + let syntax = Syntax::Typescript(ts_config); + + let lexer = Lexer::new( + session, + syntax, + JscTarget::Es2019, + SourceFileInput::from(&*swc_source_file), + Some(&self.comments), + ); + + let mut parser = Parser::new_from(session, lexer); + + let module = + parser + .parse_module() + .map_err(move |mut err: DiagnosticBuilder| { + err.cancel(); + SwcDiagnostics::from(buffered_err) + })?; + + let doc_entries = self.get_doc_nodes_for_module_body(module.body); + Ok(doc_entries) + }) + } + + pub fn get_doc_nodes_for_module_decl( + &self, + module_decl: &swc_ecma_ast::ModuleDecl, + ) -> Vec<DocNode> { + use swc_ecma_ast::ModuleDecl; + + match module_decl { + ModuleDecl::ExportDecl(export_decl) => { + vec![super::module::get_doc_node_for_export_decl( + self, + export_decl, + )] + } + ModuleDecl::ExportNamed(_named_export) => { + vec![] + // TODO(bartlomieju): + // super::module::get_doc_nodes_for_named_export(self, named_export) + } + ModuleDecl::ExportDefaultDecl(_) => vec![], + ModuleDecl::ExportDefaultExpr(_) => vec![], + ModuleDecl::ExportAll(_) => vec![], + ModuleDecl::TsExportAssignment(_) => vec![], + ModuleDecl::TsNamespaceExport(_) => vec![], + _ => vec![], + } + } + + pub fn get_doc_nodes_for_module_body( + &self, + module_body: Vec<swc_ecma_ast::ModuleItem>, + ) -> Vec<DocNode> { + let mut doc_entries: Vec<DocNode> = vec![]; + for node in module_body.iter() { + if let swc_ecma_ast::ModuleItem::ModuleDecl(module_decl) = node { + doc_entries.extend(self.get_doc_nodes_for_module_decl(module_decl)); + } + } + doc_entries + } + + pub fn js_doc_for_span(&self, span: Span) -> Option<String> { + let comments = self.comments.take_leading_comments(span.lo())?; + let js_doc_comment = comments.iter().find(|comment| { + comment.kind == CommentKind::Block && comment.text.starts_with('*') + })?; + + let mut margin_pat = String::from(""); + if let Some(margin) = self.source_map.span_to_margin(span) { + for _ in 0..margin { + margin_pat.push(' '); + } + } + + let js_doc_re = Regex::new(r#" ?\* ?"#).unwrap(); + let txt = js_doc_comment + .text + .split('\n') + .map(|line| js_doc_re.replace(line, "").to_string()) + .map(|line| { + if line.starts_with(&margin_pat) { + line[margin_pat.len()..].to_string() + } else { + line + } + }) + .collect::<Vec<String>>() + .join("\n"); + + let txt = txt.trim_start().trim_end().to_string(); + + Some(txt) + } +} diff --git a/cli/doc/printer.rs b/cli/doc/printer.rs new file mode 100644 index 000000000..a58f2fcdc --- /dev/null +++ b/cli/doc/printer.rs @@ -0,0 +1,432 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +// TODO(ry) This module builds up output by appending to a string. Instead it +// should either use a formatting trait +// https://doc.rust-lang.org/std/fmt/index.html#formatting-traits +// Or perhaps implement a Serializer for serde +// https://docs.serde.rs/serde/ser/trait.Serializer.html + +// TODO(ry) The methods in this module take ownership of the DocNodes, this is +// unnecessary and can result in unnecessary copying. Instead they should take +// references. + +use crate::doc; +use crate::doc::ts_type::TsTypeDefKind; +use crate::doc::DocNodeKind; + +pub fn format(doc_nodes: Vec<doc::DocNode>) -> String { + format_(doc_nodes, 0) +} + +pub fn format_details(node: doc::DocNode) -> String { + let mut details = String::new(); + + details.push_str(&format!( + "Defined in {}:{}:{}.\n", + node.location.filename, node.location.line, node.location.col + )); + + details.push_str(&format_signature(&node, 0)); + + let js_doc = node.js_doc.clone(); + if let Some(js_doc) = js_doc { + details.push_str(&format_jsdoc(js_doc, false, 1)); + } + details.push_str("\n"); + + let maybe_extra = match node.kind { + DocNodeKind::Class => Some(format_class_details(node)), + DocNodeKind::Namespace => Some(format_namespace_details(node)), + _ => None, + }; + + if let Some(extra) = maybe_extra { + details.push_str(&extra); + } + + details +} + +fn kind_order(kind: &doc::DocNodeKind) -> i64 { + match kind { + DocNodeKind::Function => 0, + DocNodeKind::Variable => 1, + DocNodeKind::Class => 2, + DocNodeKind::Enum => 3, + DocNodeKind::Interface => 4, + DocNodeKind::TypeAlias => 5, + DocNodeKind::Namespace => 6, + } +} + +fn format_signature(node: &doc::DocNode, indent: i64) -> String { + match node.kind { + DocNodeKind::Function => format_function_signature(&node, indent), + DocNodeKind::Variable => format_variable_signature(&node, indent), + DocNodeKind::Class => format_class_signature(&node, indent), + DocNodeKind::Enum => format_enum_signature(&node, indent), + DocNodeKind::Interface => format_interface_signature(&node, indent), + DocNodeKind::TypeAlias => format_type_alias_signature(&node, indent), + DocNodeKind::Namespace => format_namespace_signature(&node, indent), + } +} + +fn format_(doc_nodes: Vec<doc::DocNode>, indent: i64) -> String { + let mut sorted = doc_nodes; + sorted.sort_unstable_by(|a, b| { + let kind_cmp = kind_order(&a.kind).cmp(&kind_order(&b.kind)); + if kind_cmp == core::cmp::Ordering::Equal { + a.name.cmp(&b.name) + } else { + kind_cmp + } + }); + + let mut output = String::new(); + + for node in sorted { + output.push_str(&format_signature(&node, indent)); + if node.js_doc.is_some() { + output.push_str(&format_jsdoc( + node.js_doc.as_ref().unwrap().to_string(), + true, + indent, + )); + } + output.push_str("\n"); + if DocNodeKind::Namespace == node.kind { + output.push_str(&format_( + node.namespace_def.as_ref().unwrap().elements.clone(), + indent + 1, + )); + output.push_str("\n"); + }; + } + + output +} + +fn render_params(params: Vec<doc::ParamDef>) -> String { + let mut rendered = String::from(""); + if !params.is_empty() { + for param in params { + rendered += param.name.as_str(); + if param.ts_type.is_some() { + rendered += ": "; + rendered += render_ts_type(param.ts_type.unwrap()).as_str(); + } + rendered += ", "; + } + rendered.truncate(rendered.len() - 2); + } + rendered +} + +fn render_ts_type(ts_type: doc::ts_type::TsTypeDef) -> String { + let kind = ts_type.kind.unwrap(); + match kind { + TsTypeDefKind::Array => { + format!("{}[]", render_ts_type(*ts_type.array.unwrap())) + } + TsTypeDefKind::Conditional => { + let conditional = ts_type.conditional_type.unwrap(); + format!( + "{} extends {} ? {} : {}", + render_ts_type(*conditional.check_type), + render_ts_type(*conditional.extends_type), + render_ts_type(*conditional.true_type), + render_ts_type(*conditional.false_type) + ) + } + TsTypeDefKind::FnOrConstructor => { + let fn_or_constructor = ts_type.fn_or_constructor.unwrap(); + format!( + "{}({}) => {}", + if fn_or_constructor.constructor { + "new " + } else { + "" + }, + render_params(fn_or_constructor.params), + render_ts_type(fn_or_constructor.ts_type), + ) + } + TsTypeDefKind::IndexedAccess => { + let indexed_access = ts_type.indexed_access.unwrap(); + format!( + "{}[{}]", + render_ts_type(*indexed_access.obj_type), + render_ts_type(*indexed_access.index_type) + ) + } + TsTypeDefKind::Intersection => { + let intersection = ts_type.intersection.unwrap(); + let mut output = "".to_string(); + if !intersection.is_empty() { + for ts_type in intersection { + output += render_ts_type(ts_type).as_str(); + output += " & " + } + output.truncate(output.len() - 3); + } + output + } + TsTypeDefKind::Keyword => ts_type.keyword.unwrap(), + TsTypeDefKind::Literal => { + let literal = ts_type.literal.unwrap(); + match literal.kind { + doc::ts_type::LiteralDefKind::Boolean => { + format!("{}", literal.boolean.unwrap()) + } + doc::ts_type::LiteralDefKind::String => { + "\"".to_string() + literal.string.unwrap().as_str() + "\"" + } + doc::ts_type::LiteralDefKind::Number => { + format!("{}", literal.number.unwrap()) + } + } + } + TsTypeDefKind::Optional => "_optional_".to_string(), + TsTypeDefKind::Parenthesized => { + format!("({})", render_ts_type(*ts_type.parenthesized.unwrap())) + } + TsTypeDefKind::Rest => { + format!("...{}", render_ts_type(*ts_type.rest.unwrap())) + } + TsTypeDefKind::This => "this".to_string(), + TsTypeDefKind::Tuple => { + let tuple = ts_type.tuple.unwrap(); + let mut output = "".to_string(); + if !tuple.is_empty() { + for ts_type in tuple { + output += render_ts_type(ts_type).as_str(); + output += ", " + } + output.truncate(output.len() - 2); + } + output + } + TsTypeDefKind::TypeLiteral => { + let mut output = "".to_string(); + let type_literal = ts_type.type_literal.unwrap(); + for node in type_literal.call_signatures { + output += format!( + "({}): {}, ", + render_params(node.params), + render_ts_type(node.ts_type.unwrap()) + ) + .as_str() + } + for node in type_literal.methods { + output += format!( + "{}({}): {}, ", + node.name, + render_params(node.params), + render_ts_type(node.return_type.unwrap()) + ) + .as_str() + } + for node in type_literal.properties { + output += + format!("{}: {}, ", node.name, render_ts_type(node.ts_type.unwrap())) + .as_str() + } + if !output.is_empty() { + output.truncate(output.len() - 2); + } + "{ ".to_string() + output.as_str() + " }" + } + TsTypeDefKind::TypeOperator => { + let operator = ts_type.type_operator.unwrap(); + format!("{} {}", operator.operator, render_ts_type(operator.ts_type)) + } + TsTypeDefKind::TypeQuery => { + format!("typeof {}", ts_type.type_query.unwrap()) + } + TsTypeDefKind::TypeRef => { + let type_ref = ts_type.type_ref.unwrap(); + let mut final_output = type_ref.type_name; + if type_ref.type_params.is_some() { + let mut output = "".to_string(); + let type_params = type_ref.type_params.unwrap(); + if !type_params.is_empty() { + for ts_type in type_params { + output += render_ts_type(ts_type).as_str(); + output += ", " + } + output.truncate(output.len() - 2); + } + final_output += format!("<{}>", output).as_str(); + } + final_output + } + TsTypeDefKind::Union => { + let union = ts_type.union.unwrap(); + let mut output = "".to_string(); + if !union.is_empty() { + for ts_type in union { + output += render_ts_type(ts_type).as_str(); + output += " | " + } + output.truncate(output.len() - 3); + } + output + } + } +} + +fn format_indent(indent: i64) -> String { + let mut indent_str = String::new(); + for _ in 0..indent { + indent_str.push_str(" "); + } + indent_str +} + +// TODO: this should use some sort of markdown to console parser. +fn format_jsdoc(jsdoc: String, truncated: bool, indent: i64) -> String { + let mut lines = jsdoc.split("\n\n").map(|line| line.replace("\n", " ")); + + let mut js_doc = String::new(); + + if truncated { + let first_line = lines.next().unwrap_or_else(|| "".to_string()); + js_doc.push_str(&format_indent(indent + 1)); + js_doc.push_str(&format!("{}\n", first_line)); + } else { + for line in lines { + js_doc.push_str(&format_indent(indent + 1)); + js_doc.push_str(&format!("{}\n", line)); + } + } + js_doc +} + +fn format_class_details(node: doc::DocNode) -> String { + let mut details = String::new(); + + let class_def = node.class_def.unwrap(); + for node in class_def.constructors { + details.push_str(&format!( + "constructor {}({})\n", + node.name, + render_params(node.params), + )); + } + for node in class_def.properties.iter().filter(|node| { + node + .accessibility + .unwrap_or(swc_ecma_ast::Accessibility::Public) + != swc_ecma_ast::Accessibility::Private + }) { + details.push_str(&format!( + "{} {}: {}\n", + match node + .accessibility + .unwrap_or(swc_ecma_ast::Accessibility::Public) + { + swc_ecma_ast::Accessibility::Protected => "protected".to_string(), + swc_ecma_ast::Accessibility::Public => "public".to_string(), + _ => "".to_string(), + }, + node.name, + render_ts_type(node.ts_type.clone().unwrap()) + )); + } + for node in class_def.methods.iter().filter(|node| { + node + .accessibility + .unwrap_or(swc_ecma_ast::Accessibility::Public) + != swc_ecma_ast::Accessibility::Private + }) { + let function_def = node.function_def.clone(); + details.push_str(&format!( + "{} {}{}({}): {}\n", + match node + .accessibility + .unwrap_or(swc_ecma_ast::Accessibility::Public) + { + swc_ecma_ast::Accessibility::Protected => "protected".to_string(), + swc_ecma_ast::Accessibility::Public => "public".to_string(), + _ => "".to_string(), + }, + match node.kind { + swc_ecma_ast::MethodKind::Getter => "get ".to_string(), + swc_ecma_ast::MethodKind::Setter => "set ".to_string(), + _ => "".to_string(), + }, + node.name, + render_params(function_def.params), + render_ts_type(function_def.return_type.unwrap()) + )); + } + details.push_str("\n"); + details +} + +fn format_namespace_details(node: doc::DocNode) -> String { + let mut ns = String::new(); + + let elements = node.namespace_def.unwrap().elements; + for node in elements { + ns.push_str(&format_signature(&node, 0)); + } + ns.push_str("\n"); + ns +} + +fn format_function_signature(node: &doc::DocNode, indent: i64) -> String { + format_indent(indent); + let function_def = node.function_def.clone().unwrap(); + let return_type = function_def.return_type.unwrap(); + format!( + "function {}({}): {}\n", + node.name, + render_params(function_def.params), + render_ts_type(return_type).as_str() + ) +} + +fn format_class_signature(node: &doc::DocNode, indent: i64) -> String { + format_indent(indent); + format!("class {}\n", node.name) +} + +fn format_variable_signature(node: &doc::DocNode, indent: i64) -> String { + format_indent(indent); + let variable_def = node.variable_def.clone().unwrap(); + format!( + "{} {}{}\n", + match variable_def.kind { + swc_ecma_ast::VarDeclKind::Const => "const".to_string(), + swc_ecma_ast::VarDeclKind::Let => "let".to_string(), + swc_ecma_ast::VarDeclKind::Var => "var".to_string(), + }, + node.name, + if variable_def.ts_type.is_some() { + format!(": {}", render_ts_type(variable_def.ts_type.unwrap())) + } else { + "".to_string() + } + ) +} + +fn format_enum_signature(node: &doc::DocNode, indent: i64) -> String { + format_indent(indent); + format!("enum {}\n", node.name) +} + +fn format_interface_signature(node: &doc::DocNode, indent: i64) -> String { + format_indent(indent); + format!("interface {}\n", node.name) +} + +fn format_type_alias_signature(node: &doc::DocNode, indent: i64) -> String { + format_indent(indent); + format!("type {}\n", node.name) +} + +fn format_namespace_signature(node: &doc::DocNode, indent: i64) -> String { + format_indent(indent); + format!("namespace {}\n", node.name) +} diff --git a/cli/doc/tests.rs b/cli/doc/tests.rs new file mode 100644 index 000000000..c9408e8cf --- /dev/null +++ b/cli/doc/tests.rs @@ -0,0 +1,568 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +use super::DocParser; +use serde_json; +use serde_json::json; + +#[test] +fn export_fn() { + let source_code = r#"/** +* Hello there, this is a multiline JSdoc. +* +* It has many lines +* +* Or not that many? +*/ +export function foo(a: string, b: number): void { + console.log("Hello world"); +} +"#; + let entries = DocParser::default() + .parse("test.ts".to_string(), source_code.to_string()) + .unwrap(); + assert_eq!(entries.len(), 1); + let entry = &entries[0]; + let expected_json = json!({ + "functionDef": { + "isAsync": false, + "isGenerator": false, + "params": [ + { + "name": "a", + "tsType": { + "keyword": "string", + "kind": "keyword", + "repr": "string", + }, + }, + { + "name": "b", + "tsType": { + "keyword": "number", + "kind": "keyword", + "repr": "number", + }, + }, + ], + "returnType": { + "keyword": "void", + "kind": "keyword", + "repr": "void", + }, + }, + "jsDoc": "Hello there, this is a multiline JSdoc.\n\nIt has many lines\n\nOr not that many?", + "kind": "function", + "location": { + "col": 0, + "filename": "test.ts", + "line": 8, + }, + "name": "foo", + }); + let actual = serde_json::to_value(entry).unwrap(); + assert_eq!(actual, expected_json); + + assert!(super::printer::format(entries).contains("Hello there")); +} + +#[test] +fn export_const() { + let source_code = + "/** Something about fizzBuzz */\nexport const fizzBuzz = \"fizzBuzz\";\n"; + let entries = DocParser::default() + .parse("test.ts".to_string(), source_code.to_string()) + .unwrap(); + assert_eq!(entries.len(), 1); + let entry = &entries[0]; + let expected_json = json!({ + "kind": "variable", + "name": "fizzBuzz", + "location": { + "filename": "test.ts", + "line": 2, + "col": 0 + }, + "jsDoc": "Something about fizzBuzz", + "variableDef": { + "tsType": null, + "kind": "const" + } + }); + let actual = serde_json::to_value(entry).unwrap(); + assert_eq!(actual, expected_json); + + assert!(super::printer::format(entries).contains("Something about fizzBuzz")); +} + +#[test] +fn export_class() { + let source_code = r#" +/** Class doc */ +export class Foobar extends Fizz implements Buzz { + private private1: boolean; + protected protected1: number; + public public1: boolean; + public2: number; + + /** Constructor js doc */ + constructor(name: string, private private2: number, protected protected2: number) {} + + /** Async foo method */ + async foo(): Promise<void> { + // + } + + /** Sync bar method */ + bar(): void { + // + } +} +"#; + let entries = DocParser::default() + .parse("test.ts".to_string(), source_code.to_string()) + .unwrap(); + assert_eq!(entries.len(), 1); + let expected_json = json!({ + "kind": "class", + "name": "Foobar", + "location": { + "filename": "test.ts", + "line": 3, + "col": 0 + }, + "jsDoc": "Class doc", + "classDef": { + "isAbstract": false, + "constructors": [ + { + "jsDoc": "Constructor js doc", + "accessibility": null, + "name": "constructor", + "params": [ + { + "name": "name", + "tsType": { + "repr": "string", + "kind": "keyword", + "keyword": "string" + } + }, + { + "name": "<TODO>", + "tsType": null + }, + { + "name": "<TODO>", + "tsType": null + } + ], + "location": { + "filename": "test.ts", + "line": 10, + "col": 4 + } + } + ], + "properties": [ + { + "jsDoc": null, + "tsType": { + "repr": "boolean", + "kind": "keyword", + "keyword": "boolean" + }, + "readonly": false, + "accessibility": "private", + "isAbstract": false, + "isStatic": false, + "name": "private1", + "location": { + "filename": "test.ts", + "line": 4, + "col": 4 + } + }, + { + "jsDoc": null, + "tsType": { + "repr": "number", + "kind": "keyword", + "keyword": "number" + }, + "readonly": false, + "accessibility": "protected", + "isAbstract": false, + "isStatic": false, + "name": "protected1", + "location": { + "filename": "test.ts", + "line": 5, + "col": 4 + } + }, + { + "jsDoc": null, + "tsType": { + "repr": "boolean", + "kind": "keyword", + "keyword": "boolean" + }, + "readonly": false, + "accessibility": "public", + "isAbstract": false, + "isStatic": false, + "name": "public1", + "location": { + "filename": "test.ts", + "line": 6, + "col": 4 + } + }, + { + "jsDoc": null, + "tsType": { + "repr": "number", + "kind": "keyword", + "keyword": "number" + }, + "readonly": false, + "accessibility": null, + "isAbstract": false, + "isStatic": false, + "name": "public2", + "location": { + "filename": "test.ts", + "line": 7, + "col": 4 + } + } + ], + "methods": [ + { + "jsDoc": "Async foo method", + "accessibility": null, + "isAbstract": false, + "isStatic": false, + "name": "foo", + "kind": "method", + "functionDef": { + "params": [], + "returnType": { + "repr": "Promise", + "kind": "typeRef", + "typeRef": { + "typeParams": [ + { + "repr": "void", + "kind": "keyword", + "keyword": "void" + } + ], + "typeName": "Promise" + } + }, + "isAsync": true, + "isGenerator": false + }, + "location": { + "filename": "test.ts", + "line": 13, + "col": 4 + } + }, + { + "jsDoc": "Sync bar method", + "accessibility": null, + "isAbstract": false, + "isStatic": false, + "name": "bar", + "kind": "method", + "functionDef": { + "params": [], + "returnType": { + "repr": "void", + "kind": "keyword", + "keyword": "void" + }, + "isAsync": false, + "isGenerator": false + }, + "location": { + "filename": "test.ts", + "line": 18, + "col": 4 + } + } + ] + } + }); + let entry = &entries[0]; + let actual = serde_json::to_value(entry).unwrap(); + assert_eq!(actual, expected_json); + + assert!(super::printer::format(entries).contains("class Foobar")); +} + +#[test] +fn export_interface() { + let source_code = r#" +/** + * Interface js doc + */ +export interface Reader { + /** Read n bytes */ + read(buf: Uint8Array, something: unknown): Promise<number> +} + "#; + let entries = DocParser::default() + .parse("test.ts".to_string(), source_code.to_string()) + .unwrap(); + assert_eq!(entries.len(), 1); + let entry = &entries[0]; + let expected_json = json!({ + "kind": "interface", + "name": "Reader", + "location": { + "filename": "test.ts", + "line": 5, + "col": 0 + }, + "jsDoc": "Interface js doc", + "interfaceDef": { + "methods": [ + { + "name": "read", + "location": { + "filename": "test.ts", + "line": 7, + "col": 4 + }, + "jsDoc": "Read n bytes", + "params": [ + { + "name": "buf", + "tsType": { + "repr": "Uint8Array", + "kind": "typeRef", + "typeRef": { + "typeParams": null, + "typeName": "Uint8Array" + } + } + }, + { + "name": "something", + "tsType": { + "repr": "unknown", + "kind": "keyword", + "keyword": "unknown" + } + } + ], + "returnType": { + "repr": "Promise", + "kind": "typeRef", + "typeRef": { + "typeParams": [ + { + "repr": "number", + "kind": "keyword", + "keyword": "number" + } + ], + "typeName": "Promise" + } + } + } + ], + "properties": [], + "callSignatures": [] + } + }); + let actual = serde_json::to_value(entry).unwrap(); + assert_eq!(actual, expected_json); + + assert!(super::printer::format(entries).contains("interface Reader")); +} + +#[test] +fn export_type_alias() { + let source_code = r#" +/** Array holding numbers */ +export type NumberArray = Array<number>; + "#; + let entries = DocParser::default() + .parse("test.ts".to_string(), source_code.to_string()) + .unwrap(); + assert_eq!(entries.len(), 1); + let entry = &entries[0]; + let expected_json = json!({ + "kind": "typeAlias", + "name": "NumberArray", + "location": { + "filename": "test.ts", + "line": 3, + "col": 0 + }, + "jsDoc": "Array holding numbers", + "typeAliasDef": { + "tsType": { + "repr": "Array", + "kind": "typeRef", + "typeRef": { + "typeParams": [ + { + "repr": "number", + "kind": "keyword", + "keyword": "number" + } + ], + "typeName": "Array" + } + } + } + }); + let actual = serde_json::to_value(entry).unwrap(); + assert_eq!(actual, expected_json); + + assert!(super::printer::format(entries).contains("Array holding numbers")); +} + +#[test] +fn export_enum() { + let source_code = r#" +/** + * Some enum for good measure + */ +export enum Hello { + World = "world", + Fizz = "fizz", + Buzz = "buzz", +} + "#; + let entries = DocParser::default() + .parse("test.ts".to_string(), source_code.to_string()) + .unwrap(); + assert_eq!(entries.len(), 1); + let entry = &entries[0]; + let expected_json = json!({ + "kind": "enum", + "name": "Hello", + "location": { + "filename": "test.ts", + "line": 5, + "col": 0 + }, + "jsDoc": "Some enum for good measure", + "enumDef": { + "members": [ + { + "name": "World" + }, + { + "name": "Fizz" + }, + { + "name": "Buzz" + } + ] + } + }); + let actual = serde_json::to_value(entry).unwrap(); + assert_eq!(actual, expected_json); + + assert!(super::printer::format(entries.clone()) + .contains("Some enum for good measure")); + assert!(super::printer::format(entries).contains("enum Hello")); +} + +#[test] +fn export_namespace() { + let source_code = r#" +/** Namespace JSdoc */ +export namespace RootNs { + export const a = "a"; + + /** Nested namespace JSDoc */ + export namespace NestedNs { + export enum Foo { + a = 1, + b = 2, + c = 3, + } + } +} + "#; + let entries = DocParser::default() + .parse("test.ts".to_string(), source_code.to_string()) + .unwrap(); + assert_eq!(entries.len(), 1); + let entry = &entries[0]; + let expected_json = json!({ + "kind": "namespace", + "name": "RootNs", + "location": { + "filename": "test.ts", + "line": 3, + "col": 0 + }, + "jsDoc": "Namespace JSdoc", + "namespaceDef": { + "elements": [ + { + "kind": "variable", + "name": "a", + "location": { + "filename": "test.ts", + "line": 4, + "col": 4 + }, + "jsDoc": null, + "variableDef": { + "tsType": null, + "kind": "const" + } + }, + { + "kind": "namespace", + "name": "NestedNs", + "location": { + "filename": "test.ts", + "line": 7, + "col": 4 + }, + "jsDoc": "Nested namespace JSDoc", + "namespaceDef": { + "elements": [ + { + "kind": "enum", + "name": "Foo", + "location": { + "filename": "test.ts", + "line": 8, + "col": 6 + }, + "jsDoc": null, + "enumDef": { + "members": [ + { + "name": "a" + }, + { + "name": "b" + }, + { + "name": "c" + } + ] + } + } + ] + } + } + ] + } + }); + let actual = serde_json::to_value(entry).unwrap(); + assert_eq!(actual, expected_json); + assert!(super::printer::format(entries).contains("namespace RootNs")); +} diff --git a/cli/doc/ts_type.rs b/cli/doc/ts_type.rs new file mode 100644 index 000000000..9a7d191ba --- /dev/null +++ b/cli/doc/ts_type.rs @@ -0,0 +1,821 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +use super::ParamDef; +use serde::Serialize; +use swc_common::SourceMap; +use swc_ecma_ast; +use swc_ecma_ast::TsArrayType; +use swc_ecma_ast::TsConditionalType; +use swc_ecma_ast::TsFnOrConstructorType; +use swc_ecma_ast::TsIndexedAccessType; +use swc_ecma_ast::TsKeywordType; +use swc_ecma_ast::TsLit; +use swc_ecma_ast::TsLitType; +use swc_ecma_ast::TsOptionalType; +use swc_ecma_ast::TsParenthesizedType; +use swc_ecma_ast::TsRestType; +use swc_ecma_ast::TsThisType; +use swc_ecma_ast::TsTupleType; +use swc_ecma_ast::TsType; +use swc_ecma_ast::TsTypeAnn; +use swc_ecma_ast::TsTypeLit; +use swc_ecma_ast::TsTypeOperator; +use swc_ecma_ast::TsTypeQuery; +use swc_ecma_ast::TsTypeRef; +use swc_ecma_ast::TsUnionOrIntersectionType; + +// pub enum TsType { +// * TsKeywordType(TsKeywordType), +// * TsThisType(TsThisType), +// * TsFnOrConstructorType(TsFnOrConstructorType), +// * TsTypeRef(TsTypeRef), +// * TsTypeQuery(TsTypeQuery), +// * TsTypeLit(TsTypeLit), +// * TsArrayType(TsArrayType), +// * TsTupleType(TsTupleType), +// * TsOptionalType(TsOptionalType), +// * TsRestType(TsRestType), +// * TsUnionOrIntersectionType(TsUnionOrIntersectionType), +// * TsConditionalType(TsConditionalType), +// TsInferType(TsInferType), +// * TsParenthesizedType(TsParenthesizedType), +// * TsTypeOperator(TsTypeOperator), +// * TsIndexedAccessType(TsIndexedAccessType), +// TsMappedType(TsMappedType), +// * TsLitType(TsLitType), +// TsTypePredicate(TsTypePredicate), +// TsImportType(TsImportType), +// } + +impl Into<TsTypeDef> for &TsLitType { + fn into(self) -> TsTypeDef { + let (repr, lit) = match &self.lit { + TsLit::Number(num) => ( + format!("{}", num.value), + LiteralDef { + kind: LiteralDefKind::Number, + number: Some(num.value), + string: None, + boolean: None, + }, + ), + TsLit::Str(str_) => ( + str_.value.to_string(), + LiteralDef { + kind: LiteralDefKind::String, + number: None, + string: Some(str_.value.to_string()), + boolean: None, + }, + ), + TsLit::Bool(bool_) => ( + bool_.value.to_string(), + LiteralDef { + kind: LiteralDefKind::Boolean, + number: None, + string: None, + boolean: Some(bool_.value), + }, + ), + }; + + TsTypeDef { + repr, + kind: Some(TsTypeDefKind::Literal), + literal: Some(lit), + ..Default::default() + } + } +} + +impl Into<TsTypeDef> for &TsArrayType { + fn into(self) -> TsTypeDef { + let ts_type_def: TsTypeDef = (&*self.elem_type).into(); + + TsTypeDef { + array: Some(Box::new(ts_type_def)), + kind: Some(TsTypeDefKind::Array), + ..Default::default() + } + } +} + +impl Into<TsTypeDef> for &TsTupleType { + fn into(self) -> TsTypeDef { + let mut type_defs = vec![]; + + for type_box in &self.elem_types { + let ts_type: &TsType = &(*type_box); + let def: TsTypeDef = ts_type.into(); + type_defs.push(def) + } + + TsTypeDef { + tuple: Some(type_defs), + kind: Some(TsTypeDefKind::Tuple), + ..Default::default() + } + } +} + +impl Into<TsTypeDef> for &TsUnionOrIntersectionType { + fn into(self) -> TsTypeDef { + use swc_ecma_ast::TsUnionOrIntersectionType::*; + + match self { + TsUnionType(union_type) => { + let mut types_union = vec![]; + + for type_box in &union_type.types { + let ts_type: &TsType = &(*type_box); + let def: TsTypeDef = ts_type.into(); + types_union.push(def); + } + + TsTypeDef { + union: Some(types_union), + kind: Some(TsTypeDefKind::Union), + ..Default::default() + } + } + TsIntersectionType(intersection_type) => { + let mut types_intersection = vec![]; + + for type_box in &intersection_type.types { + let ts_type: &TsType = &(*type_box); + let def: TsTypeDef = ts_type.into(); + types_intersection.push(def); + } + + TsTypeDef { + intersection: Some(types_intersection), + kind: Some(TsTypeDefKind::Intersection), + ..Default::default() + } + } + } + } +} + +impl Into<TsTypeDef> for &TsKeywordType { + fn into(self) -> TsTypeDef { + use swc_ecma_ast::TsKeywordTypeKind::*; + + let keyword_str = match self.kind { + TsAnyKeyword => "any", + TsUnknownKeyword => "unknown", + TsNumberKeyword => "number", + TsObjectKeyword => "object", + TsBooleanKeyword => "boolean", + TsBigIntKeyword => "bigint", + TsStringKeyword => "string", + TsSymbolKeyword => "symbol", + TsVoidKeyword => "void", + TsUndefinedKeyword => "undefined", + TsNullKeyword => "null", + TsNeverKeyword => "never", + }; + + TsTypeDef { + repr: keyword_str.to_string(), + kind: Some(TsTypeDefKind::Keyword), + keyword: Some(keyword_str.to_string()), + ..Default::default() + } + } +} + +impl Into<TsTypeDef> for &TsTypeOperator { + fn into(self) -> TsTypeDef { + let ts_type = (&*self.type_ann).into(); + let type_operator_def = TsTypeOperatorDef { + operator: self.op.as_str().to_string(), + ts_type, + }; + + TsTypeDef { + type_operator: Some(Box::new(type_operator_def)), + kind: Some(TsTypeDefKind::TypeOperator), + ..Default::default() + } + } +} + +impl Into<TsTypeDef> for &TsParenthesizedType { + fn into(self) -> TsTypeDef { + let ts_type = (&*self.type_ann).into(); + + TsTypeDef { + parenthesized: Some(Box::new(ts_type)), + kind: Some(TsTypeDefKind::Parenthesized), + ..Default::default() + } + } +} + +impl Into<TsTypeDef> for &TsRestType { + fn into(self) -> TsTypeDef { + let ts_type = (&*self.type_ann).into(); + + TsTypeDef { + rest: Some(Box::new(ts_type)), + kind: Some(TsTypeDefKind::Rest), + ..Default::default() + } + } +} + +impl Into<TsTypeDef> for &TsOptionalType { + fn into(self) -> TsTypeDef { + let ts_type = (&*self.type_ann).into(); + + TsTypeDef { + optional: Some(Box::new(ts_type)), + kind: Some(TsTypeDefKind::Optional), + ..Default::default() + } + } +} + +impl Into<TsTypeDef> for &TsThisType { + fn into(self) -> TsTypeDef { + TsTypeDef { + repr: "this".to_string(), + this: Some(true), + kind: Some(TsTypeDefKind::This), + ..Default::default() + } + } +} + +fn ts_entity_name_to_name(entity_name: &swc_ecma_ast::TsEntityName) -> String { + use swc_ecma_ast::TsEntityName::*; + + match entity_name { + Ident(ident) => ident.sym.to_string(), + TsQualifiedName(ts_qualified_name) => { + let left = ts_entity_name_to_name(&ts_qualified_name.left); + let right = ts_qualified_name.right.sym.to_string(); + format!("{}.{}", left, right) + } + } +} + +impl Into<TsTypeDef> for &TsTypeQuery { + fn into(self) -> TsTypeDef { + use swc_ecma_ast::TsTypeQueryExpr::*; + + let type_name = match &self.expr_name { + TsEntityName(entity_name) => ts_entity_name_to_name(&*entity_name), + Import(import_type) => import_type.arg.value.to_string(), + }; + + TsTypeDef { + repr: type_name.to_string(), + type_query: Some(type_name), + kind: Some(TsTypeDefKind::TypeQuery), + ..Default::default() + } + } +} + +impl Into<TsTypeDef> for &TsTypeRef { + fn into(self) -> TsTypeDef { + let type_name = ts_entity_name_to_name(&self.type_name); + + let type_params = if let Some(type_params_inst) = &self.type_params { + let mut ts_type_defs = vec![]; + + for type_box in &type_params_inst.params { + let ts_type: &TsType = &(*type_box); + let def: TsTypeDef = ts_type.into(); + ts_type_defs.push(def); + } + + Some(ts_type_defs) + } else { + None + }; + + TsTypeDef { + repr: type_name.to_string(), + type_ref: Some(TsTypeRefDef { + type_name, + type_params, + }), + kind: Some(TsTypeDefKind::TypeRef), + ..Default::default() + } + } +} + +impl Into<TsTypeDef> for &TsIndexedAccessType { + fn into(self) -> TsTypeDef { + let indexed_access_def = TsIndexedAccessDef { + readonly: self.readonly, + obj_type: Box::new((&*self.obj_type).into()), + index_type: Box::new((&*self.index_type).into()), + }; + + TsTypeDef { + indexed_access: Some(indexed_access_def), + kind: Some(TsTypeDefKind::IndexedAccess), + ..Default::default() + } + } +} + +impl Into<TsTypeDef> for &TsTypeLit { + fn into(self) -> TsTypeDef { + let mut methods = vec![]; + let mut properties = vec![]; + let mut call_signatures = vec![]; + + for type_element in &self.members { + use swc_ecma_ast::TsTypeElement::*; + + match &type_element { + TsMethodSignature(ts_method_sig) => { + let mut params = vec![]; + + for param in &ts_method_sig.params { + use swc_ecma_ast::TsFnParam::*; + + let param_def = match param { + Ident(ident) => { + let ts_type = + ident.type_ann.as_ref().map(|rt| (&*rt.type_ann).into()); + + ParamDef { + name: ident.sym.to_string(), + ts_type, + } + } + _ => ParamDef { + name: "<TODO>".to_string(), + ts_type: None, + }, + }; + + params.push(param_def); + } + + let maybe_return_type = ts_method_sig + .type_ann + .as_ref() + .map(|rt| (&*rt.type_ann).into()); + + let method_def = LiteralMethodDef { + name: "<TODO>".to_string(), + params, + return_type: maybe_return_type, + }; + methods.push(method_def); + } + TsPropertySignature(ts_prop_sig) => { + let name = match &*ts_prop_sig.key { + swc_ecma_ast::Expr::Ident(ident) => ident.sym.to_string(), + _ => "TODO".to_string(), + }; + + let mut params = vec![]; + + for param in &ts_prop_sig.params { + use swc_ecma_ast::TsFnParam::*; + + let param_def = match param { + Ident(ident) => { + let ts_type = + ident.type_ann.as_ref().map(|rt| (&*rt.type_ann).into()); + + ParamDef { + name: ident.sym.to_string(), + ts_type, + } + } + _ => ParamDef { + name: "<TODO>".to_string(), + ts_type: None, + }, + }; + + params.push(param_def); + } + + let ts_type = ts_prop_sig + .type_ann + .as_ref() + .map(|rt| (&*rt.type_ann).into()); + + let prop_def = LiteralPropertyDef { + name, + params, + ts_type, + computed: ts_prop_sig.computed, + optional: ts_prop_sig.optional, + }; + properties.push(prop_def); + } + TsCallSignatureDecl(ts_call_sig) => { + let mut params = vec![]; + for param in &ts_call_sig.params { + use swc_ecma_ast::TsFnParam::*; + + let param_def = match param { + Ident(ident) => { + let ts_type = + ident.type_ann.as_ref().map(|rt| (&*rt.type_ann).into()); + + ParamDef { + name: ident.sym.to_string(), + ts_type, + } + } + _ => ParamDef { + name: "<TODO>".to_string(), + ts_type: None, + }, + }; + + params.push(param_def); + } + + let ts_type = ts_call_sig + .type_ann + .as_ref() + .map(|rt| (&*rt.type_ann).into()); + + let call_sig_def = LiteralCallSignatureDef { params, ts_type }; + call_signatures.push(call_sig_def); + } + // TODO: + TsConstructSignatureDecl(_) => {} + TsIndexSignature(_) => {} + } + } + + let type_literal = TsTypeLiteralDef { + methods, + properties, + call_signatures, + }; + + TsTypeDef { + kind: Some(TsTypeDefKind::TypeLiteral), + type_literal: Some(type_literal), + ..Default::default() + } + } +} + +impl Into<TsTypeDef> for &TsConditionalType { + fn into(self) -> TsTypeDef { + let conditional_type_def = TsConditionalDef { + check_type: Box::new((&*self.check_type).into()), + extends_type: Box::new((&*self.extends_type).into()), + true_type: Box::new((&*self.true_type).into()), + false_type: Box::new((&*self.false_type).into()), + }; + + TsTypeDef { + kind: Some(TsTypeDefKind::Conditional), + conditional_type: Some(conditional_type_def), + ..Default::default() + } + } +} + +impl Into<TsTypeDef> for &TsFnOrConstructorType { + fn into(self) -> TsTypeDef { + use swc_ecma_ast::TsFnOrConstructorType::*; + + let fn_def = match self { + TsFnType(ts_fn_type) => { + let mut params = vec![]; + + for param in &ts_fn_type.params { + use swc_ecma_ast::TsFnParam::*; + + let param_def = match param { + Ident(ident) => { + let ts_type: Option<TsTypeDef> = + ident.type_ann.as_ref().map(|rt| { + let type_box = &*rt.type_ann; + (&*type_box).into() + }); + + ParamDef { + name: ident.sym.to_string(), + ts_type, + } + } + _ => ParamDef { + name: "<TODO>".to_string(), + ts_type: None, + }, + }; + + params.push(param_def); + } + + TsFnOrConstructorDef { + constructor: false, + ts_type: (&*ts_fn_type.type_ann.type_ann).into(), + params, + } + } + TsConstructorType(ctor_type) => { + let mut params = vec![]; + + for param in &ctor_type.params { + use swc_ecma_ast::TsFnParam::*; + + let param_def = match param { + Ident(ident) => { + let ts_type: Option<TsTypeDef> = + ident.type_ann.as_ref().map(|rt| { + let type_box = &*rt.type_ann; + (&*type_box).into() + }); + + ParamDef { + name: ident.sym.to_string(), + ts_type, + } + } + _ => ParamDef { + name: "<TODO>".to_string(), + ts_type: None, + }, + }; + + params.push(param_def); + } + + TsFnOrConstructorDef { + constructor: true, + ts_type: (&*ctor_type.type_ann.type_ann).into(), + params: vec![], + } + } + }; + + TsTypeDef { + kind: Some(TsTypeDefKind::FnOrConstructor), + fn_or_constructor: Some(Box::new(fn_def)), + ..Default::default() + } + } +} + +impl Into<TsTypeDef> for &TsType { + fn into(self) -> TsTypeDef { + use swc_ecma_ast::TsType::*; + + match self { + TsKeywordType(ref keyword_type) => keyword_type.into(), + TsLitType(ref lit_type) => lit_type.into(), + TsTypeRef(ref type_ref) => type_ref.into(), + TsUnionOrIntersectionType(union_or_inter) => union_or_inter.into(), + TsArrayType(array_type) => array_type.into(), + TsTupleType(tuple_type) => tuple_type.into(), + TsTypeOperator(type_op_type) => type_op_type.into(), + TsParenthesizedType(paren_type) => paren_type.into(), + TsRestType(rest_type) => rest_type.into(), + TsOptionalType(optional_type) => optional_type.into(), + TsTypeQuery(type_query) => type_query.into(), + TsThisType(this_type) => this_type.into(), + TsFnOrConstructorType(fn_or_con_type) => fn_or_con_type.into(), + TsConditionalType(conditional_type) => conditional_type.into(), + TsIndexedAccessType(indexed_access_type) => indexed_access_type.into(), + TsTypeLit(type_literal) => type_literal.into(), + _ => TsTypeDef { + repr: "<UNIMPLEMENTED>".to_string(), + ..Default::default() + }, + } + } +} + +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct TsTypeRefDef { + pub type_params: Option<Vec<TsTypeDef>>, + pub type_name: String, +} + +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub enum LiteralDefKind { + Number, + String, + Boolean, +} + +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct LiteralDef { + pub kind: LiteralDefKind, + + #[serde(skip_serializing_if = "Option::is_none")] + pub number: Option<f64>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub string: Option<String>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub boolean: Option<bool>, +} + +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct TsTypeOperatorDef { + pub operator: String, + pub ts_type: TsTypeDef, +} + +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct TsFnOrConstructorDef { + // TODO: type_params + pub constructor: bool, + pub ts_type: TsTypeDef, + pub params: Vec<ParamDef>, +} + +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct TsConditionalDef { + pub check_type: Box<TsTypeDef>, + pub extends_type: Box<TsTypeDef>, + pub true_type: Box<TsTypeDef>, + pub false_type: Box<TsTypeDef>, +} + +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct TsIndexedAccessDef { + pub readonly: bool, + pub obj_type: Box<TsTypeDef>, + pub index_type: Box<TsTypeDef>, +} + +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct LiteralMethodDef { + // TODO: type_params + pub name: String, + // pub location: Location, + // pub js_doc: Option<String>, + pub params: Vec<ParamDef>, + pub return_type: Option<TsTypeDef>, +} + +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct LiteralPropertyDef { + // TODO: type_params + pub name: String, + // pub location: Location, + // pub js_doc: Option<String>, + pub params: Vec<ParamDef>, + pub computed: bool, + pub optional: bool, + pub ts_type: Option<TsTypeDef>, +} + +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct LiteralCallSignatureDef { + // TODO: type_params + // pub location: Location, + // pub js_doc: Option<String>, + pub params: Vec<ParamDef>, + pub ts_type: Option<TsTypeDef>, +} + +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct TsTypeLiteralDef { + pub methods: Vec<LiteralMethodDef>, + pub properties: Vec<LiteralPropertyDef>, + pub call_signatures: Vec<LiteralCallSignatureDef>, +} + +#[derive(Debug, PartialEq, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub enum TsTypeDefKind { + Keyword, + Literal, + TypeRef, + Union, + Intersection, + Array, + Tuple, + TypeOperator, + Parenthesized, + Rest, + Optional, + TypeQuery, + This, + FnOrConstructor, + Conditional, + IndexedAccess, + TypeLiteral, +} + +#[derive(Debug, Default, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct TsTypeDef { + pub repr: String, + + pub kind: Option<TsTypeDefKind>, + + // TODO: make this struct more conrete + #[serde(skip_serializing_if = "Option::is_none")] + pub keyword: Option<String>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub literal: Option<LiteralDef>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub type_ref: Option<TsTypeRefDef>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub union: Option<Vec<TsTypeDef>>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub intersection: Option<Vec<TsTypeDef>>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub array: Option<Box<TsTypeDef>>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub tuple: Option<Vec<TsTypeDef>>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub type_operator: Option<Box<TsTypeOperatorDef>>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub parenthesized: Option<Box<TsTypeDef>>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub rest: Option<Box<TsTypeDef>>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub optional: Option<Box<TsTypeDef>>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub type_query: Option<String>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub this: Option<bool>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub fn_or_constructor: Option<Box<TsFnOrConstructorDef>>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub conditional_type: Option<TsConditionalDef>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub indexed_access: Option<TsIndexedAccessDef>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub type_literal: Option<TsTypeLiteralDef>, +} + +pub fn ts_type_ann_to_def( + source_map: &SourceMap, + type_ann: &TsTypeAnn, +) -> TsTypeDef { + use swc_ecma_ast::TsType::*; + + match &*type_ann.type_ann { + TsKeywordType(keyword_type) => keyword_type.into(), + TsLitType(lit_type) => lit_type.into(), + TsTypeRef(type_ref) => type_ref.into(), + TsUnionOrIntersectionType(union_or_inter) => union_or_inter.into(), + TsArrayType(array_type) => array_type.into(), + TsTupleType(tuple_type) => tuple_type.into(), + TsTypeOperator(type_op_type) => type_op_type.into(), + TsParenthesizedType(paren_type) => paren_type.into(), + TsRestType(rest_type) => rest_type.into(), + TsOptionalType(optional_type) => optional_type.into(), + TsTypeQuery(type_query) => type_query.into(), + TsThisType(this_type) => this_type.into(), + TsFnOrConstructorType(fn_or_con_type) => fn_or_con_type.into(), + TsConditionalType(conditional_type) => conditional_type.into(), + TsIndexedAccessType(indexed_access_type) => indexed_access_type.into(), + TsTypeLit(type_literal) => type_literal.into(), + _ => { + let repr = source_map + .span_to_snippet(type_ann.span) + .expect("Class prop type not found"); + let repr = repr.trim_start_matches(':').trim_start().to_string(); + + TsTypeDef { + repr, + ..Default::default() + } + } + } +} diff --git a/cli/doc/type_alias.rs b/cli/doc/type_alias.rs new file mode 100644 index 000000000..3740aee84 --- /dev/null +++ b/cli/doc/type_alias.rs @@ -0,0 +1,25 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +use serde::Serialize; +use swc_ecma_ast; + +use super::parser::DocParser; +use super::ts_type::TsTypeDef; + +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct TypeAliasDef { + pub ts_type: TsTypeDef, + // TODO: type_params +} + +pub fn get_doc_for_ts_type_alias_decl( + _doc_parser: &DocParser, + type_alias_decl: &swc_ecma_ast::TsTypeAliasDecl, +) -> (String, TypeAliasDef) { + let alias_name = type_alias_decl.id.sym.to_string(); + let ts_type = type_alias_decl.type_ann.as_ref().into(); + + let type_alias_def = TypeAliasDef { ts_type }; + + (alias_name, type_alias_def) +} diff --git a/cli/doc/variable.rs b/cli/doc/variable.rs new file mode 100644 index 000000000..16bf26d25 --- /dev/null +++ b/cli/doc/variable.rs @@ -0,0 +1,43 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +use serde::Serialize; +use swc_ecma_ast; + +use super::parser::DocParser; +use super::ts_type::ts_type_ann_to_def; +use super::ts_type::TsTypeDef; + +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct VariableDef { + pub ts_type: Option<TsTypeDef>, + pub kind: swc_ecma_ast::VarDeclKind, +} + +pub fn get_doc_for_var_decl( + doc_parser: &DocParser, + var_decl: &swc_ecma_ast::VarDecl, +) -> (String, VariableDef) { + assert!(!var_decl.decls.is_empty()); + // TODO: support multiple declarators + let var_declarator = var_decl.decls.get(0).unwrap(); + + let var_name = match &var_declarator.name { + swc_ecma_ast::Pat::Ident(ident) => ident.sym.to_string(), + _ => "<TODO>".to_string(), + }; + + let maybe_ts_type = match &var_declarator.name { + swc_ecma_ast::Pat::Ident(ident) => ident + .type_ann + .as_ref() + .map(|rt| ts_type_ann_to_def(&doc_parser.source_map, rt)), + _ => None, + }; + + let variable_def = VariableDef { + ts_type: maybe_ts_type, + kind: var_decl.kind, + }; + + (var_name, variable_def) +} diff --git a/cli/flags.rs b/cli/flags.rs index 4f55d69ef..d90f1f3b9 100644 --- a/cli/flags.rs +++ b/cli/flags.rs @@ -31,6 +31,11 @@ pub enum DenoSubcommand { Completions { buf: Box<[u8]>, }, + Doc { + json: bool, + source_file: String, + filter: Option<String>, + }, Eval { code: String, as_typescript: bool, @@ -258,6 +263,8 @@ pub fn flags_from_vec_safe(args: Vec<String>) -> clap::Result<Flags> { test_parse(&mut flags, m); } else if let Some(m) = matches.subcommand_matches("upgrade") { upgrade_parse(&mut flags, m); + } else if let Some(m) = matches.subcommand_matches("doc") { + doc_parse(&mut flags, m); } else { unimplemented!(); } @@ -311,6 +318,7 @@ If the flag is set, restrict these messages to errors.", .subcommand(test_subcommand()) .subcommand(types_subcommand()) .subcommand(upgrade_subcommand()) + .subcommand(doc_subcommand()) .long_about(DENO_HELP) .after_help(ENV_VARIABLES_HELP) } @@ -550,6 +558,22 @@ fn upgrade_parse(flags: &mut Flags, matches: &clap::ArgMatches) { flags.subcommand = DenoSubcommand::Upgrade { dry_run, force }; } +fn doc_parse(flags: &mut Flags, matches: &clap::ArgMatches) { + reload_arg_parse(flags, matches); + let source_file = matches.value_of("source_file").map(String::from).unwrap(); + let json = matches.is_present("json"); + let filter = if matches.is_present("filter") { + Some(matches.value_of("filter").unwrap().to_string()) + } else { + None + }; + flags.subcommand = DenoSubcommand::Doc { + source_file, + json, + filter, + }; +} + fn types_subcommand<'a, 'b>() -> App<'a, 'b> { SubCommand::with_name("types") .about("Print runtime TypeScript declarations") @@ -770,6 +794,43 @@ and is used to replace the current executable.", ) } +fn doc_subcommand<'a, 'b>() -> App<'a, 'b> { + SubCommand::with_name("doc") + .about("Show documentation for module") + .long_about( + "Show documentation for module. + +Output documentation to terminal: + deno doc ./path/to/module.ts + +Show detail of symbol: + deno doc ./path/to/module.ts MyClass.someField + +Output documentation in JSON format: + deno doc --json ./path/to/module.ts", + ) + .arg(reload_arg()) + .arg( + Arg::with_name("json") + .long("json") + .help("Output documentation in JSON format.") + .takes_value(false), + ) + .arg( + Arg::with_name("source_file") + .takes_value(true) + .required(true), + ) + .arg( + Arg::with_name("filter") + .help("Dot separated path to symbol.") + .takes_value(true) + .required(false) + .conflicts_with("json") + .conflicts_with("pretty"), + ) +} + fn permission_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { app .arg( @@ -1219,6 +1280,7 @@ fn arg_hacks(mut args: Vec<String>) -> Vec<String> { let subcommands = sset![ "bundle", "completions", + "doc", "eval", "fetch", "fmt", @@ -2380,6 +2442,41 @@ fn repl_with_cafile() { } #[test] +fn doc() { + let r = + flags_from_vec_safe(svec!["deno", "doc", "--json", "path/to/module.ts"]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Doc { + json: true, + source_file: "path/to/module.ts".to_string(), + filter: None, + }, + ..Flags::default() + } + ); + + let r = flags_from_vec_safe(svec![ + "deno", + "doc", + "path/to/module.ts", + "SomeClass.someField" + ]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Doc { + json: false, + source_file: "path/to/module.ts".to_string(), + filter: Some("SomeClass.someField".to_string()), + }, + ..Flags::default() + } + ); +} + +#[test] fn inspect_default_host() { let r = flags_from_vec_safe(svec!["deno", "run", "--inspect", "foo.js"]); assert_eq!( diff --git a/cli/lib.rs b/cli/lib.rs index 7b5b56ba2..11ab26626 100644 --- a/cli/lib.rs +++ b/cli/lib.rs @@ -27,6 +27,7 @@ pub mod compilers; pub mod deno_dir; pub mod diagnostics; mod disk_cache; +mod doc; mod file_fetcher; pub mod flags; mod fmt; @@ -111,6 +112,18 @@ impl log::Log for Logger { fn flush(&self) {} } +fn write_to_stdout_ignore_sigpipe(bytes: &[u8]) -> Result<(), std::io::Error> { + use std::io::ErrorKind; + + match std::io::stdout().write_all(bytes) { + Ok(()) => Ok(()), + Err(e) => match e.kind() { + ErrorKind::BrokenPipe => Ok(()), + _ => Err(e), + }, + } +} + fn create_main_worker( global_state: GlobalState, main_module: ModuleSpecifier, @@ -344,6 +357,57 @@ async fn bundle_command( bundle_result } +async fn doc_command( + flags: Flags, + source_file: String, + json: bool, + maybe_filter: Option<String>, +) -> Result<(), ErrBox> { + let global_state = GlobalState::new(flags.clone())?; + let module_specifier = + ModuleSpecifier::resolve_url_or_path(&source_file).unwrap(); + let source_file = global_state + .file_fetcher + .fetch_source_file(&module_specifier, None) + .await?; + let source_code = String::from_utf8(source_file.source_code)?; + + let doc_parser = doc::DocParser::default(); + let parse_result = + doc_parser.parse(module_specifier.to_string(), source_code); + + let doc_nodes = match parse_result { + Ok(nodes) => nodes, + Err(e) => { + eprintln!("Failed to parse documentation:"); + for diagnostic in e { + eprintln!("{}", diagnostic.message()); + } + + std::process::exit(1); + } + }; + + if json { + let writer = std::io::BufWriter::new(std::io::stdout()); + serde_json::to_writer_pretty(writer, &doc_nodes).map_err(ErrBox::from) + } else { + let details = if let Some(filter) = maybe_filter { + let node = doc::find_node_by_name_recursively(doc_nodes, filter.clone()); + if let Some(node) = node { + doc::printer::format_details(node) + } else { + eprintln!("Node {} was not found!", filter); + std::process::exit(1); + } + } else { + doc::printer::format(doc_nodes) + }; + + write_to_stdout_ignore_sigpipe(details.as_bytes()).map_err(ErrBox::from) + } +} + async fn run_repl(flags: Flags) -> Result<(), ErrBox> { let main_module = ModuleSpecifier::resolve_url_or_path("./__$deno$repl.ts").unwrap(); @@ -451,6 +515,11 @@ pub fn main() { source_file, out_file, } => bundle_command(flags, source_file, out_file).boxed_local(), + DenoSubcommand::Doc { + source_file, + json, + filter, + } => doc_command(flags, source_file, json, filter).boxed_local(), DenoSubcommand::Eval { code, as_typescript, @@ -478,7 +547,10 @@ pub fn main() { allow_none, } => test_command(flags, include, fail_fast, allow_none).boxed_local(), DenoSubcommand::Completions { buf } => { - print!("{}", std::str::from_utf8(&buf).unwrap()); + if let Err(e) = write_to_stdout_ignore_sigpipe(&buf) { + eprintln!("{}", e); + std::process::exit(1); + } return; } DenoSubcommand::Types => { @@ -488,8 +560,10 @@ pub fn main() { crate::js::SHARED_GLOBALS_LIB, crate::js::WINDOW_LIB ); - // TODO(ry) Only ignore SIGPIPE. Currently ignoring all errors. - let _r = std::io::stdout().write_all(types.as_bytes()); + if let Err(e) = write_to_stdout_ignore_sigpipe(types.as_bytes()) { + eprintln!("{}", e); + std::process::exit(1); + } return; } DenoSubcommand::Upgrade { force, dry_run } => { |