From 86fd0c66a645a3dd262e57e330bb7fbe4663e468 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Tue, 7 Apr 2020 19:47:06 +0200 Subject: [PATCH] feat(doc): handle basic reexports (#4625) --- cli/doc/module.rs | 39 ------- cli/doc/node.rs | 29 +++++ cli/doc/parser.rs | 289 ++++++++++++++++++++++++++++++++++++++++------ cli/doc/tests.rs | 220 +++++++++++++++++++++++++++-------- cli/lib.rs | 42 ++++--- 5 files changed, 489 insertions(+), 130 deletions(-) diff --git a/cli/doc/module.rs b/cli/doc/module.rs index e1d629ccf3..ba62902dc4 100644 --- a/cli/doc/module.rs +++ b/cli/doc/module.rs @@ -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 { - 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 = 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 = doc_nodes - .into_iter() - .filter(|doc_node| reexports.contains(&doc_node.name)) - .collect(); - - reexports_docs -} diff --git a/cli/doc/node.rs b/cli/doc/node.rs index e1e83ad0d0..1be3ba7b1d 100644 --- a/cli/doc/node.rs +++ b/cli/doc/node.rs @@ -46,6 +46,35 @@ impl Into 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), +} + +#[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, + pub reexports: Vec, +} + #[derive(Debug, Serialize, Clone)] #[serde(rename_all = "camelCase")] pub struct DocNode { diff --git a/cli/doc/parser.rs b/cli/doc/parser.rs index 7a8fee56fc..7c67f8d090 100644 --- a/cli/doc/parser.rs +++ b/cli/doc/parser.rs @@ -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; +#[derive(Clone, Debug)] +pub struct SwcDiagnosticBuffer { + pub diagnostics: Vec, +} -#[derive(Clone, Default)] -pub struct BufferedError(Arc>); +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::>() + .join(","); + + f.pad(&msg) } } -impl From for Vec { - fn from(buf: BufferedError) -> Self { +#[derive(Clone)] +pub struct SwcErrorBuffer(Arc>); + +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 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::resolve_import(specifier, referrer).map_err(OpError::from) + } + + fn load_source_code( + &self, + specifier: &str, + ) -> Pin>>>; +} + pub struct DocParser { - pub buffered_error: BufferedError, + pub loader: Box, + pub buffered_error: SwcErrorBuffer, pub source_map: Arc, 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) -> 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, SwcDiagnostics> { + file_name: &str, + source_code: &str, + ) -> Result { 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, 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, ErrBox> { + let mut by_src: HashMap> = HashMap::new(); + + let mut processed_reexports: Vec = 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, 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 { @@ -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, + ) -> Vec { + use swc_ecma_ast::ExportSpecifier::*; + + let mut reexports: Vec = 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::>() + } 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, @@ -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) { diff --git a/cli/doc/tests.rs b/cli/doc/tests.rs index 9432ba095f..337f324667 100644 --- a/cli/doc/tests.rs +++ b/cli/doc/tests.rs @@ -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, +} + +impl TestLoader { + pub fn new(files_vec: Vec<(String, String)>) -> Box { + 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>>> { + 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 } "#; - 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; "#; - 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; ); } -#[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)") + ); +} diff --git a/cli/lib.rs b/cli/lib.rs index 9ccc8d0221..a87846c112 100644 --- a/cli/lib.rs +++ b/cli/lib.rs @@ -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>>> { + 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); } };