diff --git a/cli/args/flags.rs b/cli/args/flags.rs index f832c2a623294d..92336a0a16fdf9 100644 --- a/cli/args/flags.rs +++ b/cli/args/flags.rs @@ -109,6 +109,8 @@ pub struct CacheFlags { #[derive(Clone, Debug, Eq, PartialEq)] pub struct CheckFlags { pub files: Vec, + pub doc: bool, + pub doc_only: bool, } #[derive(Clone, Debug, Eq, PartialEq)] @@ -1694,6 +1696,19 @@ Unless --reload is specified, this command will not re-download already cached d .conflicts_with("no-remote") .hide(true) ) + .arg( + Arg::new("doc") + .long("doc") + .help("Type-check code blocks in JSDoc as well as actual code") + .action(ArgAction::SetTrue) + ) + .arg( + Arg::new("doc-only") + .long("doc-only") + .help("Type-check code blocks in JSDoc and Markdown only") + .action(ArgAction::SetTrue) + .conflicts_with("doc") + ) .arg( Arg::new("file") .num_args(1..) @@ -2789,7 +2804,7 @@ or **/__tests__/**: .arg( Arg::new("doc") .long("doc") - .help("Type-check code blocks in JSDoc and Markdown") + .help("Evaluate code blocks in JSDoc and Markdown") .action(ArgAction::SetTrue) .help_heading(TEST_HEADING), ) @@ -4121,7 +4136,11 @@ fn check_parse( if matches.get_flag("all") || matches.get_flag("remote") { flags.type_check_mode = TypeCheckMode::All; } - flags.subcommand = DenoSubcommand::Check(CheckFlags { files }); + flags.subcommand = DenoSubcommand::Check(CheckFlags { + files, + doc: matches.get_flag("doc"), + doc_only: matches.get_flag("doc-only"), + }); Ok(()) } @@ -6862,12 +6881,55 @@ mod tests { Flags { subcommand: DenoSubcommand::Check(CheckFlags { files: svec!["script.ts"], + doc: false, + doc_only: false, + }), + type_check_mode: TypeCheckMode::Local, + ..Flags::default() + } + ); + + let r = flags_from_vec(svec!["deno", "check", "--doc", "script.ts"]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Check(CheckFlags { + files: svec!["script.ts"], + doc: true, + doc_only: false, }), type_check_mode: TypeCheckMode::Local, ..Flags::default() } ); + let r = flags_from_vec(svec!["deno", "check", "--doc-only", "markdown.md"]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Check(CheckFlags { + files: svec!["markdown.md"], + doc: false, + doc_only: true, + }), + type_check_mode: TypeCheckMode::Local, + ..Flags::default() + } + ); + + // `--doc` and `--doc-only` are mutually exclusive + let r = flags_from_vec(svec![ + "deno", + "check", + "--doc", + "--doc-only", + "script.ts" + ]); + assert_eq!( + r.unwrap_err().kind(), + clap::error::ErrorKind::ArgumentConflict + ); + for all_flag in ["--remote", "--all"] { let r = flags_from_vec(svec!["deno", "check", all_flag, "script.ts"]); assert_eq!( @@ -6875,6 +6937,8 @@ mod tests { Flags { subcommand: DenoSubcommand::Check(CheckFlags { files: svec!["script.ts"], + doc: false, + doc_only: false, }), type_check_mode: TypeCheckMode::All, ..Flags::default() diff --git a/cli/lsp/testing/execution.rs b/cli/lsp/testing/execution.rs index a8cea8dd449a21..55d81f34278b96 100644 --- a/cli/lsp/testing/execution.rs +++ b/cli/lsp/testing/execution.rs @@ -234,16 +234,9 @@ impl TestRun { &cli_options.permissions_options(), )?; let main_graph_container = factory.main_module_graph_container().await?; - test::check_specifiers( - factory.file_fetcher()?, - main_graph_container, - self - .queue - .iter() - .map(|s| (s.clone(), test::TestMode::Executable)) - .collect(), - ) - .await?; + main_graph_container + .check_specifiers(&self.queue.iter().cloned().collect::>()) + .await?; let (concurrent_jobs, fail_fast) = if let DenoSubcommand::Test(test_flags) = cli_options.sub_command() { diff --git a/cli/main.rs b/cli/main.rs index 10d9ead4e846cc..a7e1c8342e5059 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -121,12 +121,7 @@ async fn run_subcommand(flags: Arc) -> Result { tools::installer::install_from_entrypoints(flags, &cache_flags.files).await }), DenoSubcommand::Check(check_flags) => spawn_subcommand(async move { - let factory = CliFactory::from_flags(flags); - let main_graph_container = - factory.main_module_graph_container().await?; - main_graph_container - .load_and_type_check_files(&check_flags.files) - .await + tools::check::check(flags, check_flags).await }), DenoSubcommand::Clean => spawn_subcommand(async move { tools::clean::clean() diff --git a/cli/tools/check.rs b/cli/tools/check.rs index d50af52307820e..9c464fa1694fe8 100644 --- a/cli/tools/check.rs +++ b/cli/tools/check.rs @@ -15,7 +15,9 @@ use once_cell::sync::Lazy; use regex::Regex; use crate::args::check_warn_tsconfig; +use crate::args::CheckFlags; use crate::args::CliOptions; +use crate::args::Flags; use crate::args::TsConfig; use crate::args::TsConfigType; use crate::args::TsTypeLib; @@ -24,13 +26,57 @@ use crate::cache::CacheDBHash; use crate::cache::Caches; use crate::cache::FastInsecureHasher; use crate::cache::TypeCheckCache; +use crate::factory::CliFactory; use crate::graph_util::BuildFastCheckGraphOptions; use crate::graph_util::ModuleGraphBuilder; use crate::npm::CliNpmResolver; use crate::tsc; use crate::tsc::Diagnostics; +use crate::util::extract; use crate::util::path::to_percent_decoded_str; +pub async fn check( + flags: Arc, + check_flags: CheckFlags, +) -> Result<(), AnyError> { + let factory = CliFactory::from_flags(flags); + + let main_graph_container = factory.main_module_graph_container().await?; + + let specifiers = + main_graph_container.collect_specifiers(&check_flags.files)?; + if specifiers.is_empty() { + log::warn!("{} No matching files found.", colors::yellow("Warning")); + } + + let specifiers_for_typecheck = if check_flags.doc || check_flags.doc_only { + let file_fetcher = factory.file_fetcher()?; + + let mut specifiers_for_typecheck = if check_flags.doc { + specifiers.clone() + } else { + vec![] + }; + + for s in specifiers { + let file = file_fetcher.fetch_bypass_permissions(&s).await?; + let snippet_files = extract::extract_snippet_files(file)?; + for snippet_file in snippet_files { + specifiers_for_typecheck.push(snippet_file.specifier.clone()); + file_fetcher.insert_memory_files(snippet_file); + } + } + + specifiers_for_typecheck + } else { + specifiers + }; + + main_graph_container + .check_specifiers(&specifiers_for_typecheck) + .await +} + /// Options for performing a check of a module graph. Note that the decision to /// emit or not is determined by the `ts_config` settings. pub struct CheckOptions { diff --git a/cli/tools/test/mod.rs b/cli/tools/test/mod.rs index 63382ffc6763e0..d043ffcbaa4dfe 100644 --- a/cli/tools/test/mod.rs +++ b/cli/tools/test/mod.rs @@ -9,21 +9,18 @@ use crate::display; use crate::factory::CliFactory; use crate::file_fetcher::File; use crate::file_fetcher::FileFetcher; -use crate::graph_container::MainModuleGraphContainer; use crate::graph_util::has_graph_root_local_dependent_changed; use crate::ops; +use crate::util::extract::extract_doc_tests; use crate::util::file_watcher; use crate::util::fs::collect_specifiers; use crate::util::path::get_extension; use crate::util::path::is_script_ext; -use crate::util::path::mapped_specifier_for_tsc; use crate::util::path::matches_pattern_or_exact_path; use crate::worker::CliMainWorkerFactory; use crate::worker::CoverageCollector; -use deno_ast::swc::common::comments::CommentKind; use deno_ast::MediaType; -use deno_ast::SourceRangedForSpanned; use deno_config::glob::FilePatterns; use deno_config::glob::WalkEntry; use deno_core::anyhow; @@ -151,6 +148,20 @@ pub enum TestMode { Both, } +impl TestMode { + /// Returns `true` if the test mode indicates that code snippet extraction is + /// needed. + fn needs_test_extraction(&self) -> bool { + matches!(self, Self::Documentation | Self::Both) + } + + /// Returns `true` if the test mode indicates that the test should be + /// type-checked and run. + fn needs_test_run(&self) -> bool { + matches!(self, Self::Executable | Self::Both) + } +} + #[derive(Clone, Debug, Default)] pub struct TestFilter { pub substring: Option, @@ -1174,233 +1185,6 @@ async fn wait_for_activity_to_stabilize( }) } -fn extract_files_from_regex_blocks( - specifier: &ModuleSpecifier, - source: &str, - media_type: MediaType, - file_line_index: usize, - blocks_regex: &Regex, - lines_regex: &Regex, -) -> Result, AnyError> { - let files = blocks_regex - .captures_iter(source) - .filter_map(|block| { - block.get(1)?; - - let maybe_attributes: Option> = block - .get(1) - .map(|attributes| attributes.as_str().split(' ').collect()); - - let file_media_type = if let Some(attributes) = maybe_attributes { - if attributes.contains(&"ignore") { - return None; - } - - match attributes.first() { - Some(&"js") => MediaType::JavaScript, - Some(&"javascript") => MediaType::JavaScript, - Some(&"mjs") => MediaType::Mjs, - Some(&"cjs") => MediaType::Cjs, - Some(&"jsx") => MediaType::Jsx, - Some(&"ts") => MediaType::TypeScript, - Some(&"typescript") => MediaType::TypeScript, - Some(&"mts") => MediaType::Mts, - Some(&"cts") => MediaType::Cts, - Some(&"tsx") => MediaType::Tsx, - _ => MediaType::Unknown, - } - } else { - media_type - }; - - if file_media_type == MediaType::Unknown { - return None; - } - - let line_offset = source[0..block.get(0).unwrap().start()] - .chars() - .filter(|c| *c == '\n') - .count(); - - let line_count = block.get(0).unwrap().as_str().split('\n').count(); - - let body = block.get(2).unwrap(); - let text = body.as_str(); - - // TODO(caspervonb) generate an inline source map - let mut file_source = String::new(); - for line in lines_regex.captures_iter(text) { - let text = line.get(1).unwrap(); - writeln!(file_source, "{}", text.as_str()).unwrap(); - } - - let file_specifier = ModuleSpecifier::parse(&format!( - "{}${}-{}", - specifier, - file_line_index + line_offset + 1, - file_line_index + line_offset + line_count + 1, - )) - .unwrap(); - let file_specifier = - mapped_specifier_for_tsc(&file_specifier, file_media_type) - .map(|s| ModuleSpecifier::parse(&s).unwrap()) - .unwrap_or(file_specifier); - - Some(File { - specifier: file_specifier, - maybe_headers: None, - source: file_source.into_bytes().into(), - }) - }) - .collect(); - - Ok(files) -} - -fn extract_files_from_source_comments( - specifier: &ModuleSpecifier, - source: Arc, - media_type: MediaType, -) -> Result, AnyError> { - let parsed_source = deno_ast::parse_module(deno_ast::ParseParams { - specifier: specifier.clone(), - text: source, - media_type, - capture_tokens: false, - maybe_syntax: None, - scope_analysis: false, - })?; - let comments = parsed_source.comments().get_vec(); - let blocks_regex = lazy_regex::regex!(r"```([^\r\n]*)\r?\n([\S\s]*?)```"); - let lines_regex = lazy_regex::regex!(r"(?:\* ?)(?:\# ?)?(.*)"); - - let files = comments - .iter() - .filter(|comment| { - if comment.kind != CommentKind::Block || !comment.text.starts_with('*') { - return false; - } - - true - }) - .flat_map(|comment| { - extract_files_from_regex_blocks( - specifier, - &comment.text, - media_type, - parsed_source.text_info_lazy().line_index(comment.start()), - blocks_regex, - lines_regex, - ) - }) - .flatten() - .collect(); - - Ok(files) -} - -fn extract_files_from_fenced_blocks( - specifier: &ModuleSpecifier, - source: &str, - media_type: MediaType, -) -> Result, AnyError> { - // The pattern matches code blocks as well as anything in HTML comment syntax, - // but it stores the latter without any capturing groups. This way, a simple - // check can be done to see if a block is inside a comment (and skip typechecking) - // or not by checking for the presence of capturing groups in the matches. - let blocks_regex = - lazy_regex::regex!(r"(?s)|```([^\r\n]*)\r?\n([\S\s]*?)```"); - let lines_regex = lazy_regex::regex!(r"(?:\# ?)?(.*)"); - - extract_files_from_regex_blocks( - specifier, - source, - media_type, - /* file line index */ 0, - blocks_regex, - lines_regex, - ) -} - -async fn fetch_inline_files( - file_fetcher: &FileFetcher, - specifiers: Vec, -) -> Result, AnyError> { - let mut files = Vec::new(); - for specifier in specifiers { - let file = file_fetcher - .fetch_bypass_permissions(&specifier) - .await? - .into_text_decoded()?; - - let inline_files = if file.media_type == MediaType::Unknown { - extract_files_from_fenced_blocks( - &file.specifier, - &file.source, - file.media_type, - ) - } else { - extract_files_from_source_comments( - &file.specifier, - file.source, - file.media_type, - ) - }; - - files.extend(inline_files?); - } - - Ok(files) -} - -/// Type check a collection of module and document specifiers. -pub async fn check_specifiers( - file_fetcher: &FileFetcher, - main_graph_container: &Arc, - specifiers: Vec<(ModuleSpecifier, TestMode)>, -) -> Result<(), AnyError> { - let inline_files = fetch_inline_files( - file_fetcher, - specifiers - .iter() - .filter_map(|(specifier, mode)| { - if *mode != TestMode::Executable { - Some(specifier.clone()) - } else { - None - } - }) - .collect(), - ) - .await?; - - let mut module_specifiers = specifiers - .into_iter() - .filter_map(|(specifier, mode)| { - if mode != TestMode::Documentation { - Some(specifier) - } else { - None - } - }) - .collect::>(); - - if !inline_files.is_empty() { - module_specifiers - .extend(inline_files.iter().map(|file| file.specifier.clone())); - - for file in inline_files { - file_fetcher.insert_memory_files(file); - } - } - - main_graph_container - .check_specifiers(&module_specifiers) - .await?; - - Ok(()) -} - static HAS_TEST_RUN_SIGINT_HANDLER: AtomicBool = AtomicBool::new(false); /// Test a collection of specifiers with test modes concurrently. @@ -1788,14 +1572,19 @@ pub async fn run_tests( return Err(generic_error("No test modules found")); } + let doc_tests = get_doc_tests(&specifiers_with_mode, file_fetcher).await?; + let specifiers_for_typecheck_and_test = + get_target_specifiers(specifiers_with_mode, &doc_tests); + for doc_test in doc_tests { + file_fetcher.insert_memory_files(doc_test); + } + let main_graph_container = factory.main_module_graph_container().await?; - check_specifiers( - file_fetcher, - main_graph_container, - specifiers_with_mode.clone(), - ) - .await?; + // Typecheck + main_graph_container + .check_specifiers(&specifiers_for_typecheck_and_test) + .await?; if workspace_test_options.no_run { return Ok(()); @@ -1804,17 +1593,12 @@ pub async fn run_tests( let worker_factory = Arc::new(factory.create_cli_main_worker_factory().await?); + // Run tests test_specifiers( worker_factory, &permissions, permission_desc_parser, - specifiers_with_mode - .into_iter() - .filter_map(|(s, m)| match m { - TestMode::Documentation => None, - _ => Some(s), - }) - .collect(), + specifiers_for_typecheck_and_test, TestSpecifiersOptions { cwd: Url::from_directory_path(cli_options.initial_cwd()).map_err( |_| { @@ -1949,8 +1733,6 @@ pub async fn run_tests_with_watch( test_modules.clone() }; - let worker_factory = - Arc::new(factory.create_cli_main_worker_factory().await?); let specifiers_with_mode = fetch_specifiers_with_test_mode( &cli_options, file_fetcher, @@ -1962,30 +1744,34 @@ pub async fn run_tests_with_watch( .filter(|(specifier, _)| test_modules_to_reload.contains(specifier)) .collect::>(); + let doc_tests = + get_doc_tests(&specifiers_with_mode, file_fetcher).await?; + let specifiers_for_typecheck_and_test = + get_target_specifiers(specifiers_with_mode, &doc_tests); + for doc_test in doc_tests { + file_fetcher.insert_memory_files(doc_test); + } + let main_graph_container = factory.main_module_graph_container().await?; - check_specifiers( - file_fetcher, - main_graph_container, - specifiers_with_mode.clone(), - ) - .await?; + + // Typecheck + main_graph_container + .check_specifiers(&specifiers_for_typecheck_and_test) + .await?; if workspace_test_options.no_run { return Ok(()); } + let worker_factory = + Arc::new(factory.create_cli_main_worker_factory().await?); + test_specifiers( worker_factory, &permissions, permission_desc_parser, - specifiers_with_mode - .into_iter() - .filter_map(|(s, m)| match m { - TestMode::Documentation => None, - _ => Some(s), - }) - .collect(), + specifiers_for_typecheck_and_test, TestSpecifiersOptions { cwd: Url::from_directory_path(cli_options.initial_cwd()).map_err( |_| { @@ -2020,6 +1806,38 @@ pub async fn run_tests_with_watch( Ok(()) } +/// Extracts doc tests from files specified by the given specifiers. +async fn get_doc_tests( + specifiers_with_mode: &[(Url, TestMode)], + file_fetcher: &FileFetcher, +) -> Result, AnyError> { + let specifiers_needing_extraction = specifiers_with_mode + .iter() + .filter(|(_, mode)| mode.needs_test_extraction()) + .map(|(s, _)| s); + + let mut doc_tests = Vec::new(); + for s in specifiers_needing_extraction { + let file = file_fetcher.fetch_bypass_permissions(s).await?; + doc_tests.extend(extract_doc_tests(file)?); + } + + Ok(doc_tests) +} + +/// Get a list of specifiers that we need to perform typecheck and run tests on. +/// The result includes "pseudo specifiers" for doc tests. +fn get_target_specifiers( + specifiers_with_mode: Vec<(Url, TestMode)>, + doc_tests: &[File], +) -> Vec { + specifiers_with_mode + .into_iter() + .filter_map(|(s, mode)| mode.needs_test_run().then_some(s)) + .chain(doc_tests.iter().map(|d| d.specifier.clone())) + .collect() +} + /// Tracks failures for the `--fail-fast` argument in /// order to tell when to stop running tests. #[derive(Clone, Default)] diff --git a/cli/util/extract.rs b/cli/util/extract.rs new file mode 100644 index 00000000000000..e27a79347a8021 --- /dev/null +++ b/cli/util/extract.rs @@ -0,0 +1,1410 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use deno_ast::swc::ast; +use deno_ast::swc::atoms::Atom; +use deno_ast::swc::common::collections::AHashSet; +use deno_ast::swc::common::comments::CommentKind; +use deno_ast::swc::common::DUMMY_SP; +use deno_ast::swc::utils as swc_utils; +use deno_ast::swc::visit::as_folder; +use deno_ast::swc::visit::FoldWith as _; +use deno_ast::swc::visit::Visit; +use deno_ast::swc::visit::VisitMut; +use deno_ast::swc::visit::VisitWith as _; +use deno_ast::MediaType; +use deno_ast::SourceRangedForSpanned as _; +use deno_core::error::AnyError; +use deno_core::ModuleSpecifier; +use regex::Regex; +use std::collections::BTreeSet; +use std::fmt::Write as _; +use std::sync::Arc; + +use crate::file_fetcher::File; +use crate::util::path::mapped_specifier_for_tsc; + +/// Extracts doc tests from a given file, transforms them into pseudo test +/// files by wrapping the content of the doc tests in a `Deno.test` call, and +/// returns a list of the pseudo test files. +/// +/// The difference from [`extract_snippet_files`] is that this function wraps +/// extracted code snippets in a `Deno.test` call. +pub fn extract_doc_tests(file: File) -> Result, AnyError> { + extract_inner(file, WrapKind::DenoTest) +} + +/// Extracts code snippets from a given file and returns a list of the extracted +/// files. +/// +/// The difference from [`extract_doc_tests`] is that this function does *not* +/// wrap extracted code snippets in a `Deno.test` call. +pub fn extract_snippet_files(file: File) -> Result, AnyError> { + extract_inner(file, WrapKind::NoWrap) +} + +#[derive(Clone, Copy)] +enum WrapKind { + DenoTest, + NoWrap, +} + +fn extract_inner( + file: File, + wrap_kind: WrapKind, +) -> Result, AnyError> { + let file = file.into_text_decoded()?; + + let exports = match deno_ast::parse_program(deno_ast::ParseParams { + specifier: file.specifier.clone(), + text: file.source.clone(), + media_type: file.media_type, + capture_tokens: false, + scope_analysis: false, + maybe_syntax: None, + }) { + Ok(parsed) => { + let mut c = ExportCollector::default(); + c.visit_program(parsed.program_ref()); + c + } + Err(_) => ExportCollector::default(), + }; + + let extracted_files = if file.media_type == MediaType::Unknown { + extract_files_from_fenced_blocks( + &file.specifier, + &file.source, + file.media_type, + )? + } else { + extract_files_from_source_comments( + &file.specifier, + file.source.clone(), + file.media_type, + )? + }; + + extracted_files + .into_iter() + .map(|extracted_file| { + generate_pseudo_file(extracted_file, &file.specifier, &exports, wrap_kind) + }) + .collect::>() +} + +fn extract_files_from_fenced_blocks( + specifier: &ModuleSpecifier, + source: &str, + media_type: MediaType, +) -> Result, AnyError> { + // The pattern matches code blocks as well as anything in HTML comment syntax, + // but it stores the latter without any capturing groups. This way, a simple + // check can be done to see if a block is inside a comment (and skip typechecking) + // or not by checking for the presence of capturing groups in the matches. + let blocks_regex = + lazy_regex::regex!(r"(?s)|```([^\r\n]*)\r?\n([\S\s]*?)```"); + let lines_regex = lazy_regex::regex!(r"(?:\# ?)?(.*)"); + + extract_files_from_regex_blocks( + specifier, + source, + media_type, + /* file line index */ 0, + blocks_regex, + lines_regex, + ) +} + +fn extract_files_from_source_comments( + specifier: &ModuleSpecifier, + source: Arc, + media_type: MediaType, +) -> Result, AnyError> { + let parsed_source = deno_ast::parse_module(deno_ast::ParseParams { + specifier: specifier.clone(), + text: source, + media_type, + capture_tokens: false, + maybe_syntax: None, + scope_analysis: false, + })?; + let comments = parsed_source.comments().get_vec(); + let blocks_regex = lazy_regex::regex!(r"```([^\r\n]*)\r?\n([\S\s]*?)```"); + let lines_regex = lazy_regex::regex!(r"(?:\* ?)(?:\# ?)?(.*)"); + + let files = comments + .iter() + .filter(|comment| { + if comment.kind != CommentKind::Block || !comment.text.starts_with('*') { + return false; + } + + true + }) + .flat_map(|comment| { + extract_files_from_regex_blocks( + specifier, + &comment.text, + media_type, + parsed_source.text_info_lazy().line_index(comment.start()), + blocks_regex, + lines_regex, + ) + }) + .flatten() + .collect(); + + Ok(files) +} + +fn extract_files_from_regex_blocks( + specifier: &ModuleSpecifier, + source: &str, + media_type: MediaType, + file_line_index: usize, + blocks_regex: &Regex, + lines_regex: &Regex, +) -> Result, AnyError> { + let files = blocks_regex + .captures_iter(source) + .filter_map(|block| { + block.get(1)?; + + let maybe_attributes: Option> = block + .get(1) + .map(|attributes| attributes.as_str().split(' ').collect()); + + let file_media_type = if let Some(attributes) = maybe_attributes { + if attributes.contains(&"ignore") { + return None; + } + + match attributes.first() { + Some(&"js") => MediaType::JavaScript, + Some(&"javascript") => MediaType::JavaScript, + Some(&"mjs") => MediaType::Mjs, + Some(&"cjs") => MediaType::Cjs, + Some(&"jsx") => MediaType::Jsx, + Some(&"ts") => MediaType::TypeScript, + Some(&"typescript") => MediaType::TypeScript, + Some(&"mts") => MediaType::Mts, + Some(&"cts") => MediaType::Cts, + Some(&"tsx") => MediaType::Tsx, + _ => MediaType::Unknown, + } + } else { + media_type + }; + + if file_media_type == MediaType::Unknown { + return None; + } + + let line_offset = source[0..block.get(0).unwrap().start()] + .chars() + .filter(|c| *c == '\n') + .count(); + + let line_count = block.get(0).unwrap().as_str().split('\n').count(); + + let body = block.get(2).unwrap(); + let text = body.as_str(); + + // TODO(caspervonb) generate an inline source map + let mut file_source = String::new(); + for line in lines_regex.captures_iter(text) { + let text = line.get(1).unwrap(); + writeln!(file_source, "{}", text.as_str()).unwrap(); + } + + let file_specifier = ModuleSpecifier::parse(&format!( + "{}${}-{}", + specifier, + file_line_index + line_offset + 1, + file_line_index + line_offset + line_count + 1, + )) + .unwrap(); + let file_specifier = + mapped_specifier_for_tsc(&file_specifier, file_media_type) + .map(|s| ModuleSpecifier::parse(&s).unwrap()) + .unwrap_or(file_specifier); + + Some(File { + specifier: file_specifier, + maybe_headers: None, + source: file_source.into_bytes().into(), + }) + }) + .collect(); + + Ok(files) +} + +#[derive(Default)] +struct ExportCollector { + named_exports: BTreeSet, + default_export: Option, +} + +impl ExportCollector { + fn to_import_specifiers( + &self, + symbols_to_exclude: &AHashSet, + ) -> Vec { + let mut import_specifiers = vec![]; + + if let Some(default_export) = &self.default_export { + if !symbols_to_exclude.contains(default_export) { + import_specifiers.push(ast::ImportSpecifier::Default( + ast::ImportDefaultSpecifier { + span: DUMMY_SP, + local: ast::Ident { + span: DUMMY_SP, + ctxt: Default::default(), + sym: default_export.clone(), + optional: false, + }, + }, + )); + } + } + + for named_export in &self.named_exports { + if symbols_to_exclude.contains(named_export) { + continue; + } + + import_specifiers.push(ast::ImportSpecifier::Named( + ast::ImportNamedSpecifier { + span: DUMMY_SP, + local: ast::Ident { + span: DUMMY_SP, + ctxt: Default::default(), + sym: named_export.clone(), + optional: false, + }, + imported: None, + is_type_only: false, + }, + )); + } + + import_specifiers + } +} + +impl Visit for ExportCollector { + fn visit_ts_module_decl(&mut self, ts_module_decl: &ast::TsModuleDecl) { + if ts_module_decl.declare { + return; + } + + ts_module_decl.visit_children_with(self); + } + + fn visit_export_decl(&mut self, export_decl: &ast::ExportDecl) { + match &export_decl.decl { + ast::Decl::Class(class) => { + self.named_exports.insert(class.ident.sym.clone()); + } + ast::Decl::Fn(func) => { + self.named_exports.insert(func.ident.sym.clone()); + } + ast::Decl::Var(var) => { + for var_decl in &var.decls { + let atoms = extract_sym_from_pat(&var_decl.name); + self.named_exports.extend(atoms); + } + } + ast::Decl::TsEnum(ts_enum) => { + self.named_exports.insert(ts_enum.id.sym.clone()); + } + ast::Decl::TsModule(ts_module) => { + if ts_module.declare { + return; + } + + match &ts_module.id { + ast::TsModuleName::Ident(ident) => { + self.named_exports.insert(ident.sym.clone()); + } + ast::TsModuleName::Str(s) => { + self.named_exports.insert(s.value.clone()); + } + } + } + ast::Decl::TsTypeAlias(ts_type_alias) => { + self.named_exports.insert(ts_type_alias.id.sym.clone()); + } + ast::Decl::TsInterface(ts_interface) => { + self.named_exports.insert(ts_interface.id.sym.clone()); + } + ast::Decl::Using(_) => {} + } + } + + fn visit_export_default_decl( + &mut self, + export_default_decl: &ast::ExportDefaultDecl, + ) { + match &export_default_decl.decl { + ast::DefaultDecl::Class(class) => { + if let Some(ident) = &class.ident { + self.default_export = Some(ident.sym.clone()); + } + } + ast::DefaultDecl::Fn(func) => { + if let Some(ident) = &func.ident { + self.default_export = Some(ident.sym.clone()); + } + } + ast::DefaultDecl::TsInterfaceDecl(_) => {} + } + } + + fn visit_export_named_specifier( + &mut self, + export_named_specifier: &ast::ExportNamedSpecifier, + ) { + fn get_atom(export_name: &ast::ModuleExportName) -> Atom { + match export_name { + ast::ModuleExportName::Ident(ident) => ident.sym.clone(), + ast::ModuleExportName::Str(s) => s.value.clone(), + } + } + + match &export_named_specifier.exported { + Some(exported) => { + self.named_exports.insert(get_atom(exported)); + } + None => { + self + .named_exports + .insert(get_atom(&export_named_specifier.orig)); + } + } + } + + fn visit_named_export(&mut self, named_export: &ast::NamedExport) { + // ExportCollector does not handle re-exports + if named_export.src.is_some() { + return; + } + + named_export.visit_children_with(self); + } +} + +fn extract_sym_from_pat(pat: &ast::Pat) -> Vec { + fn rec(pat: &ast::Pat, atoms: &mut Vec) { + match pat { + ast::Pat::Ident(binding_ident) => { + atoms.push(binding_ident.sym.clone()); + } + ast::Pat::Array(array_pat) => { + for elem in array_pat.elems.iter().flatten() { + rec(elem, atoms); + } + } + ast::Pat::Rest(rest_pat) => { + rec(&rest_pat.arg, atoms); + } + ast::Pat::Object(object_pat) => { + for prop in &object_pat.props { + match prop { + ast::ObjectPatProp::Assign(assign_pat_prop) => { + atoms.push(assign_pat_prop.key.sym.clone()); + } + ast::ObjectPatProp::KeyValue(key_value_pat_prop) => { + rec(&key_value_pat_prop.value, atoms); + } + ast::ObjectPatProp::Rest(rest_pat) => { + rec(&rest_pat.arg, atoms); + } + } + } + } + ast::Pat::Assign(assign_pat) => { + rec(&assign_pat.left, atoms); + } + ast::Pat::Invalid(_) | ast::Pat::Expr(_) => {} + } + } + + let mut atoms = vec![]; + rec(pat, &mut atoms); + atoms +} + +/// Generates a "pseudo" file from a given file by applying the following +/// transformations: +/// +/// 1. Injects `import` statements for expoted items from the base file +/// 2. If `wrap_kind` is [`WrapKind::DenoTest`], wraps the content of the file +/// in a `Deno.test` call. +/// +/// For example, given a file that looks like: +/// +/// ```ts +/// import { assertEquals } from "@std/assert/equals"; +/// +/// assertEquals(increment(1), 2); +/// ``` +/// +/// and the base file (from which the above snippet was extracted): +/// +/// ```ts +/// export function increment(n: number): number { +/// return n + 1; +/// } +/// +/// export const SOME_CONST = "HELLO"; +/// ``` +/// +/// The generated pseudo test file would look like (if `wrap_in_deno_test` is enabled): +/// +/// ```ts +/// import { assertEquals } from "@std/assert/equals"; +/// import { increment, SOME_CONST } from "./base.ts"; +/// +/// Deno.test("./base.ts$1-3.ts", async () => { +/// assertEquals(increment(1), 2); +/// }); +/// ``` +/// +/// # Edge case - duplicate identifier +/// +/// If a given file imports, say, `doSomething` from an external module while +/// the base file exports `doSomething` as well, the generated pseudo test file +/// would end up having two duplciate imports for `doSomething`, causing the +/// duplicate identifier error. +/// +/// To avoid this issue, when a given file imports `doSomething`, this takes +/// precedence over the automatic import injection for the base file's +/// `doSomething`. So the generated pseudo test file would look like: +/// +/// ```ts +/// import { assertEquals } from "@std/assert/equals"; +/// import { doSomething } from "./some_external_module.ts"; +/// +/// Deno.test("./base.ts$1-3.ts", async () => { +/// assertEquals(doSomething(1), 2); +/// }); +/// ``` +fn generate_pseudo_file( + file: File, + base_file_specifier: &ModuleSpecifier, + exports: &ExportCollector, + wrap_kind: WrapKind, +) -> Result { + let file = file.into_text_decoded()?; + + let parsed = deno_ast::parse_program(deno_ast::ParseParams { + specifier: file.specifier.clone(), + text: file.source, + media_type: file.media_type, + capture_tokens: false, + scope_analysis: true, + maybe_syntax: None, + })?; + + let top_level_atoms = swc_utils::collect_decls_with_ctxt::( + parsed.program_ref(), + parsed.top_level_context(), + ); + + let transformed = + parsed + .program_ref() + .clone() + .fold_with(&mut as_folder(Transform { + specifier: &file.specifier, + base_file_specifier, + exports_from_base: exports, + atoms_to_be_excluded_from_import: top_level_atoms, + wrap_kind, + })); + + let source = deno_ast::swc::codegen::to_code(&transformed); + + log::debug!("{}:\n{}", file.specifier, source); + + Ok(File { + specifier: file.specifier, + maybe_headers: None, + source: source.into_bytes().into(), + }) +} + +struct Transform<'a> { + specifier: &'a ModuleSpecifier, + base_file_specifier: &'a ModuleSpecifier, + exports_from_base: &'a ExportCollector, + atoms_to_be_excluded_from_import: AHashSet, + wrap_kind: WrapKind, +} + +impl<'a> VisitMut for Transform<'a> { + fn visit_mut_program(&mut self, node: &mut ast::Program) { + let new_module_items = match node { + ast::Program::Module(module) => { + let mut module_decls = vec![]; + let mut stmts = vec![]; + + for item in &module.body { + match item { + ast::ModuleItem::ModuleDecl(decl) => { + module_decls.push(decl.clone()); + } + ast::ModuleItem::Stmt(stmt) => { + stmts.push(stmt.clone()); + } + } + } + + let mut transformed_items = vec![]; + transformed_items + .extend(module_decls.into_iter().map(ast::ModuleItem::ModuleDecl)); + let import_specifiers = self + .exports_from_base + .to_import_specifiers(&self.atoms_to_be_excluded_from_import); + if !import_specifiers.is_empty() { + transformed_items.push(ast::ModuleItem::ModuleDecl( + ast::ModuleDecl::Import(ast::ImportDecl { + span: DUMMY_SP, + specifiers: import_specifiers, + src: Box::new(ast::Str { + span: DUMMY_SP, + value: self.base_file_specifier.to_string().into(), + raw: None, + }), + type_only: false, + with: None, + phase: ast::ImportPhase::Evaluation, + }), + )); + } + match self.wrap_kind { + WrapKind::DenoTest => { + transformed_items.push(ast::ModuleItem::Stmt(wrap_in_deno_test( + stmts, + self.specifier.to_string().into(), + ))); + } + WrapKind::NoWrap => { + transformed_items + .extend(stmts.into_iter().map(ast::ModuleItem::Stmt)); + } + } + + transformed_items + } + ast::Program::Script(script) => { + let mut transformed_items = vec![]; + + let import_specifiers = self + .exports_from_base + .to_import_specifiers(&self.atoms_to_be_excluded_from_import); + if !import_specifiers.is_empty() { + transformed_items.push(ast::ModuleItem::ModuleDecl( + ast::ModuleDecl::Import(ast::ImportDecl { + span: DUMMY_SP, + specifiers: import_specifiers, + src: Box::new(ast::Str { + span: DUMMY_SP, + value: self.base_file_specifier.to_string().into(), + raw: None, + }), + type_only: false, + with: None, + phase: ast::ImportPhase::Evaluation, + }), + )); + } + + match self.wrap_kind { + WrapKind::DenoTest => { + transformed_items.push(ast::ModuleItem::Stmt(wrap_in_deno_test( + script.body.clone(), + self.specifier.to_string().into(), + ))); + } + WrapKind::NoWrap => { + transformed_items.extend( + script.body.clone().into_iter().map(ast::ModuleItem::Stmt), + ); + } + } + + transformed_items + } + }; + + *node = ast::Program::Module(ast::Module { + span: DUMMY_SP, + body: new_module_items, + shebang: None, + }); + } +} + +fn wrap_in_deno_test(stmts: Vec, test_name: Atom) -> ast::Stmt { + ast::Stmt::Expr(ast::ExprStmt { + span: DUMMY_SP, + expr: Box::new(ast::Expr::Call(ast::CallExpr { + span: DUMMY_SP, + callee: ast::Callee::Expr(Box::new(ast::Expr::Member(ast::MemberExpr { + span: DUMMY_SP, + obj: Box::new(ast::Expr::Ident(ast::Ident { + span: DUMMY_SP, + sym: "Deno".into(), + optional: false, + ..Default::default() + })), + prop: ast::MemberProp::Ident(ast::IdentName { + span: DUMMY_SP, + sym: "test".into(), + }), + }))), + args: vec![ + ast::ExprOrSpread { + spread: None, + expr: Box::new(ast::Expr::Lit(ast::Lit::Str(ast::Str { + span: DUMMY_SP, + value: test_name, + raw: None, + }))), + }, + ast::ExprOrSpread { + spread: None, + expr: Box::new(ast::Expr::Arrow(ast::ArrowExpr { + span: DUMMY_SP, + params: vec![], + body: Box::new(ast::BlockStmtOrExpr::BlockStmt(ast::BlockStmt { + span: DUMMY_SP, + stmts, + ..Default::default() + })), + is_async: true, + is_generator: false, + type_params: None, + return_type: None, + ..Default::default() + })), + }, + ], + type_args: None, + ..Default::default() + })), + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::file_fetcher::TextDecodedFile; + use deno_ast::swc::atoms::Atom; + use pretty_assertions::assert_eq; + + #[test] + fn test_extract_doc_tests() { + struct Input { + source: &'static str, + specifier: &'static str, + } + struct Expected { + source: &'static str, + specifier: &'static str, + media_type: MediaType, + } + struct Test { + input: Input, + expected: Vec, + } + + let tests = [ + Test { + input: Input { + source: r#""#, + specifier: "file:///main.ts", + }, + expected: vec![], + }, + Test { + input: Input { + source: r#" +/** + * ```ts + * import { assertEquals } from "@std/assert/equal"; + * + * assertEquals(add(1, 2), 3); + * ``` + */ +export function add(a: number, b: number): number { + return a + b; +} +"#, + specifier: "file:///main.ts", + }, + expected: vec![Expected { + source: r#"import { assertEquals } from "@std/assert/equal"; +import { add } from "file:///main.ts"; +Deno.test("file:///main.ts$3-8.ts", async ()=>{ + assertEquals(add(1, 2), 3); +}); +"#, + specifier: "file:///main.ts$3-8.ts", + media_type: MediaType::TypeScript, + }], + }, + Test { + input: Input { + source: r#" +/** + * ```ts + * foo(); + * ``` + */ +export function foo() {} + +export default class Bar {} +"#, + specifier: "file:///main.ts", + }, + expected: vec![Expected { + source: r#"import Bar, { foo } from "file:///main.ts"; +Deno.test("file:///main.ts$3-6.ts", async ()=>{ + foo(); +}); +"#, + specifier: "file:///main.ts$3-6.ts", + media_type: MediaType::TypeScript, + }], + }, + Test { + input: Input { + source: r#" +/** + * ```ts + * const input = { a: 42 } satisfies Args; + * foo(input); + * ``` + */ +export function foo(args: Args) {} + +export type Args = { a: number }; +"#, + specifier: "file:///main.ts", + }, + expected: vec![Expected { + source: r#"import { Args, foo } from "file:///main.ts"; +Deno.test("file:///main.ts$3-7.ts", async ()=>{ + const input = { + a: 42 + } satisfies Args; + foo(input); +}); +"#, + specifier: "file:///main.ts$3-7.ts", + media_type: MediaType::TypeScript, + }], + }, + Test { + input: Input { + source: r#" +/** + * This is a module-level doc. + * + * ```ts + * foo(); + * ``` + * + * @module doc + */ +"#, + specifier: "file:///main.ts", + }, + expected: vec![Expected { + source: r#"Deno.test("file:///main.ts$5-8.ts", async ()=>{ + foo(); +}); +"#, + specifier: "file:///main.ts$5-8.ts", + media_type: MediaType::TypeScript, + }], + }, + Test { + input: Input { + source: r#" +/** + * This is a module-level doc. + * + * ```js + * const cls = new MyClass(); + * ``` + * + * @module doc + */ + +/** + * ```ts + * foo(); + * ``` + */ +export function foo() {} + +export default class MyClass {} + +export * from "./other.ts"; +"#, + specifier: "file:///main.ts", + }, + expected: vec![ + Expected { + source: r#"import MyClass, { foo } from "file:///main.ts"; +Deno.test("file:///main.ts$5-8.js", async ()=>{ + const cls = new MyClass(); +}); +"#, + specifier: "file:///main.ts$5-8.js", + media_type: MediaType::JavaScript, + }, + Expected { + source: r#"import MyClass, { foo } from "file:///main.ts"; +Deno.test("file:///main.ts$13-16.ts", async ()=>{ + foo(); +}); +"#, + specifier: "file:///main.ts$13-16.ts", + media_type: MediaType::TypeScript, + }, + ], + }, + // Avoid duplicate imports + Test { + input: Input { + source: r#" +/** + * ```ts + * import { DUPLICATE1 } from "./other1.ts"; + * import * as DUPLICATE2 from "./other2.js"; + * import { foo as DUPLICATE3 } from "./other3.tsx"; + * + * foo(); + * ``` + */ +export function foo() {} + +export const DUPLICATE1 = "dup1"; +const DUPLICATE2 = "dup2"; +export default DUPLICATE2; +const DUPLICATE3 = "dup3"; +export { DUPLICATE3 }; +"#, + specifier: "file:///main.ts", + }, + expected: vec![Expected { + source: r#"import { DUPLICATE1 } from "./other1.ts"; +import * as DUPLICATE2 from "./other2.js"; +import { foo as DUPLICATE3 } from "./other3.tsx"; +import { foo } from "file:///main.ts"; +Deno.test("file:///main.ts$3-10.ts", async ()=>{ + foo(); +}); +"#, + specifier: "file:///main.ts$3-10.ts", + media_type: MediaType::TypeScript, + }], + }, + // duplication of imported identifier and local identifier is fine + Test { + input: Input { + source: r#" +/** + * ```ts + * const foo = createFoo(); + * foo(); + * ``` + */ +export function createFoo() { + return () => "created foo"; +} + +export const foo = () => "foo"; +"#, + specifier: "file:///main.ts", + }, + expected: vec![Expected { + source: r#"import { createFoo } from "file:///main.ts"; +Deno.test("file:///main.ts$3-7.ts", async ()=>{ + const foo = createFoo(); + foo(); +}); +"#, + specifier: "file:///main.ts$3-7.ts", + media_type: MediaType::TypeScript, + }], + }, + // example code has an exported item `foo` - because `export` must be at + // the top level, `foo` is "hoisted" to the top level instead of being + // wrapped in `Deno.test`. + Test { + input: Input { + source: r#" +/** + * ```ts + * doSomething(); + * export const foo = 42; + * ``` + */ +export function doSomething() {} +"#, + specifier: "file:///main.ts", + }, + expected: vec![Expected { + source: r#"export const foo = 42; +import { doSomething } from "file:///main.ts"; +Deno.test("file:///main.ts$3-7.ts", async ()=>{ + doSomething(); +}); +"#, + specifier: "file:///main.ts$3-7.ts", + media_type: MediaType::TypeScript, + }], + }, + Test { + input: Input { + source: r#" +# Header + +This is a *markdown*. + +```js +import { assertEquals } from "@std/assert/equal"; +import { add } from "jsr:@deno/non-existent"; + +assertEquals(add(1, 2), 3); +``` +"#, + specifier: "file:///README.md", + }, + expected: vec![Expected { + source: r#"import { assertEquals } from "@std/assert/equal"; +import { add } from "jsr:@deno/non-existent"; +Deno.test("file:///README.md$6-12.js", async ()=>{ + assertEquals(add(1, 2), 3); +}); +"#, + specifier: "file:///README.md$6-12.js", + media_type: MediaType::JavaScript, + }], + }, + ]; + + for test in tests { + let file = File { + specifier: ModuleSpecifier::parse(test.input.specifier).unwrap(), + maybe_headers: None, + source: test.input.source.as_bytes().into(), + }; + let got_decoded = extract_doc_tests(file) + .unwrap() + .into_iter() + .map(|f| f.into_text_decoded().unwrap()) + .collect::>(); + let expected = test + .expected + .iter() + .map(|e| TextDecodedFile { + specifier: ModuleSpecifier::parse(e.specifier).unwrap(), + media_type: e.media_type, + source: e.source.into(), + }) + .collect::>(); + assert_eq!(got_decoded, expected); + } + } + + #[test] + fn test_extract_snippet_files() { + struct Input { + source: &'static str, + specifier: &'static str, + } + struct Expected { + source: &'static str, + specifier: &'static str, + media_type: MediaType, + } + struct Test { + input: Input, + expected: Vec, + } + + let tests = [ + Test { + input: Input { + source: r#""#, + specifier: "file:///main.ts", + }, + expected: vec![], + }, + Test { + input: Input { + source: r#" +/** + * ```ts + * import { assertEquals } from "@std/assert/equals"; + * + * assertEquals(add(1, 2), 3); + * ``` + */ +export function add(a: number, b: number): number { + return a + b; +} +"#, + specifier: "file:///main.ts", + }, + expected: vec![Expected { + source: r#"import { assertEquals } from "@std/assert/equals"; +import { add } from "file:///main.ts"; +assertEquals(add(1, 2), 3); +"#, + specifier: "file:///main.ts$3-8.ts", + media_type: MediaType::TypeScript, + }], + }, + Test { + input: Input { + source: r#" +/** + * ```ts + * import { assertEquals } from "@std/assert/equals"; + * import { DUPLICATE } from "./other.ts"; + * + * assertEquals(add(1, 2), 3); + * ``` + */ +export function add(a: number, b: number): number { + return a + b; +} + +export const DUPLICATE = "dup"; +"#, + specifier: "file:///main.ts", + }, + expected: vec![Expected { + source: r#"import { assertEquals } from "@std/assert/equals"; +import { DUPLICATE } from "./other.ts"; +import { add } from "file:///main.ts"; +assertEquals(add(1, 2), 3); +"#, + specifier: "file:///main.ts$3-9.ts", + media_type: MediaType::TypeScript, + }], + }, + // duplication of imported identifier and local identifier is fine, since + // we wrap the snippet in a block. + // This would be a problem if the local one is declared with `var`, as + // `var` is not block scoped but function scoped. For now we don't handle + // this case assuming that `var` is not used in modern code. + Test { + input: Input { + source: r#" + /** + * ```ts + * const foo = createFoo(); + * foo(); + * ``` + */ + export function createFoo() { + return () => "created foo"; + } + + export const foo = () => "foo"; + "#, + specifier: "file:///main.ts", + }, + expected: vec![Expected { + source: r#"import { createFoo } from "file:///main.ts"; +const foo = createFoo(); +foo(); +"#, + specifier: "file:///main.ts$3-7.ts", + media_type: MediaType::TypeScript, + }], + }, + Test { + input: Input { + source: r#" +# Header + +This is a *markdown*. + +```js +import { assertEquals } from "@std/assert/equal"; +import { add } from "jsr:@deno/non-existent"; + +assertEquals(add(1, 2), 3); +``` +"#, + specifier: "file:///README.md", + }, + expected: vec![Expected { + source: r#"import { assertEquals } from "@std/assert/equal"; +import { add } from "jsr:@deno/non-existent"; +assertEquals(add(1, 2), 3); +"#, + specifier: "file:///README.md$6-12.js", + media_type: MediaType::JavaScript, + }], + }, + ]; + + for test in tests { + let file = File { + specifier: ModuleSpecifier::parse(test.input.specifier).unwrap(), + maybe_headers: None, + source: test.input.source.as_bytes().into(), + }; + let got_decoded = extract_snippet_files(file) + .unwrap() + .into_iter() + .map(|f| f.into_text_decoded().unwrap()) + .collect::>(); + let expected = test + .expected + .iter() + .map(|e| TextDecodedFile { + specifier: ModuleSpecifier::parse(e.specifier).unwrap(), + media_type: e.media_type, + source: e.source.into(), + }) + .collect::>(); + assert_eq!(got_decoded, expected); + } + } + + #[test] + fn test_export_collector() { + fn helper(input: &'static str) -> ExportCollector { + let mut collector = ExportCollector::default(); + let parsed = deno_ast::parse_module(deno_ast::ParseParams { + specifier: deno_ast::ModuleSpecifier::parse("file:///main.ts").unwrap(), + text: input.into(), + media_type: deno_ast::MediaType::TypeScript, + capture_tokens: false, + scope_analysis: false, + maybe_syntax: None, + }) + .unwrap(); + + collector.visit_program(parsed.program_ref()); + collector + } + + struct Test { + input: &'static str, + named_expected: BTreeSet, + default_expected: Option, + } + + macro_rules! atom_set { + ($( $x:expr ),*) => { + [$( Atom::from($x) ),*].into_iter().collect::>() + }; + } + + let tests = [ + Test { + input: r#"export const foo = 42;"#, + named_expected: atom_set!("foo"), + default_expected: None, + }, + Test { + input: r#"export let foo = 42;"#, + named_expected: atom_set!("foo"), + default_expected: None, + }, + Test { + input: r#"export var foo = 42;"#, + named_expected: atom_set!("foo"), + default_expected: None, + }, + Test { + input: r#"export const foo = () => {};"#, + named_expected: atom_set!("foo"), + default_expected: None, + }, + Test { + input: r#"export function foo() {}"#, + named_expected: atom_set!("foo"), + default_expected: None, + }, + Test { + input: r#"export class Foo {}"#, + named_expected: atom_set!("Foo"), + default_expected: None, + }, + Test { + input: r#"export enum Foo {}"#, + named_expected: atom_set!("Foo"), + default_expected: None, + }, + Test { + input: r#"export module Foo {}"#, + named_expected: atom_set!("Foo"), + default_expected: None, + }, + Test { + input: r#"export module "foo" {}"#, + named_expected: atom_set!("foo"), + default_expected: None, + }, + Test { + input: r#"export namespace Foo {}"#, + named_expected: atom_set!("Foo"), + default_expected: None, + }, + Test { + input: r#"export type Foo = string;"#, + named_expected: atom_set!("Foo"), + default_expected: None, + }, + Test { + input: r#"export interface Foo {};"#, + named_expected: atom_set!("Foo"), + default_expected: None, + }, + Test { + input: r#"export let name1, name2;"#, + named_expected: atom_set!("name1", "name2"), + default_expected: None, + }, + Test { + input: r#"export const name1 = 1, name2 = 2;"#, + named_expected: atom_set!("name1", "name2"), + default_expected: None, + }, + Test { + input: r#"export function* generatorFunc() {}"#, + named_expected: atom_set!("generatorFunc"), + default_expected: None, + }, + Test { + input: r#"export const { name1, name2: bar } = obj;"#, + named_expected: atom_set!("name1", "bar"), + default_expected: None, + }, + Test { + input: r#"export const [name1, name2] = arr;"#, + named_expected: atom_set!("name1", "name2"), + default_expected: None, + }, + Test { + input: r#"export const { name1 = 42 } = arr;"#, + named_expected: atom_set!("name1"), + default_expected: None, + }, + Test { + input: r#"export default function foo() {}"#, + named_expected: atom_set!(), + default_expected: Some("foo".into()), + }, + Test { + input: r#"export { foo, bar as barAlias };"#, + named_expected: atom_set!("foo", "barAlias"), + default_expected: None, + }, + Test { + input: r#" +export default class Foo {} +export let value1 = 42; +const value2 = "Hello"; +const value3 = "World"; +export { value2 }; +"#, + named_expected: atom_set!("value1", "value2"), + default_expected: Some("Foo".into()), + }, + // overloaded function + Test { + input: r#" +export function foo(a: number): boolean; +export function foo(a: boolean): string; +export function foo(a: number | boolean): boolean | string { + return typeof a === "number" ? true : "hello"; +} +"#, + named_expected: atom_set!("foo"), + default_expected: None, + }, + // The collector deliberately does not handle re-exports, because from + // doc reader's perspective, an example code would become hard to follow + // if it uses re-exported items (as opposed to normal, non-re-exported + // items that would look verbose if an example code explicitly imports + // them). + Test { + input: r#" +export * from "./module1.ts"; +export * as name1 from "./module2.ts"; +export { name2, name3 as N3 } from "./module3.js"; +export { default } from "./module4.ts"; +export { default as myDefault } from "./module5.ts"; +"#, + named_expected: atom_set!(), + default_expected: None, + }, + Test { + input: r#" +export namespace Foo { + export type MyType = string; + export const myValue = 42; + export function myFunc(): boolean; +} +"#, + named_expected: atom_set!("Foo"), + default_expected: None, + }, + Test { + input: r#" +declare namespace Foo { + export type MyType = string; + export const myValue = 42; + export function myFunc(): boolean; +} +"#, + named_expected: atom_set!(), + default_expected: None, + }, + Test { + input: r#" +declare module Foo { + export type MyType = string; + export const myValue = 42; + export function myFunc(): boolean; +} +"#, + named_expected: atom_set!(), + default_expected: None, + }, + Test { + input: r#" +declare global { + export type MyType = string; + export const myValue = 42; + export function myFunc(): boolean; +} +"#, + named_expected: atom_set!(), + default_expected: None, + }, + ]; + + for test in tests { + let got = helper(test.input); + assert_eq!(got.named_exports, test.named_expected); + assert_eq!(got.default_export, test.default_expected); + } + } +} diff --git a/cli/util/mod.rs b/cli/util/mod.rs index b9071c496357ce..e59b09d2c7167e 100644 --- a/cli/util/mod.rs +++ b/cli/util/mod.rs @@ -7,6 +7,7 @@ pub mod console; pub mod diff; pub mod display; pub mod draw_thread; +pub mod extract; pub mod file_watcher; pub mod fs; pub mod logger; diff --git a/tests/integration/check_tests.rs b/tests/integration/check_tests.rs index 1ccec41ebbaac4..121dcb837c80df 100644 --- a/tests/integration/check_tests.rs +++ b/tests/integration/check_tests.rs @@ -185,8 +185,8 @@ fn reload_flag() { fn typecheck_declarations_ns() { let context = TestContextBuilder::for_jsr().build(); let args = vec![ - "test".to_string(), - "--doc".to_string(), + "check".to_string(), + "--doc-only".to_string(), util::root_path() .join("cli/tsc/dts/lib.deno.ns.d.ts") .to_string_lossy() @@ -208,8 +208,8 @@ fn typecheck_declarations_ns() { fn typecheck_declarations_unstable() { let context = TestContext::default(); let args = vec![ - "test".to_string(), - "--doc".to_string(), + "check".to_string(), + "--doc-only".to_string(), util::root_path() .join("cli/tsc/dts/lib.deno.unstable.d.ts") .to_string_lossy() diff --git a/tests/integration/watcher_tests.rs b/tests/integration/watcher_tests.rs index 27c59a27d8607e..56686cd14ab5b5 100644 --- a/tests/integration/watcher_tests.rs +++ b/tests/integration/watcher_tests.rs @@ -1022,6 +1022,8 @@ async fn test_watch_doc() { let mut child = util::deno_cmd() .current_dir(t.path()) .arg("test") + .arg("--config") + .arg(util::deno_config_path()) .arg("--watch") .arg("--doc") .arg(t.path()) @@ -1039,26 +1041,110 @@ async fn test_watch_doc() { wait_contains("Test finished", &mut stderr_lines).await; let foo_file = t.path().join("foo.ts"); + let foo_file_url = foo_file.url_file(); foo_file.write( r#" - export default function foo() {} + export function add(a: number, b: number) { + return a + b; + } + "#, + ); + + wait_contains("ok | 0 passed | 0 failed", &mut stdout_lines).await; + wait_contains("Test finished", &mut stderr_lines).await; + + // Trigger a type error + foo_file.write( + r#" + /** + * ```ts + * const sum: string = add(1, 2); + * ``` + */ + export function add(a: number, b: number) { + return a + b; + } "#, ); + assert_eq!( + skip_restarting_line(&mut stderr_lines).await, + format!("Check {foo_file_url}$3-6.ts") + ); + assert_eq!( + next_line(&mut stderr_lines).await.unwrap(), + "error: TS2322 [ERROR]: Type 'number' is not assignable to type 'string'." + ); + assert_eq!( + next_line(&mut stderr_lines).await.unwrap(), + " const sum: string = add(1, 2);" + ); + assert_eq!(next_line(&mut stderr_lines).await.unwrap(), " ~~~"); + assert_eq!( + next_line(&mut stderr_lines).await.unwrap(), + format!(" at {foo_file_url}$3-6.ts:3:11") + ); + wait_contains("Test failed", &mut stderr_lines).await; + + // Trigger a runtime error foo_file.write( r#" /** * ```ts - * import foo from "./foo.ts"; + * import { assertEquals } from "@std/assert/equals"; + * + * assertEquals(add(1, 2), 4); * ``` */ - export default function foo() {} + export function add(a: number, b: number) { + return a + b; + } "#, ); - // We only need to scan for a Check file://.../foo.ts$3-6 line that - // corresponds to the documentation block being type-checked. - assert_contains!(skip_restarting_line(&mut stderr_lines).await, "foo.ts$3-6"); + wait_contains("running 1 test from", &mut stdout_lines).await; + assert_contains!( + next_line(&mut stdout_lines).await.unwrap(), + &format!("{foo_file_url}$3-8.ts ... FAILED") + ); + wait_contains("ERRORS", &mut stdout_lines).await; + wait_contains( + "error: AssertionError: Values are not equal.", + &mut stdout_lines, + ) + .await; + wait_contains("- 3", &mut stdout_lines).await; + wait_contains("+ 4", &mut stdout_lines).await; + wait_contains("FAILURES", &mut stdout_lines).await; + wait_contains("FAILED | 0 passed | 1 failed", &mut stdout_lines).await; + + wait_contains("Test failed", &mut stderr_lines).await; + + // Fix the runtime error + foo_file.write( + r#" + /** + * ```ts + * import { assertEquals } from "@std/assert/equals"; + * + * assertEquals(add(1, 2), 3); + * ``` + */ + export function add(a: number, b: number) { + return a + b; + } + "#, + ); + + wait_contains("running 1 test from", &mut stdout_lines).await; + assert_contains!( + next_line(&mut stdout_lines).await.unwrap(), + &format!("{foo_file_url}$3-8.ts ... ok") + ); + wait_contains("ok | 1 passed | 0 failed", &mut stdout_lines).await; + + wait_contains("Test finished", &mut stderr_lines).await; + check_alive_then_kill(child); } diff --git a/tests/specs/check/typecheck_doc_duplicate_identifiers/__test__.jsonc b/tests/specs/check/typecheck_doc_duplicate_identifiers/__test__.jsonc new file mode 100644 index 00000000000000..8596142ddec87e --- /dev/null +++ b/tests/specs/check/typecheck_doc_duplicate_identifiers/__test__.jsonc @@ -0,0 +1,5 @@ +{ + "args": "check --doc --config ../../../config/deno.json mod.ts", + "exitCode": 0, + "output": "mod.out" +} diff --git a/tests/specs/check/typecheck_doc_duplicate_identifiers/mod.out b/tests/specs/check/typecheck_doc_duplicate_identifiers/mod.out new file mode 100644 index 00000000000000..d01daafa5aadae --- /dev/null +++ b/tests/specs/check/typecheck_doc_duplicate_identifiers/mod.out @@ -0,0 +1,2 @@ +Check [WILDCARD]/mod.ts +Check [WILDCARD]/mod.ts$2-8.ts diff --git a/tests/specs/check/typecheck_doc_duplicate_identifiers/mod.ts b/tests/specs/check/typecheck_doc_duplicate_identifiers/mod.ts new file mode 100644 index 00000000000000..576f702400b775 --- /dev/null +++ b/tests/specs/check/typecheck_doc_duplicate_identifiers/mod.ts @@ -0,0 +1,13 @@ +/** + * ```ts + * import { assertEquals } from "@std/assert/equals"; + * + * const foo = createFoo(3); + * assertEquals(foo, 9); + * ``` + */ +export function createFoo(x: number): number { + return x * x; +} + +export const foo = 42; diff --git a/tests/specs/check/typecheck_doc_failure/__test__.jsonc b/tests/specs/check/typecheck_doc_failure/__test__.jsonc new file mode 100644 index 00000000000000..5d95f2666826c1 --- /dev/null +++ b/tests/specs/check/typecheck_doc_failure/__test__.jsonc @@ -0,0 +1,5 @@ +{ + "args": "check --doc mod.ts", + "exitCode": 1, + "output": "mod.out" +} diff --git a/tests/specs/check/typecheck_doc_failure/mod.out b/tests/specs/check/typecheck_doc_failure/mod.out new file mode 100644 index 00000000000000..61fd5499e50a6b --- /dev/null +++ b/tests/specs/check/typecheck_doc_failure/mod.out @@ -0,0 +1,6 @@ +Check [WILDCARD]/mod.ts +Check [WILDCARD]/mod.ts$2-5.ts +error: TS2322 [ERROR]: Type 'number' is not assignable to type 'string'. +const sum: string = add(1, 2); + ~~~ + at [WILDCARD]/mod.ts$2-5.ts:2:7 diff --git a/tests/specs/check/typecheck_doc_failure/mod.ts b/tests/specs/check/typecheck_doc_failure/mod.ts new file mode 100644 index 00000000000000..281d7f41b97ab3 --- /dev/null +++ b/tests/specs/check/typecheck_doc_failure/mod.ts @@ -0,0 +1,8 @@ +/** + * ```ts + * const sum: string = add(1, 2); + * ``` + */ +export function add(a: number, b: number): number { + return a + b; +} diff --git a/tests/specs/check/typecheck_doc_in_markdown/__test__.jsonc b/tests/specs/check/typecheck_doc_in_markdown/__test__.jsonc new file mode 100644 index 00000000000000..00f98c4d0e9224 --- /dev/null +++ b/tests/specs/check/typecheck_doc_in_markdown/__test__.jsonc @@ -0,0 +1,5 @@ +{ + "args": "check --doc-only markdown.md", + "exitCode": 1, + "output": "markdown.out" +} diff --git a/tests/specs/check/typecheck_doc_in_markdown/markdown.md b/tests/specs/check/typecheck_doc_in_markdown/markdown.md new file mode 100644 index 00000000000000..d18dbd108761a9 --- /dev/null +++ b/tests/specs/check/typecheck_doc_in_markdown/markdown.md @@ -0,0 +1,31 @@ +# Documentation + +The following block does not have a language attribute and should be ignored: + +``` +This is a fenced block without attributes, it's invalid and it should be ignored. +``` + +The following block should be given a js extension on extraction: + +```js +console.log("js"); +``` + +The following block should be given a ts extension on extraction: + +```ts +console.log("ts"); +``` + +The following example contains the ignore attribute and will be ignored: + +```ts ignore +const value: Invalid = "ignored"; +``` + +The following example will trigger the type-checker to fail: + +```ts +const a: string = 42; +``` diff --git a/tests/specs/check/typecheck_doc_in_markdown/markdown.out b/tests/specs/check/typecheck_doc_in_markdown/markdown.out new file mode 100644 index 00000000000000..acc05dc818baac --- /dev/null +++ b/tests/specs/check/typecheck_doc_in_markdown/markdown.out @@ -0,0 +1,7 @@ +Check [WILDCARD]/markdown.md$11-14.js +Check [WILDCARD]/markdown.md$17-20.ts +Check [WILDCARD]/markdown.md$29-32.ts +error: TS2322 [ERROR]: Type 'number' is not assignable to type 'string'. +const a: string = 42; + ^ + at [WILDCARD]/markdown.md$29-32.ts:1:7 diff --git a/tests/specs/check/typecheck_doc_success/__test__.jsonc b/tests/specs/check/typecheck_doc_success/__test__.jsonc new file mode 100644 index 00000000000000..24fee3f2c906a9 --- /dev/null +++ b/tests/specs/check/typecheck_doc_success/__test__.jsonc @@ -0,0 +1,5 @@ +{ + "args": "check --doc mod.ts", + "exitCode": 0, + "output": "mod.out" +} diff --git a/tests/specs/check/typecheck_doc_success/mod.out b/tests/specs/check/typecheck_doc_success/mod.out new file mode 100644 index 00000000000000..8658af4f891ef7 --- /dev/null +++ b/tests/specs/check/typecheck_doc_success/mod.out @@ -0,0 +1,2 @@ +Check [WILDCARD]/tests/specs/check/typecheck_doc_success/mod.ts +Check [WILDCARD]/tests/specs/check/typecheck_doc_success/mod.ts$2-5.ts diff --git a/tests/specs/check/typecheck_doc_success/mod.ts b/tests/specs/check/typecheck_doc_success/mod.ts new file mode 100644 index 00000000000000..793be27117139c --- /dev/null +++ b/tests/specs/check/typecheck_doc_success/mod.ts @@ -0,0 +1,12 @@ +/** + * ```ts + * const sum: number = add(1, 2); + * ``` + * + * ```mts ignore + * const sum: boolean = add(3, 4); + * ``` + */ +export function add(a: number, b: number): number { + return a + b; +} diff --git a/tests/specs/test/doc/main.out b/tests/specs/test/doc/main.out index b55989f96e4ebb..a255823708427c 100644 --- a/tests/specs/test/doc/main.out +++ b/tests/specs/test/doc/main.out @@ -4,6 +4,6 @@ Check [WILDCARD]/main.ts$14-17.ts Check [WILDCARD]/main.ts$18-21.tsx Check [WILDCARD]/main.ts$30-35.ts error: TS2367 [ERROR]: This comparison appears to be unintentional because the types 'string' and 'number' have no overlap. -console.assert(check() == 42); - ~~~~~~~~~~~~~ - at [WILDCARD]/main.ts$30-35.ts:3:16 + console.assert(check() == 42); + ~~~~~~~~~~~~~ + at [WILDCARD]/main.ts$30-35.ts:3:20 diff --git a/tests/specs/test/doc_duplicate_identifier/__test__.jsonc b/tests/specs/test/doc_duplicate_identifier/__test__.jsonc new file mode 100644 index 00000000000000..2a8e6aafca60b5 --- /dev/null +++ b/tests/specs/test/doc_duplicate_identifier/__test__.jsonc @@ -0,0 +1,5 @@ +{ + "args": "test --doc --config ../../../config/deno.json main.ts", + "exitCode": 0, + "output": "main.out" +} diff --git a/tests/specs/test/doc_duplicate_identifier/main.out b/tests/specs/test/doc_duplicate_identifier/main.out new file mode 100644 index 00000000000000..9196405a653533 --- /dev/null +++ b/tests/specs/test/doc_duplicate_identifier/main.out @@ -0,0 +1,11 @@ +Check [WILDCARD]/main.ts +Check [WILDCARD]/main.ts$11-19.ts +Check [WILDCARD]/main.ts$25-30.ts +running 0 tests from ./main.ts +running 1 test from ./main.ts$11-19.ts +[WILDCARD]/main.ts$11-19.ts ... ok ([WILDCARD]ms) +running 1 test from ./main.ts$25-30.ts +[WILDCARD]/main.ts$25-30.ts ... ok ([WILDCARD]ms) + +ok | 2 passed | 0 failed ([WILDCARD]ms) + diff --git a/tests/specs/test/doc_duplicate_identifier/main.ts b/tests/specs/test/doc_duplicate_identifier/main.ts new file mode 100644 index 00000000000000..df78294d0ac078 --- /dev/null +++ b/tests/specs/test/doc_duplicate_identifier/main.ts @@ -0,0 +1,33 @@ +// `deno test --doc` tries to convert the example code snippets into pseudo +// test files in a way that all the exported items are available without +// explicit import statements. Therefore, in the test code, you don't have to +// write like `import { add } from "./main.ts";`. +// However, this automatic import resolution might conflict with other +// explicitly declared identifiers in the test code you write. This spec test +// makes sure that such cases will not cause any issues - explicit identifiers +// take precedence. + +/** + * ```ts + * import { assertEquals } from "@std/assert/equals"; + * import { getModuleName, createFoo } from "./mod.ts"; + * + * const foo = createFoo(); + * assertEquals(getModuleName(), "mod.ts"); + * assertEquals(add(1, 2), foo()); + * ``` + */ +export function add(a: number, b: number): number { + return a + b; +} + +/** + * ```ts + * import { assertEquals } from "@std/assert/equals"; + * + * assertEquals(getModuleName(), "main.ts"); + * ``` + */ +export const getModuleName = () => "main.ts"; + +export let foo = 1234; diff --git a/tests/specs/test/doc_duplicate_identifier/mod.ts b/tests/specs/test/doc_duplicate_identifier/mod.ts new file mode 100644 index 00000000000000..c613a99adf16e4 --- /dev/null +++ b/tests/specs/test/doc_duplicate_identifier/mod.ts @@ -0,0 +1,7 @@ +export function getModuleName() { + return "mod.ts"; +} + +export const createFoo = () => { + return () => 3; +}; diff --git a/tests/specs/test/doc_failure/__test__.jsonc b/tests/specs/test/doc_failure/__test__.jsonc new file mode 100644 index 00000000000000..79f16ad8d8d72a --- /dev/null +++ b/tests/specs/test/doc_failure/__test__.jsonc @@ -0,0 +1,5 @@ +{ + "args": "test --doc --config ../../../config/deno.json main.ts", + "exitCode": 1, + "output": "main.out" +} diff --git a/tests/specs/test/doc_failure/main.out b/tests/specs/test/doc_failure/main.out new file mode 100644 index 00000000000000..01b03297f8629b --- /dev/null +++ b/tests/specs/test/doc_failure/main.out @@ -0,0 +1,60 @@ +Check [WILDCARD]/main.ts +Check [WILDCARD]/main.ts$2-9.ts +Check [WILDCARD]/main.ts$13-18.ts +Check [WILDCARD]/main.ts$24-29.ts +running 0 tests from ./main.ts +running 1 test from ./main.ts$2-9.ts +[WILDCARD]/main.ts$2-9.ts ... FAILED ([WILDCARD]ms) +running 1 test from ./main.ts$13-18.ts +[WILDCARD]/main.ts$13-18.ts ... FAILED ([WILDCARD]ms) +running 1 test from ./main.ts$24-29.ts +[WILDCARD]/main.ts$24-29.ts ... FAILED ([WILDCARD]ms) + + ERRORS + +[WILDCARD]/main.ts$13-18.ts => ./main.ts$13-18.ts:3:6 +error: AssertionError: Values are not equal. + + + [Diff] Actual / Expected + + +- 3 ++ 4 + + throw new AssertionError(message); + ^ + at assertEquals ([WILDCARD]/std/assert/equals.ts:[WILDCARD]) + at [WILDCARD]/main.ts$13-18.ts:4:5 + +[WILDCARD]/main.ts$2-9.ts => ./main.ts$2-9.ts:3:6 +error: AssertionError: Expected actual: "2.5e+0" to be close to "2": delta "5e-1" is greater than "2e-7". + throw new AssertionError( + ^ + at assertAlmostEquals ([WILDCARD]/std/assert/almost_equals.ts:[WILDCARD]) + at [WILDCARD]/main.ts$2-9.ts:6:5 + +[WILDCARD]/main.ts$24-29.ts => ./main.ts$24-29.ts:3:6 +error: AssertionError: Values are not equal. + + + [Diff] Actual / Expected + + +- 4 ++ 3 + + throw new AssertionError(message); + ^ + at assertEquals ([WILDCARD]/std/assert/equals.ts:[WILDCARD]) + at [WILDCARD]/main.ts$24-29.ts:4:5 + + FAILURES + +[WILDCARD]/main.ts$13-18.ts => ./main.ts$13-18.ts:3:6 +[WILDCARD]/main.ts$2-9.ts => ./main.ts$2-9.ts:3:6 +[WILDCARD]/main.ts$24-29.ts => ./main.ts$24-29.ts:3:6 + +FAILED | 0 passed | 3 failed ([WILDCARD]ms) + +error: Test failed diff --git a/tests/specs/test/doc_failure/main.ts b/tests/specs/test/doc_failure/main.ts new file mode 100644 index 00000000000000..7ec678c034585a --- /dev/null +++ b/tests/specs/test/doc_failure/main.ts @@ -0,0 +1,32 @@ +/** + * ```ts + * import { assertAlmostEquals } from "@std/assert/almost-equals"; + * + * const x = sub(3, 1); + * const y = div(5, x); + * assertAlmostEquals(y, 2.0); // throws + * ``` + * @module doc + */ + +/** + * ```ts + * import { assertEquals } from "@std/assert/equals"; + * + * assertEquals(div(6, 2), 4); // throws + * ``` + */ +export function div(a: number, b: number): number { + return a / b; +} + +/** + * ```ts + * import { assertEquals } from "@std/assert/equals"; + * + * assertEquals(sub(6, 2), 3); // throws + * ``` + */ +const sub = (a: number, b: number): number => a - b; + +export { sub }; diff --git a/tests/specs/test/doc_only/__test__.jsonc b/tests/specs/test/doc_only/__test__.jsonc index 077b733a331010..f40260ae7251a9 100644 --- a/tests/specs/test/doc_only/__test__.jsonc +++ b/tests/specs/test/doc_only/__test__.jsonc @@ -1,5 +1,5 @@ { - "args": "test --doc --allow-all doc_only", + "args": "test --doc --config ../../../config/deno.json doc_only", "exitCode": 0, "output": "main.out" } diff --git a/tests/specs/test/doc_only/doc_only/mod.ts b/tests/specs/test/doc_only/doc_only/mod.ts index 467d850a27fb1d..a389302ce8b092 100644 --- a/tests/specs/test/doc_only/doc_only/mod.ts +++ b/tests/specs/test/doc_only/doc_only/mod.ts @@ -1,6 +1,8 @@ /** * ```ts - * import "./mod.ts"; + * import { assertEquals } from "@std/assert/equals"; + * + * assertEquals(42, 40 + 2); * ``` */ Deno.test("unreachable", function () { diff --git a/tests/specs/test/doc_only/main.out b/tests/specs/test/doc_only/main.out index a2eff5e89d34cd..634bd7636d88ab 100644 --- a/tests/specs/test/doc_only/main.out +++ b/tests/specs/test/doc_only/main.out @@ -1,4 +1,6 @@ -Check [WILDCARD]/doc_only/mod.ts$2-5.ts +Check [WILDCARD]/doc_only/mod.ts$2-7.ts +running 1 test from ./doc_only/mod.ts$2-7.ts +[WILDCARD]/doc_only/mod.ts$2-7.ts ... ok ([WILDCARD]ms) -ok | 0 passed | 0 failed ([WILDCARD]) +ok | 1 passed | 0 failed ([WILDCARD]ms) diff --git a/tests/specs/test/doc_permission_respected/__test__.jsonc b/tests/specs/test/doc_permission_respected/__test__.jsonc new file mode 100644 index 00000000000000..43c2910844d7ba --- /dev/null +++ b/tests/specs/test/doc_permission_respected/__test__.jsonc @@ -0,0 +1,5 @@ +{ + "args": "test --doc --allow-env=PATH --reload main.ts", + "exitCode": 1, + "output": "main.out" +} diff --git a/tests/specs/test/doc_permission_respected/main.out b/tests/specs/test/doc_permission_respected/main.out new file mode 100644 index 00000000000000..928d4f3cece0de --- /dev/null +++ b/tests/specs/test/doc_permission_respected/main.out @@ -0,0 +1,25 @@ +Check [WILDCARD]/main.ts +Check [WILDCARD]/main.ts$3-6.ts +Check [WILDCARD]/main.ts$8-11.ts +running 0 tests from ./main.ts +running 1 test from ./main.ts$3-6.ts +[WILDCARD]/main.ts$3-6.ts ... ok ([WILDCARD]ms) +running 1 test from ./main.ts$8-11.ts +[WILDCARD]/main.ts$8-11.ts ... FAILED ([WILDCARD]ms) + + ERRORS + +[WILDCARD]/main.ts$8-11.ts => ./main.ts$8-11.ts:1:6 +error: NotCapable: Requires env access to "USER", run again with the --allow-env flag + const _user = Deno.env.get("USER"); + ^ + at Object.getEnv [as get] ([WILDCARD]) + at [WILDCARD]/main.ts$8-11.ts:2:28 + + FAILURES + +[WILDCARD]/main.ts$8-11.ts => ./main.ts$8-11.ts:1:6 + +FAILED | 1 passed | 1 failed ([WILDCARD]ms) + +error: Test failed diff --git a/tests/specs/test/doc_permission_respected/main.ts b/tests/specs/test/doc_permission_respected/main.ts new file mode 100644 index 00000000000000..fdc7743a86a44a --- /dev/null +++ b/tests/specs/test/doc_permission_respected/main.ts @@ -0,0 +1,12 @@ +/** + * This should succeed because we pass `--allow-env=PATH` + * ```ts + * const _path = Deno.env.get("PATH"); + * ``` + * + * This should fail because we don't allow for env access to `USER` + * ```ts + * const _user = Deno.env.get("USER"); + * ``` + * @module doc + */ diff --git a/tests/specs/test/doc_success/__test__.jsonc b/tests/specs/test/doc_success/__test__.jsonc new file mode 100644 index 00000000000000..2a8e6aafca60b5 --- /dev/null +++ b/tests/specs/test/doc_success/__test__.jsonc @@ -0,0 +1,5 @@ +{ + "args": "test --doc --config ../../../config/deno.json main.ts", + "exitCode": 0, + "output": "main.out" +} diff --git a/tests/specs/test/doc_success/main.out b/tests/specs/test/doc_success/main.out new file mode 100644 index 00000000000000..cf1e69b6711811 --- /dev/null +++ b/tests/specs/test/doc_success/main.out @@ -0,0 +1,19 @@ +Check [WILDCARD]/main.ts$8-13.js +Check [WILDCARD]/main.ts$14-19.jsx +Check [WILDCARD]/main.ts$20-25.ts +Check [WILDCARD]/main.ts$26-31.tsx +Check [WILDCARD]/main.ts$42-47.ts +running 0 tests from ./main.ts +running 1 test from ./main.ts$8-13.js +[WILDCARD]/main.ts$8-13.js ... ok ([WILDCARD]ms) +running 1 test from ./main.ts$14-19.jsx +[WILDCARD]/main.ts$14-19.jsx ... ok ([WILDCARD]ms) +running 1 test from ./main.ts$20-25.ts +[WILDCARD]/main.ts$20-25.ts ... ok ([WILDCARD]ms) +running 1 test from ./main.ts$26-31.tsx +[WILDCARD]/main.ts$26-31.tsx ... ok ([WILDCARD]ms) +running 1 test from ./main.ts$42-47.ts +[WILDCARD]/main.ts$42-47.ts ... ok ([WILDCARD]ms) + +ok | 5 passed | 0 failed ([WILDCARD]ms) + diff --git a/tests/specs/test/doc_success/main.ts b/tests/specs/test/doc_success/main.ts new file mode 100644 index 00000000000000..6ab339ca4f3d01 --- /dev/null +++ b/tests/specs/test/doc_success/main.ts @@ -0,0 +1,50 @@ +/** + * ``` + * import { assertEquals } from "@std/assert/equals"; + * + * assertEquals(add(1, 2), 3); + * ``` + * + * ```js + * import { assertEquals } from "@std/assert/equals"; + * + * assertEquals(add(1, 2), 3); + * ``` + * + * ```jsx + * import { assertEquals } from "@std/assert/equals"; + * + * assertEquals(add(1, 2), 3); + * ``` + * + * ```ts + * import { assertEquals } from "@std/assert/equals"; + * + * assertEquals(add(1, 2), 3); + * ``` + * + * ```tsx + * import { assertEquals } from "@std/assert/equals"; + * + * assertEquals(add(1, 2), 3); + * ``` + * + * ```text + * import { assertEquals } from "@std/assert/equals"; + * + * assertEquals(add(1, 2), 3); + * ``` + * + * @module doc + */ + +/** + * ```ts + * import { assertEquals } from "@std/assert/equals"; + * + * assertEquals(add(1, 2), 3); + * ``` + */ +export function add(a: number, b: number): number { + return a + b; +} diff --git a/tests/specs/test/doc_ts_declare_global/__test__.jsonc b/tests/specs/test/doc_ts_declare_global/__test__.jsonc new file mode 100644 index 00000000000000..db1e607aadd290 --- /dev/null +++ b/tests/specs/test/doc_ts_declare_global/__test__.jsonc @@ -0,0 +1,5 @@ +{ + "args": "test --doc --config ../../../config/deno.json lib.d.ts", + "exitCode": 0, + "output": "lib.d.ts.out" +} diff --git a/tests/specs/test/doc_ts_declare_global/lib.d.ts b/tests/specs/test/doc_ts_declare_global/lib.d.ts new file mode 100644 index 00000000000000..a5f442910005b9 --- /dev/null +++ b/tests/specs/test/doc_ts_declare_global/lib.d.ts @@ -0,0 +1,13 @@ +export {}; + +declare global { + /** + * ```ts + * import { assertEquals } from "@std/assert/equals"; + * import "./mod.js"; + * + * assertEquals(myFunction(1, 2), 3); + * ``` + */ + export function myFunction(a: number, b: number): number; +} diff --git a/tests/specs/test/doc_ts_declare_global/lib.d.ts.out b/tests/specs/test/doc_ts_declare_global/lib.d.ts.out new file mode 100644 index 00000000000000..2d6d8dbc853ae9 --- /dev/null +++ b/tests/specs/test/doc_ts_declare_global/lib.d.ts.out @@ -0,0 +1,6 @@ +Check [WILDCARD]/lib$d$ts$5-11.ts +running 1 test from ./lib$d$ts$5-11.ts +[WILDCARD]/lib$d$ts$5-11.ts ... ok ([WILDCARD]ms) + +ok | 1 passed | 0 failed ([WILDCARD]ms) + diff --git a/tests/specs/test/doc_ts_declare_global/mod.js b/tests/specs/test/doc_ts_declare_global/mod.js new file mode 100644 index 00000000000000..1b378d2a7a3f92 --- /dev/null +++ b/tests/specs/test/doc_ts_declare_global/mod.js @@ -0,0 +1 @@ +globalThis.myFunction = (a, b) => a + b; diff --git a/tests/specs/test/doc_ts_namespace_decl/__test__.jsonc b/tests/specs/test/doc_ts_namespace_decl/__test__.jsonc new file mode 100644 index 00000000000000..db1e607aadd290 --- /dev/null +++ b/tests/specs/test/doc_ts_namespace_decl/__test__.jsonc @@ -0,0 +1,5 @@ +{ + "args": "test --doc --config ../../../config/deno.json lib.d.ts", + "exitCode": 0, + "output": "lib.d.ts.out" +} diff --git a/tests/specs/test/doc_ts_namespace_decl/lib.d.ts b/tests/specs/test/doc_ts_namespace_decl/lib.d.ts new file mode 100644 index 00000000000000..e7c81cb5f14477 --- /dev/null +++ b/tests/specs/test/doc_ts_namespace_decl/lib.d.ts @@ -0,0 +1,11 @@ +declare namespace MyNamespace { + /** + * ```ts + * import { assertEquals } from "@std/assert/equals"; + * import "./mod.js"; + * + * assertEquals(MyNamespace.add(1, 2), 3); + * ``` + */ + export function add(a: number, b: number): number; +} diff --git a/tests/specs/test/doc_ts_namespace_decl/lib.d.ts.out b/tests/specs/test/doc_ts_namespace_decl/lib.d.ts.out new file mode 100644 index 00000000000000..2c9e71dc4ac0aa --- /dev/null +++ b/tests/specs/test/doc_ts_namespace_decl/lib.d.ts.out @@ -0,0 +1,6 @@ +Check [WILDCARD]/lib$d$ts$3-9.ts +running 1 test from ./lib$d$ts$3-9.ts +[WILDCARD]/lib$d$ts$3-9.ts ... ok ([WILDCARD]ms) + +ok | 1 passed | 0 failed ([WILDCARD]ms) + diff --git a/tests/specs/test/doc_ts_namespace_decl/mod.js b/tests/specs/test/doc_ts_namespace_decl/mod.js new file mode 100644 index 00000000000000..6a96c342fb4d24 --- /dev/null +++ b/tests/specs/test/doc_ts_namespace_decl/mod.js @@ -0,0 +1,5 @@ +globalThis.MyNamespace = { + add(a, b) { + return a + b; + }, +}; diff --git a/tests/specs/test/markdown/main.out b/tests/specs/test/markdown/main.out index 30327c72f70088..bdbd4325f059de 100644 --- a/tests/specs/test/markdown/main.out +++ b/tests/specs/test/markdown/main.out @@ -2,6 +2,6 @@ Check [WILDCARD]/main.md$11-14.js Check [WILDCARD]/main.md$17-20.ts Check [WILDCARD]/main.md$29-32.ts error: TS2322 [ERROR]: Type 'number' is not assignable to type 'string'. -const a: string = 42; - ^ - at [WILDCARD]/main.md$29-32.ts:1:7 + const a: string = 42; + ^ + at [WILDCARD]/main.md$29-32.ts:2:11 diff --git a/tests/specs/test/markdown_full_block_names/main.out b/tests/specs/test/markdown_full_block_names/main.out index 9e64522dd225ab..d7e991ce10ea09 100644 --- a/tests/specs/test/markdown_full_block_names/main.out +++ b/tests/specs/test/markdown_full_block_names/main.out @@ -1,6 +1,6 @@ Check [WILDCARD]/main.md$5-8.js Check [WILDCARD]/main.md$17-20.ts error: TS2322 [ERROR]: Type 'number' is not assignable to type 'string'. -const a: string = 42; - ^ - at [WILDCARD]/main.md$17-20.ts:1:7 + const a: string = 42; + ^ + at [WILDCARD]/main.md$17-20.ts:2:11 diff --git a/tests/specs/test/markdown_ignore_html_comment/main.out b/tests/specs/test/markdown_ignore_html_comment/main.out index 4de73884535afb..d30ba7822bc998 100644 --- a/tests/specs/test/markdown_ignore_html_comment/main.out +++ b/tests/specs/test/markdown_ignore_html_comment/main.out @@ -1,5 +1,5 @@ Check [WILDCARD]/main.md$34-37.ts error: TS2322 [ERROR]: Type 'number' is not assignable to type 'string'. -const a: string = 42; - ^ - at [WILDCARD]/main.md$34-37.ts:1:7 + const a: string = 42; + ^ + at [WILDCARD]/main.md$34-37.ts:2:11 diff --git a/tests/specs/test/markdown_windows/main.out b/tests/specs/test/markdown_windows/main.out index 30327c72f70088..bdbd4325f059de 100644 --- a/tests/specs/test/markdown_windows/main.out +++ b/tests/specs/test/markdown_windows/main.out @@ -2,6 +2,6 @@ Check [WILDCARD]/main.md$11-14.js Check [WILDCARD]/main.md$17-20.ts Check [WILDCARD]/main.md$29-32.ts error: TS2322 [ERROR]: Type 'number' is not assignable to type 'string'. -const a: string = 42; - ^ - at [WILDCARD]/main.md$29-32.ts:1:7 + const a: string = 42; + ^ + at [WILDCARD]/main.md$29-32.ts:2:11 diff --git a/tests/specs/test/type_check_with_doc/main.out b/tests/specs/test/type_check_with_doc/main.out index 931a6a5f3ef279..56b7ba9e8b9c64 100644 --- a/tests/specs/test/type_check_with_doc/main.out +++ b/tests/specs/test/type_check_with_doc/main.out @@ -6,8 +6,8 @@ const a: string = 1; at file://[WILDCARD]/main.ts:8:7 TS2322 [ERROR]: Type 'string' is not assignable to type 'number'. -const b: number = "1"; - ^ - at file://[WILDCARD]/main.ts$2-5.ts:1:7 + const b: number = "1"; + ^ + at file://[WILDCARD]/main.ts$2-5.ts:2:11 Found 2 errors.