1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2025-01-10 08:09:06 -05:00

feat(doc): handle basic reexports (#4625)

This commit is contained in:
Bartek Iwańczuk 2020-04-07 19:47:06 +02:00 committed by GitHub
parent 51f5276e8c
commit 86fd0c66a6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 489 additions and 130 deletions

View file

@ -147,42 +147,3 @@ pub fn get_doc_node_for_export_decl(
}
}
}
#[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 crate::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
}

View file

@ -46,6 +46,35 @@ impl Into<Location> for swc_common::Loc {
}
}
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub enum ReexportKind {
/// export * from "./path/to/module.js";
All,
/// export * as someNamespace from "./path/to/module.js";
Namespace(String),
/// export default from "./path/to/module.js";
Default,
/// (identifier, optional alias)
/// export { foo } from "./path/to/module.js";
/// export { foo as bar } from "./path/to/module.js";
Named(String, Option<String>),
}
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Reexport {
pub kind: ReexportKind,
pub src: String,
}
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct ModuleDoc {
pub exports: Vec<DocNode>,
pub reexports: Vec<Reexport>,
}
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct DocNode {

View file

@ -1,4 +1,5 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
use crate::op_error::OpError;
use crate::swc_common;
use crate::swc_common::comments::CommentKind;
use crate::swc_common::comments::Comments;
@ -22,34 +23,87 @@ use crate::swc_ecma_parser::Session;
use crate::swc_ecma_parser::SourceFileInput;
use crate::swc_ecma_parser::Syntax;
use crate::swc_ecma_parser::TsConfig;
use deno_core::ErrBox;
use deno_core::ModuleSpecifier;
use futures::Future;
use regex::Regex;
use std::collections::HashMap;
use std::error::Error;
use std::fmt;
use std::pin::Pin;
use std::sync::Arc;
use std::sync::RwLock;
use super::namespace::NamespaceDef;
use super::node;
use super::node::ModuleDoc;
use super::DocNode;
use super::DocNodeKind;
use super::Location;
pub type SwcDiagnostics = Vec<Diagnostic>;
#[derive(Clone, Debug)]
pub struct SwcDiagnosticBuffer {
pub diagnostics: Vec<Diagnostic>,
}
#[derive(Clone, Default)]
pub struct BufferedError(Arc<RwLock<SwcDiagnostics>>);
impl Error for SwcDiagnosticBuffer {}
impl Emitter for BufferedError {
fn emit(&mut self, db: &DiagnosticBuilder) {
self.0.write().unwrap().push((**db).clone());
impl fmt::Display for SwcDiagnosticBuffer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let msg = self
.diagnostics
.iter()
.map(|d| d.message())
.collect::<Vec<String>>()
.join(",");
f.pad(&msg)
}
}
impl From<BufferedError> for Vec<Diagnostic> {
fn from(buf: BufferedError) -> Self {
#[derive(Clone)]
pub struct SwcErrorBuffer(Arc<RwLock<SwcDiagnosticBuffer>>);
impl SwcErrorBuffer {
pub fn default() -> Self {
Self(Arc::new(RwLock::new(SwcDiagnosticBuffer {
diagnostics: vec![],
})))
}
}
impl Emitter for SwcErrorBuffer {
fn emit(&mut self, db: &DiagnosticBuilder) {
self.0.write().unwrap().diagnostics.push((**db).clone());
}
}
impl From<SwcErrorBuffer> for SwcDiagnosticBuffer {
fn from(buf: SwcErrorBuffer) -> Self {
let s = buf.0.read().unwrap();
s.clone()
}
}
pub trait DocFileLoader {
fn resolve(
&self,
specifier: &str,
referrer: &str,
) -> Result<ModuleSpecifier, OpError> {
ModuleSpecifier::resolve_import(specifier, referrer).map_err(OpError::from)
}
fn load_source_code(
&self,
specifier: &str,
) -> Pin<Box<dyn Future<Output = Result<String, OpError>>>>;
}
pub struct DocParser {
pub buffered_error: BufferedError,
pub loader: Box<dyn DocFileLoader>,
pub buffered_error: SwcErrorBuffer,
pub source_map: Arc<SourceMap>,
pub handler: Handler,
pub comments: Comments,
@ -57,8 +111,8 @@ pub struct DocParser {
}
impl DocParser {
pub fn default() -> Self {
let buffered_error = BufferedError::default();
pub fn new(loader: Box<dyn DocFileLoader>) -> Self {
let buffered_error = SwcErrorBuffer::default();
let handler = Handler::with_emitter_and_flags(
Box::new(buffered_error.clone()),
@ -70,6 +124,7 @@ impl DocParser {
);
DocParser {
loader,
buffered_error,
source_map: Arc::new(SourceMap::default()),
handler,
@ -78,15 +133,16 @@ impl DocParser {
}
}
pub fn parse(
fn parse_module(
&self,
file_name: String,
source_code: String,
) -> Result<Vec<DocNode>, SwcDiagnostics> {
file_name: &str,
source_code: &str,
) -> Result<ModuleDoc, SwcDiagnosticBuffer> {
swc_common::GLOBALS.set(&self.globals, || {
let swc_source_file = self
.source_map
.new_source_file(FileName::Custom(file_name), source_code);
let swc_source_file = self.source_map.new_source_file(
FileName::Custom(file_name.to_string()),
source_code.to_string(),
);
let buffered_err = self.buffered_error.clone();
let session = Session {
@ -112,15 +168,130 @@ impl DocParser {
.parse_module()
.map_err(move |mut err: DiagnosticBuilder| {
err.cancel();
SwcDiagnostics::from(buffered_err)
SwcDiagnosticBuffer::from(buffered_err)
})?;
let doc_entries = self.get_doc_nodes_for_module_body(module.body);
Ok(doc_entries)
let doc_entries = 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,
reexports,
};
Ok(module_doc)
})
}
pub fn get_doc_nodes_for_module_decl(
pub async fn parse(&self, file_name: &str) -> Result<Vec<DocNode>, ErrBox> {
let source_code = self.loader.load_source_code(file_name).await?;
let module_doc = self.parse_module(file_name, &source_code)?;
Ok(module_doc.exports)
}
async fn flatten_reexports(
&self,
reexports: &[node::Reexport],
referrer: &str,
) -> Result<Vec<DocNode>, ErrBox> {
let mut by_src: HashMap<String, Vec<node::Reexport>> = HashMap::new();
let mut processed_reexports: Vec<DocNode> = vec![];
for reexport in reexports {
if by_src.get(&reexport.src).is_none() {
by_src.insert(reexport.src.to_string(), vec![]);
}
let bucket = by_src.get_mut(&reexport.src).unwrap();
bucket.push(reexport.clone());
}
for specifier in by_src.keys() {
let resolved_specifier = self.loader.resolve(specifier, referrer)?;
let doc_nodes = self.parse(&resolved_specifier.to_string()).await?;
let reexports_for_specifier = by_src.get(specifier).unwrap();
for reexport in reexports_for_specifier {
match &reexport.kind {
node::ReexportKind::All => {
processed_reexports.extend(doc_nodes.clone())
}
node::ReexportKind::Namespace(ns_name) => {
let ns_def = NamespaceDef {
elements: doc_nodes.clone(),
};
let ns_doc_node = DocNode {
kind: DocNodeKind::Namespace,
name: ns_name.to_string(),
location: Location {
filename: specifier.to_string(),
line: 1,
col: 0,
},
js_doc: None,
namespace_def: Some(ns_def),
enum_def: None,
type_alias_def: None,
interface_def: None,
variable_def: None,
function_def: None,
class_def: None,
};
processed_reexports.push(ns_doc_node);
}
node::ReexportKind::Named(ident, maybe_alias) => {
// Try to find reexport.
// NOTE: the reexport might actually be reexport from another
// module; for now we're skipping nested reexports.
let maybe_doc_node =
doc_nodes.iter().find(|node| &node.name == ident);
if let Some(doc_node) = maybe_doc_node {
let doc_node = doc_node.clone();
let doc_node = if let Some(alias) = maybe_alias {
DocNode {
name: alias.to_string(),
..doc_node
}
} else {
doc_node
};
processed_reexports.push(doc_node);
}
}
node::ReexportKind::Default => {
// TODO: handle default export from child module
}
}
}
}
Ok(processed_reexports)
}
pub async fn parse_with_reexports(
&self,
file_name: &str,
) -> Result<Vec<DocNode>, ErrBox> {
let source_code = self.loader.load_source_code(file_name).await?;
let module_doc = self.parse_module(file_name, &source_code)?;
let flattened_docs = if !module_doc.reexports.is_empty() {
let mut flattenned_reexports = self
.flatten_reexports(&module_doc.reexports, file_name)
.await?;
flattenned_reexports.extend(module_doc.exports);
flattenned_reexports
} else {
module_doc.exports
};
Ok(flattened_docs)
}
pub fn get_doc_nodes_for_module_exports(
&self,
module_decl: &ModuleDecl,
) -> Vec<DocNode> {
@ -131,16 +302,6 @@ impl DocParser {
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![],
}
}
@ -316,6 +477,67 @@ impl DocParser {
}
}
pub fn get_reexports_for_module_body(
&self,
module_body: Vec<swc_ecma_ast::ModuleItem>,
) -> Vec<node::Reexport> {
use swc_ecma_ast::ExportSpecifier::*;
let mut reexports: Vec<node::Reexport> = vec![];
for node in module_body.iter() {
if let swc_ecma_ast::ModuleItem::ModuleDecl(module_decl) = node {
let r = match module_decl {
ModuleDecl::ExportNamed(named_export) => {
if let Some(src) = &named_export.src {
let src_str = src.value.to_string();
named_export
.specifiers
.iter()
.map(|export_specifier| match export_specifier {
Namespace(ns_export) => node::Reexport {
kind: node::ReexportKind::Namespace(
ns_export.name.sym.to_string(),
),
src: src_str.to_string(),
},
Default(_) => node::Reexport {
kind: node::ReexportKind::Default,
src: src_str.to_string(),
},
Named(named_export) => {
let ident = named_export.orig.sym.to_string();
let maybe_alias =
named_export.exported.as_ref().map(|e| e.sym.to_string());
let kind = node::ReexportKind::Named(ident, maybe_alias);
node::Reexport {
kind,
src: src_str.to_string(),
}
}
})
.collect::<Vec<node::Reexport>>()
} else {
vec![]
}
}
ModuleDecl::ExportAll(export_all) => {
let reexport = node::Reexport {
kind: node::ReexportKind::All,
src: export_all.src.value.to_string(),
};
vec![reexport]
}
_ => vec![],
};
reexports.extend(r);
}
}
reexports
}
pub fn get_doc_nodes_for_module_body(
&self,
module_body: Vec<swc_ecma_ast::ModuleItem>,
@ -324,7 +546,8 @@ impl DocParser {
for node in module_body.iter() {
match node {
swc_ecma_ast::ModuleItem::ModuleDecl(module_decl) => {
doc_entries.extend(self.get_doc_nodes_for_module_decl(module_decl));
doc_entries
.extend(self.get_doc_nodes_for_module_exports(module_decl));
}
swc_ecma_ast::ModuleItem::Stmt(stmt) => {
if let Some(doc_node) = self.get_doc_node_for_stmt(stmt) {

View file

@ -4,8 +4,47 @@ use crate::colors;
use serde_json;
use serde_json::json;
#[test]
fn export_fn() {
use super::parser::DocFileLoader;
use crate::op_error::OpError;
use std::collections::HashMap;
use futures::Future;
use futures::FutureExt;
use std::pin::Pin;
pub struct TestLoader {
files: HashMap<String, String>,
}
impl TestLoader {
pub fn new(files_vec: Vec<(String, String)>) -> Box<Self> {
let mut files = HashMap::new();
for file_tuple in files_vec {
files.insert(file_tuple.0, file_tuple.1);
}
Box::new(Self { files })
}
}
impl DocFileLoader for TestLoader {
fn load_source_code(
&self,
specifier: &str,
) -> Pin<Box<dyn Future<Output = Result<String, OpError>>>> {
eprintln!("specifier {:#?}", specifier);
let res = match self.files.get(specifier) {
Some(source_code) => Ok(source_code.to_string()),
None => Err(OpError::other("not found".to_string())),
};
async move { res }.boxed_local()
}
}
#[tokio::test]
async fn export_fn() {
let source_code = r#"/**
* Hello there, this is a multiline JSdoc.
*
@ -17,9 +56,9 @@ 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();
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!({
@ -68,13 +107,13 @@ export function foo(a: string, b: number): void {
);
}
#[test]
fn export_const() {
#[tokio::test]
async 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();
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!({
@ -100,8 +139,8 @@ fn export_const() {
);
}
#[test]
fn export_class() {
#[tokio::test]
async fn export_class() {
let source_code = r#"
/** Class doc */
export class Foobar extends Fizz implements Buzz, Aldrin {
@ -124,9 +163,9 @@ export class Foobar extends Fizz implements Buzz, Aldrin {
}
}
"#;
let entries = DocParser::default()
.parse("test.ts".to_string(), source_code.to_string())
.unwrap();
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",
@ -314,8 +353,8 @@ export class Foobar extends Fizz implements Buzz, Aldrin {
);
}
#[test]
fn export_interface() {
#[tokio::test]
async fn export_interface() {
let source_code = r#"
/**
* Interface js doc
@ -325,9 +364,9 @@ export interface Reader {
read(buf: Uint8Array, something: unknown): Promise<number>
}
"#;
let entries = DocParser::default()
.parse("test.ts".to_string(), source_code.to_string())
.unwrap();
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!({
@ -399,15 +438,15 @@ export interface Reader {
);
}
#[test]
fn export_type_alias() {
#[tokio::test]
async 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();
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!({
@ -445,8 +484,8 @@ export type NumberArray = Array<number>;
);
}
#[test]
fn export_enum() {
#[tokio::test]
async fn export_enum() {
let source_code = r#"
/**
* Some enum for good measure
@ -457,9 +496,9 @@ export enum Hello {
Buzz = "buzz",
}
"#;
let entries = DocParser::default()
.parse("test.ts".to_string(), source_code.to_string())
.unwrap();
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!({
@ -498,8 +537,8 @@ export enum Hello {
);
}
#[test]
fn export_namespace() {
#[tokio::test]
async fn export_namespace() {
let source_code = r#"
/** Namespace JSdoc */
export namespace RootNs {
@ -515,9 +554,9 @@ export namespace RootNs {
}
}
"#;
let entries = DocParser::default()
.parse("test.ts".to_string(), source_code.to_string())
.unwrap();
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!({
@ -593,8 +632,8 @@ export namespace RootNs {
);
}
#[test]
fn declare_namespace() {
#[tokio::test]
async fn declare_namespace() {
let source_code = r#"
/** Namespace JSdoc */
declare namespace RootNs {
@ -610,9 +649,9 @@ declare namespace RootNs {
}
}
"#;
let entries = DocParser::default()
.parse("test.ts".to_string(), source_code.to_string())
.unwrap();
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!({
@ -687,16 +726,16 @@ declare namespace RootNs {
.contains("namespace RootNs")
);
}
#[test]
fn optional_return_type() {
#[tokio::test]
async fn optional_return_type() {
let source_code = r#"
export function foo(a: number) {
return a;
}
"#;
let entries = DocParser::default()
.parse("test.ts".to_string(), source_code.to_string())
.unwrap();
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!({
@ -732,3 +771,94 @@ fn optional_return_type() {
.contains("function foo(a: number)")
);
}
#[tokio::test]
async fn reexports() {
let nested_reexport_source_code = r#"
/**
* JSDoc for bar
*/
export const bar = "bar";
"#;
let reexport_source_code = r#"
import { bar } from "./nested_reexport.ts";
/**
* JSDoc for const
*/
export const foo = "foo";
"#;
let test_source_code = r#"
export { 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",
"location": {
"filename": "file:///test.ts",
"line": 5,
"col": 0
},
"jsDoc": "JSDoc for function",
"functionDef": {
"params": [
{
"name": "a",
"tsType": {
"keyword": "number",
"kind": "keyword",
"repr": "number",
},
}
],
"returnType": null,
"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)")
);
}

View file

@ -66,9 +66,12 @@ pub use dprint_plugin_typescript::swc_ecma_ast;
pub use dprint_plugin_typescript::swc_ecma_parser;
use crate::compilers::TargetLib;
use crate::doc::parser::DocFileLoader;
use crate::file_fetcher::SourceFile;
use crate::file_fetcher::SourceFileFetcher;
use crate::global_state::GlobalState;
use crate::msg::MediaType;
use crate::op_error::OpError;
use crate::ops::io::get_stdio;
use crate::state::DebugType;
use crate::state::State;
@ -79,12 +82,14 @@ use deno_core::ModuleSpecifier;
use flags::DenoSubcommand;
use flags::Flags;
use futures::future::FutureExt;
use futures::Future;
use log::Level;
use log::Metadata;
use log::Record;
use std::env;
use std::io::Write;
use std::path::PathBuf;
use std::pin::Pin;
use upgrade::upgrade_command;
use url::Url;
@ -371,24 +376,35 @@ async fn doc_command(
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);
impl DocFileLoader for SourceFileFetcher {
fn load_source_code(
&self,
specifier: &str,
) -> Pin<Box<dyn Future<Output = Result<String, OpError>>>> {
let specifier =
ModuleSpecifier::resolve_url_or_path(specifier).expect("Bad specifier");
let fetcher = self.clone();
async move {
let source_file = fetcher.fetch_source_file(&specifier, None).await?;
String::from_utf8(source_file.source_code)
.map_err(|_| OpError::other("failed to parse".to_string()))
}
.boxed_local()
}
}
let loader = Box::new(global_state.file_fetcher.clone());
let doc_parser = doc::DocParser::new(loader);
let parse_result = doc_parser
.parse_with_reexports(&module_specifier.to_string())
.await;
let doc_nodes = match parse_result {
Ok(nodes) => nodes,
Err(e) => {
eprintln!("Failed to parse documentation:");
for diagnostic in e {
eprintln!("{}", diagnostic.message());
}
eprintln!("{}", e);
std::process::exit(1);
}
};