mirror of
https://github.com/denoland/deno.git
synced 2025-02-02 04:38:21 -05:00
d1ef561dbf
Adds a new `--lint` flag to `deno doc` that surfaces three kinds of diagnostics: 1. Diagnostic for non-exported type referenced in an exported type. * Why? People often forget to export types from a module in TypeScript. To supress this diagnostic, add an `@internal` jsdoc tag to the internal type. 1. Diagnostic for missing return type or missing property type on a **public** type. * Why? Otherwise `deno doc` will not display good documentation. Adding explicit types also helps with type checking performance. 1. Diagnostic for missing jsdoc on a **public** type. * Why? Everything should be documented. This diagnostic can be supressed by adding a jsdoc comment description. If the lint passes, `deno doc` generates documentation as usual. For example, checking for deno doc diagnostics on the CI: ```shellsession $ deno doc --lint mod.ts second_entrypoint.ts > /dev/null ``` This feature is incredibly useful for library authors. ## Why not include this in `deno lint`? 1. The command needs the documenation output in order to figure out the diagnostics. 1. `deno lint` doesn't understand where the entrypoints are. That's critical for the diagnostics to be useful. 1. It's much more performant to do this while generating documentation. 1. There is precedence in rustdoc (ex. `#![warn(missing_docs)]`). ## Why not `--check`? It is confusing with `deno run --check`, since that means to run type checking (and confusing with `deno check --docs`). ## Output Future Improvement The output is not ideal atm, but it's fine for a first pass. We will improve it in the future. Closes https://github.com/denoland/deno_lint/pull/972 Closes https://github.com/denoland/deno_lint/issues/970 Closes https://github.com/denoland/deno/issues/19356
195 lines
5.9 KiB
Rust
195 lines
5.9 KiB
Rust
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
|
|
|
use std::collections::BTreeMap;
|
|
|
|
use crate::args::DocFlags;
|
|
use crate::args::DocSourceFileFlag;
|
|
use crate::args::Flags;
|
|
use crate::colors;
|
|
use crate::display::write_json_to_stdout;
|
|
use crate::display::write_to_stdout_ignore_sigpipe;
|
|
use crate::factory::CliFactory;
|
|
use crate::graph_util::graph_lock_or_exit;
|
|
use crate::graph_util::CreateGraphOptions;
|
|
use crate::tsc::get_types_declaration_file_text;
|
|
use deno_core::anyhow::bail;
|
|
use deno_core::error::AnyError;
|
|
use deno_core::resolve_url_or_path;
|
|
use deno_doc as doc;
|
|
use deno_graph::CapturingModuleParser;
|
|
use deno_graph::DefaultParsedSourceStore;
|
|
use deno_graph::GraphKind;
|
|
use deno_graph::ModuleSpecifier;
|
|
use doc::DocDiagnostic;
|
|
use indexmap::IndexMap;
|
|
|
|
pub async fn print_docs(
|
|
flags: Flags,
|
|
doc_flags: DocFlags,
|
|
) -> Result<(), AnyError> {
|
|
let factory = CliFactory::from_flags(flags).await?;
|
|
let cli_options = factory.cli_options();
|
|
let module_info_cache = factory.module_info_cache()?;
|
|
let source_parser = deno_graph::DefaultModuleParser::new_for_analysis();
|
|
let store = DefaultParsedSourceStore::default();
|
|
let analyzer =
|
|
module_info_cache.as_module_analyzer(Some(&source_parser), &store);
|
|
let capturing_parser =
|
|
CapturingModuleParser::new(Some(&source_parser), &store);
|
|
|
|
let mut doc_nodes = match doc_flags.source_files {
|
|
DocSourceFileFlag::Builtin => {
|
|
let source_file_specifier =
|
|
ModuleSpecifier::parse("internal://lib.deno.d.ts").unwrap();
|
|
let content = get_types_declaration_file_text(cli_options.unstable());
|
|
let mut loader = deno_graph::source::MemoryLoader::new(
|
|
vec![(
|
|
source_file_specifier.to_string(),
|
|
deno_graph::source::Source::Module {
|
|
specifier: source_file_specifier.to_string(),
|
|
content,
|
|
maybe_headers: None,
|
|
},
|
|
)],
|
|
Vec::new(),
|
|
);
|
|
let mut graph = deno_graph::ModuleGraph::new(GraphKind::TypesOnly);
|
|
graph
|
|
.build(
|
|
vec![source_file_specifier.clone()],
|
|
&mut loader,
|
|
deno_graph::BuildOptions {
|
|
module_analyzer: Some(&analyzer),
|
|
..Default::default()
|
|
},
|
|
)
|
|
.await;
|
|
let doc_parser = doc::DocParser::new(
|
|
&graph,
|
|
capturing_parser,
|
|
doc::DocParserOptions {
|
|
private: doc_flags.private,
|
|
diagnostics: false,
|
|
},
|
|
)?;
|
|
doc_parser.parse_module(&source_file_specifier)?.definitions
|
|
}
|
|
DocSourceFileFlag::Paths(source_files) => {
|
|
let module_graph_builder = factory.module_graph_builder().await?;
|
|
let maybe_lockfile = factory.maybe_lockfile();
|
|
|
|
let module_specifiers: Result<Vec<ModuleSpecifier>, AnyError> =
|
|
source_files
|
|
.iter()
|
|
.map(|source_file| {
|
|
Ok(resolve_url_or_path(source_file, cli_options.initial_cwd())?)
|
|
})
|
|
.collect();
|
|
let module_specifiers = module_specifiers?;
|
|
let mut loader = module_graph_builder.create_graph_loader();
|
|
let graph = module_graph_builder
|
|
.create_graph_with_options(CreateGraphOptions {
|
|
graph_kind: GraphKind::TypesOnly,
|
|
roots: module_specifiers.clone(),
|
|
loader: &mut loader,
|
|
analyzer: &analyzer,
|
|
})
|
|
.await?;
|
|
|
|
if let Some(lockfile) = maybe_lockfile {
|
|
graph_lock_or_exit(&graph, &mut lockfile.lock());
|
|
}
|
|
|
|
let doc_parser = doc::DocParser::new(
|
|
&graph,
|
|
capturing_parser,
|
|
doc::DocParserOptions {
|
|
private: doc_flags.private,
|
|
diagnostics: doc_flags.lint,
|
|
},
|
|
)?;
|
|
|
|
let mut doc_nodes = vec![];
|
|
|
|
for module_specifier in module_specifiers {
|
|
let nodes = doc_parser.parse_with_reexports(&module_specifier)?;
|
|
doc_nodes.extend_from_slice(&nodes);
|
|
}
|
|
|
|
if doc_flags.lint {
|
|
let diagnostics = doc_parser.take_diagnostics();
|
|
check_diagnostics(&diagnostics)?;
|
|
}
|
|
|
|
doc_nodes
|
|
}
|
|
};
|
|
|
|
if doc_flags.json {
|
|
write_json_to_stdout(&doc_nodes)
|
|
} else {
|
|
doc_nodes.retain(|doc_node| doc_node.kind != doc::DocNodeKind::Import);
|
|
let details = if let Some(filter) = doc_flags.filter {
|
|
let nodes =
|
|
doc::find_nodes_by_name_recursively(doc_nodes, filter.clone());
|
|
if nodes.is_empty() {
|
|
bail!("Node {} was not found!", filter);
|
|
}
|
|
format!(
|
|
"{}",
|
|
doc::DocPrinter::new(&nodes, colors::use_color(), doc_flags.private)
|
|
)
|
|
} else {
|
|
format!(
|
|
"{}",
|
|
doc::DocPrinter::new(
|
|
&doc_nodes,
|
|
colors::use_color(),
|
|
doc_flags.private
|
|
)
|
|
)
|
|
};
|
|
|
|
write_to_stdout_ignore_sigpipe(details.as_bytes()).map_err(AnyError::from)
|
|
}
|
|
}
|
|
|
|
fn check_diagnostics(diagnostics: &[DocDiagnostic]) -> Result<(), AnyError> {
|
|
if diagnostics.is_empty() {
|
|
return Ok(());
|
|
}
|
|
|
|
// group by location then by line (sorted) then column (sorted)
|
|
let mut diagnostic_groups = IndexMap::new();
|
|
for diagnostic in diagnostics {
|
|
diagnostic_groups
|
|
.entry(diagnostic.location.filename.clone())
|
|
.or_insert_with(BTreeMap::new)
|
|
.entry(diagnostic.location.line)
|
|
.or_insert_with(BTreeMap::new)
|
|
.entry(diagnostic.location.col)
|
|
.or_insert_with(Vec::new)
|
|
.push(diagnostic);
|
|
}
|
|
|
|
for (filename, diagnostics_by_lc) in diagnostic_groups {
|
|
for (line, diagnostics_by_col) in diagnostics_by_lc {
|
|
for (col, diagnostics) in diagnostics_by_col {
|
|
for diagnostic in diagnostics {
|
|
log::warn!("{}", diagnostic.kind);
|
|
}
|
|
log::warn!(
|
|
" at {}:{}:{}\n",
|
|
colors::cyan(filename.as_str()),
|
|
colors::yellow(&line.to_string()),
|
|
colors::yellow(&(col + 1).to_string())
|
|
)
|
|
}
|
|
}
|
|
}
|
|
bail!(
|
|
"Found {} documentation diagnostic{}.",
|
|
colors::bold(diagnostics.len().to_string()),
|
|
if diagnostics.len() == 1 { "" } else { "s" }
|
|
);
|
|
}
|