1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2025-01-11 16:42:21 -05:00

feat: auto-discover config file (#13313)

This commit is contained in:
Ryan Dahl 2022-01-17 20:10:17 -05:00 committed by GitHub
parent b10563cb20
commit 39ea4abff4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 319 additions and 131 deletions

View file

@ -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"

View file

@ -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<Option<Vec<(ModuleSpecifier, Vec<String>)>>, 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<Option<ConfigFile>, 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<PathBuf>,
) -> Result<Option<ConfigFile>, 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::<std::io::Error>() {
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"));
}
}

View file

@ -361,6 +361,28 @@ impl Flags {
args
}
/// Extract path arguments for config search paths.
pub fn config_path_args(&self) -> Vec<PathBuf> {
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<Flags> 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")]
);
}
}

View file

@ -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")));

View file

@ -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 {

View file

@ -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<Arc<ImportMap>> =
match flags.import_map_path.as_ref() {

View file

@ -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",
});

View file

@ -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::<Value>().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::<Value>().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::<Value>().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::<Value>().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::<Value>().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::<Value>().unwrap();
assert_eq!(method, "textDocument/publishDiagnostics");
@ -2641,7 +2693,7 @@ fn lsp_code_actions_deadlock() {
let (id, method, _) = client.read_request::<Value>().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::<Value>().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::<Value>().unwrap();
assert_eq!(method, "workspace/configuration");
client
.write_response(id, json!({ "enable": false }))
.unwrap();
let (method, _) = client.read_notification::<Value>().unwrap();
assert_eq!(method, "textDocument/publishDiagnostics");
let (method, _) = client.read_notification::<Value>().unwrap();
assert_eq!(method, "textDocument/publishDiagnostics");
let (method, maybe_params) = client.read_notification::<Value>().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(

View file

@ -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"
}
}
}

View file

@ -0,0 +1,19 @@
{
"fmt": {
"files": {
"include": [
"./subdir/"
],
"exclude": [
"./subdir/b.ts"
]
},
"options": {
"useTabs": true,
"lineWidth": 40,
"indentWidth": 8,
"singleQuote": true,
"proseWrap": "always"
}
}
}

View file

@ -0,0 +1 @@
not a json file

View file

@ -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")));
}