diff --git a/.dprint.json b/.dprint.json index 73204979ae..f870ab7727 100644 --- a/.dprint.json +++ b/.dprint.json @@ -21,19 +21,20 @@ "cli/dts/lib.scripthost.d.ts", "cli/dts/lib.webworker*.d.ts", "cli/dts/typescript.d.ts", - "cli/tests/testdata/encoding", - "cli/tests/testdata/inline_js_source_map*", - "cli/tests/testdata/badly_formatted.md", "cli/tests/testdata/badly_formatted.json", + "cli/tests/testdata/badly_formatted.md", "cli/tests/testdata/byte_order_mark.ts", + "cli/tests/testdata/encoding", "cli/tests/testdata/fmt/*", "cli/tests/testdata/import_assertions/json_with_shebang.json", + "cli/tests/testdata/inline_js_source_map*", + "cli/tests/testdata/malformed_config/*", "cli/tests/testdata/test/markdown_windows.md", "cli/tsc/*typescript.js", - "test_util/std", - "test_util/wpt", "gh-pages", "target", + "test_util/std", + "test_util/wpt", "third_party", "tools/wpt/expectation.json", "tools/wpt/manifest.json" diff --git a/cli/config_file.rs b/cli/config_file.rs index f18e56fc1b..3ca00b0f4c 100644 --- a/cli/config_file.rs +++ b/cli/config_file.rs @@ -18,8 +18,10 @@ use deno_core::serde_json::Value; use deno_core::ModuleSpecifier; use std::collections::BTreeMap; use std::collections::HashMap; +use std::collections::HashSet; use std::fmt; use std::path::Path; +use std::path::PathBuf; pub(crate) type MaybeImportsResult = Result)>>, AnyError>; @@ -156,6 +158,61 @@ pub const IGNORED_RUNTIME_COMPILER_OPTIONS: &[&str] = &[ "watch", ]; +/// Filenames that Deno will recognize when discovering config. +const CONFIG_FILE_NAMES: [&str; 2] = ["deno.json", "deno.jsonc"]; + +pub fn discover(flags: &crate::Flags) -> Result, AnyError> { + if let Some(config_path) = flags.config_path.as_ref() { + Ok(Some(ConfigFile::read(config_path)?)) + } else { + let mut checked = HashSet::new(); + for f in flags.config_path_args() { + if let Some(cf) = discover_from(&f, &mut checked)? { + return Ok(Some(cf)); + } + } + + // From CWD walk up to root looking for deno.json or deno.jsonc + let cwd = std::env::current_dir()?; + discover_from(&cwd, &mut checked) + } +} + +pub fn discover_from( + start: &Path, + checked: &mut HashSet, +) -> Result, AnyError> { + for ancestor in start.ancestors() { + if checked.insert(ancestor.to_path_buf()) { + for config_filename in CONFIG_FILE_NAMES { + let f = ancestor.join(config_filename); + match ConfigFile::read(f) { + Ok(cf) => { + return Ok(Some(cf)); + } + Err(e) => { + if let Some(ioerr) = e.downcast_ref::() { + use std::io::ErrorKind::*; + match ioerr.kind() { + InvalidInput | PermissionDenied | NotFound => { + // ok keep going + } + _ => { + return Err(e); // Unknown error. Stop. + } + } + } else { + return Err(e); // Parse error or something else. Stop. + } + } + } + } + } + } + // No config file found. + Ok(None) +} + /// A function that works like JavaScript's `Object.assign()`. pub fn json_merge(a: &mut Value, b: &Value) { match (a, b) { @@ -823,4 +880,38 @@ mod tests { })); assert_eq!(tsconfig1.as_bytes(), tsconfig2.as_bytes()); } + + #[test] + fn discover_from_success() { + // testdata/fmt/deno.jsonc exists + let testdata = test_util::testdata_path(); + let c_md = testdata.join("fmt/with_config/subdir/c.md"); + let mut checked = HashSet::new(); + let config_file = discover_from(&c_md, &mut checked).unwrap().unwrap(); + assert!(checked.contains(c_md.parent().unwrap())); + assert!(!checked.contains(&testdata)); + let fmt_config = config_file.to_fmt_config().unwrap().unwrap(); + let expected_exclude = ModuleSpecifier::from_file_path( + testdata.join("fmt/with_config/subdir/b.ts"), + ) + .unwrap(); + assert_eq!(fmt_config.files.exclude, vec![expected_exclude]); + + // Now add all ancestors of testdata to checked. + for a in testdata.ancestors() { + checked.insert(a.to_path_buf()); + } + + // If we call discover_from again starting at testdata, we ought to get None. + assert!(discover_from(&testdata, &mut checked).unwrap().is_none()); + } + + #[test] + fn discover_from_malformed() { + let testdata = test_util::testdata_path(); + let d = testdata.join("malformed_config/"); + let mut checked = HashSet::new(); + let err = discover_from(&d, &mut checked).unwrap_err(); + assert!(err.to_string().contains("Unable to parse config file")); + } } diff --git a/cli/flags.rs b/cli/flags.rs index 854b54416b..8b308658da 100644 --- a/cli/flags.rs +++ b/cli/flags.rs @@ -361,6 +361,28 @@ impl Flags { args } + + /// Extract path arguments for config search paths. + pub fn config_path_args(&self) -> Vec { + use DenoSubcommand::*; + if let Fmt(FmtFlags { files, .. }) = &self.subcommand { + files.clone() + } else if let Lint(LintFlags { files, .. }) = &self.subcommand { + files.clone() + } else if let Run(RunFlags { script }) = &self.subcommand { + if let Ok(module_specifier) = deno_core::resolve_url_or_path(script) { + if let Ok(p) = module_specifier.to_file_path() { + vec![p] + } else { + vec![] + } + } else { + vec![] + } + } else { + vec![] + } + } } impl From for PermissionsOptions { @@ -4628,4 +4650,30 @@ mod tests { } ); } + + #[test] + fn test_config_path_args() { + let flags = flags_from_vec(svec!["deno", "run", "foo.js"]).unwrap(); + assert_eq!( + flags.config_path_args(), + vec![std::env::current_dir().unwrap().join("foo.js")] + ); + + let flags = + flags_from_vec(svec!["deno", "lint", "dir/a.js", "dir/b.js"]).unwrap(); + assert_eq!( + flags.config_path_args(), + vec![PathBuf::from("dir/a.js"), PathBuf::from("dir/b.js")] + ); + + let flags = flags_from_vec(svec!["deno", "lint"]).unwrap(); + assert!(flags.config_path_args().is_empty()); + + let flags = + flags_from_vec(svec!["deno", "fmt", "dir/a.js", "dir/b.js"]).unwrap(); + assert_eq!( + flags.config_path_args(), + vec![PathBuf::from("dir/a.js"), PathBuf::from("dir/b.js")] + ); + } } diff --git a/cli/fs_util.rs b/cli/fs_util.rs index 7290d36968..fbdcdc81ae 100644 --- a/cli/fs_util.rs +++ b/cli/fs_util.rs @@ -139,34 +139,6 @@ pub fn is_supported_ext(path: &Path) -> bool { } } -/// This function is similar to is_supported_ext but adds additional extensions -/// supported by `deno fmt`. -pub fn is_supported_ext_fmt(path: &Path) -> bool { - if let Some(ext) = get_extension(path) { - matches!( - ext.as_str(), - "ts" - | "tsx" - | "js" - | "jsx" - | "mjs" - | "mts" - | "cjs" - | "cts" - | "json" - | "jsonc" - | "md" - | "mkd" - | "mkdn" - | "mdwn" - | "mdown" - | "markdown" - ) - } else { - false - } -} - /// Checks if the path has a basename and extension Deno supports for tests. pub fn is_supported_test_path(path: &Path) -> bool { if let Some(name) = path.file_stem() { @@ -458,46 +430,11 @@ mod tests { assert!(!is_supported_ext(Path::new("foo.mjsx"))); } - #[test] - fn test_is_supported_ext_fmt() { - assert!(!is_supported_ext_fmt(Path::new("tests/subdir/redirects"))); - assert!(is_supported_ext_fmt(Path::new("README.md"))); - assert!(is_supported_ext_fmt(Path::new("readme.MD"))); - assert!(is_supported_ext_fmt(Path::new("readme.mkd"))); - assert!(is_supported_ext_fmt(Path::new("readme.mkdn"))); - assert!(is_supported_ext_fmt(Path::new("readme.mdwn"))); - assert!(is_supported_ext_fmt(Path::new("readme.mdown"))); - assert!(is_supported_ext_fmt(Path::new("readme.markdown"))); - assert!(is_supported_ext_fmt(Path::new("lib/typescript.d.ts"))); - assert!(is_supported_ext_fmt(Path::new("testdata/001_hello.js"))); - assert!(is_supported_ext_fmt(Path::new("testdata/002_hello.ts"))); - assert!(is_supported_ext_fmt(Path::new("foo.jsx"))); - assert!(is_supported_ext_fmt(Path::new("foo.tsx"))); - assert!(is_supported_ext_fmt(Path::new("foo.TS"))); - assert!(is_supported_ext_fmt(Path::new("foo.TSX"))); - assert!(is_supported_ext_fmt(Path::new("foo.JS"))); - assert!(is_supported_ext_fmt(Path::new("foo.JSX"))); - assert!(is_supported_ext_fmt(Path::new("foo.mjs"))); - assert!(is_supported_ext_fmt(Path::new("foo.mts"))); - assert!(is_supported_ext_fmt(Path::new("foo.cjs"))); - assert!(is_supported_ext_fmt(Path::new("foo.cts"))); - assert!(!is_supported_ext_fmt(Path::new("foo.mjsx"))); - assert!(is_supported_ext_fmt(Path::new("foo.jsonc"))); - assert!(is_supported_ext_fmt(Path::new("foo.JSONC"))); - assert!(is_supported_ext_fmt(Path::new("foo.json"))); - assert!(is_supported_ext_fmt(Path::new("foo.JsON"))); - } - #[test] fn test_is_supported_test_ext() { assert!(!is_supported_test_ext(Path::new("tests/subdir/redirects"))); assert!(is_supported_test_ext(Path::new("README.md"))); assert!(is_supported_test_ext(Path::new("readme.MD"))); - assert!(is_supported_ext_fmt(Path::new("readme.mkd"))); - assert!(is_supported_ext_fmt(Path::new("readme.mkdn"))); - assert!(is_supported_ext_fmt(Path::new("readme.mdwn"))); - assert!(is_supported_ext_fmt(Path::new("readme.mdown"))); - assert!(is_supported_ext_fmt(Path::new("readme.markdown"))); assert!(is_supported_test_ext(Path::new("lib/typescript.d.ts"))); assert!(is_supported_test_ext(Path::new("testdata/001_hello.js"))); assert!(is_supported_test_ext(Path::new("testdata/002_hello.ts"))); diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index 5b04b8a251..fc0ae1312d 100644 --- a/cli/lsp/language_server.rs +++ b/cli/lsp/language_server.rs @@ -320,7 +320,23 @@ impl Inner { } } - Ok(None) + // Auto-discover config + + // It is possible that root_uri is not set, for example when having a single + // file open and not a workspace. In those situations we can't + // automatically discover the configuration + if let Some(root_uri) = maybe_root_uri { + let root_path = root_uri.to_file_path().unwrap(); + let mut checked = std::collections::HashSet::new(); + let maybe_config = + crate::config_file::discover_from(&root_path, &mut checked)?; + Ok(maybe_config.map(|c| { + lsp_log!(" Auto-resolved configuration file: \"{}\"", c.specifier); + c + })) + } else { + Ok(None) + } } fn is_diagnosable(&self, specifier: &ModuleSpecifier) -> bool { diff --git a/cli/proc_state.rs b/cli/proc_state.rs index 6e8507810b..13a17b8fbd 100644 --- a/cli/proc_state.rs +++ b/cli/proc_state.rs @@ -207,12 +207,7 @@ impl ProcState { None }; - let maybe_config_file = - if let Some(config_path) = flags.config_path.as_ref() { - Some(ConfigFile::read(config_path)?) - } else { - None - }; + let maybe_config_file = crate::config_file::discover(&flags)?; let maybe_import_map: Option> = match flags.import_map_path.as_ref() { diff --git a/cli/tests/integration/fmt_tests.rs b/cli/tests/integration/fmt_tests.rs index 18743fb006..bb4c8b4510 100644 --- a/cli/tests/integration/fmt_tests.rs +++ b/cli/tests/integration/fmt_tests.rs @@ -177,13 +177,18 @@ itest!(fmt_stdin_check_not_formatted { }); itest!(fmt_with_config { - args: "fmt --config fmt/deno.jsonc fmt/fmt_with_config/", + args: "fmt --config fmt/with_config/deno.jsonc fmt/with_config/subdir", + output: "fmt/fmt_with_config.out", +}); + +itest!(fmt_with_config_default { + args: "fmt fmt/with_config/subdir", output: "fmt/fmt_with_config.out", }); // Check if CLI flags take precedence itest!(fmt_with_config_and_flags { - args: "fmt --config fmt/deno.jsonc --ignore=fmt/fmt_with_config/a.ts,fmt/fmt_with_config/b.ts", + args: "fmt --config fmt/with_config/deno.jsonc --ignore=fmt/with_config/subdir/a.ts,fmt/with_config/subdir/b.ts", output: "fmt/fmt_with_config_and_flags.out", }); diff --git a/cli/tests/integration/lsp_tests.rs b/cli/tests/integration/lsp_tests.rs index 21894f7e82..c992ca4cb3 100644 --- a/cli/tests/integration/lsp_tests.rs +++ b/cli/tests/integration/lsp_tests.rs @@ -55,12 +55,12 @@ where client .write_response( id, - json!({ + json!([{ "enable": true, "codeLens": { "test": true } - }), + }]), ) .unwrap(); @@ -564,7 +564,7 @@ fn lsp_hover_disabled() { let (id, method, _) = client.read_request::().unwrap(); assert_eq!(method, "workspace/configuration"); client - .write_response(id, json!({ "enable": false })) + .write_response(id, json!([{ "enable": false }])) .unwrap(); let (maybe_res, maybe_err) = client @@ -814,7 +814,7 @@ fn lsp_hover_closed_document() { let (id, method, _) = client.read_request::().unwrap(); assert_eq!(method, "workspace/configuration"); client - .write_response(id, json!({ "enable": true })) + .write_response(id, json!([{ "enable": true }])) .unwrap(); client @@ -833,7 +833,7 @@ fn lsp_hover_closed_document() { let (id, method, _) = client.read_request::().unwrap(); assert_eq!(method, "workspace/configuration"); client - .write_response(id, json!({ "enable": true })) + .write_response(id, json!([{ "enable": true }])) .unwrap(); let (method, _) = client.read_notification::().unwrap(); @@ -1542,6 +1542,58 @@ fn lsp_format_exclude_with_config() { shutdown(&mut client); } +#[test] +fn lsp_format_exclude_default_config() { + let temp_dir = TempDir::new().unwrap(); + let workspace_root = temp_dir.path().canonicalize().unwrap(); + let mut params: lsp::InitializeParams = + serde_json::from_value(load_fixture("initialize_params.json")).unwrap(); + let deno_jsonc = + serde_json::to_vec_pretty(&load_fixture("deno.fmt.exclude.jsonc")).unwrap(); + fs::write(workspace_root.join("deno.jsonc"), deno_jsonc).unwrap(); + + params.root_uri = Some(Url::from_file_path(workspace_root.clone()).unwrap()); + + let deno_exe = deno_exe_path(); + let mut client = LspClient::new(&deno_exe).unwrap(); + client + .write_request::<_, _, Value>("initialize", params) + .unwrap(); + + let file_uri = + ModuleSpecifier::from_file_path(workspace_root.join("ignored.ts")) + .unwrap() + .to_string(); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": file_uri, + "languageId": "typescript", + "version": 1, + "text": "function myFunc(){}" + } + }), + ); + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/formatting", + json!({ + "textDocument": { + "uri": file_uri + }, + "options": { + "tabSize": 2, + "insertSpaces": true + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!(maybe_res, Some(json!(null))); + shutdown(&mut client); +} + #[test] fn lsp_large_doc_changes() { let mut client = init("initialize_params.json"); @@ -2085,12 +2137,12 @@ fn lsp_code_lens_test_disabled() { client .write_response( id, - json!({ + json!([{ "enable": true, "codeLens": { "test": false } - }), + }]), ) .unwrap(); @@ -2467,7 +2519,7 @@ fn lsp_code_actions_deno_cache() { let (id, method, _) = client.read_request::().unwrap(); assert_eq!(method, "workspace/configuration"); client - .write_response(id, json!({ "enable": true })) + .write_response(id, json!([{ "enable": true }])) .unwrap(); let (method, _) = client.read_notification::().unwrap(); assert_eq!(method, "textDocument/publishDiagnostics"); @@ -2641,7 +2693,7 @@ fn lsp_code_actions_deadlock() { let (id, method, _) = client.read_request::().unwrap(); assert_eq!(method, "workspace/configuration"); client - .write_response(id, json!({ "enable": true })) + .write_response(id, json!([{ "enable": true }])) .unwrap(); let (maybe_res, maybe_err) = client .write_request::<_, _, Value>( @@ -3301,7 +3353,7 @@ fn lsp_diagnostics_deno_types() { let (id, method, _) = client.read_request::().unwrap(); assert_eq!(method, "workspace/configuration"); client - .write_response(id, json!({ "enable": true })) + .write_response(id, json!([{ "enable": true }])) .unwrap(); let (maybe_res, maybe_err) = client .write_request::<_, _, Value>( @@ -3354,34 +3406,20 @@ fn lsp_diagnostics_refresh_dependents() { }, }), ); - client - .write_notification( - "textDocument/didOpen", - json!({ - "textDocument": { - "uri": "file:///a/file_02.ts", - "languageId": "typescript", - "version": 1, - "text": "import { a, b } from \"./file_01.ts\";\n\nconsole.log(a, b);\n" - } - }), - ) - .unwrap(); - - let (id, method, _) = client.read_request::().unwrap(); - assert_eq!(method, "workspace/configuration"); - client - .write_response(id, json!({ "enable": false })) - .unwrap(); - let (method, _) = client.read_notification::().unwrap(); - assert_eq!(method, "textDocument/publishDiagnostics"); - let (method, _) = client.read_notification::().unwrap(); - assert_eq!(method, "textDocument/publishDiagnostics"); - let (method, maybe_params) = client.read_notification::().unwrap(); - assert_eq!(method, "textDocument/publishDiagnostics"); + let diagnostics = did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file_02.ts", + "languageId": "typescript", + "version": 1, + "text": "import { a, b } from \"./file_01.ts\";\n\nconsole.log(a, b);\n" + } + }), + ); assert_eq!( - maybe_params, - Some(json!({ + json!(diagnostics[2]), + json!({ "uri": "file:///a/file_02.ts", "diagnostics": [ { @@ -3402,7 +3440,7 @@ fn lsp_diagnostics_refresh_dependents() { } ], "version": 1 - })) + }) ); client .write_notification( diff --git a/cli/tests/testdata/fmt/deno.jsonc b/cli/tests/testdata/fmt/deno.jsonc deleted file mode 100644 index 9c330d34a8..0000000000 --- a/cli/tests/testdata/fmt/deno.jsonc +++ /dev/null @@ -1,15 +0,0 @@ -{ - "fmt": { - "files": { - "include": ["fmt_with_config/"], - "exclude": ["fmt_with_config/b.ts"] - }, - "options": { - "useTabs": true, - "lineWidth": 40, - "indentWidth": 8, - "singleQuote": true, - "proseWrap": "always" - } - } -} diff --git a/cli/tests/testdata/fmt/with_config/deno.jsonc b/cli/tests/testdata/fmt/with_config/deno.jsonc new file mode 100644 index 0000000000..3b9474e644 --- /dev/null +++ b/cli/tests/testdata/fmt/with_config/deno.jsonc @@ -0,0 +1,19 @@ +{ + "fmt": { + "files": { + "include": [ + "./subdir/" + ], + "exclude": [ + "./subdir/b.ts" + ] + }, + "options": { + "useTabs": true, + "lineWidth": 40, + "indentWidth": 8, + "singleQuote": true, + "proseWrap": "always" + } + } +} diff --git a/cli/tests/testdata/fmt/fmt_with_config/a.ts b/cli/tests/testdata/fmt/with_config/subdir/a.ts similarity index 100% rename from cli/tests/testdata/fmt/fmt_with_config/a.ts rename to cli/tests/testdata/fmt/with_config/subdir/a.ts diff --git a/cli/tests/testdata/fmt/fmt_with_config/b.ts b/cli/tests/testdata/fmt/with_config/subdir/b.ts similarity index 100% rename from cli/tests/testdata/fmt/fmt_with_config/b.ts rename to cli/tests/testdata/fmt/with_config/subdir/b.ts diff --git a/cli/tests/testdata/fmt/fmt_with_config/c.md b/cli/tests/testdata/fmt/with_config/subdir/c.md similarity index 100% rename from cli/tests/testdata/fmt/fmt_with_config/c.md rename to cli/tests/testdata/fmt/with_config/subdir/c.md diff --git a/cli/tests/testdata/malformed_config/deno.json b/cli/tests/testdata/malformed_config/deno.json new file mode 100644 index 0000000000..60df565273 --- /dev/null +++ b/cli/tests/testdata/malformed_config/deno.json @@ -0,0 +1 @@ +not a json file diff --git a/cli/tools/fmt.rs b/cli/tools/fmt.rs index 1d48e70255..3044f13d5b 100644 --- a/cli/tools/fmt.rs +++ b/cli/tools/fmt.rs @@ -16,7 +16,7 @@ use crate::file_watcher; use crate::file_watcher::ResolutionResult; use crate::flags::FmtFlags; use crate::fs_util::specifier_to_file_path; -use crate::fs_util::{collect_files, get_extension, is_supported_ext_fmt}; +use crate::fs_util::{collect_files, get_extension}; use crate::text_encoding; use deno_ast::ParsedSource; use deno_core::anyhow::bail; @@ -591,3 +591,55 @@ where Ok(()) } } + +/// This function is similar to is_supported_ext but adds additional extensions +/// supported by `deno fmt`. +fn is_supported_ext_fmt(path: &Path) -> bool { + if let Some(ext) = get_extension(path) { + matches!( + ext.as_str(), + "ts" + | "tsx" + | "js" + | "jsx" + | "mjs" + | "json" + | "jsonc" + | "md" + | "mkd" + | "mkdn" + | "mdwn" + | "mdown" + | "markdown" + ) + } else { + false + } +} + +#[test] +fn test_is_supported_ext_fmt() { + assert!(!is_supported_ext_fmt(Path::new("tests/subdir/redirects"))); + assert!(is_supported_ext_fmt(Path::new("README.md"))); + assert!(is_supported_ext_fmt(Path::new("readme.MD"))); + assert!(is_supported_ext_fmt(Path::new("readme.mkd"))); + assert!(is_supported_ext_fmt(Path::new("readme.mkdn"))); + assert!(is_supported_ext_fmt(Path::new("readme.mdwn"))); + assert!(is_supported_ext_fmt(Path::new("readme.mdown"))); + assert!(is_supported_ext_fmt(Path::new("readme.markdown"))); + assert!(is_supported_ext_fmt(Path::new("lib/typescript.d.ts"))); + assert!(is_supported_ext_fmt(Path::new("testdata/001_hello.js"))); + assert!(is_supported_ext_fmt(Path::new("testdata/002_hello.ts"))); + assert!(is_supported_ext_fmt(Path::new("foo.jsx"))); + assert!(is_supported_ext_fmt(Path::new("foo.tsx"))); + assert!(is_supported_ext_fmt(Path::new("foo.TS"))); + assert!(is_supported_ext_fmt(Path::new("foo.TSX"))); + assert!(is_supported_ext_fmt(Path::new("foo.JS"))); + assert!(is_supported_ext_fmt(Path::new("foo.JSX"))); + assert!(is_supported_ext_fmt(Path::new("foo.mjs"))); + assert!(!is_supported_ext_fmt(Path::new("foo.mjsx"))); + assert!(is_supported_ext_fmt(Path::new("foo.jsonc"))); + assert!(is_supported_ext_fmt(Path::new("foo.JSONC"))); + assert!(is_supported_ext_fmt(Path::new("foo.json"))); + assert!(is_supported_ext_fmt(Path::new("foo.JsON"))); +}