0
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2025-03-03 09:31:22 -05:00

feat: Add "deno doc" subcommand (#4500)

This commit is contained in:
Bartek Iwańczuk 2020-03-28 19:16:57 +01:00 committed by GitHub
parent bced52505f
commit 3fac487461
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 3233 additions and 4 deletions

4
Cargo.lock generated
View file

@ -493,6 +493,10 @@ dependencies = [
"serde_derive",
"serde_json",
"sourcemap",
"swc_common",
"swc_ecma_ast",
"swc_ecma_parser",
"swc_ecma_parser_macros",
"sys-info",
"tempfile",
"termcolor",

View file

@ -63,9 +63,14 @@ webpki-roots = "0.19.0"
walkdir = "2.3.1"
warp = "0.2.2"
semver-parser = "0.9.0"
# TODO(bartlomieju): make sure we're using exactly same versions
# of "swc_*" as dprint-plugin-typescript
swc_common = "=0.5.9"
swc_ecma_ast = "=0.18.1"
swc_ecma_parser = "=0.21.8"
swc_ecma_parser_macros = "=0.4.1"
uuid = { version = "0.8", features = ["v4"] }
[target.'cfg(windows)'.dependencies]
winapi = "0.3.8"
fwdansi = "1.1.0"

207
cli/doc/class.rs Normal file
View file

@ -0,0 +1,207 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
use serde::Serialize;
use swc_common;
use swc_common::SourceMap;
use swc_common::Spanned;
use swc_ecma_ast;
use super::function::function_to_function_def;
use super::function::FunctionDef;
use super::parser::DocParser;
use super::ts_type::ts_type_ann_to_def;
use super::ts_type::TsTypeDef;
use super::Location;
use super::ParamDef;
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct ClassConstructorDef {
pub js_doc: Option<String>,
pub accessibility: Option<swc_ecma_ast::Accessibility>,
pub name: String,
pub params: Vec<ParamDef>,
pub location: Location,
}
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct ClassPropertyDef {
pub js_doc: Option<String>,
pub ts_type: Option<TsTypeDef>,
pub readonly: bool,
pub accessibility: Option<swc_ecma_ast::Accessibility>,
pub is_abstract: bool,
pub is_static: bool,
pub name: String,
pub location: Location,
}
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct ClassMethodDef {
pub js_doc: Option<String>,
// pub ts_type: Option<TsTypeDef>,
// pub readonly: bool,
pub accessibility: Option<swc_ecma_ast::Accessibility>,
pub is_abstract: bool,
pub is_static: bool,
pub name: String,
pub kind: swc_ecma_ast::MethodKind,
pub function_def: FunctionDef,
pub location: Location,
}
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct ClassDef {
// TODO: decorators, super_class, implements,
// type_params, super_type_params
pub is_abstract: bool,
pub constructors: Vec<ClassConstructorDef>,
pub properties: Vec<ClassPropertyDef>,
pub methods: Vec<ClassMethodDef>,
}
fn prop_name_to_string(
source_map: &SourceMap,
prop_name: &swc_ecma_ast::PropName,
) -> String {
use swc_ecma_ast::PropName;
match prop_name {
PropName::Ident(ident) => ident.sym.to_string(),
PropName::Str(str_) => str_.value.to_string(),
PropName::Num(num) => num.value.to_string(),
PropName::Computed(comp_prop_name) => {
source_map.span_to_snippet(comp_prop_name.span).unwrap()
}
}
}
pub fn get_doc_for_class_decl(
doc_parser: &DocParser,
class_decl: &swc_ecma_ast::ClassDecl,
) -> (String, ClassDef) {
let mut constructors = vec![];
let mut methods = vec![];
let mut properties = vec![];
for member in &class_decl.class.body {
use swc_ecma_ast::ClassMember::*;
match member {
Constructor(ctor) => {
let ctor_js_doc = doc_parser.js_doc_for_span(ctor.span());
let constructor_name =
prop_name_to_string(&doc_parser.source_map, &ctor.key);
let mut params = vec![];
for param in &ctor.params {
use swc_ecma_ast::Pat;
use swc_ecma_ast::PatOrTsParamProp::*;
let param_def = match param {
Pat(pat) => match pat {
Pat::Ident(ident) => {
let ts_type = ident
.type_ann
.as_ref()
.map(|rt| ts_type_ann_to_def(&doc_parser.source_map, rt));
ParamDef {
name: ident.sym.to_string(),
ts_type,
}
}
_ => ParamDef {
name: "<TODO>".to_string(),
ts_type: None,
},
},
TsParamProp(_) => ParamDef {
name: "<TODO>".to_string(),
ts_type: None,
},
};
params.push(param_def);
}
let constructor_def = ClassConstructorDef {
js_doc: ctor_js_doc,
accessibility: ctor.accessibility,
name: constructor_name,
params,
location: doc_parser
.source_map
.lookup_char_pos(ctor.span.lo())
.into(),
};
constructors.push(constructor_def);
}
Method(class_method) => {
let method_js_doc = doc_parser.js_doc_for_span(class_method.span());
let method_name =
prop_name_to_string(&doc_parser.source_map, &class_method.key);
let fn_def =
function_to_function_def(doc_parser, &class_method.function);
let method_def = ClassMethodDef {
js_doc: method_js_doc,
accessibility: class_method.accessibility,
is_abstract: class_method.is_abstract,
is_static: class_method.is_static,
name: method_name,
kind: class_method.kind,
function_def: fn_def,
location: doc_parser
.source_map
.lookup_char_pos(class_method.span.lo())
.into(),
};
methods.push(method_def);
}
ClassProp(class_prop) => {
let prop_js_doc = doc_parser.js_doc_for_span(class_prop.span());
let ts_type = class_prop
.type_ann
.as_ref()
.map(|rt| ts_type_ann_to_def(&doc_parser.source_map, rt));
use swc_ecma_ast::Expr;
let prop_name = match &*class_prop.key {
Expr::Ident(ident) => ident.sym.to_string(),
_ => "<TODO>".to_string(),
};
let prop_def = ClassPropertyDef {
js_doc: prop_js_doc,
ts_type,
readonly: class_prop.readonly,
is_abstract: class_prop.is_abstract,
is_static: class_prop.is_static,
accessibility: class_prop.accessibility,
name: prop_name,
location: doc_parser
.source_map
.lookup_char_pos(class_prop.span.lo())
.into(),
};
properties.push(prop_def);
}
// TODO:
TsIndexSignature(_) => {}
PrivateMethod(_) => {}
PrivateProp(_) => {}
}
}
let class_name = class_decl.ident.sym.to_string();
let class_def = ClassDef {
is_abstract: class_decl.class.is_abstract,
constructors,
properties,
methods,
};
(class_name, class_def)
}

41
cli/doc/enum.rs Normal file
View file

@ -0,0 +1,41 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
use serde::Serialize;
use swc_ecma_ast;
use super::parser::DocParser;
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct EnumMemberDef {
pub name: String,
}
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct EnumDef {
pub members: Vec<EnumMemberDef>,
}
pub fn get_doc_for_ts_enum_decl(
_doc_parser: &DocParser,
enum_decl: &swc_ecma_ast::TsEnumDecl,
) -> (String, EnumDef) {
let enum_name = enum_decl.id.sym.to_string();
let mut members = vec![];
for enum_member in &enum_decl.members {
use swc_ecma_ast::TsEnumMemberId::*;
let member_name = match &enum_member.id {
Ident(ident) => ident.sym.to_string(),
Str(str_) => str_.value.to_string(),
};
let member_def = EnumMemberDef { name: member_name };
members.push(member_def);
}
let enum_def = EnumDef { members };
(enum_name, enum_def)
}

70
cli/doc/function.rs Normal file
View file

@ -0,0 +1,70 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
use serde::Serialize;
use swc_ecma_ast;
use super::parser::DocParser;
use super::ts_type::ts_type_ann_to_def;
use super::ts_type::TsTypeDef;
use super::ParamDef;
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct FunctionDef {
pub params: Vec<ParamDef>,
pub return_type: Option<TsTypeDef>,
pub is_async: bool,
pub is_generator: bool,
// TODO: type_params, decorators
}
pub fn function_to_function_def(
doc_parser: &DocParser,
function: &swc_ecma_ast::Function,
) -> FunctionDef {
let mut params = vec![];
for param in &function.params {
use swc_ecma_ast::Pat;
let param_def = match param {
Pat::Ident(ident) => {
let ts_type = ident
.type_ann
.as_ref()
.map(|rt| ts_type_ann_to_def(&doc_parser.source_map, rt));
ParamDef {
name: ident.sym.to_string(),
ts_type,
}
}
_ => ParamDef {
name: "<TODO>".to_string(),
ts_type: None,
},
};
params.push(param_def);
}
let maybe_return_type = function
.return_type
.as_ref()
.map(|rt| ts_type_ann_to_def(&doc_parser.source_map, rt));
FunctionDef {
params,
return_type: maybe_return_type,
is_async: function.is_async,
is_generator: function.is_generator,
}
}
pub fn get_doc_for_fn_decl(
doc_parser: &DocParser,
fn_decl: &swc_ecma_ast::FnDecl,
) -> (String, FunctionDef) {
let name = fn_decl.ident.sym.to_string();
let fn_def = function_to_function_def(doc_parser, &fn_decl.function);
(name, fn_def)
}

243
cli/doc/interface.rs Normal file
View file

@ -0,0 +1,243 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
use serde::Serialize;
use swc_ecma_ast;
use super::parser::DocParser;
use super::ts_type::ts_type_ann_to_def;
use super::ts_type::TsTypeDef;
use super::Location;
use super::ParamDef;
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct InterfaceMethodDef {
// TODO: type_params
pub name: String,
pub location: Location,
pub js_doc: Option<String>,
pub params: Vec<ParamDef>,
pub return_type: Option<TsTypeDef>,
}
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct InterfacePropertyDef {
// TODO: type_params
pub name: String,
pub location: Location,
pub js_doc: Option<String>,
pub params: Vec<ParamDef>,
pub computed: bool,
pub optional: bool,
pub ts_type: Option<TsTypeDef>,
}
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct InterfaceCallSignatureDef {
// TODO: type_params
pub location: Location,
pub js_doc: Option<String>,
pub params: Vec<ParamDef>,
pub ts_type: Option<TsTypeDef>,
}
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct InterfaceDef {
// TODO: extends, type params
pub methods: Vec<InterfaceMethodDef>,
pub properties: Vec<InterfacePropertyDef>,
pub call_signatures: Vec<InterfaceCallSignatureDef>,
}
fn expr_to_name(expr: &swc_ecma_ast::Expr) -> String {
use swc_ecma_ast::Expr::*;
use swc_ecma_ast::ExprOrSuper::*;
match expr {
Ident(ident) => ident.sym.to_string(),
Member(member_expr) => {
let left = match &member_expr.obj {
Super(_) => "TODO".to_string(),
Expr(boxed_expr) => expr_to_name(&*boxed_expr),
};
let right = expr_to_name(&*member_expr.prop);
format!("[{}.{}]", left, right)
}
_ => "<TODO>".to_string(),
}
}
pub fn get_doc_for_ts_interface_decl(
doc_parser: &DocParser,
interface_decl: &swc_ecma_ast::TsInterfaceDecl,
) -> (String, InterfaceDef) {
let interface_name = interface_decl.id.sym.to_string();
let mut methods = vec![];
let mut properties = vec![];
let mut call_signatures = vec![];
for type_element in &interface_decl.body.body {
use swc_ecma_ast::TsTypeElement::*;
match &type_element {
TsMethodSignature(ts_method_sig) => {
let method_js_doc = doc_parser.js_doc_for_span(ts_method_sig.span);
let mut params = vec![];
for param in &ts_method_sig.params {
use swc_ecma_ast::TsFnParam::*;
let param_def = match param {
Ident(ident) => {
let ts_type = ident
.type_ann
.as_ref()
.map(|rt| ts_type_ann_to_def(&doc_parser.source_map, rt));
ParamDef {
name: ident.sym.to_string(),
ts_type,
}
}
_ => ParamDef {
name: "<TODO>".to_string(),
ts_type: None,
},
};
params.push(param_def);
}
let name = expr_to_name(&*ts_method_sig.key);
let maybe_return_type = ts_method_sig
.type_ann
.as_ref()
.map(|rt| ts_type_ann_to_def(&doc_parser.source_map, rt));
let method_def = InterfaceMethodDef {
name,
js_doc: method_js_doc,
location: doc_parser
.source_map
.lookup_char_pos(ts_method_sig.span.lo())
.into(),
params,
return_type: maybe_return_type,
};
methods.push(method_def);
}
TsPropertySignature(ts_prop_sig) => {
let prop_js_doc = doc_parser.js_doc_for_span(ts_prop_sig.span);
let name = match &*ts_prop_sig.key {
swc_ecma_ast::Expr::Ident(ident) => ident.sym.to_string(),
_ => "TODO".to_string(),
};
let mut params = vec![];
for param in &ts_prop_sig.params {
use swc_ecma_ast::TsFnParam::*;
let param_def = match param {
Ident(ident) => {
let ts_type = ident
.type_ann
.as_ref()
.map(|rt| ts_type_ann_to_def(&doc_parser.source_map, rt));
ParamDef {
name: ident.sym.to_string(),
ts_type,
}
}
_ => ParamDef {
name: "<TODO>".to_string(),
ts_type: None,
},
};
params.push(param_def);
}
let ts_type = ts_prop_sig
.type_ann
.as_ref()
.map(|rt| ts_type_ann_to_def(&doc_parser.source_map, rt));
let prop_def = InterfacePropertyDef {
name,
js_doc: prop_js_doc,
location: doc_parser
.source_map
.lookup_char_pos(ts_prop_sig.span.lo())
.into(),
params,
ts_type,
computed: ts_prop_sig.computed,
optional: ts_prop_sig.optional,
};
properties.push(prop_def);
}
TsCallSignatureDecl(ts_call_sig) => {
let call_sig_js_doc = doc_parser.js_doc_for_span(ts_call_sig.span);
let mut params = vec![];
for param in &ts_call_sig.params {
use swc_ecma_ast::TsFnParam::*;
let param_def = match param {
Ident(ident) => {
let ts_type = ident
.type_ann
.as_ref()
.map(|rt| ts_type_ann_to_def(&doc_parser.source_map, rt));
ParamDef {
name: ident.sym.to_string(),
ts_type,
}
}
_ => ParamDef {
name: "<TODO>".to_string(),
ts_type: None,
},
};
params.push(param_def);
}
let ts_type = ts_call_sig
.type_ann
.as_ref()
.map(|rt| ts_type_ann_to_def(&doc_parser.source_map, rt));
let call_sig_def = InterfaceCallSignatureDef {
js_doc: call_sig_js_doc,
location: doc_parser
.source_map
.lookup_char_pos(ts_call_sig.span.lo())
.into(),
params,
ts_type,
};
call_signatures.push(call_sig_def);
}
// TODO:
TsConstructSignatureDecl(_) => {}
TsIndexSignature(_) => {}
}
}
let interface_def = InterfaceDef {
methods,
properties,
call_signatures,
};
(interface_name, interface_def)
}

63
cli/doc/mod.rs Normal file
View file

@ -0,0 +1,63 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
pub mod class;
pub mod r#enum;
pub mod function;
pub mod interface;
pub mod module;
pub mod namespace;
mod node;
pub mod parser;
pub mod printer;
pub mod ts_type;
pub mod type_alias;
pub mod variable;
pub use node::DocNode;
pub use node::DocNodeKind;
pub use node::Location;
pub use node::ParamDef;
pub use parser::DocParser;
#[cfg(test)]
mod tests;
pub fn find_node_by_name_recursively(
doc_nodes: Vec<DocNode>,
name: String,
) -> Option<DocNode> {
let mut parts = name.splitn(2, '.');
let name = parts.next();
let leftover = parts.next();
name?;
let node = find_node_by_name(doc_nodes, name.unwrap().to_string());
match node {
Some(node) => match node.kind {
DocNodeKind::Namespace => {
if let Some(leftover) = leftover {
find_node_by_name_recursively(
node.namespace_def.unwrap().elements,
leftover.to_string(),
)
} else {
Some(node)
}
}
_ => {
if leftover.is_none() {
Some(node)
} else {
None
}
}
},
_ => None,
}
}
fn find_node_by_name(doc_nodes: Vec<DocNode>, name: String) -> Option<DocNode> {
let node = doc_nodes.iter().find(|node| node.name == name);
match node {
Some(node) => Some(node.clone()),
None => None,
}
}

189
cli/doc/module.rs Normal file
View file

@ -0,0 +1,189 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
use swc_common;
use swc_common::Spanned;
use swc_ecma_ast;
use super::parser::DocParser;
use super::DocNode;
use super::DocNodeKind;
pub fn get_doc_node_for_export_decl(
doc_parser: &DocParser,
export_decl: &swc_ecma_ast::ExportDecl,
) -> DocNode {
let export_span = export_decl.span();
use swc_ecma_ast::Decl;
let js_doc = doc_parser.js_doc_for_span(export_span);
let location = doc_parser
.source_map
.lookup_char_pos(export_span.lo())
.into();
match &export_decl.decl {
Decl::Class(class_decl) => {
let (name, class_def) =
super::class::get_doc_for_class_decl(doc_parser, class_decl);
DocNode {
kind: DocNodeKind::Class,
name,
location,
js_doc,
class_def: Some(class_def),
function_def: None,
variable_def: None,
enum_def: None,
type_alias_def: None,
namespace_def: None,
interface_def: None,
}
}
Decl::Fn(fn_decl) => {
let (name, function_def) =
super::function::get_doc_for_fn_decl(doc_parser, fn_decl);
DocNode {
kind: DocNodeKind::Function,
name,
location,
js_doc,
function_def: Some(function_def),
class_def: None,
variable_def: None,
enum_def: None,
type_alias_def: None,
namespace_def: None,
interface_def: None,
}
}
Decl::Var(var_decl) => {
let (name, var_def) =
super::variable::get_doc_for_var_decl(doc_parser, var_decl);
DocNode {
kind: DocNodeKind::Variable,
name,
location,
js_doc,
variable_def: Some(var_def),
function_def: None,
class_def: None,
enum_def: None,
type_alias_def: None,
namespace_def: None,
interface_def: None,
}
}
Decl::TsInterface(ts_interface_decl) => {
let (name, interface_def) =
super::interface::get_doc_for_ts_interface_decl(
doc_parser,
ts_interface_decl,
);
DocNode {
kind: DocNodeKind::Interface,
name,
location,
js_doc,
interface_def: Some(interface_def),
variable_def: None,
function_def: None,
class_def: None,
enum_def: None,
type_alias_def: None,
namespace_def: None,
}
}
Decl::TsTypeAlias(ts_type_alias) => {
let (name, type_alias_def) =
super::type_alias::get_doc_for_ts_type_alias_decl(
doc_parser,
ts_type_alias,
);
DocNode {
kind: DocNodeKind::TypeAlias,
name,
location,
js_doc,
type_alias_def: Some(type_alias_def),
interface_def: None,
variable_def: None,
function_def: None,
class_def: None,
enum_def: None,
namespace_def: None,
}
}
Decl::TsEnum(ts_enum) => {
let (name, enum_def) =
super::r#enum::get_doc_for_ts_enum_decl(doc_parser, ts_enum);
DocNode {
kind: DocNodeKind::Enum,
name,
location,
js_doc,
enum_def: Some(enum_def),
type_alias_def: None,
interface_def: None,
variable_def: None,
function_def: None,
class_def: None,
namespace_def: None,
}
}
Decl::TsModule(ts_module) => {
let (name, namespace_def) =
super::namespace::get_doc_for_ts_module(doc_parser, ts_module);
DocNode {
kind: DocNodeKind::Namespace,
name,
location,
js_doc,
namespace_def: Some(namespace_def),
enum_def: None,
type_alias_def: None,
interface_def: None,
variable_def: None,
function_def: None,
class_def: None,
}
}
}
}
#[allow(unused)]
pub fn get_doc_nodes_for_named_export(
doc_parser: &DocParser,
named_export: &swc_ecma_ast::NamedExport,
) -> Vec<DocNode> {
let file_name = named_export.src.as_ref().expect("").value.to_string();
// TODO: resolve specifier
let source_code =
std::fs::read_to_string(&file_name).expect("Failed to read file");
let doc_nodes = doc_parser
.parse(file_name, source_code)
.expect("Failed to print docs");
let reexports: Vec<String> = named_export
.specifiers
.iter()
.map(|export_specifier| {
use swc_ecma_ast::ExportSpecifier::*;
match export_specifier {
Named(named_export_specifier) => {
Some(named_export_specifier.orig.sym.to_string())
}
// TODO:
Namespace(_) => None,
Default(_) => None,
}
})
.filter(|s| s.is_some())
.map(|s| s.unwrap())
.collect();
let reexports_docs: Vec<DocNode> = doc_nodes
.into_iter()
.filter(|doc_node| reexports.contains(&doc_node.name))
.collect();
reexports_docs
}

81
cli/doc/namespace.rs Normal file
View file

@ -0,0 +1,81 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
use serde::Serialize;
use swc_ecma_ast;
use super::parser::DocParser;
use super::DocNode;
use super::DocNodeKind;
#[derive(Debug, Serialize, Clone)]
pub struct NamespaceDef {
pub elements: Vec<DocNode>,
}
pub fn get_doc_for_ts_namespace_decl(
doc_parser: &DocParser,
ts_namespace_decl: &swc_ecma_ast::TsNamespaceDecl,
) -> DocNode {
let js_doc = doc_parser.js_doc_for_span(ts_namespace_decl.span);
let location = doc_parser
.source_map
.lookup_char_pos(ts_namespace_decl.span.lo())
.into();
let namespace_name = ts_namespace_decl.id.sym.to_string();
use swc_ecma_ast::TsNamespaceBody::*;
let elements = match &*ts_namespace_decl.body {
TsModuleBlock(ts_module_block) => {
doc_parser.get_doc_nodes_for_module_body(ts_module_block.body.clone())
}
TsNamespaceDecl(ts_namespace_decl) => {
vec![get_doc_for_ts_namespace_decl(doc_parser, ts_namespace_decl)]
}
};
let ns_def = NamespaceDef { elements };
DocNode {
kind: DocNodeKind::Namespace,
name: namespace_name,
location,
js_doc,
namespace_def: Some(ns_def),
function_def: None,
variable_def: None,
enum_def: None,
class_def: None,
type_alias_def: None,
interface_def: None,
}
}
pub fn get_doc_for_ts_module(
doc_parser: &DocParser,
ts_module_decl: &swc_ecma_ast::TsModuleDecl,
) -> (String, NamespaceDef) {
use swc_ecma_ast::TsModuleName;
let namespace_name = match &ts_module_decl.id {
TsModuleName::Ident(ident) => ident.sym.to_string(),
TsModuleName::Str(str_) => str_.value.to_string(),
};
let elements = if let Some(body) = &ts_module_decl.body {
use swc_ecma_ast::TsNamespaceBody::*;
match &body {
TsModuleBlock(ts_module_block) => {
doc_parser.get_doc_nodes_for_module_body(ts_module_block.body.clone())
}
TsNamespaceDecl(ts_namespace_decl) => {
vec![get_doc_for_ts_namespace_decl(doc_parser, ts_namespace_decl)]
}
}
} else {
vec![]
};
let ns_def = NamespaceDef { elements };
(namespace_name, ns_def)
}

77
cli/doc/node.rs Normal file
View file

@ -0,0 +1,77 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
use serde::Serialize;
use swc_common;
#[derive(Debug, PartialEq, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub enum DocNodeKind {
Function,
Variable,
Class,
Enum,
Interface,
TypeAlias,
Namespace,
}
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct ParamDef {
pub name: String,
pub ts_type: Option<super::ts_type::TsTypeDef>,
}
#[derive(Debug, Serialize, Clone)]
pub struct Location {
pub filename: String,
pub line: usize,
pub col: usize,
}
impl Into<Location> for swc_common::Loc {
fn into(self) -> Location {
use swc_common::FileName::*;
let filename = match &self.file.name {
Real(path_buf) => path_buf.to_string_lossy().to_string(),
Custom(str_) => str_.to_string(),
_ => panic!("invalid filename"),
};
Location {
filename,
line: self.line,
col: self.col_display,
}
}
}
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct DocNode {
pub kind: DocNodeKind,
pub name: String,
pub location: Location,
pub js_doc: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub function_def: Option<super::function::FunctionDef>,
#[serde(skip_serializing_if = "Option::is_none")]
pub variable_def: Option<super::variable::VariableDef>,
#[serde(skip_serializing_if = "Option::is_none")]
pub enum_def: Option<super::r#enum::EnumDef>,
#[serde(skip_serializing_if = "Option::is_none")]
pub class_def: Option<super::class::ClassDef>,
#[serde(skip_serializing_if = "Option::is_none")]
pub type_alias_def: Option<super::type_alias::TypeAliasDef>,
#[serde(skip_serializing_if = "Option::is_none")]
pub namespace_def: Option<super::namespace::NamespaceDef>,
#[serde(skip_serializing_if = "Option::is_none")]
pub interface_def: Option<super::interface::InterfaceDef>,
}

189
cli/doc/parser.rs Normal file
View file

@ -0,0 +1,189 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
use regex::Regex;
use std::sync::Arc;
use std::sync::RwLock;
use swc_common;
use swc_common::comments::CommentKind;
use swc_common::comments::Comments;
use swc_common::errors::Diagnostic;
use swc_common::errors::DiagnosticBuilder;
use swc_common::errors::Emitter;
use swc_common::errors::Handler;
use swc_common::errors::HandlerFlags;
use swc_common::FileName;
use swc_common::Globals;
use swc_common::SourceMap;
use swc_common::Span;
use swc_ecma_parser::lexer::Lexer;
use swc_ecma_parser::JscTarget;
use swc_ecma_parser::Parser;
use swc_ecma_parser::Session;
use swc_ecma_parser::SourceFileInput;
use swc_ecma_parser::Syntax;
use swc_ecma_parser::TsConfig;
use super::DocNode;
pub type SwcDiagnostics = Vec<Diagnostic>;
#[derive(Clone, Default)]
pub struct BufferedError(Arc<RwLock<SwcDiagnostics>>);
impl Emitter for BufferedError {
fn emit(&mut self, db: &DiagnosticBuilder) {
self.0.write().unwrap().push((**db).clone());
}
}
impl From<BufferedError> for Vec<Diagnostic> {
fn from(buf: BufferedError) -> Self {
let s = buf.0.read().unwrap();
s.clone()
}
}
pub struct DocParser {
pub buffered_error: BufferedError,
pub source_map: Arc<SourceMap>,
pub handler: Handler,
pub comments: Comments,
pub globals: Globals,
}
impl DocParser {
pub fn default() -> Self {
let buffered_error = BufferedError::default();
let handler = Handler::with_emitter_and_flags(
Box::new(buffered_error.clone()),
HandlerFlags {
dont_buffer_diagnostics: true,
can_emit_warnings: true,
..Default::default()
},
);
DocParser {
buffered_error,
source_map: Arc::new(SourceMap::default()),
handler,
comments: Comments::default(),
globals: Globals::new(),
}
}
pub fn parse(
&self,
file_name: String,
source_code: String,
) -> Result<Vec<DocNode>, SwcDiagnostics> {
swc_common::GLOBALS.set(&self.globals, || {
let swc_source_file = self
.source_map
.new_source_file(FileName::Custom(file_name), source_code);
let buffered_err = self.buffered_error.clone();
let session = Session {
handler: &self.handler,
};
let mut ts_config = TsConfig::default();
ts_config.dynamic_import = true;
let syntax = Syntax::Typescript(ts_config);
let lexer = Lexer::new(
session,
syntax,
JscTarget::Es2019,
SourceFileInput::from(&*swc_source_file),
Some(&self.comments),
);
let mut parser = Parser::new_from(session, lexer);
let module =
parser
.parse_module()
.map_err(move |mut err: DiagnosticBuilder| {
err.cancel();
SwcDiagnostics::from(buffered_err)
})?;
let doc_entries = self.get_doc_nodes_for_module_body(module.body);
Ok(doc_entries)
})
}
pub fn get_doc_nodes_for_module_decl(
&self,
module_decl: &swc_ecma_ast::ModuleDecl,
) -> Vec<DocNode> {
use swc_ecma_ast::ModuleDecl;
match module_decl {
ModuleDecl::ExportDecl(export_decl) => {
vec![super::module::get_doc_node_for_export_decl(
self,
export_decl,
)]
}
ModuleDecl::ExportNamed(_named_export) => {
vec![]
// TODO(bartlomieju):
// super::module::get_doc_nodes_for_named_export(self, named_export)
}
ModuleDecl::ExportDefaultDecl(_) => vec![],
ModuleDecl::ExportDefaultExpr(_) => vec![],
ModuleDecl::ExportAll(_) => vec![],
ModuleDecl::TsExportAssignment(_) => vec![],
ModuleDecl::TsNamespaceExport(_) => vec![],
_ => vec![],
}
}
pub fn get_doc_nodes_for_module_body(
&self,
module_body: Vec<swc_ecma_ast::ModuleItem>,
) -> Vec<DocNode> {
let mut doc_entries: Vec<DocNode> = vec![];
for node in module_body.iter() {
if let swc_ecma_ast::ModuleItem::ModuleDecl(module_decl) = node {
doc_entries.extend(self.get_doc_nodes_for_module_decl(module_decl));
}
}
doc_entries
}
pub fn js_doc_for_span(&self, span: Span) -> Option<String> {
let comments = self.comments.take_leading_comments(span.lo())?;
let js_doc_comment = comments.iter().find(|comment| {
comment.kind == CommentKind::Block && comment.text.starts_with('*')
})?;
let mut margin_pat = String::from("");
if let Some(margin) = self.source_map.span_to_margin(span) {
for _ in 0..margin {
margin_pat.push(' ');
}
}
let js_doc_re = Regex::new(r#" ?\* ?"#).unwrap();
let txt = js_doc_comment
.text
.split('\n')
.map(|line| js_doc_re.replace(line, "").to_string())
.map(|line| {
if line.starts_with(&margin_pat) {
line[margin_pat.len()..].to_string()
} else {
line
}
})
.collect::<Vec<String>>()
.join("\n");
let txt = txt.trim_start().trim_end().to_string();
Some(txt)
}
}

432
cli/doc/printer.rs Normal file
View file

@ -0,0 +1,432 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
// TODO(ry) This module builds up output by appending to a string. Instead it
// should either use a formatting trait
// https://doc.rust-lang.org/std/fmt/index.html#formatting-traits
// Or perhaps implement a Serializer for serde
// https://docs.serde.rs/serde/ser/trait.Serializer.html
// TODO(ry) The methods in this module take ownership of the DocNodes, this is
// unnecessary and can result in unnecessary copying. Instead they should take
// references.
use crate::doc;
use crate::doc::ts_type::TsTypeDefKind;
use crate::doc::DocNodeKind;
pub fn format(doc_nodes: Vec<doc::DocNode>) -> String {
format_(doc_nodes, 0)
}
pub fn format_details(node: doc::DocNode) -> String {
let mut details = String::new();
details.push_str(&format!(
"Defined in {}:{}:{}.\n",
node.location.filename, node.location.line, node.location.col
));
details.push_str(&format_signature(&node, 0));
let js_doc = node.js_doc.clone();
if let Some(js_doc) = js_doc {
details.push_str(&format_jsdoc(js_doc, false, 1));
}
details.push_str("\n");
let maybe_extra = match node.kind {
DocNodeKind::Class => Some(format_class_details(node)),
DocNodeKind::Namespace => Some(format_namespace_details(node)),
_ => None,
};
if let Some(extra) = maybe_extra {
details.push_str(&extra);
}
details
}
fn kind_order(kind: &doc::DocNodeKind) -> i64 {
match kind {
DocNodeKind::Function => 0,
DocNodeKind::Variable => 1,
DocNodeKind::Class => 2,
DocNodeKind::Enum => 3,
DocNodeKind::Interface => 4,
DocNodeKind::TypeAlias => 5,
DocNodeKind::Namespace => 6,
}
}
fn format_signature(node: &doc::DocNode, indent: i64) -> String {
match node.kind {
DocNodeKind::Function => format_function_signature(&node, indent),
DocNodeKind::Variable => format_variable_signature(&node, indent),
DocNodeKind::Class => format_class_signature(&node, indent),
DocNodeKind::Enum => format_enum_signature(&node, indent),
DocNodeKind::Interface => format_interface_signature(&node, indent),
DocNodeKind::TypeAlias => format_type_alias_signature(&node, indent),
DocNodeKind::Namespace => format_namespace_signature(&node, indent),
}
}
fn format_(doc_nodes: Vec<doc::DocNode>, indent: i64) -> String {
let mut sorted = doc_nodes;
sorted.sort_unstable_by(|a, b| {
let kind_cmp = kind_order(&a.kind).cmp(&kind_order(&b.kind));
if kind_cmp == core::cmp::Ordering::Equal {
a.name.cmp(&b.name)
} else {
kind_cmp
}
});
let mut output = String::new();
for node in sorted {
output.push_str(&format_signature(&node, indent));
if node.js_doc.is_some() {
output.push_str(&format_jsdoc(
node.js_doc.as_ref().unwrap().to_string(),
true,
indent,
));
}
output.push_str("\n");
if DocNodeKind::Namespace == node.kind {
output.push_str(&format_(
node.namespace_def.as_ref().unwrap().elements.clone(),
indent + 1,
));
output.push_str("\n");
};
}
output
}
fn render_params(params: Vec<doc::ParamDef>) -> String {
let mut rendered = String::from("");
if !params.is_empty() {
for param in params {
rendered += param.name.as_str();
if param.ts_type.is_some() {
rendered += ": ";
rendered += render_ts_type(param.ts_type.unwrap()).as_str();
}
rendered += ", ";
}
rendered.truncate(rendered.len() - 2);
}
rendered
}
fn render_ts_type(ts_type: doc::ts_type::TsTypeDef) -> String {
let kind = ts_type.kind.unwrap();
match kind {
TsTypeDefKind::Array => {
format!("{}[]", render_ts_type(*ts_type.array.unwrap()))
}
TsTypeDefKind::Conditional => {
let conditional = ts_type.conditional_type.unwrap();
format!(
"{} extends {} ? {} : {}",
render_ts_type(*conditional.check_type),
render_ts_type(*conditional.extends_type),
render_ts_type(*conditional.true_type),
render_ts_type(*conditional.false_type)
)
}
TsTypeDefKind::FnOrConstructor => {
let fn_or_constructor = ts_type.fn_or_constructor.unwrap();
format!(
"{}({}) => {}",
if fn_or_constructor.constructor {
"new "
} else {
""
},
render_params(fn_or_constructor.params),
render_ts_type(fn_or_constructor.ts_type),
)
}
TsTypeDefKind::IndexedAccess => {
let indexed_access = ts_type.indexed_access.unwrap();
format!(
"{}[{}]",
render_ts_type(*indexed_access.obj_type),
render_ts_type(*indexed_access.index_type)
)
}
TsTypeDefKind::Intersection => {
let intersection = ts_type.intersection.unwrap();
let mut output = "".to_string();
if !intersection.is_empty() {
for ts_type in intersection {
output += render_ts_type(ts_type).as_str();
output += " & "
}
output.truncate(output.len() - 3);
}
output
}
TsTypeDefKind::Keyword => ts_type.keyword.unwrap(),
TsTypeDefKind::Literal => {
let literal = ts_type.literal.unwrap();
match literal.kind {
doc::ts_type::LiteralDefKind::Boolean => {
format!("{}", literal.boolean.unwrap())
}
doc::ts_type::LiteralDefKind::String => {
"\"".to_string() + literal.string.unwrap().as_str() + "\""
}
doc::ts_type::LiteralDefKind::Number => {
format!("{}", literal.number.unwrap())
}
}
}
TsTypeDefKind::Optional => "_optional_".to_string(),
TsTypeDefKind::Parenthesized => {
format!("({})", render_ts_type(*ts_type.parenthesized.unwrap()))
}
TsTypeDefKind::Rest => {
format!("...{}", render_ts_type(*ts_type.rest.unwrap()))
}
TsTypeDefKind::This => "this".to_string(),
TsTypeDefKind::Tuple => {
let tuple = ts_type.tuple.unwrap();
let mut output = "".to_string();
if !tuple.is_empty() {
for ts_type in tuple {
output += render_ts_type(ts_type).as_str();
output += ", "
}
output.truncate(output.len() - 2);
}
output
}
TsTypeDefKind::TypeLiteral => {
let mut output = "".to_string();
let type_literal = ts_type.type_literal.unwrap();
for node in type_literal.call_signatures {
output += format!(
"({}): {}, ",
render_params(node.params),
render_ts_type(node.ts_type.unwrap())
)
.as_str()
}
for node in type_literal.methods {
output += format!(
"{}({}): {}, ",
node.name,
render_params(node.params),
render_ts_type(node.return_type.unwrap())
)
.as_str()
}
for node in type_literal.properties {
output +=
format!("{}: {}, ", node.name, render_ts_type(node.ts_type.unwrap()))
.as_str()
}
if !output.is_empty() {
output.truncate(output.len() - 2);
}
"{ ".to_string() + output.as_str() + " }"
}
TsTypeDefKind::TypeOperator => {
let operator = ts_type.type_operator.unwrap();
format!("{} {}", operator.operator, render_ts_type(operator.ts_type))
}
TsTypeDefKind::TypeQuery => {
format!("typeof {}", ts_type.type_query.unwrap())
}
TsTypeDefKind::TypeRef => {
let type_ref = ts_type.type_ref.unwrap();
let mut final_output = type_ref.type_name;
if type_ref.type_params.is_some() {
let mut output = "".to_string();
let type_params = type_ref.type_params.unwrap();
if !type_params.is_empty() {
for ts_type in type_params {
output += render_ts_type(ts_type).as_str();
output += ", "
}
output.truncate(output.len() - 2);
}
final_output += format!("<{}>", output).as_str();
}
final_output
}
TsTypeDefKind::Union => {
let union = ts_type.union.unwrap();
let mut output = "".to_string();
if !union.is_empty() {
for ts_type in union {
output += render_ts_type(ts_type).as_str();
output += " | "
}
output.truncate(output.len() - 3);
}
output
}
}
}
fn format_indent(indent: i64) -> String {
let mut indent_str = String::new();
for _ in 0..indent {
indent_str.push_str(" ");
}
indent_str
}
// TODO: this should use some sort of markdown to console parser.
fn format_jsdoc(jsdoc: String, truncated: bool, indent: i64) -> String {
let mut lines = jsdoc.split("\n\n").map(|line| line.replace("\n", " "));
let mut js_doc = String::new();
if truncated {
let first_line = lines.next().unwrap_or_else(|| "".to_string());
js_doc.push_str(&format_indent(indent + 1));
js_doc.push_str(&format!("{}\n", first_line));
} else {
for line in lines {
js_doc.push_str(&format_indent(indent + 1));
js_doc.push_str(&format!("{}\n", line));
}
}
js_doc
}
fn format_class_details(node: doc::DocNode) -> String {
let mut details = String::new();
let class_def = node.class_def.unwrap();
for node in class_def.constructors {
details.push_str(&format!(
"constructor {}({})\n",
node.name,
render_params(node.params),
));
}
for node in class_def.properties.iter().filter(|node| {
node
.accessibility
.unwrap_or(swc_ecma_ast::Accessibility::Public)
!= swc_ecma_ast::Accessibility::Private
}) {
details.push_str(&format!(
"{} {}: {}\n",
match node
.accessibility
.unwrap_or(swc_ecma_ast::Accessibility::Public)
{
swc_ecma_ast::Accessibility::Protected => "protected".to_string(),
swc_ecma_ast::Accessibility::Public => "public".to_string(),
_ => "".to_string(),
},
node.name,
render_ts_type(node.ts_type.clone().unwrap())
));
}
for node in class_def.methods.iter().filter(|node| {
node
.accessibility
.unwrap_or(swc_ecma_ast::Accessibility::Public)
!= swc_ecma_ast::Accessibility::Private
}) {
let function_def = node.function_def.clone();
details.push_str(&format!(
"{} {}{}({}): {}\n",
match node
.accessibility
.unwrap_or(swc_ecma_ast::Accessibility::Public)
{
swc_ecma_ast::Accessibility::Protected => "protected".to_string(),
swc_ecma_ast::Accessibility::Public => "public".to_string(),
_ => "".to_string(),
},
match node.kind {
swc_ecma_ast::MethodKind::Getter => "get ".to_string(),
swc_ecma_ast::MethodKind::Setter => "set ".to_string(),
_ => "".to_string(),
},
node.name,
render_params(function_def.params),
render_ts_type(function_def.return_type.unwrap())
));
}
details.push_str("\n");
details
}
fn format_namespace_details(node: doc::DocNode) -> String {
let mut ns = String::new();
let elements = node.namespace_def.unwrap().elements;
for node in elements {
ns.push_str(&format_signature(&node, 0));
}
ns.push_str("\n");
ns
}
fn format_function_signature(node: &doc::DocNode, indent: i64) -> String {
format_indent(indent);
let function_def = node.function_def.clone().unwrap();
let return_type = function_def.return_type.unwrap();
format!(
"function {}({}): {}\n",
node.name,
render_params(function_def.params),
render_ts_type(return_type).as_str()
)
}
fn format_class_signature(node: &doc::DocNode, indent: i64) -> String {
format_indent(indent);
format!("class {}\n", node.name)
}
fn format_variable_signature(node: &doc::DocNode, indent: i64) -> String {
format_indent(indent);
let variable_def = node.variable_def.clone().unwrap();
format!(
"{} {}{}\n",
match variable_def.kind {
swc_ecma_ast::VarDeclKind::Const => "const".to_string(),
swc_ecma_ast::VarDeclKind::Let => "let".to_string(),
swc_ecma_ast::VarDeclKind::Var => "var".to_string(),
},
node.name,
if variable_def.ts_type.is_some() {
format!(": {}", render_ts_type(variable_def.ts_type.unwrap()))
} else {
"".to_string()
}
)
}
fn format_enum_signature(node: &doc::DocNode, indent: i64) -> String {
format_indent(indent);
format!("enum {}\n", node.name)
}
fn format_interface_signature(node: &doc::DocNode, indent: i64) -> String {
format_indent(indent);
format!("interface {}\n", node.name)
}
fn format_type_alias_signature(node: &doc::DocNode, indent: i64) -> String {
format_indent(indent);
format!("type {}\n", node.name)
}
fn format_namespace_signature(node: &doc::DocNode, indent: i64) -> String {
format_indent(indent);
format!("namespace {}\n", node.name)
}

568
cli/doc/tests.rs Normal file
View file

@ -0,0 +1,568 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
use super::DocParser;
use serde_json;
use serde_json::json;
#[test]
fn export_fn() {
let source_code = r#"/**
* Hello there, this is a multiline JSdoc.
*
* It has many lines
*
* Or not that many?
*/
export function foo(a: string, b: number): void {
console.log("Hello world");
}
"#;
let entries = DocParser::default()
.parse("test.ts".to_string(), source_code.to_string())
.unwrap();
assert_eq!(entries.len(), 1);
let entry = &entries[0];
let expected_json = json!({
"functionDef": {
"isAsync": false,
"isGenerator": false,
"params": [
{
"name": "a",
"tsType": {
"keyword": "string",
"kind": "keyword",
"repr": "string",
},
},
{
"name": "b",
"tsType": {
"keyword": "number",
"kind": "keyword",
"repr": "number",
},
},
],
"returnType": {
"keyword": "void",
"kind": "keyword",
"repr": "void",
},
},
"jsDoc": "Hello there, this is a multiline JSdoc.\n\nIt has many lines\n\nOr not that many?",
"kind": "function",
"location": {
"col": 0,
"filename": "test.ts",
"line": 8,
},
"name": "foo",
});
let actual = serde_json::to_value(entry).unwrap();
assert_eq!(actual, expected_json);
assert!(super::printer::format(entries).contains("Hello there"));
}
#[test]
fn export_const() {
let source_code =
"/** Something about fizzBuzz */\nexport const fizzBuzz = \"fizzBuzz\";\n";
let entries = DocParser::default()
.parse("test.ts".to_string(), source_code.to_string())
.unwrap();
assert_eq!(entries.len(), 1);
let entry = &entries[0];
let expected_json = json!({
"kind": "variable",
"name": "fizzBuzz",
"location": {
"filename": "test.ts",
"line": 2,
"col": 0
},
"jsDoc": "Something about fizzBuzz",
"variableDef": {
"tsType": null,
"kind": "const"
}
});
let actual = serde_json::to_value(entry).unwrap();
assert_eq!(actual, expected_json);
assert!(super::printer::format(entries).contains("Something about fizzBuzz"));
}
#[test]
fn export_class() {
let source_code = r#"
/** Class doc */
export class Foobar extends Fizz implements Buzz {
private private1: boolean;
protected protected1: number;
public public1: boolean;
public2: number;
/** Constructor js doc */
constructor(name: string, private private2: number, protected protected2: number) {}
/** Async foo method */
async foo(): Promise<void> {
//
}
/** Sync bar method */
bar(): void {
//
}
}
"#;
let entries = DocParser::default()
.parse("test.ts".to_string(), source_code.to_string())
.unwrap();
assert_eq!(entries.len(), 1);
let expected_json = json!({
"kind": "class",
"name": "Foobar",
"location": {
"filename": "test.ts",
"line": 3,
"col": 0
},
"jsDoc": "Class doc",
"classDef": {
"isAbstract": false,
"constructors": [
{
"jsDoc": "Constructor js doc",
"accessibility": null,
"name": "constructor",
"params": [
{
"name": "name",
"tsType": {
"repr": "string",
"kind": "keyword",
"keyword": "string"
}
},
{
"name": "<TODO>",
"tsType": null
},
{
"name": "<TODO>",
"tsType": null
}
],
"location": {
"filename": "test.ts",
"line": 10,
"col": 4
}
}
],
"properties": [
{
"jsDoc": null,
"tsType": {
"repr": "boolean",
"kind": "keyword",
"keyword": "boolean"
},
"readonly": false,
"accessibility": "private",
"isAbstract": false,
"isStatic": false,
"name": "private1",
"location": {
"filename": "test.ts",
"line": 4,
"col": 4
}
},
{
"jsDoc": null,
"tsType": {
"repr": "number",
"kind": "keyword",
"keyword": "number"
},
"readonly": false,
"accessibility": "protected",
"isAbstract": false,
"isStatic": false,
"name": "protected1",
"location": {
"filename": "test.ts",
"line": 5,
"col": 4
}
},
{
"jsDoc": null,
"tsType": {
"repr": "boolean",
"kind": "keyword",
"keyword": "boolean"
},
"readonly": false,
"accessibility": "public",
"isAbstract": false,
"isStatic": false,
"name": "public1",
"location": {
"filename": "test.ts",
"line": 6,
"col": 4
}
},
{
"jsDoc": null,
"tsType": {
"repr": "number",
"kind": "keyword",
"keyword": "number"
},
"readonly": false,
"accessibility": null,
"isAbstract": false,
"isStatic": false,
"name": "public2",
"location": {
"filename": "test.ts",
"line": 7,
"col": 4
}
}
],
"methods": [
{
"jsDoc": "Async foo method",
"accessibility": null,
"isAbstract": false,
"isStatic": false,
"name": "foo",
"kind": "method",
"functionDef": {
"params": [],
"returnType": {
"repr": "Promise",
"kind": "typeRef",
"typeRef": {
"typeParams": [
{
"repr": "void",
"kind": "keyword",
"keyword": "void"
}
],
"typeName": "Promise"
}
},
"isAsync": true,
"isGenerator": false
},
"location": {
"filename": "test.ts",
"line": 13,
"col": 4
}
},
{
"jsDoc": "Sync bar method",
"accessibility": null,
"isAbstract": false,
"isStatic": false,
"name": "bar",
"kind": "method",
"functionDef": {
"params": [],
"returnType": {
"repr": "void",
"kind": "keyword",
"keyword": "void"
},
"isAsync": false,
"isGenerator": false
},
"location": {
"filename": "test.ts",
"line": 18,
"col": 4
}
}
]
}
});
let entry = &entries[0];
let actual = serde_json::to_value(entry).unwrap();
assert_eq!(actual, expected_json);
assert!(super::printer::format(entries).contains("class Foobar"));
}
#[test]
fn export_interface() {
let source_code = r#"
/**
* Interface js doc
*/
export interface Reader {
/** Read n bytes */
read(buf: Uint8Array, something: unknown): Promise<number>
}
"#;
let entries = DocParser::default()
.parse("test.ts".to_string(), source_code.to_string())
.unwrap();
assert_eq!(entries.len(), 1);
let entry = &entries[0];
let expected_json = json!({
"kind": "interface",
"name": "Reader",
"location": {
"filename": "test.ts",
"line": 5,
"col": 0
},
"jsDoc": "Interface js doc",
"interfaceDef": {
"methods": [
{
"name": "read",
"location": {
"filename": "test.ts",
"line": 7,
"col": 4
},
"jsDoc": "Read n bytes",
"params": [
{
"name": "buf",
"tsType": {
"repr": "Uint8Array",
"kind": "typeRef",
"typeRef": {
"typeParams": null,
"typeName": "Uint8Array"
}
}
},
{
"name": "something",
"tsType": {
"repr": "unknown",
"kind": "keyword",
"keyword": "unknown"
}
}
],
"returnType": {
"repr": "Promise",
"kind": "typeRef",
"typeRef": {
"typeParams": [
{
"repr": "number",
"kind": "keyword",
"keyword": "number"
}
],
"typeName": "Promise"
}
}
}
],
"properties": [],
"callSignatures": []
}
});
let actual = serde_json::to_value(entry).unwrap();
assert_eq!(actual, expected_json);
assert!(super::printer::format(entries).contains("interface Reader"));
}
#[test]
fn export_type_alias() {
let source_code = r#"
/** Array holding numbers */
export type NumberArray = Array<number>;
"#;
let entries = DocParser::default()
.parse("test.ts".to_string(), source_code.to_string())
.unwrap();
assert_eq!(entries.len(), 1);
let entry = &entries[0];
let expected_json = json!({
"kind": "typeAlias",
"name": "NumberArray",
"location": {
"filename": "test.ts",
"line": 3,
"col": 0
},
"jsDoc": "Array holding numbers",
"typeAliasDef": {
"tsType": {
"repr": "Array",
"kind": "typeRef",
"typeRef": {
"typeParams": [
{
"repr": "number",
"kind": "keyword",
"keyword": "number"
}
],
"typeName": "Array"
}
}
}
});
let actual = serde_json::to_value(entry).unwrap();
assert_eq!(actual, expected_json);
assert!(super::printer::format(entries).contains("Array holding numbers"));
}
#[test]
fn export_enum() {
let source_code = r#"
/**
* Some enum for good measure
*/
export enum Hello {
World = "world",
Fizz = "fizz",
Buzz = "buzz",
}
"#;
let entries = DocParser::default()
.parse("test.ts".to_string(), source_code.to_string())
.unwrap();
assert_eq!(entries.len(), 1);
let entry = &entries[0];
let expected_json = json!({
"kind": "enum",
"name": "Hello",
"location": {
"filename": "test.ts",
"line": 5,
"col": 0
},
"jsDoc": "Some enum for good measure",
"enumDef": {
"members": [
{
"name": "World"
},
{
"name": "Fizz"
},
{
"name": "Buzz"
}
]
}
});
let actual = serde_json::to_value(entry).unwrap();
assert_eq!(actual, expected_json);
assert!(super::printer::format(entries.clone())
.contains("Some enum for good measure"));
assert!(super::printer::format(entries).contains("enum Hello"));
}
#[test]
fn export_namespace() {
let source_code = r#"
/** Namespace JSdoc */
export namespace RootNs {
export const a = "a";
/** Nested namespace JSDoc */
export namespace NestedNs {
export enum Foo {
a = 1,
b = 2,
c = 3,
}
}
}
"#;
let entries = DocParser::default()
.parse("test.ts".to_string(), source_code.to_string())
.unwrap();
assert_eq!(entries.len(), 1);
let entry = &entries[0];
let expected_json = json!({
"kind": "namespace",
"name": "RootNs",
"location": {
"filename": "test.ts",
"line": 3,
"col": 0
},
"jsDoc": "Namespace JSdoc",
"namespaceDef": {
"elements": [
{
"kind": "variable",
"name": "a",
"location": {
"filename": "test.ts",
"line": 4,
"col": 4
},
"jsDoc": null,
"variableDef": {
"tsType": null,
"kind": "const"
}
},
{
"kind": "namespace",
"name": "NestedNs",
"location": {
"filename": "test.ts",
"line": 7,
"col": 4
},
"jsDoc": "Nested namespace JSDoc",
"namespaceDef": {
"elements": [
{
"kind": "enum",
"name": "Foo",
"location": {
"filename": "test.ts",
"line": 8,
"col": 6
},
"jsDoc": null,
"enumDef": {
"members": [
{
"name": "a"
},
{
"name": "b"
},
{
"name": "c"
}
]
}
}
]
}
}
]
}
});
let actual = serde_json::to_value(entry).unwrap();
assert_eq!(actual, expected_json);
assert!(super::printer::format(entries).contains("namespace RootNs"));
}

821
cli/doc/ts_type.rs Normal file
View file

@ -0,0 +1,821 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
use super::ParamDef;
use serde::Serialize;
use swc_common::SourceMap;
use swc_ecma_ast;
use swc_ecma_ast::TsArrayType;
use swc_ecma_ast::TsConditionalType;
use swc_ecma_ast::TsFnOrConstructorType;
use swc_ecma_ast::TsIndexedAccessType;
use swc_ecma_ast::TsKeywordType;
use swc_ecma_ast::TsLit;
use swc_ecma_ast::TsLitType;
use swc_ecma_ast::TsOptionalType;
use swc_ecma_ast::TsParenthesizedType;
use swc_ecma_ast::TsRestType;
use swc_ecma_ast::TsThisType;
use swc_ecma_ast::TsTupleType;
use swc_ecma_ast::TsType;
use swc_ecma_ast::TsTypeAnn;
use swc_ecma_ast::TsTypeLit;
use swc_ecma_ast::TsTypeOperator;
use swc_ecma_ast::TsTypeQuery;
use swc_ecma_ast::TsTypeRef;
use swc_ecma_ast::TsUnionOrIntersectionType;
// pub enum TsType {
// * TsKeywordType(TsKeywordType),
// * TsThisType(TsThisType),
// * TsFnOrConstructorType(TsFnOrConstructorType),
// * TsTypeRef(TsTypeRef),
// * TsTypeQuery(TsTypeQuery),
// * TsTypeLit(TsTypeLit),
// * TsArrayType(TsArrayType),
// * TsTupleType(TsTupleType),
// * TsOptionalType(TsOptionalType),
// * TsRestType(TsRestType),
// * TsUnionOrIntersectionType(TsUnionOrIntersectionType),
// * TsConditionalType(TsConditionalType),
// TsInferType(TsInferType),
// * TsParenthesizedType(TsParenthesizedType),
// * TsTypeOperator(TsTypeOperator),
// * TsIndexedAccessType(TsIndexedAccessType),
// TsMappedType(TsMappedType),
// * TsLitType(TsLitType),
// TsTypePredicate(TsTypePredicate),
// TsImportType(TsImportType),
// }
impl Into<TsTypeDef> for &TsLitType {
fn into(self) -> TsTypeDef {
let (repr, lit) = match &self.lit {
TsLit::Number(num) => (
format!("{}", num.value),
LiteralDef {
kind: LiteralDefKind::Number,
number: Some(num.value),
string: None,
boolean: None,
},
),
TsLit::Str(str_) => (
str_.value.to_string(),
LiteralDef {
kind: LiteralDefKind::String,
number: None,
string: Some(str_.value.to_string()),
boolean: None,
},
),
TsLit::Bool(bool_) => (
bool_.value.to_string(),
LiteralDef {
kind: LiteralDefKind::Boolean,
number: None,
string: None,
boolean: Some(bool_.value),
},
),
};
TsTypeDef {
repr,
kind: Some(TsTypeDefKind::Literal),
literal: Some(lit),
..Default::default()
}
}
}
impl Into<TsTypeDef> for &TsArrayType {
fn into(self) -> TsTypeDef {
let ts_type_def: TsTypeDef = (&*self.elem_type).into();
TsTypeDef {
array: Some(Box::new(ts_type_def)),
kind: Some(TsTypeDefKind::Array),
..Default::default()
}
}
}
impl Into<TsTypeDef> for &TsTupleType {
fn into(self) -> TsTypeDef {
let mut type_defs = vec![];
for type_box in &self.elem_types {
let ts_type: &TsType = &(*type_box);
let def: TsTypeDef = ts_type.into();
type_defs.push(def)
}
TsTypeDef {
tuple: Some(type_defs),
kind: Some(TsTypeDefKind::Tuple),
..Default::default()
}
}
}
impl Into<TsTypeDef> for &TsUnionOrIntersectionType {
fn into(self) -> TsTypeDef {
use swc_ecma_ast::TsUnionOrIntersectionType::*;
match self {
TsUnionType(union_type) => {
let mut types_union = vec![];
for type_box in &union_type.types {
let ts_type: &TsType = &(*type_box);
let def: TsTypeDef = ts_type.into();
types_union.push(def);
}
TsTypeDef {
union: Some(types_union),
kind: Some(TsTypeDefKind::Union),
..Default::default()
}
}
TsIntersectionType(intersection_type) => {
let mut types_intersection = vec![];
for type_box in &intersection_type.types {
let ts_type: &TsType = &(*type_box);
let def: TsTypeDef = ts_type.into();
types_intersection.push(def);
}
TsTypeDef {
intersection: Some(types_intersection),
kind: Some(TsTypeDefKind::Intersection),
..Default::default()
}
}
}
}
}
impl Into<TsTypeDef> for &TsKeywordType {
fn into(self) -> TsTypeDef {
use swc_ecma_ast::TsKeywordTypeKind::*;
let keyword_str = match self.kind {
TsAnyKeyword => "any",
TsUnknownKeyword => "unknown",
TsNumberKeyword => "number",
TsObjectKeyword => "object",
TsBooleanKeyword => "boolean",
TsBigIntKeyword => "bigint",
TsStringKeyword => "string",
TsSymbolKeyword => "symbol",
TsVoidKeyword => "void",
TsUndefinedKeyword => "undefined",
TsNullKeyword => "null",
TsNeverKeyword => "never",
};
TsTypeDef {
repr: keyword_str.to_string(),
kind: Some(TsTypeDefKind::Keyword),
keyword: Some(keyword_str.to_string()),
..Default::default()
}
}
}
impl Into<TsTypeDef> for &TsTypeOperator {
fn into(self) -> TsTypeDef {
let ts_type = (&*self.type_ann).into();
let type_operator_def = TsTypeOperatorDef {
operator: self.op.as_str().to_string(),
ts_type,
};
TsTypeDef {
type_operator: Some(Box::new(type_operator_def)),
kind: Some(TsTypeDefKind::TypeOperator),
..Default::default()
}
}
}
impl Into<TsTypeDef> for &TsParenthesizedType {
fn into(self) -> TsTypeDef {
let ts_type = (&*self.type_ann).into();
TsTypeDef {
parenthesized: Some(Box::new(ts_type)),
kind: Some(TsTypeDefKind::Parenthesized),
..Default::default()
}
}
}
impl Into<TsTypeDef> for &TsRestType {
fn into(self) -> TsTypeDef {
let ts_type = (&*self.type_ann).into();
TsTypeDef {
rest: Some(Box::new(ts_type)),
kind: Some(TsTypeDefKind::Rest),
..Default::default()
}
}
}
impl Into<TsTypeDef> for &TsOptionalType {
fn into(self) -> TsTypeDef {
let ts_type = (&*self.type_ann).into();
TsTypeDef {
optional: Some(Box::new(ts_type)),
kind: Some(TsTypeDefKind::Optional),
..Default::default()
}
}
}
impl Into<TsTypeDef> for &TsThisType {
fn into(self) -> TsTypeDef {
TsTypeDef {
repr: "this".to_string(),
this: Some(true),
kind: Some(TsTypeDefKind::This),
..Default::default()
}
}
}
fn ts_entity_name_to_name(entity_name: &swc_ecma_ast::TsEntityName) -> String {
use swc_ecma_ast::TsEntityName::*;
match entity_name {
Ident(ident) => ident.sym.to_string(),
TsQualifiedName(ts_qualified_name) => {
let left = ts_entity_name_to_name(&ts_qualified_name.left);
let right = ts_qualified_name.right.sym.to_string();
format!("{}.{}", left, right)
}
}
}
impl Into<TsTypeDef> for &TsTypeQuery {
fn into(self) -> TsTypeDef {
use swc_ecma_ast::TsTypeQueryExpr::*;
let type_name = match &self.expr_name {
TsEntityName(entity_name) => ts_entity_name_to_name(&*entity_name),
Import(import_type) => import_type.arg.value.to_string(),
};
TsTypeDef {
repr: type_name.to_string(),
type_query: Some(type_name),
kind: Some(TsTypeDefKind::TypeQuery),
..Default::default()
}
}
}
impl Into<TsTypeDef> for &TsTypeRef {
fn into(self) -> TsTypeDef {
let type_name = ts_entity_name_to_name(&self.type_name);
let type_params = if let Some(type_params_inst) = &self.type_params {
let mut ts_type_defs = vec![];
for type_box in &type_params_inst.params {
let ts_type: &TsType = &(*type_box);
let def: TsTypeDef = ts_type.into();
ts_type_defs.push(def);
}
Some(ts_type_defs)
} else {
None
};
TsTypeDef {
repr: type_name.to_string(),
type_ref: Some(TsTypeRefDef {
type_name,
type_params,
}),
kind: Some(TsTypeDefKind::TypeRef),
..Default::default()
}
}
}
impl Into<TsTypeDef> for &TsIndexedAccessType {
fn into(self) -> TsTypeDef {
let indexed_access_def = TsIndexedAccessDef {
readonly: self.readonly,
obj_type: Box::new((&*self.obj_type).into()),
index_type: Box::new((&*self.index_type).into()),
};
TsTypeDef {
indexed_access: Some(indexed_access_def),
kind: Some(TsTypeDefKind::IndexedAccess),
..Default::default()
}
}
}
impl Into<TsTypeDef> for &TsTypeLit {
fn into(self) -> TsTypeDef {
let mut methods = vec![];
let mut properties = vec![];
let mut call_signatures = vec![];
for type_element in &self.members {
use swc_ecma_ast::TsTypeElement::*;
match &type_element {
TsMethodSignature(ts_method_sig) => {
let mut params = vec![];
for param in &ts_method_sig.params {
use swc_ecma_ast::TsFnParam::*;
let param_def = match param {
Ident(ident) => {
let ts_type =
ident.type_ann.as_ref().map(|rt| (&*rt.type_ann).into());
ParamDef {
name: ident.sym.to_string(),
ts_type,
}
}
_ => ParamDef {
name: "<TODO>".to_string(),
ts_type: None,
},
};
params.push(param_def);
}
let maybe_return_type = ts_method_sig
.type_ann
.as_ref()
.map(|rt| (&*rt.type_ann).into());
let method_def = LiteralMethodDef {
name: "<TODO>".to_string(),
params,
return_type: maybe_return_type,
};
methods.push(method_def);
}
TsPropertySignature(ts_prop_sig) => {
let name = match &*ts_prop_sig.key {
swc_ecma_ast::Expr::Ident(ident) => ident.sym.to_string(),
_ => "TODO".to_string(),
};
let mut params = vec![];
for param in &ts_prop_sig.params {
use swc_ecma_ast::TsFnParam::*;
let param_def = match param {
Ident(ident) => {
let ts_type =
ident.type_ann.as_ref().map(|rt| (&*rt.type_ann).into());
ParamDef {
name: ident.sym.to_string(),
ts_type,
}
}
_ => ParamDef {
name: "<TODO>".to_string(),
ts_type: None,
},
};
params.push(param_def);
}
let ts_type = ts_prop_sig
.type_ann
.as_ref()
.map(|rt| (&*rt.type_ann).into());
let prop_def = LiteralPropertyDef {
name,
params,
ts_type,
computed: ts_prop_sig.computed,
optional: ts_prop_sig.optional,
};
properties.push(prop_def);
}
TsCallSignatureDecl(ts_call_sig) => {
let mut params = vec![];
for param in &ts_call_sig.params {
use swc_ecma_ast::TsFnParam::*;
let param_def = match param {
Ident(ident) => {
let ts_type =
ident.type_ann.as_ref().map(|rt| (&*rt.type_ann).into());
ParamDef {
name: ident.sym.to_string(),
ts_type,
}
}
_ => ParamDef {
name: "<TODO>".to_string(),
ts_type: None,
},
};
params.push(param_def);
}
let ts_type = ts_call_sig
.type_ann
.as_ref()
.map(|rt| (&*rt.type_ann).into());
let call_sig_def = LiteralCallSignatureDef { params, ts_type };
call_signatures.push(call_sig_def);
}
// TODO:
TsConstructSignatureDecl(_) => {}
TsIndexSignature(_) => {}
}
}
let type_literal = TsTypeLiteralDef {
methods,
properties,
call_signatures,
};
TsTypeDef {
kind: Some(TsTypeDefKind::TypeLiteral),
type_literal: Some(type_literal),
..Default::default()
}
}
}
impl Into<TsTypeDef> for &TsConditionalType {
fn into(self) -> TsTypeDef {
let conditional_type_def = TsConditionalDef {
check_type: Box::new((&*self.check_type).into()),
extends_type: Box::new((&*self.extends_type).into()),
true_type: Box::new((&*self.true_type).into()),
false_type: Box::new((&*self.false_type).into()),
};
TsTypeDef {
kind: Some(TsTypeDefKind::Conditional),
conditional_type: Some(conditional_type_def),
..Default::default()
}
}
}
impl Into<TsTypeDef> for &TsFnOrConstructorType {
fn into(self) -> TsTypeDef {
use swc_ecma_ast::TsFnOrConstructorType::*;
let fn_def = match self {
TsFnType(ts_fn_type) => {
let mut params = vec![];
for param in &ts_fn_type.params {
use swc_ecma_ast::TsFnParam::*;
let param_def = match param {
Ident(ident) => {
let ts_type: Option<TsTypeDef> =
ident.type_ann.as_ref().map(|rt| {
let type_box = &*rt.type_ann;
(&*type_box).into()
});
ParamDef {
name: ident.sym.to_string(),
ts_type,
}
}
_ => ParamDef {
name: "<TODO>".to_string(),
ts_type: None,
},
};
params.push(param_def);
}
TsFnOrConstructorDef {
constructor: false,
ts_type: (&*ts_fn_type.type_ann.type_ann).into(),
params,
}
}
TsConstructorType(ctor_type) => {
let mut params = vec![];
for param in &ctor_type.params {
use swc_ecma_ast::TsFnParam::*;
let param_def = match param {
Ident(ident) => {
let ts_type: Option<TsTypeDef> =
ident.type_ann.as_ref().map(|rt| {
let type_box = &*rt.type_ann;
(&*type_box).into()
});
ParamDef {
name: ident.sym.to_string(),
ts_type,
}
}
_ => ParamDef {
name: "<TODO>".to_string(),
ts_type: None,
},
};
params.push(param_def);
}
TsFnOrConstructorDef {
constructor: true,
ts_type: (&*ctor_type.type_ann.type_ann).into(),
params: vec![],
}
}
};
TsTypeDef {
kind: Some(TsTypeDefKind::FnOrConstructor),
fn_or_constructor: Some(Box::new(fn_def)),
..Default::default()
}
}
}
impl Into<TsTypeDef> for &TsType {
fn into(self) -> TsTypeDef {
use swc_ecma_ast::TsType::*;
match self {
TsKeywordType(ref keyword_type) => keyword_type.into(),
TsLitType(ref lit_type) => lit_type.into(),
TsTypeRef(ref type_ref) => type_ref.into(),
TsUnionOrIntersectionType(union_or_inter) => union_or_inter.into(),
TsArrayType(array_type) => array_type.into(),
TsTupleType(tuple_type) => tuple_type.into(),
TsTypeOperator(type_op_type) => type_op_type.into(),
TsParenthesizedType(paren_type) => paren_type.into(),
TsRestType(rest_type) => rest_type.into(),
TsOptionalType(optional_type) => optional_type.into(),
TsTypeQuery(type_query) => type_query.into(),
TsThisType(this_type) => this_type.into(),
TsFnOrConstructorType(fn_or_con_type) => fn_or_con_type.into(),
TsConditionalType(conditional_type) => conditional_type.into(),
TsIndexedAccessType(indexed_access_type) => indexed_access_type.into(),
TsTypeLit(type_literal) => type_literal.into(),
_ => TsTypeDef {
repr: "<UNIMPLEMENTED>".to_string(),
..Default::default()
},
}
}
}
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct TsTypeRefDef {
pub type_params: Option<Vec<TsTypeDef>>,
pub type_name: String,
}
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub enum LiteralDefKind {
Number,
String,
Boolean,
}
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct LiteralDef {
pub kind: LiteralDefKind,
#[serde(skip_serializing_if = "Option::is_none")]
pub number: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub string: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub boolean: Option<bool>,
}
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct TsTypeOperatorDef {
pub operator: String,
pub ts_type: TsTypeDef,
}
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct TsFnOrConstructorDef {
// TODO: type_params
pub constructor: bool,
pub ts_type: TsTypeDef,
pub params: Vec<ParamDef>,
}
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct TsConditionalDef {
pub check_type: Box<TsTypeDef>,
pub extends_type: Box<TsTypeDef>,
pub true_type: Box<TsTypeDef>,
pub false_type: Box<TsTypeDef>,
}
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct TsIndexedAccessDef {
pub readonly: bool,
pub obj_type: Box<TsTypeDef>,
pub index_type: Box<TsTypeDef>,
}
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct LiteralMethodDef {
// TODO: type_params
pub name: String,
// pub location: Location,
// pub js_doc: Option<String>,
pub params: Vec<ParamDef>,
pub return_type: Option<TsTypeDef>,
}
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct LiteralPropertyDef {
// TODO: type_params
pub name: String,
// pub location: Location,
// pub js_doc: Option<String>,
pub params: Vec<ParamDef>,
pub computed: bool,
pub optional: bool,
pub ts_type: Option<TsTypeDef>,
}
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct LiteralCallSignatureDef {
// TODO: type_params
// pub location: Location,
// pub js_doc: Option<String>,
pub params: Vec<ParamDef>,
pub ts_type: Option<TsTypeDef>,
}
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct TsTypeLiteralDef {
pub methods: Vec<LiteralMethodDef>,
pub properties: Vec<LiteralPropertyDef>,
pub call_signatures: Vec<LiteralCallSignatureDef>,
}
#[derive(Debug, PartialEq, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub enum TsTypeDefKind {
Keyword,
Literal,
TypeRef,
Union,
Intersection,
Array,
Tuple,
TypeOperator,
Parenthesized,
Rest,
Optional,
TypeQuery,
This,
FnOrConstructor,
Conditional,
IndexedAccess,
TypeLiteral,
}
#[derive(Debug, Default, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct TsTypeDef {
pub repr: String,
pub kind: Option<TsTypeDefKind>,
// TODO: make this struct more conrete
#[serde(skip_serializing_if = "Option::is_none")]
pub keyword: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub literal: Option<LiteralDef>,
#[serde(skip_serializing_if = "Option::is_none")]
pub type_ref: Option<TsTypeRefDef>,
#[serde(skip_serializing_if = "Option::is_none")]
pub union: Option<Vec<TsTypeDef>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub intersection: Option<Vec<TsTypeDef>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub array: Option<Box<TsTypeDef>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tuple: Option<Vec<TsTypeDef>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub type_operator: Option<Box<TsTypeOperatorDef>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parenthesized: Option<Box<TsTypeDef>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rest: Option<Box<TsTypeDef>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub optional: Option<Box<TsTypeDef>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub type_query: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub this: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fn_or_constructor: Option<Box<TsFnOrConstructorDef>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub conditional_type: Option<TsConditionalDef>,
#[serde(skip_serializing_if = "Option::is_none")]
pub indexed_access: Option<TsIndexedAccessDef>,
#[serde(skip_serializing_if = "Option::is_none")]
pub type_literal: Option<TsTypeLiteralDef>,
}
pub fn ts_type_ann_to_def(
source_map: &SourceMap,
type_ann: &TsTypeAnn,
) -> TsTypeDef {
use swc_ecma_ast::TsType::*;
match &*type_ann.type_ann {
TsKeywordType(keyword_type) => keyword_type.into(),
TsLitType(lit_type) => lit_type.into(),
TsTypeRef(type_ref) => type_ref.into(),
TsUnionOrIntersectionType(union_or_inter) => union_or_inter.into(),
TsArrayType(array_type) => array_type.into(),
TsTupleType(tuple_type) => tuple_type.into(),
TsTypeOperator(type_op_type) => type_op_type.into(),
TsParenthesizedType(paren_type) => paren_type.into(),
TsRestType(rest_type) => rest_type.into(),
TsOptionalType(optional_type) => optional_type.into(),
TsTypeQuery(type_query) => type_query.into(),
TsThisType(this_type) => this_type.into(),
TsFnOrConstructorType(fn_or_con_type) => fn_or_con_type.into(),
TsConditionalType(conditional_type) => conditional_type.into(),
TsIndexedAccessType(indexed_access_type) => indexed_access_type.into(),
TsTypeLit(type_literal) => type_literal.into(),
_ => {
let repr = source_map
.span_to_snippet(type_ann.span)
.expect("Class prop type not found");
let repr = repr.trim_start_matches(':').trim_start().to_string();
TsTypeDef {
repr,
..Default::default()
}
}
}
}

25
cli/doc/type_alias.rs Normal file
View file

@ -0,0 +1,25 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
use serde::Serialize;
use swc_ecma_ast;
use super::parser::DocParser;
use super::ts_type::TsTypeDef;
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct TypeAliasDef {
pub ts_type: TsTypeDef,
// TODO: type_params
}
pub fn get_doc_for_ts_type_alias_decl(
_doc_parser: &DocParser,
type_alias_decl: &swc_ecma_ast::TsTypeAliasDecl,
) -> (String, TypeAliasDef) {
let alias_name = type_alias_decl.id.sym.to_string();
let ts_type = type_alias_decl.type_ann.as_ref().into();
let type_alias_def = TypeAliasDef { ts_type };
(alias_name, type_alias_def)
}

43
cli/doc/variable.rs Normal file
View file

@ -0,0 +1,43 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
use serde::Serialize;
use swc_ecma_ast;
use super::parser::DocParser;
use super::ts_type::ts_type_ann_to_def;
use super::ts_type::TsTypeDef;
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct VariableDef {
pub ts_type: Option<TsTypeDef>,
pub kind: swc_ecma_ast::VarDeclKind,
}
pub fn get_doc_for_var_decl(
doc_parser: &DocParser,
var_decl: &swc_ecma_ast::VarDecl,
) -> (String, VariableDef) {
assert!(!var_decl.decls.is_empty());
// TODO: support multiple declarators
let var_declarator = var_decl.decls.get(0).unwrap();
let var_name = match &var_declarator.name {
swc_ecma_ast::Pat::Ident(ident) => ident.sym.to_string(),
_ => "<TODO>".to_string(),
};
let maybe_ts_type = match &var_declarator.name {
swc_ecma_ast::Pat::Ident(ident) => ident
.type_ann
.as_ref()
.map(|rt| ts_type_ann_to_def(&doc_parser.source_map, rt)),
_ => None,
};
let variable_def = VariableDef {
ts_type: maybe_ts_type,
kind: var_decl.kind,
};
(var_name, variable_def)
}

View file

@ -31,6 +31,11 @@ pub enum DenoSubcommand {
Completions {
buf: Box<[u8]>,
},
Doc {
json: bool,
source_file: String,
filter: Option<String>,
},
Eval {
code: String,
as_typescript: bool,
@ -258,6 +263,8 @@ pub fn flags_from_vec_safe(args: Vec<String>) -> clap::Result<Flags> {
test_parse(&mut flags, m);
} else if let Some(m) = matches.subcommand_matches("upgrade") {
upgrade_parse(&mut flags, m);
} else if let Some(m) = matches.subcommand_matches("doc") {
doc_parse(&mut flags, m);
} else {
unimplemented!();
}
@ -311,6 +318,7 @@ If the flag is set, restrict these messages to errors.",
.subcommand(test_subcommand())
.subcommand(types_subcommand())
.subcommand(upgrade_subcommand())
.subcommand(doc_subcommand())
.long_about(DENO_HELP)
.after_help(ENV_VARIABLES_HELP)
}
@ -550,6 +558,22 @@ fn upgrade_parse(flags: &mut Flags, matches: &clap::ArgMatches) {
flags.subcommand = DenoSubcommand::Upgrade { dry_run, force };
}
fn doc_parse(flags: &mut Flags, matches: &clap::ArgMatches) {
reload_arg_parse(flags, matches);
let source_file = matches.value_of("source_file").map(String::from).unwrap();
let json = matches.is_present("json");
let filter = if matches.is_present("filter") {
Some(matches.value_of("filter").unwrap().to_string())
} else {
None
};
flags.subcommand = DenoSubcommand::Doc {
source_file,
json,
filter,
};
}
fn types_subcommand<'a, 'b>() -> App<'a, 'b> {
SubCommand::with_name("types")
.about("Print runtime TypeScript declarations")
@ -770,6 +794,43 @@ and is used to replace the current executable.",
)
}
fn doc_subcommand<'a, 'b>() -> App<'a, 'b> {
SubCommand::with_name("doc")
.about("Show documentation for module")
.long_about(
"Show documentation for module.
Output documentation to terminal:
deno doc ./path/to/module.ts
Show detail of symbol:
deno doc ./path/to/module.ts MyClass.someField
Output documentation in JSON format:
deno doc --json ./path/to/module.ts",
)
.arg(reload_arg())
.arg(
Arg::with_name("json")
.long("json")
.help("Output documentation in JSON format.")
.takes_value(false),
)
.arg(
Arg::with_name("source_file")
.takes_value(true)
.required(true),
)
.arg(
Arg::with_name("filter")
.help("Dot separated path to symbol.")
.takes_value(true)
.required(false)
.conflicts_with("json")
.conflicts_with("pretty"),
)
}
fn permission_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
app
.arg(
@ -1219,6 +1280,7 @@ fn arg_hacks(mut args: Vec<String>) -> Vec<String> {
let subcommands = sset![
"bundle",
"completions",
"doc",
"eval",
"fetch",
"fmt",
@ -2379,6 +2441,41 @@ fn repl_with_cafile() {
);
}
#[test]
fn doc() {
let r =
flags_from_vec_safe(svec!["deno", "doc", "--json", "path/to/module.ts"]);
assert_eq!(
r.unwrap(),
Flags {
subcommand: DenoSubcommand::Doc {
json: true,
source_file: "path/to/module.ts".to_string(),
filter: None,
},
..Flags::default()
}
);
let r = flags_from_vec_safe(svec![
"deno",
"doc",
"path/to/module.ts",
"SomeClass.someField"
]);
assert_eq!(
r.unwrap(),
Flags {
subcommand: DenoSubcommand::Doc {
json: false,
source_file: "path/to/module.ts".to_string(),
filter: Some("SomeClass.someField".to_string()),
},
..Flags::default()
}
);
}
#[test]
fn inspect_default_host() {
let r = flags_from_vec_safe(svec!["deno", "run", "--inspect", "foo.js"]);

View file

@ -27,6 +27,7 @@ pub mod compilers;
pub mod deno_dir;
pub mod diagnostics;
mod disk_cache;
mod doc;
mod file_fetcher;
pub mod flags;
mod fmt;
@ -111,6 +112,18 @@ impl log::Log for Logger {
fn flush(&self) {}
}
fn write_to_stdout_ignore_sigpipe(bytes: &[u8]) -> Result<(), std::io::Error> {
use std::io::ErrorKind;
match std::io::stdout().write_all(bytes) {
Ok(()) => Ok(()),
Err(e) => match e.kind() {
ErrorKind::BrokenPipe => Ok(()),
_ => Err(e),
},
}
}
fn create_main_worker(
global_state: GlobalState,
main_module: ModuleSpecifier,
@ -344,6 +357,57 @@ async fn bundle_command(
bundle_result
}
async fn doc_command(
flags: Flags,
source_file: String,
json: bool,
maybe_filter: Option<String>,
) -> Result<(), ErrBox> {
let global_state = GlobalState::new(flags.clone())?;
let module_specifier =
ModuleSpecifier::resolve_url_or_path(&source_file).unwrap();
let source_file = global_state
.file_fetcher
.fetch_source_file(&module_specifier, None)
.await?;
let source_code = String::from_utf8(source_file.source_code)?;
let doc_parser = doc::DocParser::default();
let parse_result =
doc_parser.parse(module_specifier.to_string(), source_code);
let doc_nodes = match parse_result {
Ok(nodes) => nodes,
Err(e) => {
eprintln!("Failed to parse documentation:");
for diagnostic in e {
eprintln!("{}", diagnostic.message());
}
std::process::exit(1);
}
};
if json {
let writer = std::io::BufWriter::new(std::io::stdout());
serde_json::to_writer_pretty(writer, &doc_nodes).map_err(ErrBox::from)
} else {
let details = if let Some(filter) = maybe_filter {
let node = doc::find_node_by_name_recursively(doc_nodes, filter.clone());
if let Some(node) = node {
doc::printer::format_details(node)
} else {
eprintln!("Node {} was not found!", filter);
std::process::exit(1);
}
} else {
doc::printer::format(doc_nodes)
};
write_to_stdout_ignore_sigpipe(details.as_bytes()).map_err(ErrBox::from)
}
}
async fn run_repl(flags: Flags) -> Result<(), ErrBox> {
let main_module =
ModuleSpecifier::resolve_url_or_path("./__$deno$repl.ts").unwrap();
@ -451,6 +515,11 @@ pub fn main() {
source_file,
out_file,
} => bundle_command(flags, source_file, out_file).boxed_local(),
DenoSubcommand::Doc {
source_file,
json,
filter,
} => doc_command(flags, source_file, json, filter).boxed_local(),
DenoSubcommand::Eval {
code,
as_typescript,
@ -478,7 +547,10 @@ pub fn main() {
allow_none,
} => test_command(flags, include, fail_fast, allow_none).boxed_local(),
DenoSubcommand::Completions { buf } => {
print!("{}", std::str::from_utf8(&buf).unwrap());
if let Err(e) = write_to_stdout_ignore_sigpipe(&buf) {
eprintln!("{}", e);
std::process::exit(1);
}
return;
}
DenoSubcommand::Types => {
@ -488,8 +560,10 @@ pub fn main() {
crate::js::SHARED_GLOBALS_LIB,
crate::js::WINDOW_LIB
);
// TODO(ry) Only ignore SIGPIPE. Currently ignoring all errors.
let _r = std::io::stdout().write_all(types.as_bytes());
if let Err(e) = write_to_stdout_ignore_sigpipe(types.as_bytes()) {
eprintln!("{}", e);
std::process::exit(1);
}
return;
}
DenoSubcommand::Upgrade { force, dry_run } => {