summaryrefslogtreecommitdiff
path: root/cli/tools
diff options
context:
space:
mode:
Diffstat (limited to 'cli/tools')
-rw-r--r--cli/tools/check.rs46
-rw-r--r--cli/tools/test/mod.rs336
2 files changed, 123 insertions, 259 deletions
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)]