summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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.