summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYusuke Tanaka <yusuktan@maguro.dev>2024-09-18 13:35:48 +0900
committerGitHub <noreply@github.com>2024-09-17 21:35:48 -0700
commitd5c00ef50e6519fccde54a577e038f0ebb1282e9 (patch)
tree9429c5f09c6969fd8236041f48b354a9b0841e1f
parent37315917625179063cb5653e2edd4ee0e5de99c5 (diff)
feat(cli): evaluate code snippets in JSDoc and markdown (#25220)
This commit lets `deno test --doc` command actually evaluate code snippets in JSDoc and markdown files. ## How it works 1. Extract code snippets from JSDoc or code fences 2. Convert them into pseudo files by wrapping them in `Deno.test(...)` 3. Register the pseudo files as in-memory files 4. Run type-check and evaluation We apply some magic at the step 2 - let's say we have the following file named `mod.ts` as an input: ````ts /** * ```ts * import { assertEquals } from "jsr:@std/assert/equals"; * * assertEquals(add(1, 2), 3); * ``` */ export function add(a: number, b: number) { return a + b; } ```` This is virtually transformed into: ```ts import { assertEquals } from "jsr:@std/assert/equals"; import { add } from "files:///path/to/mod.ts"; Deno.test("mod.ts$2-7.ts", async () => { assertEquals(add(1, 2), 3); }); ``` Note that a new import statement is inserted here to make `add` function available. In a nutshell, all items exported from `mod.ts` become available in the generated pseudo file with this automatic import insertion. The intention behind this design is that, from library user's standpoint, it should be very obvious that this `add` function is what this example code is attached to. Also, if there is an explicit import statement like `import { add } from "./mod.ts"`, this import path `./mod.ts` is not helpful for doc readers because they will need to import it in a different way. The automatic import insertion has some edge cases, in particular where there is a local variable in a snippet with the same name as one of the exported items. This case is addressed by employing swc's scope analysis (see test cases for more details). ## "type-checking only" mode stays around This change will likely impact a lot of existing doc tests in the ecosystem because some doc tests rely on the fact that they are not evaluated - some cause side effects if executed, some throw errors at runtime although they do pass the type check, etc. To help those tests gradually transition to the ones runnable with the new `deno test --doc`, we will keep providing the ability to run type-checking only via `deno check --doc`. Additionally there is a `--doc-only` option added to the `check` subcommand too, which is useful when you want to type-check on code snippets in markdown files, as normal `deno check` command doesn't accept markdown. ## Demo https://github.com/user-attachments/assets/47e9af73-d16e-472d-b09e-1853b9e8f5ce --- Closes #4716
-rw-r--r--cli/args/flags.rs68
-rw-r--r--cli/lsp/testing/execution.rs13
-rw-r--r--cli/main.rs7
-rw-r--r--cli/tools/check.rs46
-rw-r--r--cli/tools/test/mod.rs336
-rw-r--r--cli/util/extract.rs1410
-rw-r--r--cli/util/mod.rs1
-rw-r--r--tests/integration/check_tests.rs8
-rw-r--r--tests/integration/watcher_tests.rs98
-rw-r--r--tests/specs/check/typecheck_doc_duplicate_identifiers/__test__.jsonc5
-rw-r--r--tests/specs/check/typecheck_doc_duplicate_identifiers/mod.out2
-rw-r--r--tests/specs/check/typecheck_doc_duplicate_identifiers/mod.ts13
-rw-r--r--tests/specs/check/typecheck_doc_failure/__test__.jsonc5
-rw-r--r--tests/specs/check/typecheck_doc_failure/mod.out6
-rw-r--r--tests/specs/check/typecheck_doc_failure/mod.ts8
-rw-r--r--tests/specs/check/typecheck_doc_in_markdown/__test__.jsonc5
-rw-r--r--tests/specs/check/typecheck_doc_in_markdown/markdown.md31
-rw-r--r--tests/specs/check/typecheck_doc_in_markdown/markdown.out7
-rw-r--r--tests/specs/check/typecheck_doc_success/__test__.jsonc5
-rw-r--r--tests/specs/check/typecheck_doc_success/mod.out2
-rw-r--r--tests/specs/check/typecheck_doc_success/mod.ts12
-rw-r--r--tests/specs/test/doc/main.out6
-rw-r--r--tests/specs/test/doc_duplicate_identifier/__test__.jsonc5
-rw-r--r--tests/specs/test/doc_duplicate_identifier/main.out11
-rw-r--r--tests/specs/test/doc_duplicate_identifier/main.ts33
-rw-r--r--tests/specs/test/doc_duplicate_identifier/mod.ts7
-rw-r--r--tests/specs/test/doc_failure/__test__.jsonc5
-rw-r--r--tests/specs/test/doc_failure/main.out60
-rw-r--r--tests/specs/test/doc_failure/main.ts32
-rw-r--r--tests/specs/test/doc_only/__test__.jsonc2
-rw-r--r--tests/specs/test/doc_only/doc_only/mod.ts4
-rw-r--r--tests/specs/test/doc_only/main.out6
-rw-r--r--tests/specs/test/doc_permission_respected/__test__.jsonc5
-rw-r--r--tests/specs/test/doc_permission_respected/main.out25
-rw-r--r--tests/specs/test/doc_permission_respected/main.ts12
-rw-r--r--tests/specs/test/doc_success/__test__.jsonc5
-rw-r--r--tests/specs/test/doc_success/main.out19
-rw-r--r--tests/specs/test/doc_success/main.ts50
-rw-r--r--tests/specs/test/doc_ts_declare_global/__test__.jsonc5
-rw-r--r--tests/specs/test/doc_ts_declare_global/lib.d.ts13
-rw-r--r--tests/specs/test/doc_ts_declare_global/lib.d.ts.out6
-rw-r--r--tests/specs/test/doc_ts_declare_global/mod.js1
-rw-r--r--tests/specs/test/doc_ts_namespace_decl/__test__.jsonc5
-rw-r--r--tests/specs/test/doc_ts_namespace_decl/lib.d.ts11
-rw-r--r--tests/specs/test/doc_ts_namespace_decl/lib.d.ts.out6
-rw-r--r--tests/specs/test/doc_ts_namespace_decl/mod.js5
-rw-r--r--tests/specs/test/markdown/main.out6
-rw-r--r--tests/specs/test/markdown_full_block_names/main.out6
-rw-r--r--tests/specs/test/markdown_ignore_html_comment/main.out6
-rw-r--r--tests/specs/test/markdown_windows/main.out6
-rw-r--r--tests/specs/test/type_check_with_doc/main.out6
51 files changed, 2148 insertions, 309 deletions
diff --git a/cli/args/flags.rs b/cli/args/flags.rs
index f832c2a62..92336a0a1 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<String>,
+ pub doc: bool,
+ pub doc_only: bool,
}
#[derive(Clone, Debug, Eq, PartialEq)]
@@ -1695,6 +1697,19 @@ Unless --reload is specified, this command will not re-download already cached d
.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..)
.required_unless_present("help")
@@ -2789,7 +2804,7 @@ or <c>**/__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 a8cea8dd4..55d81f342 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::<Vec<_>>())
+ .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 10d9ead4e..a7e1c8342 100644
--- a/cli/main.rs
+++ b/cli/main.rs
@@ -121,12 +121,7 @@ async fn run_subcommand(flags: Arc<Flags>) -> Result<i32, AnyError> {
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 d50af5230..9c464fa16 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<Flags>,
+ 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 63382ffc6..d043ffcba 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<String>,
@@ -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<Vec<File>, AnyError> {
- let files = blocks_regex
- .captures_iter(source)
- .filter_map(|block| {
- block.get(1)?;
-
- let maybe_attributes: Option<Vec<_>> = 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<str>,
- media_type: MediaType,
-) -> Result<Vec<File>, 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<Vec<File>, 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<ModuleSpecifier>,
-) -> Result<Vec<File>, 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<MainModuleGraphContainer>,
- 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::<Vec<_>>();
-
- 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::<Vec<(ModuleSpecifier, TestMode)>>();
+ 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<Vec<File>, 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<Url> {
+ 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 000000000..e27a79347
--- /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<Vec<File>, 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<Vec<File>, AnyError> {
+ extract_inner(file, WrapKind::NoWrap)
+}
+
+#[derive(Clone, Copy)]
+enum WrapKind {
+ DenoTest,
+ NoWrap,
+}
+
+fn extract_inner(
+ file: File,
+ wrap_kind: WrapKind,
+) -> Result<Vec<File>, 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::<Result<_, _>>()
+}
+
+fn extract_files_from_fenced_blocks(
+ specifier: &ModuleSpecifier,
+ source: &str,
+ media_type: MediaType,
+) -> Result<Vec<File>, 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<str>,
+ media_type: MediaType,
+) -> Result<Vec<File>, 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<Vec<File>, AnyError> {
+ let files = blocks_regex
+ .captures_iter(source)
+ .filter_map(|block| {
+ block.get(1)?;
+
+ let maybe_attributes: Option<Vec<_>> = 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<Atom>,
+ default_export: Option<Atom>,
+}
+
+impl ExportCollector {
+ fn to_import_specifiers(
+ &self,
+ symbols_to_exclude: &AHashSet<Atom>,
+ ) -> Vec<ast::ImportSpecifier> {
+ 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<Atom> {
+ fn rec(pat: &ast::Pat, atoms: &mut Vec<Atom>) {
+ 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<File, AnyError> {
+ 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::<Atom, _>(
+ 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<Atom>,
+ 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<ast::Stmt>, 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<Expected>,
+ }
+
+ 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::<Vec<_>>();
+ let expected = test
+ .expected
+ .iter()
+ .map(|e| TextDecodedFile {
+ specifier: ModuleSpecifier::parse(e.specifier).unwrap(),
+ media_type: e.media_type,
+ source: e.source.into(),
+ })
+ .collect::<Vec<_>>();
+ 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<Expected>,
+ }
+
+ 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::<Vec<_>>();
+ let expected = test
+ .expected
+ .iter()
+ .map(|e| TextDecodedFile {
+ specifier: ModuleSpecifier::parse(e.specifier).unwrap(),
+ media_type: e.media_type,
+ source: e.source.into(),
+ })
+ .collect::<Vec<_>>();
+ 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<Atom>,
+ default_expected: Option<Atom>,
+ }
+
+ macro_rules! atom_set {
+ ($( $x:expr ),*) => {
+ [$( Atom::from($x) ),*].into_iter().collect::<BTreeSet<_>>()
+ };
+ }
+
+ 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 b9071c496..e59b09d2c 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 1ccec41eb..121dcb837 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 27c59a27d..56686cd14 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 000000000..8596142dd
--- /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 000000000..d01daafa5
--- /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 000000000..576f70240
--- /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 000000000..5d95f2666
--- /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 000000000..61fd5499e
--- /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 000000000..281d7f41b
--- /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 000000000..00f98c4d0
--- /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 000000000..d18dbd108
--- /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 000000000..acc05dc81
--- /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 000000000..24fee3f2c
--- /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 000000000..8658af4f8
--- /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 000000000..793be2711
--- /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 b55989f96..a25582370 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 000000000..2a8e6aafc
--- /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 000000000..9196405a6
--- /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 000000000..df78294d0
--- /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 000000000..c613a99ad
--- /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 000000000..79f16ad8d
--- /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 000000000..01b03297f
--- /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 000000000..7ec678c03
--- /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 077b733a3..f40260ae7 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 467d850a2..a389302ce 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 a2eff5e89..634bd7636 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 000000000..43c291084
--- /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 000000000..928d4f3ce
--- /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 000000000..fdc7743a8
--- /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 000000000..2a8e6aafc
--- /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 000000000..cf1e69b67
--- /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 000000000..6ab339ca4
--- /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 000000000..db1e607aa
--- /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 000000000..a5f442910
--- /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 000000000..2d6d8dbc8
--- /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 000000000..1b378d2a7
--- /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 000000000..db1e607aa
--- /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 000000000..e7c81cb5f
--- /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 000000000..2c9e71dc4
--- /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 000000000..6a96c342f
--- /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 30327c72f..bdbd4325f 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 9e64522dd..d7e991ce1 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 4de738845..d30ba7822 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 30327c72f..bdbd4325f 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 931a6a5f3..56b7ba9e8 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.