diff --git a/cli/colors.rs b/cli/colors.rs index ccbcc926a5..870f7f4357 100644 --- a/cli/colors.rs +++ b/cli/colors.rs @@ -3,7 +3,7 @@ use regex::Regex; use std::env; use std::fmt; use std::io::Write; -use termcolor::Color::{Ansi256, Black, Magenta, Red, White}; +use termcolor::Color::{Ansi256, Black, Blue, Green, Magenta, Red, White}; use termcolor::{Ansi, ColorSpec, WriteColor}; #[cfg(windows)] @@ -54,7 +54,10 @@ pub fn red_bold(s: &str) -> impl fmt::Display { pub fn green_bold(s: &str) -> impl fmt::Display { let mut style_spec = ColorSpec::new(); - style_spec.set_fg(Some(Ansi256(10))).set_bold(true); + style_spec + .set_fg(Some(Green)) + .set_bold(true) + .set_intense(true); style(&s, style_spec) } @@ -102,7 +105,7 @@ pub fn red(s: &str) -> impl fmt::Display { pub fn green(s: &str) -> impl fmt::Display { let mut style_spec = ColorSpec::new(); - style_spec.set_fg(Some(Ansi256(10))); + style_spec.set_fg(Some(Green)).set_intense(true); style(&s, style_spec) } @@ -124,6 +127,12 @@ pub fn gray(s: &str) -> impl fmt::Display { style(&s, style_spec) } +pub fn italic_gray(s: &str) -> impl fmt::Display { + let mut style_spec = ColorSpec::new(); + style_spec.set_fg(Some(Ansi256(8))).set_italic(true); + style(&s, style_spec) +} + pub fn italic_bold_gray(s: &str) -> impl fmt::Display { let mut style_spec = ColorSpec::new(); style_spec @@ -132,3 +141,9 @@ pub fn italic_bold_gray(s: &str) -> impl fmt::Display { .set_italic(true); style(&s, style_spec) } + +pub fn intense_blue(s: &str) -> impl fmt::Display { + let mut style_spec = ColorSpec::new(); + style_spec.set_fg(Some(Blue)).set_intense(true); + style(&s, style_spec) +} diff --git a/cli/doc/class.rs b/cli/doc/class.rs index af1964ac9c..d8fa29ded3 100644 --- a/cli/doc/class.rs +++ b/cli/doc/class.rs @@ -1,5 +1,10 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. -use crate::swc_common::SourceMap; +use crate::colors; +use crate::doc::display::{ + display_abstract, display_accessibility, display_async, display_generator, + display_method, display_optional, display_readonly, display_static, + SliceDisplayer, +}; use crate::swc_common::Spanned; use crate::swc_ecma_ast; use serde::Serialize; @@ -7,18 +12,21 @@ use serde::Serialize; use super::function::function_to_function_def; use super::function::FunctionDef; use super::interface::expr_to_name; -use super::params::assign_pat_to_param_def; -use super::params::ident_to_param_def; -use super::params::pat_to_param_def; +use super::params::{ + assign_pat_to_param_def, ident_to_param_def, pat_to_param_def, + prop_name_to_string, ts_fn_param_to_param_def, +}; use super::parser::DocParser; -use super::ts_type::ts_entity_name_to_name; -use super::ts_type::ts_type_ann_to_def; -use super::ts_type::TsTypeDef; +use super::ts_type::{ + maybe_type_param_instantiation_to_type_defs, ts_type_ann_to_def, TsTypeDef, +}; use super::ts_type_param::maybe_type_param_decl_to_type_param_defs; use super::ts_type_param::TsTypeParamDef; use super::Location; use super::ParamDef; +use std::fmt::{Display, Formatter, Result as FmtResult}; + #[derive(Debug, Serialize, Clone)] #[serde(rename_all = "camelCase")] pub struct ClassConstructorDef { @@ -29,6 +37,18 @@ pub struct ClassConstructorDef { pub location: Location, } +impl Display for ClassConstructorDef { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + write!( + f, + "{}{}({})", + display_accessibility(self.accessibility), + colors::magenta("constructor"), + SliceDisplayer::new(&self.params, ", ", false), + ) + } +} + #[derive(Debug, Serialize, Clone)] #[serde(rename_all = "camelCase")] pub struct ClassPropertyDef { @@ -43,6 +63,48 @@ pub struct ClassPropertyDef { pub location: Location, } +impl Display for ClassPropertyDef { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + write!( + f, + "{}{}{}{}{}{}", + display_abstract(self.is_abstract), + display_accessibility(self.accessibility), + display_static(self.is_static), + display_readonly(self.readonly), + colors::bold(&self.name), + display_optional(self.optional), + )?; + if let Some(ts_type) = &self.ts_type { + write!(f, ": {}", ts_type)?; + } + Ok(()) + } +} + +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ClassIndexSignatureDef { + pub readonly: bool, + pub params: Vec, + pub ts_type: Option, +} + +impl Display for ClassIndexSignatureDef { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + write!( + f, + "{}[{}]", + display_readonly(self.readonly), + SliceDisplayer::new(&self.params, ", ", false) + )?; + if let Some(ts_type) = &self.ts_type { + write!(f, ": {}", ts_type)?; + } + Ok(()) + } +} + #[derive(Debug, Serialize, Clone)] #[serde(rename_all = "camelCase")] pub struct ClassMethodDef { @@ -57,32 +119,41 @@ pub struct ClassMethodDef { pub location: Location, } +impl Display for ClassMethodDef { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + write!( + f, + "{}{}{}{}{}{}{}{}({})", + display_abstract(self.is_abstract), + display_accessibility(self.accessibility), + display_static(self.is_static), + display_async(self.function_def.is_async), + display_method(self.kind), + display_generator(self.function_def.is_generator), + colors::bold(&self.name), + display_optional(self.optional), + SliceDisplayer::new(&self.function_def.params, ", ", false), + )?; + if let Some(return_type) = &self.function_def.return_type { + write!(f, ": {}", return_type)?; + } + Ok(()) + } +} + #[derive(Debug, Serialize, Clone)] #[serde(rename_all = "camelCase")] pub struct ClassDef { - // TODO(bartlomieju): decorators, super_type_params + // TODO(bartlomieju): decorators pub is_abstract: bool, pub constructors: Vec, pub properties: Vec, + pub index_signatures: Vec, pub methods: Vec, pub extends: Option, - pub implements: Vec, + pub implements: Vec, pub type_params: Vec, -} - -fn prop_name_to_string( - source_map: &SourceMap, - prop_name: &swc_ecma_ast::PropName, -) -> String { - use crate::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 super_type_params: Vec, } pub fn class_to_class_def( @@ -92,6 +163,7 @@ pub fn class_to_class_def( let mut constructors = vec![]; let mut methods = vec![]; let mut properties = vec![]; + let mut index_signatures = vec![]; let extends: Option = match &class.super_class { Some(boxed) => { @@ -105,11 +177,11 @@ pub fn class_to_class_def( None => None, }; - let implements: Vec = class + let implements = class .implements .iter() - .map(|expr| ts_entity_name_to_name(&expr.expr)) - .collect(); + .map(|expr| expr.into()) + .collect::>(); for member in &class.body { use crate::swc_ecma_ast::ClassMember::*; @@ -117,8 +189,10 @@ pub fn class_to_class_def( 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.ast_parser.source_map, &ctor.key); + let constructor_name = prop_name_to_string( + &ctor.key, + Some(&doc_parser.ast_parser.source_map), + ); let mut params = vec![]; @@ -126,14 +200,23 @@ pub fn class_to_class_def( use crate::swc_ecma_ast::ParamOrTsParamProp::*; let param_def = match param { - Param(param) => pat_to_param_def(¶m.pat), + Param(param) => pat_to_param_def( + ¶m.pat, + Some(&doc_parser.ast_parser.source_map), + ), TsParamProp(ts_param_prop) => { use swc_ecma_ast::TsParamPropParam; match &ts_param_prop.param { - TsParamPropParam::Ident(ident) => ident_to_param_def(ident), + TsParamPropParam::Ident(ident) => ident_to_param_def( + ident, + Some(&doc_parser.ast_parser.source_map), + ), TsParamPropParam::Assign(assign_pat) => { - assign_pat_to_param_def(assign_pat) + assign_pat_to_param_def( + assign_pat, + Some(&doc_parser.ast_parser.source_map), + ) } } } @@ -153,10 +236,11 @@ pub fn class_to_class_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.ast_parser.source_map, &class_method.key, + Some(&doc_parser.ast_parser.source_map), ); - let fn_def = function_to_function_def(&class_method.function); + 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, @@ -199,8 +283,26 @@ pub fn class_to_class_def( }; properties.push(prop_def); } + TsIndexSignature(ts_index_sig) => { + let mut params = vec![]; + for param in &ts_index_sig.params { + let param_def = ts_fn_param_to_param_def(param, None); + params.push(param_def); + } + + let ts_type = ts_index_sig + .type_ann + .as_ref() + .map(|rt| (&*rt.type_ann).into()); + + let index_sig_def = ClassIndexSignatureDef { + readonly: ts_index_sig.readonly, + params, + ts_type, + }; + index_signatures.push(index_sig_def); + } // TODO(bartlomieju): - TsIndexSignature(_) => {} PrivateMethod(_) => {} PrivateProp(_) => {} _ => {} @@ -210,14 +312,20 @@ pub fn class_to_class_def( let type_params = maybe_type_param_decl_to_type_param_defs(class.type_params.as_ref()); + let super_type_params = maybe_type_param_instantiation_to_type_defs( + class.super_type_params.as_ref(), + ); + ClassDef { is_abstract: class.is_abstract, extends, implements, constructors, properties, + index_signatures, methods, type_params, + super_type_params, } } diff --git a/cli/doc/display.rs b/cli/doc/display.rs new file mode 100644 index 0000000000..9da04363f6 --- /dev/null +++ b/cli/doc/display.rs @@ -0,0 +1,90 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +use crate::colors; +use crate::swc_ecma_ast; +use std::fmt::{Display, Formatter, Result}; + +pub(crate) struct Indent(pub i64); + +impl Display for Indent { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + for _ in 0..self.0 { + write!(f, " ")?; + } + Ok(()) + } +} + +pub(crate) struct SliceDisplayer<'a, T: Display>(&'a [T], &'a str, bool); + +impl<'a, T: Display> SliceDisplayer<'a, T> { + pub fn new( + slice: &'a [T], + separator: &'a str, + trailing: bool, + ) -> SliceDisplayer<'a, T> { + SliceDisplayer(slice, separator, trailing) + } +} + +impl Display for SliceDisplayer<'_, T> { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + if self.0.is_empty() { + return Ok(()); + } + + write!(f, "{}", self.0[0])?; + for v in &self.0[1..] { + write!(f, "{}{}", self.1, v)?; + } + + if self.2 { + write!(f, "{}", self.1)?; + } + + Ok(()) + } +} + +pub(crate) fn display_abstract(is_abstract: bool) -> impl Display { + colors::magenta(if is_abstract { "abstract " } else { "" }) +} + +pub(crate) fn display_accessibility( + accessibility: Option, +) -> impl Display { + colors::magenta( + match accessibility.unwrap_or(swc_ecma_ast::Accessibility::Public) { + swc_ecma_ast::Accessibility::Public => "", + swc_ecma_ast::Accessibility::Protected => "protected ", + swc_ecma_ast::Accessibility::Private => "private ", + }, + ) +} + +pub(crate) fn display_async(is_async: bool) -> impl Display { + colors::magenta(if is_async { "async " } else { "" }) +} + +pub(crate) fn display_generator(is_generator: bool) -> impl Display { + colors::magenta(if is_generator { "*" } else { "" }) +} + +pub(crate) fn display_method(method: swc_ecma_ast::MethodKind) -> impl Display { + colors::magenta(match method { + swc_ecma_ast::MethodKind::Getter => "get ", + swc_ecma_ast::MethodKind::Setter => "set ", + _ => "", + }) +} + +pub(crate) fn display_optional(is_optional: bool) -> impl Display { + colors::magenta(if is_optional { "?" } else { "" }) +} + +pub(crate) fn display_readonly(is_readonly: bool) -> impl Display { + colors::magenta(if is_readonly { "readonly " } else { "" }) +} + +pub(crate) fn display_static(is_static: bool) -> impl Display { + colors::magenta(if is_static { "static " } else { "" }) +} diff --git a/cli/doc/function.rs b/cli/doc/function.rs index e8abf56f92..ede8bdbbd2 100644 --- a/cli/doc/function.rs +++ b/cli/doc/function.rs @@ -1,5 +1,6 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. use super::params::pat_to_param_def; +use super::parser::DocParser; use super::ts_type::ts_type_ann_to_def; use super::ts_type::TsTypeDef; use super::ts_type_param::maybe_type_param_decl_to_type_param_defs; @@ -20,12 +21,14 @@ pub struct FunctionDef { } pub fn function_to_function_def( + doc_parser: &DocParser, function: &swc_ecma_ast::Function, ) -> FunctionDef { let mut params = vec![]; for param in &function.params { - let param_def = pat_to_param_def(¶m.pat); + let param_def = + pat_to_param_def(¶m.pat, Some(&doc_parser.ast_parser.source_map)); params.push(param_def); } @@ -47,9 +50,10 @@ pub fn function_to_function_def( } 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(&fn_decl.function); + 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 index 9e3c1fc909..d3caa87484 100644 --- a/cli/doc/interface.rs +++ b/cli/doc/interface.rs @@ -1,10 +1,11 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +use crate::colors; +use crate::doc::display::{display_optional, display_readonly, SliceDisplayer}; use crate::swc_ecma_ast; use serde::Serialize; use super::params::ts_fn_param_to_param_def; use super::parser::DocParser; -use super::ts_type::ts_entity_name_to_name; use super::ts_type::ts_type_ann_to_def; use super::ts_type::TsTypeDef; use super::ts_type_param::maybe_type_param_decl_to_type_param_defs; @@ -12,6 +13,8 @@ use super::ts_type_param::TsTypeParamDef; use super::Location; use super::ParamDef; +use std::fmt::{Display, Formatter, Result as FmtResult}; + #[derive(Debug, Serialize, Clone)] #[serde(rename_all = "camelCase")] pub struct InterfaceMethodDef { @@ -24,6 +27,22 @@ pub struct InterfaceMethodDef { pub type_params: Vec, } +impl Display for InterfaceMethodDef { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + write!( + f, + "{}{}({})", + colors::bold(&self.name), + display_optional(self.optional), + SliceDisplayer::new(&self.params, ", ", false), + )?; + if let Some(return_type) = &self.return_type { + write!(f, ": {}", return_type)?; + } + Ok(()) + } +} + #[derive(Debug, Serialize, Clone)] #[serde(rename_all = "camelCase")] pub struct InterfacePropertyDef { @@ -37,6 +56,44 @@ pub struct InterfacePropertyDef { pub type_params: Vec, } +impl Display for InterfacePropertyDef { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + write!( + f, + "{}{}", + colors::bold(&self.name), + display_optional(self.optional), + )?; + if let Some(ts_type) = &self.ts_type { + write!(f, ": {}", ts_type)?; + } + Ok(()) + } +} + +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct InterfaceIndexSignatureDef { + pub readonly: bool, + pub params: Vec, + pub ts_type: Option, +} + +impl Display for InterfaceIndexSignatureDef { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + write!( + f, + "{}[{}]", + display_readonly(self.readonly), + SliceDisplayer::new(&self.params, ", ", false) + )?; + if let Some(ts_type) = &self.ts_type { + write!(f, ": {}", ts_type)?; + } + Ok(()) + } +} + #[derive(Debug, Serialize, Clone)] #[serde(rename_all = "camelCase")] pub struct InterfaceCallSignatureDef { @@ -50,10 +107,11 @@ pub struct InterfaceCallSignatureDef { #[derive(Debug, Serialize, Clone)] #[serde(rename_all = "camelCase")] pub struct InterfaceDef { - pub extends: Vec, + pub extends: Vec, pub methods: Vec, pub properties: Vec, pub call_signatures: Vec, + pub index_signatures: Vec, pub type_params: Vec, } @@ -84,6 +142,7 @@ pub fn get_doc_for_ts_interface_decl( let mut methods = vec![]; let mut properties = vec![]; let mut call_signatures = vec![]; + let mut index_signatures = vec![]; for type_element in &interface_decl.body.body { use crate::swc_ecma_ast::TsTypeElement::*; @@ -95,7 +154,10 @@ pub fn get_doc_for_ts_interface_decl( let mut params = vec![]; for param in &ts_method_sig.params { - let param_def = ts_fn_param_to_param_def(param); + let param_def = ts_fn_param_to_param_def( + param, + Some(&doc_parser.ast_parser.source_map), + ); params.push(param_def); } @@ -131,7 +193,10 @@ pub fn get_doc_for_ts_interface_decl( let mut params = vec![]; for param in &ts_prop_sig.params { - let param_def = ts_fn_param_to_param_def(param); + let param_def = ts_fn_param_to_param_def( + param, + Some(&doc_parser.ast_parser.source_map), + ); params.push(param_def); } @@ -164,7 +229,10 @@ pub fn get_doc_for_ts_interface_decl( let mut params = vec![]; for param in &ts_call_sig.params { - let param_def = ts_fn_param_to_param_def(param); + let param_def = ts_fn_param_to_param_def( + param, + Some(&doc_parser.ast_parser.source_map), + ); params.push(param_def); } @@ -189,9 +257,27 @@ pub fn get_doc_for_ts_interface_decl( }; call_signatures.push(call_sig_def); } + TsIndexSignature(ts_index_sig) => { + let mut params = vec![]; + for param in &ts_index_sig.params { + let param_def = ts_fn_param_to_param_def(param, None); + params.push(param_def); + } + + let ts_type = ts_index_sig + .type_ann + .as_ref() + .map(|rt| (&*rt.type_ann).into()); + + let index_sig_def = InterfaceIndexSignatureDef { + readonly: ts_index_sig.readonly, + params, + ts_type, + }; + index_signatures.push(index_sig_def); + } // TODO: TsConstructSignatureDecl(_) => {} - TsIndexSignature(_) => {} } } @@ -199,17 +285,18 @@ pub fn get_doc_for_ts_interface_decl( interface_decl.type_params.as_ref(), ); - let extends: Vec = interface_decl + let extends = interface_decl .extends .iter() - .map(|expr| ts_entity_name_to_name(&expr.expr)) - .collect(); + .map(|expr| expr.into()) + .collect::>(); let interface_def = InterfaceDef { extends, methods, properties, call_signatures, + index_signatures, type_params, }; diff --git a/cli/doc/mod.rs b/cli/doc/mod.rs index 81cc2f9fb4..49f8d7453e 100644 --- a/cli/doc/mod.rs +++ b/cli/doc/mod.rs @@ -1,5 +1,6 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. pub mod class; +mod display; pub mod r#enum; pub mod function; pub mod interface; @@ -17,9 +18,9 @@ pub mod variable; pub use node::DocNode; pub use node::DocNodeKind; pub use node::Location; -pub use node::ParamDef; -pub use node::ParamKind; +pub use params::ParamDef; pub use parser::DocParser; +pub use printer::DocPrinter; #[cfg(test)] mod tests; diff --git a/cli/doc/module.rs b/cli/doc/module.rs index 2de9c7ca83..79b1b92bee 100644 --- a/cli/doc/module.rs +++ b/cli/doc/module.rs @@ -35,7 +35,8 @@ pub fn get_doc_node_for_export_decl( } } Decl::Fn(fn_decl) => { - let (name, function_def) = super::function::get_doc_for_fn_decl(fn_decl); + let (name, function_def) = + super::function::get_doc_for_fn_decl(doc_parser, fn_decl); DocNode { kind: DocNodeKind::Function, name, diff --git a/cli/doc/node.rs b/cli/doc/node.rs index 0f97ed65ab..690221ed00 100644 --- a/cli/doc/node.rs +++ b/cli/doc/node.rs @@ -14,24 +14,6 @@ pub enum DocNodeKind { Namespace, } -#[derive(Debug, Serialize, Clone)] -#[serde(rename_all = "camelCase")] -pub enum ParamKind { - Identifier, - Rest, - Array, - Object, -} - -#[derive(Debug, Serialize, Clone)] -#[serde(rename_all = "camelCase")] -pub struct ParamDef { - pub name: String, - pub kind: ParamKind, - pub optional: bool, - pub ts_type: Option, -} - #[derive(Debug, Serialize, Clone, PartialEq)] pub struct Location { pub filename: String, @@ -82,7 +64,7 @@ pub struct Reexport { #[derive(Debug, Serialize, Clone)] #[serde(rename_all = "camelCase")] pub struct ModuleDoc { - pub exports: Vec, + pub definitions: Vec, pub reexports: Vec, } diff --git a/cli/doc/params.rs b/cli/doc/params.rs index 0869c19695..3e7967c816 100644 --- a/cli/doc/params.rs +++ b/cli/doc/params.rs @@ -1,58 +1,228 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +use super::display::{display_optional, SliceDisplayer}; +use super::ts_type::{ts_type_ann_to_def, TsTypeDef}; +use crate::swc_common::SourceMap; use crate::swc_ecma_ast; +use crate::swc_ecma_ast::{ObjectPatProp, Pat, TsFnParam}; +use serde::Serialize; +use std::fmt::{Display, Formatter, Result as FmtResult}; -use super::ts_type::ts_type_ann_to_def; -use super::ParamDef; -use super::ParamKind; -use crate::swc_ecma_ast::Pat; -use crate::swc_ecma_ast::TsFnParam; +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +#[serde(tag = "kind")] +pub enum ParamDef { + #[serde(rename_all = "camelCase")] + Array { + elements: Vec>, + optional: bool, + ts_type: Option, + }, + #[serde(rename_all = "camelCase")] + Assign { + left: Box, + right: String, + ts_type: Option, + }, + #[serde(rename_all = "camelCase")] + Identifier { + name: String, + optional: bool, + ts_type: Option, + }, + #[serde(rename_all = "camelCase")] + Object { + props: Vec, + optional: bool, + ts_type: Option, + }, + #[serde(rename_all = "camelCase")] + Rest { + arg: Box, + ts_type: Option, + }, +} -pub fn ident_to_param_def(ident: &swc_ecma_ast::Ident) -> ParamDef { +impl Display for ParamDef { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + match self { + Self::Array { + elements, + optional, + ts_type, + } => { + write!(f, "[")?; + if !elements.is_empty() { + if let Some(v) = &elements[0] { + write!(f, "{}", v)?; + } + for maybe_v in &elements[1..] { + write!(f, ", ")?; + if let Some(v) = maybe_v { + write!(f, "{}", v)?; + } + } + } + write!(f, "]")?; + write!(f, "{}", display_optional(*optional))?; + if let Some(ts_type) = ts_type { + write!(f, ": {}", ts_type)?; + } + Ok(()) + } + Self::Assign { left, ts_type, .. } => { + write!(f, "{}", left)?; + if let Some(ts_type) = ts_type { + write!(f, ": {}", ts_type)?; + } + // TODO(SyrupThinker) As we cannot display expressions the value is just omitted + // write!(f, " = {}", right)?; + Ok(()) + } + Self::Identifier { + name, + optional, + ts_type, + } => { + write!(f, "{}{}", name, display_optional(*optional))?; + if let Some(ts_type) = ts_type { + write!(f, ": {}", ts_type)?; + } + Ok(()) + } + Self::Object { + props, + optional, + ts_type, + } => { + write!( + f, + "{{{}}}{}", + SliceDisplayer::new(&props, ", ", false), + display_optional(*optional) + )?; + if let Some(ts_type) = ts_type { + write!(f, ": {}", ts_type)?; + } + Ok(()) + } + Self::Rest { arg, ts_type } => { + write!(f, "...{}", arg)?; + if let Some(ts_type) = ts_type { + write!(f, ": {}", ts_type)?; + } + Ok(()) + } + } + } +} + +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +#[serde(tag = "kind")] +pub enum ObjectPatPropDef { + Assign { key: String, value: Option }, + KeyValue { key: String, value: Box }, + Rest { arg: Box }, +} + +impl Display for ObjectPatPropDef { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + match self { + Self::KeyValue { key, .. } => { + // The internal identifier does not need to be exposed + write!(f, "{}", key) + } + Self::Assign { key, value } => { + if let Some(_value) = value { + // TODO(SyrupThinker) As we cannot display expressions the value is just omitted + write!(f, "{}", key) + } else { + write!(f, "{}", key) + } + } + Self::Rest { arg } => write!(f, "...{}", arg), + } + } +} + +pub fn ident_to_param_def( + ident: &swc_ecma_ast::Ident, + _source_map: Option<&SourceMap>, +) -> ParamDef { let ts_type = ident.type_ann.as_ref().map(|rt| ts_type_ann_to_def(rt)); - ParamDef { + ParamDef::Identifier { name: ident.sym.to_string(), - kind: ParamKind::Identifier, optional: ident.optional, ts_type, } } -fn rest_pat_to_param_def(rest_pat: &swc_ecma_ast::RestPat) -> ParamDef { - let name = match &*rest_pat.arg { - Pat::Ident(ident) => ident.sym.to_string(), - _ => "".to_string(), - }; +fn rest_pat_to_param_def( + rest_pat: &swc_ecma_ast::RestPat, + source_map: Option<&SourceMap>, +) -> ParamDef { let ts_type = rest_pat.type_ann.as_ref().map(|rt| ts_type_ann_to_def(rt)); - ParamDef { - name, - kind: ParamKind::Rest, - optional: false, + ParamDef::Rest { + arg: Box::new(pat_to_param_def(&*rest_pat.arg, source_map)), ts_type, } } -fn object_pat_to_param_def(object_pat: &swc_ecma_ast::ObjectPat) -> ParamDef { +fn object_pat_prop_to_def( + object_pat_prop: &ObjectPatProp, + source_map: Option<&SourceMap>, +) -> ObjectPatPropDef { + match object_pat_prop { + ObjectPatProp::Assign(assign) => ObjectPatPropDef::Assign { + key: assign.key.sym.to_string(), + value: assign.value.as_ref().map(|_| "".to_string()), + }, + ObjectPatProp::KeyValue(keyvalue) => ObjectPatPropDef::KeyValue { + key: prop_name_to_string(&keyvalue.key, source_map), + value: Box::new(pat_to_param_def(&*keyvalue.value, source_map)), + }, + ObjectPatProp::Rest(rest) => ObjectPatPropDef::Rest { + arg: Box::new(pat_to_param_def(&*rest.arg, source_map)), + }, + } +} + +fn object_pat_to_param_def( + object_pat: &swc_ecma_ast::ObjectPat, + source_map: Option<&SourceMap>, +) -> ParamDef { + let props = object_pat + .props + .iter() + .map(|prop| object_pat_prop_to_def(prop, source_map)) + .collect::>(); let ts_type = object_pat .type_ann .as_ref() .map(|rt| ts_type_ann_to_def(rt)); - ParamDef { - name: "".to_string(), - kind: ParamKind::Object, + ParamDef::Object { + props, optional: object_pat.optional, ts_type, } } -fn array_pat_to_param_def(array_pat: &swc_ecma_ast::ArrayPat) -> ParamDef { +fn array_pat_to_param_def( + array_pat: &swc_ecma_ast::ArrayPat, + source_map: Option<&SourceMap>, +) -> ParamDef { + let elements = array_pat + .elems + .iter() + .map(|elem| elem.as_ref().map(|e| pat_to_param_def(e, source_map))) + .collect::>>(); let ts_type = array_pat.type_ann.as_ref().map(|rt| ts_type_ann_to_def(rt)); - ParamDef { - name: "".to_string(), - kind: ParamKind::Array, + ParamDef::Array { + elements, optional: array_pat.optional, ts_type, } @@ -60,28 +230,61 @@ fn array_pat_to_param_def(array_pat: &swc_ecma_ast::ArrayPat) -> ParamDef { pub fn assign_pat_to_param_def( assign_pat: &swc_ecma_ast::AssignPat, + source_map: Option<&SourceMap>, ) -> ParamDef { - pat_to_param_def(&*assign_pat.left) + let ts_type = assign_pat + .type_ann + .as_ref() + .map(|rt| ts_type_ann_to_def(rt)); + + ParamDef::Assign { + left: Box::new(pat_to_param_def(&*assign_pat.left, source_map)), + right: "".to_string(), + ts_type, + } } -pub fn pat_to_param_def(pat: &swc_ecma_ast::Pat) -> ParamDef { +pub fn pat_to_param_def( + pat: &swc_ecma_ast::Pat, + source_map: Option<&SourceMap>, +) -> ParamDef { match pat { - Pat::Ident(ident) => ident_to_param_def(ident), - Pat::Array(array_pat) => array_pat_to_param_def(array_pat), - Pat::Rest(rest_pat) => rest_pat_to_param_def(rest_pat), - Pat::Object(object_pat) => object_pat_to_param_def(object_pat), - Pat::Assign(assign_pat) => assign_pat_to_param_def(assign_pat), + Pat::Ident(ident) => ident_to_param_def(ident, source_map), + Pat::Array(array_pat) => array_pat_to_param_def(array_pat, source_map), + Pat::Rest(rest_pat) => rest_pat_to_param_def(rest_pat, source_map), + Pat::Object(object_pat) => object_pat_to_param_def(object_pat, source_map), + Pat::Assign(assign_pat) => assign_pat_to_param_def(assign_pat, source_map), _ => unreachable!(), } } pub fn ts_fn_param_to_param_def( ts_fn_param: &swc_ecma_ast::TsFnParam, + source_map: Option<&SourceMap>, ) -> ParamDef { match ts_fn_param { - TsFnParam::Ident(ident) => ident_to_param_def(ident), - TsFnParam::Array(array_pat) => array_pat_to_param_def(array_pat), - TsFnParam::Rest(rest_pat) => rest_pat_to_param_def(rest_pat), - TsFnParam::Object(object_pat) => object_pat_to_param_def(object_pat), + TsFnParam::Ident(ident) => ident_to_param_def(ident, source_map), + TsFnParam::Array(array_pat) => { + array_pat_to_param_def(array_pat, source_map) + } + TsFnParam::Rest(rest_pat) => rest_pat_to_param_def(rest_pat, source_map), + TsFnParam::Object(object_pat) => { + object_pat_to_param_def(object_pat, source_map) + } + } +} + +pub fn prop_name_to_string( + prop_name: &swc_ecma_ast::PropName, + source_map: Option<&SourceMap>, +) -> String { + use crate::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 + .map(|sm| sm.span_to_snippet(comp_prop_name.span).unwrap()) + .unwrap_or_else(|| "".to_string()), } } diff --git a/cli/doc/parser.rs b/cli/doc/parser.rs index 3746e3dfba..59fa2b7342 100644 --- a/cli/doc/parser.rs +++ b/cli/doc/parser.rs @@ -44,13 +44,15 @@ pub trait DocFileLoader { pub struct DocParser { pub ast_parser: AstParser, pub loader: Box, + pub private: bool, } impl DocParser { - pub fn new(loader: Box) -> Self { + pub fn new(loader: Box, private: bool) -> Self { DocParser { loader, ast_parser: AstParser::new(), + private, } } @@ -70,7 +72,7 @@ impl DocParser { self.get_doc_nodes_for_module_body(module.body.clone()); let reexports = self.get_reexports_for_module_body(module.body); let module_doc = ModuleDoc { - exports: doc_entries, + definitions: doc_entries, reexports, }; Ok(module_doc) @@ -90,7 +92,7 @@ impl DocParser { source_code: &str, ) -> Result, ErrBox> { let module_doc = self.parse_module(file_name, &source_code)?; - Ok(module_doc.exports) + Ok(module_doc.definitions) } async fn flatten_reexports( @@ -187,10 +189,10 @@ impl DocParser { let mut flattenned_reexports = self .flatten_reexports(&module_doc.reexports, file_name) .await?; - flattenned_reexports.extend(module_doc.exports); + flattenned_reexports.extend(module_doc.definitions); flattenned_reexports } else { - module_doc.exports + module_doc.definitions }; Ok(flattened_docs) @@ -231,8 +233,10 @@ impl DocParser { } } DefaultDecl::Fn(fn_expr) => { - let function_def = - crate::doc::function::function_to_function_def(&fn_expr.function); + let function_def = crate::doc::function::function_to_function_def( + self, + &fn_expr.function, + ); DocNode { kind: DocNodeKind::Function, name, @@ -292,7 +296,7 @@ impl DocParser { pub fn get_doc_node_for_decl(&self, decl: &Decl) -> Option { match decl { Decl::Class(class_decl) => { - if !class_decl.declare { + if !self.private && !class_decl.declare { return None; } let (name, class_def) = @@ -313,11 +317,11 @@ impl DocParser { }) } Decl::Fn(fn_decl) => { - if !fn_decl.declare { + if !self.private && !fn_decl.declare { return None; } let (name, function_def) = - super::function::get_doc_for_fn_decl(fn_decl); + super::function::get_doc_for_fn_decl(self, fn_decl); let (js_doc, location) = self.details_for_span(fn_decl.function.span); Some(DocNode { kind: DocNodeKind::Function, @@ -334,7 +338,7 @@ impl DocParser { }) } Decl::Var(var_decl) => { - if !var_decl.declare { + if !self.private && !var_decl.declare { return None; } let (name, var_def) = super::variable::get_doc_for_var_decl(var_decl); @@ -354,7 +358,7 @@ impl DocParser { }) } Decl::TsInterface(ts_interface_decl) => { - if !ts_interface_decl.declare { + if !self.private && !ts_interface_decl.declare { return None; } let (name, interface_def) = @@ -378,7 +382,7 @@ impl DocParser { }) } Decl::TsTypeAlias(ts_type_alias) => { - if !ts_type_alias.declare { + if !self.private && !ts_type_alias.declare { return None; } let (name, type_alias_def) = @@ -402,7 +406,7 @@ impl DocParser { }) } Decl::TsEnum(ts_enum) => { - if !ts_enum.declare { + if !self.private && !ts_enum.declare { return None; } let (name, enum_def) = @@ -423,7 +427,7 @@ impl DocParser { }) } Decl::TsModule(ts_module) => { - if !ts_module.declare { + if !self.private && !ts_module.declare { return None; } let (name, namespace_def) = diff --git a/cli/doc/printer.rs b/cli/doc/printer.rs index b563a6adcf..2e55c752a6 100644 --- a/cli/doc/printer.rs +++ b/cli/doc/printer.rs @@ -12,540 +12,463 @@ use crate::colors; use crate::doc; -use crate::doc::ts_type::TsTypeDefKind; +use crate::doc::display::{ + display_abstract, display_async, display_generator, Indent, SliceDisplayer, +}; use crate::doc::DocNodeKind; use crate::swc_ecma_ast; +use std::fmt::{Display, Formatter, Result as FmtResult}; -pub fn format(doc_nodes: Vec) -> String { - format_(doc_nodes, 0) +pub struct DocPrinter<'a> { + doc_nodes: &'a [doc::DocNode], + details: bool, + private: bool, } -pub fn format_details(node: doc::DocNode) -> String { - let mut details = String::new(); - - details.push_str(&format!( - "{}", - colors::gray(&format!( - "Defined in {}:{}:{} \n\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, 1)); - } - details.push_str("\n"); - - let maybe_extra = match node.kind { - DocNodeKind::Class => Some(format_class_details(node)), - DocNodeKind::Enum => Some(format_enum_details(node)), - DocNodeKind::Namespace => Some(format_namespace_details(node)), - _ => None, - }; - - if let Some(extra) = maybe_extra { - details.push_str(&extra); +impl<'a> DocPrinter<'a> { + pub fn new( + doc_nodes: &[doc::DocNode], + details: bool, + private: bool, + ) -> DocPrinter { + DocPrinter { + doc_nodes, + details, + private, + } } - 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, 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) + pub fn format(&self, w: &mut Formatter<'_>) -> FmtResult { + if self.details { + self.format_details(w, self.doc_nodes, 0) } else { - kind_cmp - } - }); - - let mut output = String::new(); - - for node in sorted { - output.push_str(&format_signature(&node, indent)); - if let Some(js_doc) = node.js_doc { - output.push_str(&format_jsdoc(js_doc, 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) -> String { - let mut rendered = String::from(""); - if !params.is_empty() { - for param in params { - rendered += param.name.as_str(); - if param.optional { - rendered += "?"; - } - if let Some(ts_type) = param.ts_type { - rendered += ": "; - rendered += render_ts_type(ts_type).as_str(); - } - rendered += ", "; - } - rendered.truncate(rendered.len() - 2); - } - rendered -} - -fn render_ts_type(ts_type: doc::ts_type::TsTypeDef) -> String { - if ts_type.kind.is_none() { - return "".to_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 => { - format!("{}?", render_ts_type(*ts_type.optional.unwrap())) - } - 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 += "]"; - 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), - if let Some(ts_type) = node.ts_type { - format!(": {}", render_ts_type(ts_type)) - } else { - "".to_string() - } - ) - .as_str() - } - for node in type_literal.methods { - output += format!( - "{}({}){}, ", - node.name, - render_params(node.params), - if let Some(return_type) = node.return_type { - format!(": {}", render_ts_type(return_type)) - } else { - "".to_string() - } - ) - .as_str() - } - for node in type_literal.properties { - output += format!( - "{}{}, ", - node.name, - if let Some(ts_type) = node.ts_type { - format!(": {}", render_ts_type(ts_type)) - } else { - "".to_string() - } - ) - .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 let Some(type_params) = type_ref.type_params { - let mut output = "".to_string(); - 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 + self.format_summary(w, self.doc_nodes, 0) } } -} -fn add_indent(string: String, indent: i64) -> String { - let mut indent_str = String::new(); - for _ in 0..(indent * 2) { - indent_str += " "; - } - indent_str += string.as_str(); - indent_str -} - -// TODO: this should use some sort of markdown to console parser. -fn format_jsdoc(jsdoc: String, indent: i64) -> String { - let lines = jsdoc.split("\n\n").map(|line| line.replace("\n", " ")); - - let mut js_doc = String::new(); - - for line in lines { - js_doc.push_str(&add_indent(format!("{}\n", line), indent + 1)); - } - - format!("{}", colors::gray(&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(&add_indent( - format!( - "{} {}({})\n", - colors::magenta("constructor"), - colors::bold(&node.name), - render_params(node.params), - ), - 1, - )); - } - 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(&add_indent( - format!( - "{}{}{}{}\n", - colors::magenta( - match node - .accessibility - .unwrap_or(swc_ecma_ast::Accessibility::Public) - { - swc_ecma_ast::Accessibility::Protected => "protected ", - _ => "", - } - ), - colors::bold(&node.name), - if node.optional { - "?".to_string() - } else { - "".to_string() - }, - if let Some(ts_type) = node.ts_type.clone() { - format!(": {}", render_ts_type(ts_type)) - } else { - "".to_string() - } - ), - 1, - )); - } - 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(&add_indent( - format!( - "{}{}{}{}({}){}\n", - colors::magenta( - match node - .accessibility - .unwrap_or(swc_ecma_ast::Accessibility::Public) - { - swc_ecma_ast::Accessibility::Protected => "protected ", - _ => "", - } - ), - colors::magenta(match node.kind { - swc_ecma_ast::MethodKind::Getter => "get ", - swc_ecma_ast::MethodKind::Setter => "set ", - _ => "", - }), - colors::bold(&node.name), - if node.optional { "?" } else { "" }, - render_params(function_def.params), - if let Some(return_type) = function_def.return_type { - format!(": {}", render_ts_type(return_type)) - } else { - "".to_string() - } - ), - 1, - )); - } - details.push_str("\n"); - details -} - -fn format_enum_details(node: doc::DocNode) -> String { - let mut details = String::new(); - let enum_def = node.enum_def.unwrap(); - for member in enum_def.members { - details - .push_str(&add_indent(format!("{}\n", colors::bold(&member.name)), 1)); - } - 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, 1)); - } - ns.push_str("\n"); - ns -} - -fn format_function_signature(node: &doc::DocNode, indent: i64) -> String { - let function_def = node.function_def.clone().unwrap(); - add_indent( - format!( - "{} {}({}){}\n", - colors::magenta("function"), - colors::bold(&node.name), - render_params(function_def.params), - if let Some(return_type) = function_def.return_type { - format!(": {}", render_ts_type(return_type).as_str()) + fn format_summary( + &self, + w: &mut Formatter<'_>, + doc_nodes: &[doc::DocNode], + indent: i64, + ) -> FmtResult { + let mut sorted = Vec::from(doc_nodes); + sorted.sort_unstable_by(|a, b| { + let kind_cmp = self.kind_order(&a.kind).cmp(&self.kind_order(&b.kind)); + if kind_cmp == core::cmp::Ordering::Equal { + a.name.cmp(&b.name) } else { - "".to_string() + kind_cmp } - ), - indent, - ) -} + }); -fn format_class_signature(node: &doc::DocNode, indent: i64) -> String { - let class_def = node.class_def.clone().unwrap(); - let extends_suffix = if let Some(extends) = class_def.extends { - format!(" {} {}", colors::magenta("extends"), colors::bold(&extends)) - } else { - String::from("") - }; + for node in sorted { + self.format_signature(w, &node, indent)?; - let implements = &class_def.implements; - let implements_suffix = if !implements.is_empty() { - format!( - " {} {}", - colors::magenta("implements"), - colors::bold(&implements.join(", ")) - ) - } else { - String::from("") - }; + if let Some(js_doc) = &node.js_doc { + self.format_jsdoc(w, js_doc, indent + 1, self.details)?; + } - add_indent( - format!( - "{} {}{}{}\n", + writeln!(w)?; + + if DocNodeKind::Namespace == node.kind { + self.format_summary( + w, + &node.namespace_def.as_ref().unwrap().elements, + indent + 1, + )?; + + writeln!(w)?; + }; + } + + Ok(()) + } + + fn format_details( + &self, + w: &mut Formatter<'_>, + doc_nodes: &[doc::DocNode], + indent: i64, + ) -> FmtResult { + for node in doc_nodes { + write!( + w, + "{}", + colors::italic_gray(&format!( + "Defined in {}:{}:{} \n\n", + node.location.filename, node.location.line, node.location.col + )) + )?; + + self.format_signature(w, &node, indent)?; + + let js_doc = &node.js_doc; + if let Some(js_doc) = js_doc { + self.format_jsdoc(w, js_doc, indent + 1, self.details)?; + } + writeln!(w)?; + + match node.kind { + DocNodeKind::Class => self.format_class_details(w, node)?, + DocNodeKind::Enum => self.format_enum_details(w, node)?, + DocNodeKind::Interface => self.format_interface_details(w, node)?, + DocNodeKind::Namespace => self.format_namespace_details(w, node)?, + _ => {} + } + } + + Ok(()) + } + + fn kind_order(&self, 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( + &self, + w: &mut Formatter<'_>, + node: &doc::DocNode, + indent: i64, + ) -> FmtResult { + match node.kind { + DocNodeKind::Function => self.format_function_signature(w, node, indent), + DocNodeKind::Variable => self.format_variable_signature(w, node, indent), + DocNodeKind::Class => self.format_class_signature(w, node, indent), + DocNodeKind::Enum => self.format_enum_signature(w, node, indent), + DocNodeKind::Interface => { + self.format_interface_signature(w, node, indent) + } + DocNodeKind::TypeAlias => { + self.format_type_alias_signature(w, node, indent) + } + DocNodeKind::Namespace => { + self.format_namespace_signature(w, node, indent) + } + } + } + + // TODO(SyrupThinker) this should use a JSDoc parser + fn format_jsdoc( + &self, + w: &mut Formatter<'_>, + jsdoc: &str, + indent: i64, + details: bool, + ) -> FmtResult { + for line in jsdoc.lines() { + // Only show the first paragraph when summarising + // This should use the @summary JSDoc tag instead + if !details && line.is_empty() { + break; + } + + writeln!(w, "{}{}", Indent(indent), colors::gray(&line))?; + } + + Ok(()) + } + + fn format_class_details( + &self, + w: &mut Formatter<'_>, + node: &doc::DocNode, + ) -> FmtResult { + let class_def = node.class_def.as_ref().unwrap(); + for node in &class_def.constructors { + writeln!(w, "{}{}", Indent(1), node,)?; + if let Some(js_doc) = &node.js_doc { + self.format_jsdoc(w, &js_doc, 2, self.details)?; + } + } + for node in class_def.properties.iter().filter(|node| { + self.private + || node + .accessibility + .unwrap_or(swc_ecma_ast::Accessibility::Public) + != swc_ecma_ast::Accessibility::Private + }) { + writeln!(w, "{}{}", Indent(1), node,)?; + if let Some(js_doc) = &node.js_doc { + self.format_jsdoc(w, &js_doc, 2, self.details)?; + } + } + for index_sign_def in &class_def.index_signatures { + writeln!(w, "{}{}", Indent(1), index_sign_def)?; + } + for node in class_def.methods.iter().filter(|node| { + self.private + || node + .accessibility + .unwrap_or(swc_ecma_ast::Accessibility::Public) + != swc_ecma_ast::Accessibility::Private + }) { + writeln!(w, "{}{}", Indent(1), node,)?; + if let Some(js_doc) = &node.js_doc { + self.format_jsdoc(w, js_doc, 2, self.details)?; + } + } + writeln!(w) + } + + fn format_enum_details( + &self, + w: &mut Formatter<'_>, + node: &doc::DocNode, + ) -> FmtResult { + let enum_def = node.enum_def.as_ref().unwrap(); + for member in &enum_def.members { + writeln!(w, "{}{}", Indent(1), colors::bold(&member.name))?; + } + writeln!(w) + } + + fn format_interface_details( + &self, + w: &mut Formatter<'_>, + node: &doc::DocNode, + ) -> FmtResult { + let interface_def = node.interface_def.as_ref().unwrap(); + + for property_def in &interface_def.properties { + writeln!(w, "{}{}", Indent(1), property_def)?; + if let Some(js_doc) = &property_def.js_doc { + self.format_jsdoc(w, js_doc, 2, self.details)?; + } + } + for method_def in &interface_def.methods { + writeln!(w, "{}{}", Indent(1), method_def)?; + if let Some(js_doc) = &method_def.js_doc { + self.format_jsdoc(w, js_doc, 2, self.details)?; + } + } + for index_sign_def in &interface_def.index_signatures { + writeln!(w, "{}{}", Indent(1), index_sign_def)?; + } + writeln!(w) + } + + fn format_namespace_details( + &self, + w: &mut Formatter<'_>, + node: &doc::DocNode, + ) -> FmtResult { + let elements = &node.namespace_def.as_ref().unwrap().elements; + for node in elements { + self.format_signature(w, &node, 1)?; + if let Some(js_doc) = &node.js_doc { + self.format_jsdoc(w, js_doc, 2, false)?; + } + } + writeln!(w) + } + + fn format_class_signature( + &self, + w: &mut Formatter<'_>, + node: &doc::DocNode, + indent: i64, + ) -> FmtResult { + let class_def = node.class_def.as_ref().unwrap(); + write!( + w, + "{}{}{} {}", + Indent(indent), + display_abstract(class_def.is_abstract), colors::magenta("class"), colors::bold(&node.name), - extends_suffix, - implements_suffix, - ), - indent, - ) -} + )?; + if !class_def.type_params.is_empty() { + write!( + w, + "<{}>", + SliceDisplayer::new(&class_def.type_params, ", ", false) + )?; + } -fn format_variable_signature(node: &doc::DocNode, indent: i64) -> String { - let variable_def = node.variable_def.clone().unwrap(); - add_indent( - format!( - "{} {}{}\n", + if let Some(extends) = &class_def.extends { + write!(w, " {} {}", colors::magenta("extends"), extends)?; + } + if !class_def.super_type_params.is_empty() { + write!( + w, + "<{}>", + SliceDisplayer::new(&class_def.super_type_params, ", ", false) + )?; + } + + if !class_def.implements.is_empty() { + write!( + w, + " {} {}", + colors::magenta("implements"), + SliceDisplayer::new(&class_def.implements, ", ", false) + )?; + } + + writeln!(w) + } + + fn format_enum_signature( + &self, + w: &mut Formatter<'_>, + node: &doc::DocNode, + indent: i64, + ) -> FmtResult { + writeln!( + w, + "{}{} {}", + Indent(indent), + colors::magenta("enum"), + colors::bold(&node.name) + ) + } + + fn format_function_signature( + &self, + w: &mut Formatter<'_>, + node: &doc::DocNode, + indent: i64, + ) -> FmtResult { + let function_def = node.function_def.as_ref().unwrap(); + write!( + w, + "{}{}{}{} {}", + Indent(indent), + display_async(function_def.is_async), + colors::magenta("function"), + display_generator(function_def.is_generator), + colors::bold(&node.name) + )?; + if !function_def.type_params.is_empty() { + write!( + w, + "<{}>", + SliceDisplayer::new(&function_def.type_params, ", ", false) + )?; + } + write!( + w, + "({})", + SliceDisplayer::new(&function_def.params, ", ", false) + )?; + if let Some(return_type) = &function_def.return_type { + write!(w, ": {}", return_type)?; + } + writeln!(w) + } + + fn format_interface_signature( + &self, + w: &mut Formatter<'_>, + node: &doc::DocNode, + indent: i64, + ) -> FmtResult { + let interface_def = node.interface_def.as_ref().unwrap(); + write!( + w, + "{}{} {}", + Indent(indent), + colors::magenta("interface"), + colors::bold(&node.name) + )?; + + if !interface_def.type_params.is_empty() { + write!( + w, + "<{}>", + SliceDisplayer::new(&interface_def.type_params, ", ", false) + )?; + } + + if !interface_def.extends.is_empty() { + write!( + w, + " {} {}", + colors::magenta("extends"), + SliceDisplayer::new(&interface_def.extends, ", ", false) + )?; + } + + writeln!(w) + } + + fn format_type_alias_signature( + &self, + w: &mut Formatter<'_>, + node: &doc::DocNode, + indent: i64, + ) -> FmtResult { + let type_alias_def = node.type_alias_def.as_ref().unwrap(); + write!( + w, + "{}{} {}", + Indent(indent), + colors::magenta("type"), + colors::bold(&node.name), + )?; + + if !type_alias_def.type_params.is_empty() { + write!( + w, + "<{}>", + SliceDisplayer::new(&type_alias_def.type_params, ", ", false) + )?; + } + + writeln!(w, " = {}", type_alias_def.ts_type) + } + + fn format_namespace_signature( + &self, + w: &mut Formatter<'_>, + node: &doc::DocNode, + indent: i64, + ) -> FmtResult { + writeln!( + w, + "{}{} {}", + Indent(indent), + colors::magenta("namespace"), + colors::bold(&node.name) + ) + } + + fn format_variable_signature( + &self, + w: &mut Formatter<'_>, + node: &doc::DocNode, + indent: i64, + ) -> FmtResult { + let variable_def = node.variable_def.as_ref().unwrap(); + write!( + w, + "{}{} {}", + Indent(indent), colors::magenta(match variable_def.kind { swc_ecma_ast::VarDeclKind::Const => "const", swc_ecma_ast::VarDeclKind::Let => "let", swc_ecma_ast::VarDeclKind::Var => "var", }), colors::bold(&node.name), - if let Some(ts_type) = variable_def.ts_type { - format!(": {}", render_ts_type(ts_type)) - } else { - "".to_string() - } - ), - indent, - ) + )?; + if let Some(ts_type) = &variable_def.ts_type { + write!(w, ": {}", ts_type)?; + } + writeln!(w) + } } -fn format_enum_signature(node: &doc::DocNode, indent: i64) -> String { - add_indent( - format!("{} {}\n", colors::magenta("enum"), colors::bold(&node.name)), - indent, - ) -} - -fn format_interface_signature(node: &doc::DocNode, indent: i64) -> String { - let interface_def = node.interface_def.clone().unwrap(); - let extends = &interface_def.extends; - let extends_suffix = if !extends.is_empty() { - format!( - " {} {}", - colors::magenta("extends"), - colors::bold(&extends.join(", ")) - ) - } else { - String::from("") - }; - add_indent( - format!( - "{} {}{}\n", - colors::magenta("interface"), - colors::bold(&node.name), - extends_suffix - ), - indent, - ) -} - -fn format_type_alias_signature(node: &doc::DocNode, indent: i64) -> String { - add_indent( - format!("{} {}\n", colors::magenta("type"), colors::bold(&node.name)), - indent, - ) -} - -fn format_namespace_signature(node: &doc::DocNode, indent: i64) -> String { - add_indent( - format!( - "{} {}\n", - colors::magenta("namespace"), - colors::bold(&node.name) - ), - indent, - ) +impl<'a> Display for DocPrinter<'a> { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + self.format(f) + } } diff --git a/cli/doc/tests.rs b/cli/doc/tests.rs index 7efc3857b7..e46fff6210 100644 --- a/cli/doc/tests.rs +++ b/cli/doc/tests.rs @@ -1,5 +1,6 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. use super::DocParser; +use super::DocPrinter; use crate::colors; use serde_json::json; @@ -41,360 +42,334 @@ impl DocFileLoader for TestLoader { } } +macro_rules! doc_test { + ( $name:ident, $source:expr; $block:block ) => { + doc_test!($name, $source, false, false; $block); + }; + + ( $name:ident, $source:expr, details; $block:block ) => { + doc_test!($name, $source, true, false; $block); + }; + + ( $name:ident, $source:expr, private; $block:block ) => { + doc_test!($name, $source, false, true; $block); + }; + + ( $name:ident, $source:expr, details, private; $block:block ) => { + doc_test!($name, $source, true, true; $block); + }; + + ( $name:ident, $source:expr, $details:expr, $private:expr; $block:block ) => { + #[tokio::test] + async fn $name() { + let source_code = $source; + let details = $details; + let private = $private; + + let loader = + TestLoader::new(vec![("test.ts".to_string(), source_code.to_string())]); + let entries = DocParser::new(loader, private) + .parse("test.ts") + .await + .unwrap(); + + let doc = DocPrinter::new(&entries, details, private).to_string(); + #[allow(unused_variables)] + let doc = colors::strip_ansi_codes(&doc); + + $block + } + }; +} + +macro_rules! contains_test { + ( $name:ident, $source:expr; + $( $contains:expr ),* $( ; $( $notcontains:expr ),* )? ) => { + contains_test!($name, $source, false, false; $($contains),* $(;$($notcontains),*)?); + }; + + ( $name:ident, $source:expr, details; + $( $contains:expr ),* $( ; $( $notcontains:expr ),* )? ) => { + contains_test!($name, $source, true, false; $($contains),* $(;$($notcontains),*)?); + }; + + ( $name:ident, $source:expr, private; + $( $contains:expr ),* $( ; $( $notcontains:expr ),* )? ) => { + contains_test!($name, $source, false, true; $($contains),* $(;$($notcontains),*)?); + }; + + ( $name:ident, $source:expr, details, private; + $( $contains:expr ),* $( ; $( $notcontains:expr ),* )? ) => { + contains_test!($name, $source, true, true; $($contains),* $(;$($notcontains),*)?); + }; + + ( $name:ident, $source:expr, $details:expr, $private:expr; + $( $contains:expr ),* $( ; $( $notcontains:expr ),* )? ) => { + doc_test!($name, $source, $details, $private; { + $( + assert!(doc.contains($contains)); + )* + $( + $( + assert!(!doc.contains($notcontains)); + )* + )? + }); + }; +} + +macro_rules! json_test { + ( $name:ident, $source:expr; $json:tt ) => { + json_test!($name, $source, false; $json); + }; + + ( $name:ident, $source:expr, private; $json:tt ) => { + json_test!($name, $source, true; $json); + }; + + ( $name:ident, $source:expr, $private:expr; $json:tt ) => { + doc_test!($name, $source, false, $private; { + let actual = serde_json::to_value(&entries).unwrap(); + let expected_json = json!($json); + assert_eq!(actual, expected_json); + }); + }; +} + #[tokio::test] -async fn export_fn() { - let source_code = r#"/** -* @module foo -*/ +async fn reexports() { + let nested_reexport_source_code = r#" +/** + * JSDoc for bar + */ +export const bar = "bar"; + +export default 42; +"#; + let reexport_source_code = r#" +import { bar } from "./nested_reexport.ts"; /** -* Hello there, this is a multiline JSdoc. -* -* It has many lines -* -* Or not that many? -*/ -export function foo(a: string, b?: number, cb: (...cbArgs: unknown[]) => void, ...args: unknown[]): void { - /** - * @todo document all the things. - */ - console.log("Hello world"); + * JSDoc for const + */ +export const foo = "foo"; +"#; + let test_source_code = r#" +export { default, foo as fooConst } from "./reexport.ts"; + +/** JSDoc for function */ +export function fooFn(a: number) { + return a; } "#; - let loader = - TestLoader::new(vec![("test.ts".to_string(), source_code.to_string())]); - let entries = DocParser::new(loader).parse("test.ts").await.unwrap(); - assert_eq!(entries.len(), 1); - let entry = &entries[0]; - let expected_json = json!({ - "functionDef": { - "isAsync": false, - "isGenerator": false, - "typeParams": [], - "params": [ - { - "name": "a", - "kind": "identifier", - "optional": false, - "tsType": { - "keyword": "string", - "kind": "keyword", - "repr": "string", - }, - }, - { - "name": "b", - "kind": "identifier", - "optional": true, - "tsType": { - "keyword": "number", - "kind": "keyword", - "repr": "number", - }, - }, - { - "name": "cb", - "kind": "identifier", - "optional": false, - "tsType": { - "repr": "", - "kind": "fnOrConstructor", - "fnOrConstructor": { - "constructor": false, - "tsType": { - "keyword": "void", - "kind": "keyword", - "repr": "void" - }, - "typeParams": [], - "params": [{ - "kind": "rest", - "name": "cbArgs", - "optional": false, - "tsType": { - "repr": "", - "kind": "array", - "array": { - "repr": "unknown", - "kind": "keyword", - "keyword": "unknown" - } - }, - }] - } - }, - }, - { - "name": "args", - "kind": "rest", - "optional": false, - "tsType": { - "repr": "", - "kind": "array", - "array": { - "repr": "unknown", - "kind": "keyword", - "keyword": "unknown" - } - } - } - ], - "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": 12, - }, - "name": "foo", - }); + let loader = TestLoader::new(vec![ + ("file:///test.ts".to_string(), test_source_code.to_string()), + ( + "file:///reexport.ts".to_string(), + reexport_source_code.to_string(), + ), + ( + "file:///nested_reexport.ts".to_string(), + nested_reexport_source_code.to_string(), + ), + ]); + let entries = DocParser::new(loader, false) + .parse_with_reexports("file:///test.ts") + .await + .unwrap(); + assert_eq!(entries.len(), 2); - let actual = serde_json::to_value(entry).unwrap(); + let expected_json = json!([ + { + "kind": "variable", + "name": "fooConst", + "location": { + "filename": "file:///reexport.ts", + "line": 7, + "col": 0 + }, + "jsDoc": "JSDoc for const", + "variableDef": { + "tsType": null, + "kind": "const" + } + }, + { + "kind": "function", + "name": "fooFn", + "location": { + "filename": "file:///test.ts", + "line": 5, + "col": 0 + }, + "jsDoc": "JSDoc for function", + "functionDef": { + "params": [ + { + "name": "a", + "kind": "identifier", + "optional": false, + "tsType": { + "keyword": "number", + "kind": "keyword", + "repr": "number", + }, + } + ], + "typeParams": [], + "returnType": null, + "isAsync": false, + "isGenerator": false + } + } + ]); + let actual = serde_json::to_value(&entries).unwrap(); assert_eq!(actual, expected_json); assert!(colors::strip_ansi_codes( - super::printer::format(entries.clone()).as_str() + DocPrinter::new(&entries, false, false).to_string().as_str() ) - .contains("Hello there")); - assert!( - colors::strip_ansi_codes(super::printer::format(entries).as_str()) - .contains("b?: number") - ); + .contains("function fooFn(a: number)")); } #[tokio::test] -async fn format_type_predicate() { +async fn filter_nodes_by_name() { + use super::find_nodes_by_name_recursively; let source_code = r#" -export function isFish(pet: Fish | Bird): pet is Fish { - return (pet as Fish).swim !== undefined; +export namespace Deno { + export class Buffer {} + export function test(options: object): void; + export function test(name: string, fn: Function): void; + export function test(name: string | object, fn?: Function): void {} +} + +export namespace Deno { + export namespace Inner { + export function a(): void {} + export const b = 100; + } } "#; let loader = TestLoader::new(vec![("test.ts".to_string(), source_code.to_string())]); - let entries = DocParser::new(loader).parse("test.ts").await.unwrap(); - super::printer::format(entries); + let entries = DocParser::new(loader, false) + .parse("test.ts") + .await + .unwrap(); + + let found = + find_nodes_by_name_recursively(entries.clone(), "Deno".to_string()); + assert_eq!(found.len(), 2); + assert_eq!(found[0].name, "Deno".to_string()); + assert_eq!(found[1].name, "Deno".to_string()); + + let found = + find_nodes_by_name_recursively(entries.clone(), "Deno.test".to_string()); + assert_eq!(found.len(), 3); + assert_eq!(found[0].name, "test".to_string()); + assert_eq!(found[1].name, "test".to_string()); + assert_eq!(found[2].name, "test".to_string()); + + let found = + find_nodes_by_name_recursively(entries.clone(), "Deno.Inner.a".to_string()); + assert_eq!(found.len(), 1); + assert_eq!(found[0].name, "a".to_string()); + + let found = + find_nodes_by_name_recursively(entries.clone(), "Deno.test.a".to_string()); + assert_eq!(found.len(), 0); + + let found = find_nodes_by_name_recursively(entries, "a.b.c".to_string()); + assert_eq!(found.len(), 0); } -#[tokio::test] -async fn export_fn2() { - let source_code = r#" -interface AssignOpts { - a: string; - b: number; -} +mod serialization { + use super::*; -export function foo([e,,f, ...g]: number[], { c, d: asdf, i = "asdf", ...rest}, ops: AssignOpts = {}): void { - console.log("Hello world"); -} -"#; - let loader = - TestLoader::new(vec![("test.ts".to_string(), source_code.to_string())]); - let entries = DocParser::new(loader).parse("test.ts").await.unwrap(); - assert_eq!(entries.len(), 1); - let entry = &entries[0]; - let expected_json = json!({ - "functionDef": { - "isAsync": false, - "isGenerator": false, - "typeParams": [], - "params": [ - { - "name": "", - "kind": "array", - "optional": false, - "tsType": { - "repr": "", - "kind": "array", - "array": { - "repr": "number", - "kind": "keyword", - "keyword": "number" - } - } - }, - { - "name": "", - "kind": "object", - "optional": false, - "tsType": null - }, - { - "name": "ops", - "kind": "identifier", - "optional": false, - "tsType": { - "repr": "AssignOpts", - "kind": "typeRef", - "typeRef": { - "typeName": "AssignOpts", - "typeParams": null, - } - } - }, - ], - "returnType": { - "keyword": "void", - "kind": "keyword", - "repr": "void", - }, - }, - "jsDoc": null, - "kind": "function", - "location": { - "col": 0, - "filename": "test.ts", - "line": 7, - }, - "name": "foo", - }); + json_test!(declare_namespace, + r#" +/** Namespace JSdoc */ +declare namespace RootNs { + declare const a = "a"; - let actual = serde_json::to_value(entry).unwrap(); - assert_eq!(actual, expected_json); - - assert!( - colors::strip_ansi_codes(super::printer::format(entries).as_str()) - .contains("foo") - ); -} - -#[tokio::test] -async fn export_const() { - let source_code = r#" -/** Something about fizzBuzz */ -export const fizzBuzz = "fizzBuzz"; - -export const env: { - /** get doc */ - get(key: string): string | undefined; - - /** set doc */ - set(key: string, value: string): void; -} -"#; - let loader = - TestLoader::new(vec![("test.ts".to_string(), source_code.to_string())]); - let entries = DocParser::new(loader).parse("test.ts").await.unwrap(); - assert_eq!(entries.len(), 2); - let expected_json = json!([ - { - "kind":"variable", - "name":"fizzBuzz", - "location":{ - "filename":"test.ts", - "line":3, - "col":0 - }, - "jsDoc":"Something about fizzBuzz", - "variableDef":{ - "tsType":null, - "kind":"const" - } - }, - { - "kind":"variable", - "name":"env", - "location":{ - "filename":"test.ts", - "line":5, - "col":0 - }, - "jsDoc":null, - "variableDef":{ - "tsType":{ - "repr":"", - "kind":"typeLiteral", - "typeLiteral":{ - "methods":[{ - "name":"get", - "params":[ - { - "name":"key", - "kind":"identifier", - "optional":false, - "tsType":{ - "repr":"string", - "kind":"keyword", - "keyword":"string" - } - } - ], - "returnType":{ - "repr":"", - "kind":"union", - "union":[ - { - "repr":"string", - "kind":"keyword", - "keyword":"string" - }, - { - "repr":"undefined", - "kind":"keyword", - "keyword":"undefined" - } - ] - }, - "typeParams":[] - }, { - "name":"set", - "params":[ - { - "name":"key", - "kind":"identifier", - "optional":false, - "tsType":{ - "repr":"string", - "kind":"keyword", - "keyword":"string" - } - }, - { - "name":"value", - "kind":"identifier", - "optional":false, - "tsType":{ - "repr":"string", - "kind":"keyword", - "keyword":"string" - } - } - ], - "returnType":{ - "repr":"void", - "kind":"keyword", - "keyword":"void" - }, - "typeParams":[] - } - ], - "properties":[], - "callSignatures":[] - } - }, - "kind":"const" + /** Nested namespace JSDoc */ + declare namespace NestedNs { + declare enum Foo { + a = 1, + b = 2, + c = 3, } } - ] - ); - - let actual = serde_json::to_value(entries.clone()).unwrap(); - assert_eq!(actual, expected_json); - - assert!( - colors::strip_ansi_codes(super::printer::format(entries).as_str()) - .contains("Something about fizzBuzz") - ); } + "#; + [{ + "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": 12 + }, + "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" + } + ] + } + } + ] + } + } + ] + } + }]); -#[tokio::test] -async fn export_class() { - let source_code = r#" + json_test!(export_class, + r#" /** Class doc */ export class Foobar extends Fizz implements Buzz, Aldrin { private private1?: boolean; @@ -415,12 +390,8 @@ export class Foobar extends Fizz implements Buzz, Aldrin { // } } -"#; - let loader = - TestLoader::new(vec![("test.ts".to_string(), source_code.to_string())]); - let entries = DocParser::new(loader).parse("test.ts").await.unwrap(); - assert_eq!(entries.len(), 1); - let expected_json = json!({ + "#; + [{ "kind": "class", "name": "Foobar", "location": { @@ -432,8 +403,26 @@ export class Foobar extends Fizz implements Buzz, Aldrin { "classDef": { "isAbstract": false, "extends": "Fizz", - "implements": ["Buzz", "Aldrin"], + "implements": [ + { + "repr": "Buzz", + "kind": "typeRef", + "typeRef": { + "typeParams": null, + "typeName": "Buzz" + } + }, + { + "repr": "Aldrin", + "kind": "typeRef", + "typeRef": { + "typeParams": null, + "typeName": "Aldrin" + } + } + ], "typeParams": [], + "superTypeParams": [], "constructors": [ { "jsDoc": "Constructor js doc", @@ -556,6 +545,7 @@ export class Foobar extends Fizz implements Buzz, Aldrin { } } ], + "indexSignatures": [], "methods": [ { "jsDoc": "Async foo method", @@ -618,489 +608,204 @@ export class Foobar extends Fizz implements Buzz, Aldrin { } ] } - }); - let entry = &entries[0]; - let actual = serde_json::to_value(entry).unwrap(); - assert_eq!(actual, expected_json); + }]); - assert!(colors::strip_ansi_codes( - super::printer::format_details(entry.clone()).as_str() - ) - .contains("bar?(): void")); + json_test!(export_const, + r#" +/** Something about fizzBuzz */ +export const fizzBuzz = "fizzBuzz"; - assert!( - colors::strip_ansi_codes(super::printer::format(entries).as_str()) - .contains("class Foobar extends Fizz implements Buzz, Aldrin") - ); -} +export const env: { + /** get doc */ + get(key: string): string | undefined; -#[tokio::test] -async fn export_interface() { - let source_code = r#" -interface Foo { - foo(): void; -} -interface Bar { - bar(): void; -} -/** - * Interface js doc - */ -export interface Reader extends Foo, Bar { - /** Read n bytes */ - read?(buf: Uint8Array, something: unknown): Promise + /** set doc */ + set(key: string, value: string): void; } "#; - let loader = - TestLoader::new(vec![("test.ts".to_string(), source_code.to_string())]); - let entries = DocParser::new(loader).parse("test.ts").await.unwrap(); - assert_eq!(entries.len(), 1); - let entry = &entries[0]; - let expected_json = json!({ - "kind": "interface", - "name": "Reader", - "location": { - "filename": "test.ts", - "line": 11, - "col": 0 - }, - "jsDoc": "Interface js doc", - "interfaceDef": { - "extends": ["Foo", "Bar"], - "methods": [ - { - "name": "read", - "location": { - "filename": "test.ts", - "line": 13, - "col": 4 - }, - "optional": true, - "jsDoc": "Read n bytes", - "params": [ + [ + { + "kind":"variable", + "name":"fizzBuzz", + "location":{ + "filename":"test.ts", + "line":3, + "col":0 + }, + "jsDoc":"Something about fizzBuzz", + "variableDef":{ + "tsType":null, + "kind":"const" + } + }, + { + "kind":"variable", + "name":"env", + "location":{ + "filename":"test.ts", + "line":5, + "col":0 + }, + "jsDoc":null, + "variableDef":{ + "tsType":{ + "repr":"", + "kind":"typeLiteral", + "typeLiteral":{ + "methods":[{ + "name":"get", + "params":[ { - "name": "buf", - "kind": "identifier", - "optional": false, - "tsType": { - "repr": "Uint8Array", - "kind": "typeRef", - "typeRef": { - "typeParams": null, - "typeName": "Uint8Array" - } - } - }, - { - "name": "something", - "kind": "identifier", - "optional": false, - "tsType": { - "repr": "unknown", - "kind": "keyword", - "keyword": "unknown" + "name":"key", + "kind":"identifier", + "optional":false, + "tsType":{ + "repr":"string", + "kind":"keyword", + "keyword":"string" } } ], - "typeParams": [], - "returnType": { - "repr": "Promise", - "kind": "typeRef", - "typeRef": { - "typeParams": [ - { - "repr": "number", - "kind": "keyword", - "keyword": "number" - } - ], - "typeName": "Promise" + "returnType":{ + "repr":"", + "kind":"union", + "union":[ + { + "repr":"string", + "kind":"keyword", + "keyword":"string" + }, + { + "repr":"undefined", + "kind":"keyword", + "keyword":"undefined" + } + ] + }, + "typeParams":[] + }, { + "name":"set", + "params":[ + { + "name":"key", + "kind":"identifier", + "optional":false, + "tsType":{ + "repr":"string", + "kind":"keyword", + "keyword":"string" + } + }, + { + "name":"value", + "kind":"identifier", + "optional":false, + "tsType":{ + "repr":"string", + "kind":"keyword", + "keyword":"string" + } } + ], + "returnType":{ + "repr":"void", + "kind":"keyword", + "keyword":"void" + }, + "typeParams":[] } + ], + "properties":[], + "callSignatures":[], + "indexSignatures": [] } - ], - "properties": [], - "callSignatures": [], - "typeParams": [], + }, + "kind":"const" + } } - }); - let actual = serde_json::to_value(entry).unwrap(); - assert_eq!(actual, expected_json); - - assert!( - colors::strip_ansi_codes(super::printer::format(entries).as_str()) - .contains("interface Reader extends Foo, Bar") + ] ); -} -#[tokio::test] -async fn export_interface2() { - let source_code = r#" -export interface TypedIface { - something(): T + json_test!(export_default_class, + r#" +/** Class doc */ +export default class Foobar { + /** Constructor js doc */ + constructor(name: string, private private2: number, protected protected2: number) {} } "#; - let loader = - TestLoader::new(vec![("test.ts".to_string(), source_code.to_string())]); - let entries = DocParser::new(loader).parse("test.ts").await.unwrap(); - assert_eq!(entries.len(), 1); - let entry = &entries[0]; - let expected_json = json!({ - "kind": "interface", - "name": "TypedIface", + [{ + "kind": "class", + "name": "default", "location": { "filename": "test.ts", - "line": 2, + "line": 3, "col": 0 }, - "jsDoc": null, - "interfaceDef": { - "extends": [], - "methods": [ + "jsDoc": "Class doc", + "classDef": { + "isAbstract": false, + "extends": null, + "implements": [], + "typeParams": [], + "superTypeParams": [], + "constructors": [ { - "name": "something", + "jsDoc": "Constructor js doc", + "accessibility": null, + "name": "constructor", + "params": [ + { + "name": "name", + "kind": "identifier", + "optional": false, + "tsType": { + "repr": "string", + "kind": "keyword", + "keyword": "string" + } + }, + { + "name": "private2", + "kind": "identifier", + "optional": false, + "tsType": { + "repr": "number", + "kind": "keyword", + "keyword": "number" + } + }, + { + "name": "protected2", + "kind": "identifier", + "optional": false, + "tsType": { + "repr": "number", + "kind": "keyword", + "keyword": "number" + } + } + ], "location": { "filename": "test.ts", - "line": 3, + "line": 5, "col": 4 - }, - "jsDoc": null, - "optional": false, - "params": [], - "typeParams": [], - "returnType": { - "repr": "T", - "kind": "typeRef", - "typeRef": { - "typeParams": null, - "typeName": "T" - } } } ], "properties": [], - "callSignatures": [], - "typeParams": [ - { "name": "T" } - ], - } - }); - let actual = serde_json::to_value(entry).unwrap(); - assert_eq!(actual, expected_json); - - assert!( - colors::strip_ansi_codes(super::printer::format(entries).as_str()) - .contains("interface TypedIface") - ); -} - -#[tokio::test] -async fn export_type_alias() { - let source_code = r#" -/** Array holding numbers */ -export type NumberArray = Array; - "#; - let loader = - TestLoader::new(vec![("test.ts".to_string(), source_code.to_string())]); - let entries = DocParser::new(loader).parse("test.ts").await.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": { - "typeParams": [], - "tsType": { - "repr": "Array", - "kind": "typeRef", - "typeRef": { - "typeParams": [ - { - "repr": "number", - "kind": "keyword", - "keyword": "number" - } - ], - "typeName": "Array" - } + "indexSignatures": [], + "methods": [] } - } - }); - let actual = serde_json::to_value(entry).unwrap(); - assert_eq!(actual, expected_json); + }]); - assert!( - colors::strip_ansi_codes(super::printer::format(entries).as_str()) - .contains("Array holding numbers") - ); -} - -#[tokio::test] -async fn export_enum() { - let source_code = r#" -/** - * Some enum for good measure - */ -export enum Hello { - World = "world", - Fizz = "fizz", - Buzz = "buzz", -} - "#; - let loader = - TestLoader::new(vec![("test.ts".to_string(), source_code.to_string())]); - let entries = DocParser::new(loader).parse("test.ts").await.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!(colors::strip_ansi_codes( - super::printer::format_details(entry.clone()).as_str() - ) - .contains("World")); - assert!(colors::strip_ansi_codes( - super::printer::format(entries.clone()).as_str() - ) - .contains("Some enum for good measure")); - assert!( - colors::strip_ansi_codes(super::printer::format(entries).as_str()) - .contains("enum Hello") - ); -} - -#[tokio::test] -async 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 loader = - TestLoader::new(vec![("test.ts".to_string(), source_code.to_string())]); - let entries = DocParser::new(loader).parse("test.ts").await.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!( - colors::strip_ansi_codes(super::printer::format(entries).as_str()) - .contains("namespace RootNs") - ); -} - -#[tokio::test] -async fn declare_namespace() { - let source_code = r#" -/** Namespace JSdoc */ -declare namespace RootNs { - declare const a = "a"; - - /** Nested namespace JSDoc */ - declare namespace NestedNs { - declare enum Foo { - a = 1, - b = 2, - c = 3, - } - } -} - "#; - let loader = - TestLoader::new(vec![("test.ts".to_string(), source_code.to_string())]); - let entries = DocParser::new(loader).parse("test.ts").await.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": 12 - }, - "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!( - colors::strip_ansi_codes(super::printer::format(entries).as_str()) - .contains("namespace RootNs") - ); -} - -#[tokio::test] -async fn export_default_fn() { - let source_code = r#" + json_test!(export_default_fn, + r#" export default function foo(a: number) { return a; } "#; - let loader = - TestLoader::new(vec![("test.ts".to_string(), source_code.to_string())]); - let entries = DocParser::new(loader).parse("test.ts").await.unwrap(); - assert_eq!(entries.len(), 1); - let entry = &entries[0]; - let expected_json = json!({ + [{ "kind": "function", "name": "default", "location": { @@ -1127,104 +832,10 @@ export default function foo(a: number) { "isAsync": false, "isGenerator": false } - }); - let actual = serde_json::to_value(entry).unwrap(); - assert_eq!(actual, expected_json); + }]); - assert!( - colors::strip_ansi_codes(super::printer::format(entries).as_str()) - .contains("function default(a: number)") - ); -} - -#[tokio::test] -async fn export_default_class() { - let source_code = r#" -/** Class doc */ -export default class Foobar { - /** Constructor js doc */ - constructor(name: string, private private2: number, protected protected2: number) {} -} -"#; - let loader = - TestLoader::new(vec![("test.ts".to_string(), source_code.to_string())]); - let entries = DocParser::new(loader).parse("test.ts").await.unwrap(); - assert_eq!(entries.len(), 1); - let expected_json = json!({ - "kind": "class", - "name": "default", - "location": { - "filename": "test.ts", - "line": 3, - "col": 0 - }, - "jsDoc": "Class doc", - "classDef": { - "isAbstract": false, - "extends": null, - "implements": [], - "typeParams": [], - "constructors": [ - { - "jsDoc": "Constructor js doc", - "accessibility": null, - "name": "constructor", - "params": [ - { - "name": "name", - "kind": "identifier", - "optional": false, - "tsType": { - "repr": "string", - "kind": "keyword", - "keyword": "string" - } - }, - { - "name": "private2", - "kind": "identifier", - "optional": false, - "tsType": { - "repr": "number", - "kind": "keyword", - "keyword": "number" - } - }, - { - "name": "protected2", - "kind": "identifier", - "optional": false, - "tsType": { - "repr": "number", - "kind": "keyword", - "keyword": "number" - } - } - ], - "location": { - "filename": "test.ts", - "line": 5, - "col": 4 - } - } - ], - "properties": [], - "methods": [] - } - }); - let entry = &entries[0]; - let actual = serde_json::to_value(entry).unwrap(); - assert_eq!(actual, expected_json); - - assert!( - colors::strip_ansi_codes(super::printer::format(entries).as_str()) - .contains("class default") - ); -} - -#[tokio::test] -async fn export_default_interface() { - let source_code = r#" + json_test!(export_default_interface, + r#" /** * Interface js doc */ @@ -1233,12 +844,7 @@ export default interface Reader { read?(buf: Uint8Array, something: unknown): Promise } "#; - let loader = - TestLoader::new(vec![("test.ts".to_string(), source_code.to_string())]); - let entries = DocParser::new(loader).parse("test.ts").await.unwrap(); - assert_eq!(entries.len(), 1); - let entry = &entries[0]; - let expected_json = json!({ + [{ "kind": "interface", "name": "default", "location": { @@ -1303,134 +909,569 @@ export default interface Reader { ], "properties": [], "callSignatures": [], - "typeParams": [], + "indexSignatures": [], + "typeParams": [] } - }); - let actual = serde_json::to_value(entry).unwrap(); - assert_eq!(actual, expected_json); + }]); - assert!( - colors::strip_ansi_codes(super::printer::format(entries).as_str()) - .contains("interface default") - ); + json_test!(export_enum, + r#" +/** + * Some enum for good measure + */ +export enum Hello { + World = "world", + Fizz = "fizz", + Buzz = "buzz", +} + "#; + [{ + "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" + } + ] + } + }]); + + json_test!(export_fn, + r#"/** +* @module foo +*/ + +/** +* Hello there, this is a multiline JSdoc. +* +* It has many lines +* +* Or not that many? +*/ +export function foo(a: string, b?: number, cb: (...cbArgs: unknown[]) => void, ...args: unknown[]): void { + /** + * @todo document all the things. + */ + console.log("Hello world"); +} + "#; + [{ + "functionDef": { + "isAsync": false, + "isGenerator": false, + "typeParams": [], + "params": [ + { + "name": "a", + "kind": "identifier", + "optional": false, + "tsType": { + "keyword": "string", + "kind": "keyword", + "repr": "string", + }, + }, + { + "name": "b", + "kind": "identifier", + "optional": true, + "tsType": { + "keyword": "number", + "kind": "keyword", + "repr": "number", + }, + }, + { + "name": "cb", + "kind": "identifier", + "optional": false, + "tsType": { + "repr": "", + "kind": "fnOrConstructor", + "fnOrConstructor": { + "constructor": false, + "tsType": { + "keyword": "void", + "kind": "keyword", + "repr": "void" + }, + "typeParams": [], + "params": [{ + "arg": { + "name": "cbArgs", + "kind": "identifier", + "optional": false, + "tsType": null + }, + "kind": "rest", + "tsType": { + "repr": "", + "kind": "array", + "array": { + "repr": "unknown", + "kind": "keyword", + "keyword": "unknown" + } + }, + }] + } + }, + }, + { + "arg": { + "name": "args", + "kind": "identifier", + "optional": false, + "tsType": null + }, + "kind": "rest", + "tsType": { + "array": { + "keyword": "unknown", + "kind": "keyword", + "repr": "unknown" + }, + "kind": "array", + "repr": "" + } + } + ], + "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": 12, + }, + "name": "foo", + }]); + + json_test!(export_fn2, + r#" +interface AssignOpts { + a: string; + b: number; } -#[tokio::test] -async fn optional_return_type() { - let source_code = r#" +export function foo([e,,f, ...g]: number[], { c, d: asdf, i = "asdf", ...rest}, ops: AssignOpts = {}): void { + console.log("Hello world"); +} + "#; + [{ + "functionDef": { + "isAsync": false, + "isGenerator": false, + "typeParams": [], + "params": [ + { + "elements": [ + { + "name": "e", + "kind": "identifier", + "optional": false, + "tsType": null + }, + null, + { + "name": "f", + "kind": "identifier", + "optional": false, + "tsType": null + }, + { + "arg": { + "name": "g", + "kind": "identifier", + "optional": false, + "tsType": null + }, + "kind": "rest", + "tsType": null + } + ], + "kind": "array", + "optional": false, + "tsType": { + "repr": "", + "kind": "array", + "array": { + "repr": "number", + "kind": "keyword", + "keyword": "number" + } + } + }, + { + "kind": "object", + "optional": false, + "props": [ + { + "kind": "assign", + "key": "c", + "value": null + }, + { + "kind": "keyValue", + "key": "d", + "value": { + "name": "asdf", + "kind": "identifier", + "optional": false, + "tsType": null + } + }, + { + "kind": "assign", + "key": "i", + "value": "" + }, + { + "arg": { + "name": "rest", + "kind": "identifier", + "optional": false, + "tsType": null + }, + "kind": "rest" + } + ], + "tsType": null + }, + { + "kind": "assign", + "left": { + "name": "ops", + "kind": "identifier", + "optional": false, + "tsType": { + "repr": "AssignOpts", + "kind": "typeRef", + "typeRef": { + "typeName": "AssignOpts", + "typeParams": null, + } + } + }, + "right": "", + "tsType": null + } + ], + "returnType": { + "keyword": "void", + "kind": "keyword", + "repr": "void", + }, + }, + "jsDoc": null, + "kind": "function", + "location": { + "col": 0, + "filename": "test.ts", + "line": 7, + }, + "name": "foo", + }]); + + json_test!(export_interface, + r#" +interface Foo { + foo(): void; +} +interface Bar { + bar(): void; +} +/** + * Interface js doc + */ +export interface Reader extends Foo, Bar { + /** Read n bytes */ + read?(buf: Uint8Array, something: unknown): Promise +} + "#; + [{ + "kind": "interface", + "name": "Reader", + "location": { + "filename": "test.ts", + "line": 11, + "col": 0 + }, + "jsDoc": "Interface js doc", + "interfaceDef": { + "extends": [ + { + "repr": "Foo", + "kind": "typeRef", + "typeRef": { + "typeParams": null, + "typeName": "Foo" + } + }, + { + "repr": "Bar", + "kind": "typeRef", + "typeRef": { + "typeParams": null, + "typeName": "Bar" + } + } + ], + "methods": [ + { + "name": "read", + "location": { + "filename": "test.ts", + "line": 13, + "col": 4 + }, + "optional": true, + "jsDoc": "Read n bytes", + "params": [ + { + "name": "buf", + "kind": "identifier", + "optional": false, + "tsType": { + "repr": "Uint8Array", + "kind": "typeRef", + "typeRef": { + "typeParams": null, + "typeName": "Uint8Array" + } + } + }, + { + "name": "something", + "kind": "identifier", + "optional": false, + "tsType": { + "repr": "unknown", + "kind": "keyword", + "keyword": "unknown" + } + } + ], + "typeParams": [], + "returnType": { + "repr": "Promise", + "kind": "typeRef", + "typeRef": { + "typeParams": [ + { + "repr": "number", + "kind": "keyword", + "keyword": "number" + } + ], + "typeName": "Promise" + } + } + } + ], + "properties": [], + "callSignatures": [], + "indexSignatures": [], + "typeParams": [], + } + }]); + + json_test!(export_interface2, + r#" +export interface TypedIface { + something(): T +} + "#; + [{ + "kind": "interface", + "name": "TypedIface", + "location": { + "filename": "test.ts", + "line": 2, + "col": 0 + }, + "jsDoc": null, + "interfaceDef": { + "extends": [], + "methods": [ + { + "name": "something", + "location": { + "filename": "test.ts", + "line": 3, + "col": 4 + }, + "jsDoc": null, + "optional": false, + "params": [], + "typeParams": [], + "returnType": { + "repr": "T", + "kind": "typeRef", + "typeRef": { + "typeParams": null, + "typeName": "T" + } + } + } + ], + "properties": [], + "callSignatures": [], + "indexSignatures": [], + "typeParams": [ + { "name": "T" } + ], + } + }]); + + json_test!(export_type_alias, + r#" +/** Array holding numbers */ +export type NumberArray = Array; + "#; + [{ + "kind": "typeAlias", + "name": "NumberArray", + "location": { + "filename": "test.ts", + "line": 3, + "col": 0 + }, + "jsDoc": "Array holding numbers", + "typeAliasDef": { + "typeParams": [], + "tsType": { + "repr": "Array", + "kind": "typeRef", + "typeRef": { + "typeParams": [ + { + "repr": "number", + "kind": "keyword", + "keyword": "number" + } + ], + "typeName": "Array" + } + } + } + }]); + + json_test!(export_namespace, + 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, + } + } +} + "#; + [{ + "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" + } + ] + } + } + ] + } + } + ] + } + }]); + + json_test!(optional_return_type, + r#" export function foo(a: number) { return a; } "#; - let loader = - TestLoader::new(vec![("test.ts".to_string(), source_code.to_string())]); - let entries = DocParser::new(loader).parse("test.ts").await.unwrap(); - assert_eq!(entries.len(), 1); - let entry = &entries[0]; - let expected_json = json!({ - "kind": "function", - "name": "foo", - "location": { - "filename": "test.ts", - "line": 2, - "col": 2 - }, - "jsDoc": null, - "functionDef": { - "params": [ - { - "name": "a", - "kind": "identifier", - "optional": false, - "tsType": { - "keyword": "number", - "kind": "keyword", - "repr": "number", - }, - } - ], - "typeParams": [], - "returnType": null, - "isAsync": false, - "isGenerator": false - } - }); - let actual = serde_json::to_value(entry).unwrap(); - assert_eq!(actual, expected_json); - - assert!( - colors::strip_ansi_codes(super::printer::format(entries).as_str()) - .contains("function foo(a: number)") - ); -} - -#[tokio::test] -async fn reexports() { - let nested_reexport_source_code = r#" -/** - * JSDoc for bar - */ -export const bar = "bar"; - -export default 42; -"#; - let reexport_source_code = r#" -import { bar } from "./nested_reexport.ts"; - -/** - * JSDoc for const - */ -export const foo = "foo"; -"#; - let test_source_code = r#" -export { default, foo as fooConst } from "./reexport.ts"; - -/** JSDoc for function */ -export function fooFn(a: number) { - return a; -} -"#; - let loader = TestLoader::new(vec![ - ("file:///test.ts".to_string(), test_source_code.to_string()), - ( - "file:///reexport.ts".to_string(), - reexport_source_code.to_string(), - ), - ( - "file:///nested_reexport.ts".to_string(), - nested_reexport_source_code.to_string(), - ), - ]); - let entries = DocParser::new(loader) - .parse_with_reexports("file:///test.ts") - .await - .unwrap(); - assert_eq!(entries.len(), 2); - - let expected_json = json!([ - { - "kind": "variable", - "name": "fooConst", - "location": { - "filename": "file:///reexport.ts", - "line": 7, - "col": 0 - }, - "jsDoc": "JSDoc for const", - "variableDef": { - "tsType": null, - "kind": "const" - } - }, - { + [{ "kind": "function", - "name": "fooFn", + "name": "foo", "location": { - "filename": "file:///test.ts", - "line": 5, - "col": 0 + "filename": "test.ts", + "line": 2, + "col": 2 }, - "jsDoc": "JSDoc for function", + "jsDoc": null, "functionDef": { "params": [ { @@ -1449,30 +1490,17 @@ export function fooFn(a: number) { "isAsync": false, "isGenerator": false } - } - ]); - let actual = serde_json::to_value(entries.clone()).unwrap(); - assert_eq!(actual, expected_json); - - assert!( - colors::strip_ansi_codes(super::printer::format(entries).as_str()) - .contains("function fooFn(a: number)") + }] ); -} -#[tokio::test] -async fn ts_lit_types() { - let source_code = r#" + json_test!(ts_lit_types, + r#" export type boolLit = false; export type strLit = "text"; export type tplLit = `text`; export type numLit = 5; -"#; - let loader = - TestLoader::new(vec![("test.ts".to_string(), source_code.to_string())]); - let entries = DocParser::new(loader).parse("test.ts").await.unwrap(); - let actual = serde_json::to_value(entries).unwrap(); - let expected_json = json!([ + "#; + [ { "kind": "typeAlias", "name": "boolLit", @@ -1555,70 +1583,491 @@ export type numLit = 5; } } ]); - assert_eq!(actual, expected_json); } -#[tokio::test] -async fn filter_nodes_by_name() { - use super::find_nodes_by_name_recursively; - let source_code = r#" -export namespace Deno { - export class Buffer {} - export function test(options: object): void; - export function test(name: string, fn: Function): void; - export function test(name: string | object, fn?: Function): void {} -} +mod printer { + use super::*; -export namespace Deno { - export namespace Inner { - export function a(): void {} - export const b = 100; + contains_test!(abstract_class, + "export abstract class Class {}", + details; + "abstract class Class" + ); + + contains_test!(abstract_class_abstract_method, + r#" +export abstract class Class { + abstract method() {} +} + "#, + details; + "abstract method()" + ); + + contains_test!(class_async_method, + r#" +export class Class { + async amethod(v) {} +} + "#, + details; + "async amethod(v)" + ); + + contains_test!(class_constructor, + r#" +export class Class { + constructor(a, b) {} +} + "#, + details; + "constructor(a, b)" + ); + + const CLASS_SOURCE: &str = r#" +export class C { + /** a doc */ + a() {} + f: number; +} + "#; + + contains_test!(class_details, + CLASS_SOURCE, + details; + "class C", + "a()", + "f: number" + ); + + contains_test!(class_details_all_with_private, + r#" +export class Class { + private pri() {} + protected pro() {} + public pub() {} +} + "#, + details, + private; + "private pri()", + "protected pro()", + "pub()" + ); + + contains_test!(class_details_only_non_private_without_private, + r#" +export class Class { + private pri() {} + protected pro() {} + public pub() {} +} + "#, + details; + "protected pro()", + "pub()" + ); + + contains_test!(class_declaration, + "export class Class {}"; + "class Class" + ); + + contains_test!(class_extends, + "export class Class extends Object {}"; + "class Class extends Object" + ); + + contains_test!(class_extends_implements, + "export class Class extends Object implements Iterator, Iterable {}"; + "class Class extends Object implements Iterator, Iterable" + ); + + contains_test!(class_generic_extends_implements, + "export class Class extends Map implements Iterator, Iterable {}"; + "class Class extends Map implements Iterator, Iterable" + ); + + contains_test!(class_getter_and_setter, + r#" +export class Class { + get a(): void {} + set b(_v: void) {} +} + "#, + details; + "get a(): void", + "set b(_v: void)" + ); + + contains_test!(class_index_signature, + r#" +export class C { + [key: string]: number; +} + "#, + details; + "[key: string]: number" + ); + + contains_test!(class_implements, + "export class Class implements Iterator {}"; + "class Class implements Iterator" + ); + + contains_test!(class_implements2, + "export class Class implements Iterator, Iterable {}"; + "class Class implements Iterator, Iterable" + ); + + contains_test!(class_method, + r#" +export class Class { + method(v) {} +} + "#, + details; + "method(v)" + ); + + contains_test!(class_property, + r#" +export class Class { + someproperty: bool; + optproperty: bigint; +} + "#, + details; + "someproperty: bool", + "optproperty: bigint" + ); + + contains_test!(class_readonly_index_signature, + r#" +export class C { + readonly [key: string]: number; +} + "#, + details; + "readonly [key: string]: number" + ); + + contains_test!(class_static_property, + r#" +export class Class { + static property = ""; +} + "#, + details; + "static property" + ); + + contains_test!(class_summary, + CLASS_SOURCE; + "class C"; + "a()", + "f: number" + ); + + contains_test!(class_readonly_property, + r#" +export class Class { + readonly property = ""; +} + "#, + details; + "readonly property" + ); + + contains_test!(class_private_property, + r#" +export class Class { + private property = ""; +} + "#, + details, + private; + "private property" + ); + + contains_test!(const_declaration, + "export const Const = 0;"; + "const Const" + ); + + contains_test!(enum_declaration, + "export enum Enum {}"; + "enum Enum" + ); + + const EXPORT_SOURCE: &str = r#" +export function a() {} +function b() {} +export class C {} +class D {} +export interface E {} +interface F {} +export namespace G {} +namespace H {} + "#; + + contains_test!(exports_all_with_private, + EXPORT_SOURCE, + private; + "function a()", + "class C", + "interface E", + "namespace G", + "function b()", + "class D", + "interface F", + "namespace H" + ); + + contains_test!(exports_only_exports_without_private, + EXPORT_SOURCE; + "function a()", + "class C", + "interface E", + "namespace G"; + "function b()", + "class D", + "interface F", + "namespace H" + ); + + contains_test!(function_async, + "export async function a() {}"; + "async function a()" + ); + + contains_test!(function_array_deconstruction, + "export function f([a, b, ...c]) {}"; + "function f([a, b, ...c])" + ); + + contains_test!(function_async_generator, + "export async function* ag() {}"; + "async function* ag()" + ); + + contains_test!(function_declaration, + "export function fun() {}"; + "function fun()" + ); + + contains_test!(function_generator, + "export function* g() {}"; + "function* g()" + ); + + contains_test!(function_generic, + "export function add(a: T, b: T) { return a + b; }"; + "function add(a: T, b: T)" + ); + + contains_test!(function_object_deconstruction, + "export function f({ a, b, ...c }) {}"; + "function f({a, b, ...c})" + ); + + /* TODO(SyrupThinker) NYI + contains_test!(function_type_predicate, + r#" + export function isFish(pet: Fish | Bird): pet is Fish { + return (pet as Fish).swim !== undefined; } -} -"#; - let loader = - TestLoader::new(vec![("test.ts".to_string(), source_code.to_string())]); - let entries = DocParser::new(loader).parse("test.ts").await.unwrap(); + "#; + "pet is Fish" + ); + */ - let found = - find_nodes_by_name_recursively(entries.clone(), "Deno".to_string()); - assert_eq!(found.len(), 2); - assert_eq!(found[0].name, "Deno".to_string()); - assert_eq!(found[1].name, "Deno".to_string()); - - let found = - find_nodes_by_name_recursively(entries.clone(), "Deno.test".to_string()); - assert_eq!(found.len(), 3); - assert_eq!(found[0].name, "test".to_string()); - assert_eq!(found[1].name, "test".to_string()); - assert_eq!(found[2].name, "test".to_string()); - - let found = - find_nodes_by_name_recursively(entries.clone(), "Deno.Inner.a".to_string()); - assert_eq!(found.len(), 1); - assert_eq!(found[0].name, "a".to_string()); - - let found = - find_nodes_by_name_recursively(entries.clone(), "Deno.test.a".to_string()); - assert_eq!(found.len(), 0); - - let found = find_nodes_by_name_recursively(entries, "a.b.c".to_string()); - assert_eq!(found.len(), 0); -} - -#[tokio::test] -async fn generic_instantiated_with_tuple_type() { - let source_code = r#" + contains_test!(generic_instantiated_with_tuple_type, + r#" interface Generic {} export function f(): Generic<[string, number]> { return {}; } - "#; + "#; + "Generic<[string, number]>" + ); - let loader = - TestLoader::new(vec![("test.ts".to_string(), source_code.to_string())]); - let entries = DocParser::new(loader).parse("test.ts").await.unwrap(); + contains_test!(type_literal_declaration, + "export type T = {}"; + "{ }" + ); - assert!(colors::strip_ansi_codes( - crate::doc::printer::format(entries).as_str() - ) - .contains("Generic<[string, number]>")) + contains_test!(type_literal_index_signature, + "export type T = { [key: string]: number; }"; + "[key: string]: number" + ); + + contains_test!(type_literal_readonly_index_signature, + "export type T = { readonly [key: string]: number; }"; + "readonly [key: string]: number" + ); + + contains_test!(interface_declaration, + "export interface Interface {}"; + "interface Interface" + ); + + contains_test!(interface_extends, + "export interface Interface extends Iterator {}"; + "interface Interface extends Iterator" + ); + + contains_test!(interface_extends2, + "export interface Interface extends Iterator, Iterable {}"; + "interface Interface extends Iterator, Iterable" + ); + + contains_test!(interface_generic, + "export interface Interface {}"; + "interface Interface" + ); + + contains_test!(interface_generic_extends, + "export interface Interface extends Iterable {}"; + "interface Interface extends Iterable" + ); + + contains_test!(interface_index_signature, + r#" +export interface Interface { + [index: number]: Interface; +} + "#, + details; + "[index: number]: Interface" + ); + + contains_test!(interface_method, + r#" +export interface I { + m(a, b); + mo?(c); +} + "#, + details; + "m(a, b)", + "mo?(c)" + ); + + contains_test!(interface_property, + r#" +export interface I { + p: string; + po?: number; +} + "#, + details; + "p: string", + "po?: number" + ); + + contains_test!(interface_readonly_index_signature, + r#" +export interface Interface { + readonly [index: number]: Interface; +} + "#, + details; + "readonly [index: number]: Interface" + ); + + const JSDOC_SOURCE: &str = r#" +/** + * A is a class + * + * Nothing more + */ +export class A {} +/** + * B is an interface + * + * Should be + */ +export interface B {} +/** + * C is a function + * + * Summarised + */ +export function C() {} + "#; + + contains_test!(jsdoc_details, + JSDOC_SOURCE, + details; + "A is a class", + "B is an interface", + "C is a function", + "Nothing more", + "Should be", + "Summarised" + ); + + contains_test!(jsdoc_summary, + JSDOC_SOURCE; + "A is a class", + "B is an interface", + "C is a function"; + "Nothing more", + "Should be", + "Summarised" + ); + + contains_test!(namespace_declaration, + "export namespace Namespace {}"; + "namespace Namespace" + ); + + const NAMESPACE_SOURCE: &str = r#" +export namespace Namespace { + /** + * Doc comment 1 + * + * Details 1 + */ + export function a() {} + /** + * Doc comment 2 + * + * Details 2 + */ + export class B {} +} + "#; + + contains_test!(namespace_details, + NAMESPACE_SOURCE, + details; + "namespace Namespace", + "function a()", + "class B", + "Doc comment 1", + "Doc comment 2"; + "Details 1", + "Details 2" + ); + + contains_test!(namespace_summary, + NAMESPACE_SOURCE; + "namespace Namespace", + "function a()", + "class B", + "Doc comment 1", + "Doc comment 2"; + "Details 1", + "Details 2" + ); + + contains_test!(type_alias, + "export type A = number"; + "type A = number" + ); + + contains_test!(type_generic_alias, + "export type A = T"; + "type A = T" + ); } diff --git a/cli/doc/ts_type.rs b/cli/doc/ts_type.rs index 7bb494541f..36502ee5e4 100644 --- a/cli/doc/ts_type.rs +++ b/cli/doc/ts_type.rs @@ -1,30 +1,23 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +use super::display::{display_readonly, SliceDisplayer}; use super::interface::expr_to_name; use super::params::ts_fn_param_to_param_def; use super::ts_type_param::maybe_type_param_decl_to_type_param_defs; use super::ts_type_param::TsTypeParamDef; use super::ParamDef; +use crate::colors; +use crate::doc; use crate::swc_ecma_ast; -use crate::swc_ecma_ast::TsArrayType; -use crate::swc_ecma_ast::TsConditionalType; -use crate::swc_ecma_ast::TsFnOrConstructorType; -use crate::swc_ecma_ast::TsIndexedAccessType; -use crate::swc_ecma_ast::TsKeywordType; -use crate::swc_ecma_ast::TsLit; -use crate::swc_ecma_ast::TsLitType; -use crate::swc_ecma_ast::TsOptionalType; -use crate::swc_ecma_ast::TsParenthesizedType; -use crate::swc_ecma_ast::TsRestType; -use crate::swc_ecma_ast::TsThisType; -use crate::swc_ecma_ast::TsTupleType; -use crate::swc_ecma_ast::TsType; -use crate::swc_ecma_ast::TsTypeAnn; -use crate::swc_ecma_ast::TsTypeLit; -use crate::swc_ecma_ast::TsTypeOperator; -use crate::swc_ecma_ast::TsTypeQuery; -use crate::swc_ecma_ast::TsTypeRef; -use crate::swc_ecma_ast::TsUnionOrIntersectionType; +use crate::swc_ecma_ast::{ + TsArrayType, TsConditionalType, TsExprWithTypeArgs, TsFnOrConstructorType, + TsIndexedAccessType, TsKeywordType, TsLit, TsLitType, TsOptionalType, + TsParenthesizedType, TsRestType, TsThisType, TsTupleType, TsType, TsTypeAnn, + TsTypeLit, TsTypeOperator, TsTypeParamInstantiation, TsTypeQuery, TsTypeRef, + TsUnionOrIntersectionType, +}; use serde::Serialize; +use std::fmt::{Display, Formatter, Result as FmtResult}; + // pub enum TsType { // * TsKeywordType(TsKeywordType), // * TsThisType(TsThisType), @@ -316,7 +309,37 @@ impl Into for &TsTypeRef { }; TsTypeDef { - repr: type_name.to_string(), + repr: type_name.clone(), + type_ref: Some(TsTypeRefDef { + type_name, + type_params, + }), + kind: Some(TsTypeDefKind::TypeRef), + ..Default::default() + } + } +} + +impl Into for &TsExprWithTypeArgs { + fn into(self) -> TsTypeDef { + let type_name = ts_entity_name_to_name(&self.expr); + + let type_params = if let Some(type_params_inst) = &self.type_args { + 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.clone(), type_ref: Some(TsTypeRefDef { type_name, type_params, @@ -348,6 +371,7 @@ impl Into for &TsTypeLit { let mut methods = vec![]; let mut properties = vec![]; let mut call_signatures = vec![]; + let mut index_signatures = vec![]; for type_element in &self.members { use crate::swc_ecma_ast::TsTypeElement::*; @@ -357,7 +381,7 @@ impl Into for &TsTypeLit { let mut params = vec![]; for param in &ts_method_sig.params { - let param_def = ts_fn_param_to_param_def(param); + let param_def = ts_fn_param_to_param_def(param, None); params.push(param_def); } @@ -384,7 +408,7 @@ impl Into for &TsTypeLit { let mut params = vec![]; for param in &ts_prop_sig.params { - let param_def = ts_fn_param_to_param_def(param); + let param_def = ts_fn_param_to_param_def(param, None); params.push(param_def); } @@ -409,7 +433,7 @@ impl Into for &TsTypeLit { TsCallSignatureDecl(ts_call_sig) => { let mut params = vec![]; for param in &ts_call_sig.params { - let param_def = ts_fn_param_to_param_def(param); + let param_def = ts_fn_param_to_param_def(param, None); params.push(param_def); } @@ -429,9 +453,27 @@ impl Into for &TsTypeLit { }; call_signatures.push(call_sig_def); } + TsIndexSignature(ts_index_sig) => { + let mut params = vec![]; + for param in &ts_index_sig.params { + let param_def = ts_fn_param_to_param_def(param, None); + params.push(param_def); + } + + let ts_type = ts_index_sig + .type_ann + .as_ref() + .map(|rt| (&*rt.type_ann).into()); + + let index_sig_def = LiteralIndexSignatureDef { + readonly: ts_index_sig.readonly, + params, + ts_type, + }; + index_signatures.push(index_sig_def); + } // TODO: TsConstructSignatureDecl(_) => {} - TsIndexSignature(_) => {} } } @@ -439,6 +481,7 @@ impl Into for &TsTypeLit { methods, properties, call_signatures, + index_signatures, }; TsTypeDef { @@ -475,7 +518,7 @@ impl Into for &TsFnOrConstructorType { let mut params = vec![]; for param in &ts_fn_type.params { - let param_def = ts_fn_param_to_param_def(param); + let param_def = ts_fn_param_to_param_def(param, None); params.push(param_def); } @@ -494,7 +537,7 @@ impl Into for &TsFnOrConstructorType { let mut params = vec![]; for param in &ctor_type.params { - let param_def = ts_fn_param_to_param_def(param); + let param_def = ts_fn_param_to_param_def(param, None); params.push(param_def); } @@ -619,6 +662,21 @@ pub struct LiteralMethodDef { pub type_params: Vec, } +impl Display for LiteralMethodDef { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + write!( + f, + "{}({})", + self.name, + SliceDisplayer::new(&self.params, ", ", false) + )?; + if let Some(return_type) = &self.return_type { + write!(f, ": {}", return_type)?; + } + Ok(()) + } +} + #[derive(Debug, Serialize, Clone)] #[serde(rename_all = "camelCase")] pub struct LiteralPropertyDef { @@ -630,6 +688,15 @@ pub struct LiteralPropertyDef { pub type_params: Vec, } +impl Display for LiteralPropertyDef { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + write!(f, "{}", self.name)?; + if let Some(ts_type) = &self.ts_type { + write!(f, ": {}", ts_type)?; + } + Ok(()) + } +} #[derive(Debug, Serialize, Clone)] #[serde(rename_all = "camelCase")] pub struct LiteralCallSignatureDef { @@ -638,12 +705,46 @@ pub struct LiteralCallSignatureDef { pub type_params: Vec, } +impl Display for LiteralCallSignatureDef { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + write!(f, "({})", SliceDisplayer::new(&self.params, ", ", false))?; + if let Some(ts_type) = &self.ts_type { + write!(f, ": {}", ts_type)?; + } + Ok(()) + } +} + +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct LiteralIndexSignatureDef { + pub readonly: bool, + pub params: Vec, + pub ts_type: Option, +} + +impl Display for LiteralIndexSignatureDef { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + write!( + f, + "{}[{}]", + display_readonly(self.readonly), + SliceDisplayer::new(&self.params, ", ", false) + )?; + if let Some(ts_type) = &self.ts_type { + write!(f, ": {}", ts_type)?; + } + Ok(()) + } +} + #[derive(Debug, Serialize, Clone)] #[serde(rename_all = "camelCase")] pub struct TsTypeLiteralDef { pub methods: Vec, pub properties: Vec, pub call_signatures: Vec, + pub index_signatures: Vec, } #[derive(Debug, PartialEq, Serialize, Clone)] @@ -753,3 +854,133 @@ pub fn ts_type_ann_to_def(type_ann: &TsTypeAnn) -> TsTypeDef { }, } } + +impl Display for TsTypeDef { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + if self.kind.is_none() { + return write!(f, "{}", colors::red("")); + } + + let kind = self.kind.as_ref().unwrap(); + match kind { + TsTypeDefKind::Array => write!(f, "{}[]", &*self.array.as_ref().unwrap()), + TsTypeDefKind::Conditional => { + let conditional = self.conditional_type.as_ref().unwrap(); + write!( + f, + "{} {} {} ? {} : {}", + &*conditional.check_type, + colors::magenta("extends"), + &*conditional.extends_type, + &*conditional.true_type, + &*conditional.false_type + ) + } + TsTypeDefKind::FnOrConstructor => { + let fn_or_constructor = self.fn_or_constructor.as_ref().unwrap(); + write!( + f, + "{}({}) => {}", + colors::magenta(if fn_or_constructor.constructor { + "new " + } else { + "" + }), + SliceDisplayer::new(&fn_or_constructor.params, ", ", false), + &fn_or_constructor.ts_type, + ) + } + TsTypeDefKind::IndexedAccess => { + let indexed_access = self.indexed_access.as_ref().unwrap(); + write!( + f, + "{}[{}]", + &*indexed_access.obj_type, &*indexed_access.index_type + ) + } + TsTypeDefKind::Intersection => { + let intersection = self.intersection.as_ref().unwrap(); + write!(f, "{}", SliceDisplayer::new(&intersection, " & ", false)) + } + TsTypeDefKind::Keyword => { + write!(f, "{}", colors::cyan(self.keyword.as_ref().unwrap())) + } + TsTypeDefKind::Literal => { + let literal = self.literal.as_ref().unwrap(); + match literal.kind { + doc::ts_type::LiteralDefKind::Boolean => write!( + f, + "{}", + colors::yellow(&literal.boolean.unwrap().to_string()) + ), + doc::ts_type::LiteralDefKind::String => write!( + f, + "{}", + colors::green(&format!("\"{}\"", literal.string.as_ref().unwrap())) + ), + doc::ts_type::LiteralDefKind::Number => write!( + f, + "{}", + colors::yellow(&literal.number.unwrap().to_string()) + ), + } + } + TsTypeDefKind::Optional => { + write!(f, "{}?", &*self.optional.as_ref().unwrap()) + } + TsTypeDefKind::Parenthesized => { + write!(f, "({})", &*self.parenthesized.as_ref().unwrap()) + } + TsTypeDefKind::Rest => write!(f, "...{}", &*self.rest.as_ref().unwrap()), + TsTypeDefKind::This => write!(f, "this"), + TsTypeDefKind::Tuple => { + let tuple = self.tuple.as_ref().unwrap(); + write!(f, "[{}]", SliceDisplayer::new(&tuple, ", ", false)) + } + TsTypeDefKind::TypeLiteral => { + let type_literal = self.type_literal.as_ref().unwrap(); + write!( + f, + "{{ {}{}{}{}}}", + SliceDisplayer::new(&type_literal.call_signatures, "; ", true), + SliceDisplayer::new(&type_literal.methods, "; ", true), + SliceDisplayer::new(&type_literal.properties, "; ", true), + SliceDisplayer::new(&type_literal.index_signatures, "; ", true), + ) + } + TsTypeDefKind::TypeOperator => { + let operator = self.type_operator.as_ref().unwrap(); + write!(f, "{} {}", operator.operator, &operator.ts_type) + } + TsTypeDefKind::TypeQuery => { + write!(f, "typeof {}", self.type_query.as_ref().unwrap()) + } + TsTypeDefKind::TypeRef => { + let type_ref = self.type_ref.as_ref().unwrap(); + write!(f, "{}", colors::intense_blue(&type_ref.type_name))?; + if let Some(type_params) = &type_ref.type_params { + write!(f, "<{}>", SliceDisplayer::new(type_params, ", ", false))?; + } + Ok(()) + } + TsTypeDefKind::Union => { + let union = self.union.as_ref().unwrap(); + write!(f, "{}", SliceDisplayer::new(union, " | ", false)) + } + } + } +} + +pub fn maybe_type_param_instantiation_to_type_defs( + maybe_type_param_instantiation: Option<&TsTypeParamInstantiation>, +) -> Vec { + if let Some(type_param_instantiation) = maybe_type_param_instantiation { + type_param_instantiation + .params + .iter() + .map(|type_param| type_param.as_ref().into()) + .collect::>() + } else { + vec![] + } +} diff --git a/cli/doc/ts_type_param.rs b/cli/doc/ts_type_param.rs index 4edb7dee1c..0483708b6f 100644 --- a/cli/doc/ts_type_param.rs +++ b/cli/doc/ts_type_param.rs @@ -3,6 +3,7 @@ use super::ts_type::TsTypeDef; use crate::swc_ecma_ast::TsTypeParam; use crate::swc_ecma_ast::TsTypeParamDecl; use serde::Serialize; +use std::fmt::{Display, Formatter, Result as FmtResult}; #[derive(Debug, Serialize, Clone)] #[serde(rename_all = "camelCase")] @@ -16,6 +17,19 @@ pub struct TsTypeParamDef { pub default: Option, } +impl Display for TsTypeParamDef { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + write!(f, "{}", self.name)?; + if let Some(constraint) = &self.constraint { + write!(f, " extends {}", constraint)?; + } + if let Some(default) = &self.default { + write!(f, " = {}", default)?; + } + Ok(()) + } +} + impl Into for &TsTypeParam { fn into(self) -> TsTypeParamDef { let name = self.name.sym.to_string(); diff --git a/cli/flags.rs b/cli/flags.rs index 313d8ff1a5..80e8565813 100644 --- a/cli/flags.rs +++ b/cli/flags.rs @@ -23,6 +23,7 @@ pub enum DenoSubcommand { buf: Box<[u8]>, }, Doc { + private: bool, json: bool, source_file: Option, filter: Option, @@ -598,12 +599,14 @@ fn doc_parse(flags: &mut Flags, matches: &clap::ArgMatches) { unstable_arg_parse(flags, matches); let source_file = matches.value_of("source_file").map(String::from); + let private = matches.is_present("private"); let json = matches.is_present("json"); let filter = matches.value_of("filter").map(String::from); flags.subcommand = DenoSubcommand::Doc { source_file, json, filter, + private, }; } @@ -915,6 +918,9 @@ fn doc_subcommand<'a, 'b>() -> App<'a, 'b> { Output documentation to standard output: deno doc ./path/to/module.ts +Output private documentation to standard output: + deno doc --private ./path/to/module.ts + Output documentation in JSON format: deno doc --json ./path/to/module.ts @@ -932,6 +938,12 @@ Show documentation for runtime built-ins: .help("Output documentation in JSON format.") .takes_value(false), ) + .arg( + Arg::with_name("private") + .long("private") + .help("Output private documentation") + .takes_value(false), + ) // TODO(nayeemrmn): Make `--builtin` a proper option. Blocked by // https://github.com/clap-rs/clap/issues/1794. Currently `--builtin` is // just a possible value of `source_file` so leading hyphens must be @@ -2910,6 +2922,7 @@ mod tests { r.unwrap(), Flags { subcommand: DenoSubcommand::Doc { + private: false, json: true, source_file: Some("path/to/module.ts".to_string()), filter: None, @@ -2928,6 +2941,7 @@ mod tests { r.unwrap(), Flags { subcommand: DenoSubcommand::Doc { + private: false, json: false, source_file: Some("path/to/module.ts".to_string()), filter: Some("SomeClass.someField".to_string()), @@ -2941,6 +2955,7 @@ mod tests { r.unwrap(), Flags { subcommand: DenoSubcommand::Doc { + private: false, json: false, source_file: None, filter: None, @@ -2955,6 +2970,7 @@ mod tests { r.unwrap(), Flags { subcommand: DenoSubcommand::Doc { + private: false, json: false, source_file: Some("--builtin".to_string()), filter: Some("Deno.Listener".to_string()), @@ -2962,6 +2978,25 @@ mod tests { ..Flags::default() } ); + + let r = flags_from_vec_safe(svec![ + "deno", + "doc", + "--private", + "path/to/module.js" + ]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Doc { + private: true, + json: false, + source_file: Some("path/to/module.js".to_string()), + filter: None, + }, + ..Flags::default() + } + ); } #[test] diff --git a/cli/main.rs b/cli/main.rs index 9669d0938d..088d65ecd1 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -521,6 +521,7 @@ async fn doc_command( source_file: Option, json: bool, maybe_filter: Option, + private: bool, ) -> Result<(), ErrBox> { let global_state = GlobalState::new(flags.clone())?; let source_file = source_file.unwrap_or_else(|| "--builtin".to_string()); @@ -546,7 +547,7 @@ async fn doc_command( } let loader = Box::new(global_state.file_fetcher.clone()); - let doc_parser = doc::DocParser::new(loader); + let doc_parser = doc::DocParser::new(loader, private); let parse_result = if source_file == "--builtin" { doc_parser.parse_source("lib.deno.d.ts", get_types(flags.unstable).as_str()) @@ -576,13 +577,9 @@ async fn doc_command( eprintln!("Node {} was not found!", filter); std::process::exit(1); } - let mut details = String::new(); - for node in nodes { - details.push_str(doc::printer::format_details(node).as_str()); - } - details + format!("{}", doc::DocPrinter::new(&nodes, true, private)) } else { - doc::printer::format(doc_nodes) + format!("{}", doc::DocPrinter::new(&doc_nodes, false, private)) }; write_to_stdout_ignore_sigpipe(details.as_bytes()).map_err(ErrBox::from) @@ -720,7 +717,8 @@ pub fn main() { source_file, json, filter, - } => doc_command(flags, source_file, json, filter).boxed_local(), + private, + } => doc_command(flags, source_file, json, filter, private).boxed_local(), DenoSubcommand::Eval { print, code,