diff --git a/cli/args/mod.rs b/cli/args/mod.rs index a1a9c49cbe..901c2f7c08 100644 --- a/cli/args/mod.rs +++ b/cli/args/mod.rs @@ -68,6 +68,7 @@ use once_cell::sync::Lazy; use serde::Deserialize; use serde::Serialize; use std::borrow::Cow; +use std::collections::BTreeSet; use std::collections::HashMap; use std::env; use std::io::BufReader; @@ -790,6 +791,15 @@ struct CliOptionOverrides { import_map_specifier: Option>, } +/// Overrides for the options below that when set will +/// use these values over the values derived from the +/// CLI flags or config file. +#[derive(Debug, Clone)] +pub struct ScopeOptions { + pub scope: Option>, + pub all_scopes: Arc>>, +} + /// Holds the resolved options of many sources used by subcommands /// and provides some helper function for creating common objects. pub struct CliOptions { @@ -804,6 +814,7 @@ pub struct CliOptions { overrides: CliOptionOverrides, pub start_dir: Arc, pub deno_dir_provider: Arc, + pub scope_options: Option>, } impl CliOptions { @@ -814,6 +825,7 @@ impl CliOptions { npmrc: Arc, start_dir: Arc, force_global_cache: bool, + scope_options: Option>, ) -> Result { if let Some(insecure_allowlist) = flags.unsafely_ignore_certificate_errors.as_ref() @@ -853,6 +865,7 @@ impl CliOptions { main_module_cell: std::sync::OnceLock::new(), start_dir, deno_dir_provider, + scope_options, }) } @@ -937,6 +950,7 @@ impl CliOptions { npmrc, Arc::new(start_dir), false, + None, ) } diff --git a/cli/lsp/config.rs b/cli/lsp/config.rs index ea77e36bcf..e363e75bdb 100644 --- a/cli/lsp/config.rs +++ b/cli/lsp/config.rs @@ -1410,9 +1410,17 @@ impl ConfigData { .unwrap_or_default(), ); - let ts_config = LspTsConfig::new( - member_dir.workspace.root_deno_json().map(|c| c.as_ref()), - ); + // TODO(nayeemrmn): This is a hack to get member-specific compiler options. + let ts_config = if let Some(config_file) = member_dir + .maybe_deno_json() + .filter(|c| c.json.compiler_options.is_some()) + { + LspTsConfig::new(Some(config_file)) + } else { + LspTsConfig::new( + member_dir.workspace.root_deno_json().map(|c| c.as_ref()), + ) + }; let deno_lint_config = if ts_config.inner.0.get("jsx").and_then(|v| v.as_str()) == Some("react") diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index cbe194e14e..a9ffcd9682 100644 --- a/cli/lsp/language_server.rs +++ b/cli/lsp/language_server.rs @@ -3681,6 +3681,7 @@ impl Inner { .unwrap_or_else(create_default_npmrc), workspace, force_global_cache, + None, )?; let open_docs = self.documents.documents(DocumentsFilter::OpenDiagnosable); diff --git a/cli/tools/check.rs b/cli/tools/check.rs index ad5c7c3ab1..ce7e66ccf4 100644 --- a/cli/tools/check.rs +++ b/cli/tools/check.rs @@ -1,11 +1,16 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +use std::collections::BTreeMap; +use std::collections::BTreeSet; use std::collections::HashSet; use std::collections::VecDeque; use std::sync::Arc; use deno_ast::MediaType; use deno_ast::ModuleSpecifier; +use deno_config::deno_json::get_ts_config_for_emit; +use deno_config::glob::PathOrPattern; +use deno_core::anyhow::anyhow; use deno_core::error::AnyError; use deno_graph::Module; use deno_graph::ModuleGraph; @@ -15,9 +20,15 @@ use once_cell::sync::Lazy; use regex::Regex; use crate::args::check_warn_tsconfig; +use crate::args::discover_npmrc_from_workspace; use crate::args::CheckFlags; +use crate::args::CliLockfile; use crate::args::CliOptions; +use crate::args::ConfigFlag; +use crate::args::FileFlags; use crate::args::Flags; +use crate::args::LintFlags; +use crate::args::ScopeOptions; use crate::args::TsConfig; use crate::args::TsConfigType; use crate::args::TsTypeLib; @@ -40,6 +51,159 @@ pub async fn check( flags: Arc, check_flags: CheckFlags, ) -> Result<(), AnyError> { + let is_discovered_config = match flags.config_flag { + ConfigFlag::Discover => true, + ConfigFlag::Path(_) => false, + ConfigFlag::Disabled => false, + }; + if is_discovered_config { + let factory = CliFactory::from_flags(flags.clone()); + let cli_options = factory.cli_options()?; + let (remote_files, files) = check_flags + .files + .iter() + .cloned() + .partition::, _>(|f| { + f.starts_with("http://") + || f.starts_with("https://") + || f.starts_with("npm:") + || f.starts_with("jsr:") + }); + let cwd_prefix = format!( + "{}{}", + cli_options.initial_cwd().to_string_lossy(), + std::path::MAIN_SEPARATOR + ); + // TODO(nayeemrmn): Using lint options for now. Add proper API to deno_config. + let mut by_workspace_directory = cli_options + .resolve_lint_options_for_members(&LintFlags { + files: FileFlags { + ignore: Default::default(), + include: files, + }, + ..Default::default() + })? + .into_iter() + .map(|(d, o)| { + let files = o + .files + .include + .iter() + .flat_map(|p| { + p.inner().iter().flat_map(|p| match p { + PathOrPattern::NegatedPath(_) => None, + PathOrPattern::Path(p) => Some(p.to_string_lossy().to_string()), + PathOrPattern::Pattern(p) => { + // TODO(nayeemrmn): Absolute globs don't work for specifier + // collection, we make them relative here for now. + let s = p.as_str(); + Some(s.strip_prefix(&cwd_prefix).unwrap_or(&s).to_string()) + } + PathOrPattern::RemoteUrl(_) => None, + }) + }) + .collect::>(); + (d.dir_url().clone(), (Arc::new(d), files)) + }) + .collect::>(); + if !remote_files.is_empty() { + by_workspace_directory + .entry(cli_options.start_dir.dir_url().clone()) + .or_insert((cli_options.start_dir.clone(), vec![])) + .1 + .extend(remote_files); + } + let all_scopes = Arc::new( + by_workspace_directory + .iter() + .filter(|(_, (d, _))| d.has_deno_or_pkg_json()) + .map(|(s, (_, _))| s.clone()) + .collect::>(), + ); + let dir_count = by_workspace_directory.len(); + let mut check_errors = vec![]; + let mut found_specifiers = false; + for (dir_url, (workspace_directory, files)) in by_workspace_directory { + let check_flags = CheckFlags { + files, + doc: check_flags.doc, + doc_only: check_flags.doc_only, + }; + let (npmrc, _) = + discover_npmrc_from_workspace(&workspace_directory.workspace)?; + let lockfile = + CliLockfile::discover(&flags, &workspace_directory.workspace)?; + let scope_options = (dir_count > 1).then(|| ScopeOptions { + scope: workspace_directory + .has_deno_or_pkg_json() + .then(|| dir_url.clone()), + all_scopes: all_scopes.clone(), + }); + let cli_options = CliOptions::new( + flags.clone(), + cli_options.initial_cwd().to_path_buf(), + lockfile.map(Arc::new), + npmrc, + workspace_directory, + false, + scope_options.map(Arc::new), + )?; + let factory = CliFactory::from_cli_options(Arc::new(cli_options)); + let main_graph_container = factory.main_module_graph_container().await?; + let specifiers = + main_graph_container.collect_specifiers(&check_flags.files)?; + if specifiers.is_empty() { + continue; + } else { + found_specifiers = true; + } + let specifiers_for_typecheck = if check_flags.doc || check_flags.doc_only + { + let file_fetcher = factory.file_fetcher()?; + let root_permissions = factory.root_permissions_container()?; + let mut specifiers_for_typecheck = if check_flags.doc { + specifiers.clone() + } else { + vec![] + }; + for s in specifiers { + let file = file_fetcher.fetch(&s, root_permissions).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 + }; + if let Err(err) = main_graph_container + .check_specifiers(&specifiers_for_typecheck, None) + .await + { + check_errors.push(err); + } + } + if !found_specifiers { + log::warn!("{} No matching files found.", colors::yellow("Warning")); + } + if !check_errors.is_empty() { + // TODO(nayeemrmn): More integrated way of concatenating diagnostics from + // different checks. + return Err(anyhow!( + "{}", + check_errors + .into_iter() + .map(|e| e.to_string()) + .collect::>() + .join("\n\n"), + )); + } + return Ok(()); + } + let factory = CliFactory::from_flags(flags); let main_graph_container = factory.main_module_graph_container().await?; @@ -168,9 +332,22 @@ impl TypeChecker { } log::debug!("Type checking."); - let ts_config_result = self + // TODO(nayeemrmn): This is a hack to get member-specific compiler options. + let ts_config_result = if let Some(config_file) = self .cli_options - .resolve_ts_config_for_emit(TsConfigType::Check { lib: options.lib })?; + .start_dir + .maybe_deno_json() + .filter(|c| c.json.compiler_options.is_some()) + { + get_ts_config_for_emit( + TsConfigType::Check { lib: options.lib }, + Some(config_file), + )? + } else { + self + .cli_options + .resolve_ts_config_for_emit(TsConfigType::Check { lib: options.lib })? + }; if options.log_ignored_options { check_warn_tsconfig(&ts_config_result); } @@ -259,10 +436,33 @@ impl TypeChecker { let mut diagnostics = response.diagnostics.filter(|d| { if self.is_remote_diagnostic(d) { - type_check_mode == TypeCheckMode::All && d.include_when_remote() - } else { - true + return type_check_mode == TypeCheckMode::All + && d.include_when_remote() + && self + .cli_options + .scope_options + .as_ref() + .map(|o| o.scope.is_none()) + .unwrap_or(true); } + let Some(scope_options) = &self.cli_options.scope_options else { + return true; + }; + let Some(specifier) = d + .file_name + .as_ref() + .and_then(|s| ModuleSpecifier::parse(s).ok()) + else { + return true; + }; + if specifier.scheme() != "file" { + return true; + } + let scope = scope_options + .all_scopes + .iter() + .rfind(|s| specifier.as_str().starts_with(s.as_str())); + scope == scope_options.scope.as_ref() }); diagnostics.apply_fast_check_source_maps(&graph); diff --git a/tests/specs/check/check_workspace/__test__.jsonc b/tests/specs/check/check_workspace/__test__.jsonc new file mode 100644 index 0000000000..08f50df7db --- /dev/null +++ b/tests/specs/check/check_workspace/__test__.jsonc @@ -0,0 +1,14 @@ +{ + "tests": { + "discover": { + "args": "check --quiet main.ts member/mod.ts", + "output": "check_discover.out", + "exitCode": 1 + }, + "config_flag": { + "args": "check --quiet --config deno.json main.ts member/mod.ts", + "output": "check_config_flag.out", + "exitCode": 1 + } + } +} diff --git a/tests/specs/check/check_workspace/check_config_flag.out b/tests/specs/check/check_workspace/check_config_flag.out new file mode 100644 index 0000000000..519f60b88b --- /dev/null +++ b/tests/specs/check/check_workspace/check_config_flag.out @@ -0,0 +1,11 @@ +error: TS2304 [ERROR]: Cannot find name 'onmessage'. +onmessage; +~~~~~~~~~ + at file:///home/nayeem/projects/deno/tests/specs/check/check_workspace/main.ts:8:1 + +TS2304 [ERROR]: Cannot find name 'onmessage'. +onmessage; +~~~~~~~~~ + at file:///home/nayeem/projects/deno/tests/specs/check/check_workspace/member/mod.ts:5:1 + +Found 2 errors. diff --git a/tests/specs/check/check_workspace/check_discover.out b/tests/specs/check/check_workspace/check_discover.out new file mode 100644 index 0000000000..14c733c9c0 --- /dev/null +++ b/tests/specs/check/check_workspace/check_discover.out @@ -0,0 +1,9 @@ +error: TS2304 [ERROR]: Cannot find name 'onmessage'. +onmessage; +~~~~~~~~~ + at file:///[WILDCARD]/main.ts:8:1 + +TS2304 [ERROR]: Cannot find name 'localStorage'. +localStorage; +~~~~~~~~~~~~ + at file:///[WILDCARD]/member/mod.ts:2:1 diff --git a/tests/specs/check/check_workspace/deno.json b/tests/specs/check/check_workspace/deno.json new file mode 100644 index 0000000000..c914638468 --- /dev/null +++ b/tests/specs/check/check_workspace/deno.json @@ -0,0 +1,3 @@ +{ + "workspace": ["member"] +} diff --git a/tests/specs/check/check_workspace/main.ts b/tests/specs/check/check_workspace/main.ts new file mode 100644 index 0000000000..713e64febe --- /dev/null +++ b/tests/specs/check/check_workspace/main.ts @@ -0,0 +1,8 @@ +// We shouldn't get diagnostics from this import under this check scope. +import "./member/mod.ts"; + +// Only defined for window. +localStorage; + +// Only defined for worker. +onmessage; diff --git a/tests/specs/check/check_workspace/member/deno.json b/tests/specs/check/check_workspace/member/deno.json new file mode 100644 index 0000000000..00feda1e44 --- /dev/null +++ b/tests/specs/check/check_workspace/member/deno.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "lib": ["deno.worker"] + } +} diff --git a/tests/specs/check/check_workspace/member/mod.ts b/tests/specs/check/check_workspace/member/mod.ts new file mode 100644 index 0000000000..846c13a74a --- /dev/null +++ b/tests/specs/check/check_workspace/member/mod.ts @@ -0,0 +1,5 @@ +// Only defined for window. +localStorage; + +// Only defined for worker. +onmessage;