summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBartek IwaƄczuk <biwanczuk@gmail.com>2021-09-13 20:19:10 +0200
committerGitHub <noreply@github.com>2021-09-13 20:19:10 +0200
commit0dbeb774ba9ea618ff1e92b63ab31e5caf3003dd (patch)
tree592244102cf046716acbb144233586c312ee7a7e
parenta655a0f3e4201840eda94938fc8d6222c2b94a99 (diff)
feat(fmt): add support for configuration file (#11944)
This commit adds support for configuration file for "deno fmt" subcommand. It is also respected by LSP when formatting files. Example configuration: { "fmt": { "files": { "include": ["src/"], "exclude": ["src/testdata/"] }, "options": { "useTabs": true, "lineWidth": 80, "indentWidth": 4, "singleQuote": true, "textWrap": "preserve" } } }
-rw-r--r--.dprint.json1
-rw-r--r--cli/config_file.rs64
-rw-r--r--cli/flags.rs40
-rw-r--r--cli/lsp/language_server.rs104
-rw-r--r--cli/main.rs15
-rw-r--r--cli/tests/integration/fmt_tests.rs31
-rw-r--r--cli/tests/integration/lsp_tests.rs163
-rw-r--r--cli/tests/testdata/fmt/deno.jsonc15
-rw-r--r--cli/tests/testdata/fmt/deno.malformed.jsonc12
-rw-r--r--cli/tests/testdata/fmt/deno.malformed2.jsonc12
-rw-r--r--cli/tests/testdata/fmt/fmt_with_config.out1
-rw-r--r--cli/tests/testdata/fmt/fmt_with_config/a.ts46
-rw-r--r--cli/tests/testdata/fmt/fmt_with_config/b.ts15
-rw-r--r--cli/tests/testdata/fmt/fmt_with_config/c.md17
-rw-r--r--cli/tests/testdata/fmt/fmt_with_config_and_flags.out1
-rw-r--r--cli/tests/testdata/fmt/fmt_with_malformed_config.out4
-rw-r--r--cli/tests/testdata/fmt/fmt_with_malformed_config2.out4
-rw-r--r--cli/tests/testdata/fmt/regular/formatted1.js (renamed from cli/tests/testdata/fmt/formatted1.js)0
-rw-r--r--cli/tests/testdata/fmt/regular/formatted2.ts (renamed from cli/tests/testdata/fmt/formatted2.ts)0
-rw-r--r--cli/tests/testdata/fmt/regular/formatted3.md (renamed from cli/tests/testdata/fmt/formatted3.md)0
-rw-r--r--cli/tests/testdata/fmt/regular/formatted4.jsonc (renamed from cli/tests/testdata/fmt/formatted4.jsonc)0
-rw-r--r--cli/tests/testdata/lsp/deno.fmt.jsonc11
-rw-r--r--cli/tools/fmt.rs223
23 files changed, 687 insertions, 92 deletions
diff --git a/.dprint.json b/.dprint.json
index fcba86c7b..56397dfd9 100644
--- a/.dprint.json
+++ b/.dprint.json
@@ -24,6 +24,7 @@
"cli/tests/testdata/badly_formatted.md",
"cli/tests/testdata/badly_formatted.json",
"cli/tests/testdata/byte_order_mark.ts",
+ "cli/tests/testdata/fmt/*",
"cli/tsc/*typescript.js",
"test_util/std",
"test_util/wpt",
diff --git a/cli/config_file.rs b/cli/config_file.rs
index 94373334a..dd002ca97 100644
--- a/cli/config_file.rs
+++ b/cli/config_file.rs
@@ -277,7 +277,7 @@ pub struct LintRulesConfig {
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(default, deny_unknown_fields)]
-pub struct LintFilesConfig {
+pub struct FilesConfig {
pub include: Vec<String>,
pub exclude: Vec<String>,
}
@@ -286,7 +286,32 @@ pub struct LintFilesConfig {
#[serde(default, deny_unknown_fields)]
pub struct LintConfig {
pub rules: LintRulesConfig,
- pub files: LintFilesConfig,
+ pub files: FilesConfig,
+}
+
+#[derive(Clone, Copy, Debug, Deserialize)]
+#[serde(deny_unknown_fields, rename_all = "camelCase")]
+pub enum ProseWrap {
+ Always,
+ Never,
+ Preserve,
+}
+
+#[derive(Clone, Debug, Default, Deserialize)]
+#[serde(default, deny_unknown_fields, rename_all = "camelCase")]
+pub struct FmtOptionsConfig {
+ pub use_tabs: Option<bool>,
+ pub line_width: Option<u32>,
+ pub indent_width: Option<u8>,
+ pub single_quote: Option<bool>,
+ pub prose_wrap: Option<ProseWrap>,
+}
+
+#[derive(Clone, Debug, Default, Deserialize)]
+#[serde(default, deny_unknown_fields)]
+pub struct FmtConfig {
+ pub options: FmtOptionsConfig,
+ pub files: FilesConfig,
}
#[derive(Clone, Debug, Deserialize)]
@@ -294,6 +319,7 @@ pub struct LintConfig {
pub struct ConfigFileJson {
pub compiler_options: Option<Value>,
pub lint: Option<Value>,
+ pub fmt: Option<Value>,
}
#[derive(Clone, Debug)]
@@ -374,6 +400,16 @@ impl ConfigFile {
Ok(None)
}
}
+
+ pub fn to_fmt_config(&self) -> Result<Option<FmtConfig>, AnyError> {
+ if let Some(config) = self.json.fmt.clone() {
+ let fmt_config: FmtConfig = serde_json::from_value(config)
+ .context("Failed to parse \"fmt\" configuration")?;
+ Ok(Some(fmt_config))
+ } else {
+ Ok(None)
+ }
+ }
}
#[cfg(test)]
@@ -441,6 +477,19 @@ mod tests {
"tags": ["recommended"],
"include": ["ban-untagged-todo"]
}
+ },
+ "fmt": {
+ "files": {
+ "include": ["src/"],
+ "exclude": ["src/testdata/"]
+ },
+ "options": {
+ "useTabs": true,
+ "lineWidth": 80,
+ "indentWidth": 4,
+ "singleQuote": true,
+ "proseWrap": "preserve"
+ }
}
}"#;
let config_path = PathBuf::from("/deno/tsconfig.json");
@@ -474,6 +523,17 @@ mod tests {
Some(vec!["recommended".to_string()])
);
assert!(lint_config.rules.exclude.is_none());
+
+ let fmt_config = config_file
+ .to_fmt_config()
+ .expect("error parsing fmt object")
+ .expect("fmt object should be defined");
+ assert_eq!(fmt_config.files.include, vec!["src/"]);
+ assert_eq!(fmt_config.files.exclude, vec!["src/testdata/"]);
+ assert_eq!(fmt_config.options.use_tabs, Some(true));
+ assert_eq!(fmt_config.options.line_width, Some(80));
+ assert_eq!(fmt_config.options.indent_width, Some(4));
+ assert_eq!(fmt_config.options.single_quote, Some(true));
}
#[test]
diff --git a/cli/flags.rs b/cli/flags.rs
index b35b7bad7..eb7d0901f 100644
--- a/cli/flags.rs
+++ b/cli/flags.rs
@@ -815,6 +815,7 @@ Ignore formatting a file by adding an ignore comment at the top of the file:
// deno-fmt-ignore-file",
)
+ .arg(config_arg())
.arg(
Arg::with_name("check")
.long("check")
@@ -1732,6 +1733,7 @@ fn eval_parse(flags: &mut Flags, matches: &clap::ArgMatches) {
}
fn fmt_parse(flags: &mut Flags, matches: &clap::ArgMatches) {
+ config_arg_parse(flags, matches);
flags.watch = matches.is_present("watch");
let files = match matches.values_of("files") {
Some(f) => f.map(PathBuf::from).collect(),
@@ -2534,6 +2536,44 @@ mod tests {
..Flags::default()
}
);
+
+ let r = flags_from_vec(svec!["deno", "fmt", "--config", "deno.jsonc"]);
+ assert_eq!(
+ r.unwrap(),
+ Flags {
+ subcommand: DenoSubcommand::Fmt(FmtFlags {
+ ignore: vec![],
+ check: false,
+ files: vec![],
+ ext: "ts".to_string()
+ }),
+ config_path: Some("deno.jsonc".to_string()),
+ ..Flags::default()
+ }
+ );
+
+ let r = flags_from_vec(svec![
+ "deno",
+ "fmt",
+ "--config",
+ "deno.jsonc",
+ "--watch",
+ "foo.ts"
+ ]);
+ assert_eq!(
+ r.unwrap(),
+ Flags {
+ subcommand: DenoSubcommand::Fmt(FmtFlags {
+ ignore: vec![],
+ check: false,
+ files: vec![PathBuf::from("foo.ts")],
+ ext: "ts".to_string()
+ }),
+ config_path: Some("deno.jsonc".to_string()),
+ watch: true,
+ ..Flags::default()
+ }
+ );
}
#[test]
diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs
index 3f1bfc923..87fc5f7e5 100644
--- a/cli/lsp/language_server.rs
+++ b/cli/lsp/language_server.rs
@@ -336,15 +336,15 @@ impl Inner {
Ok(navigation_tree)
}
- fn merge_user_tsconfig(
- &mut self,
- maybe_config: &Option<String>,
- maybe_root_uri: &Option<Url>,
- tsconfig: &mut TsConfig,
- ) -> Result<(), AnyError> {
- self.maybe_config_file = None;
- self.maybe_config_uri = None;
- if let Some(config_str) = maybe_config {
+ /// Returns a tuple with parsed `ConfigFile` and `Url` pointing to that file.
+ /// If there's no config file specified in settings returns `None`.
+ fn get_config_file_and_url(
+ &self,
+ ) -> Result<Option<(ConfigFile, Url)>, AnyError> {
+ let workspace_settings = self.config.get_workspace_settings();
+ let maybe_root_uri = self.config.root_uri.clone();
+ let maybe_config = workspace_settings.config;
+ if let Some(config_str) = &maybe_config {
if !config_str.is_empty() {
info!("Setting TypeScript configuration from: \"{}\"", config_str);
let config_url = if let Ok(url) = Url::from_file_path(config_str) {
@@ -374,18 +374,34 @@ impl Inner {
.ok_or_else(|| anyhow!("Bad uri: \"{}\"", config_url))?;
ConfigFile::read(path)?
};
- let (value, maybe_ignored_options) =
- config_file.to_compiler_options()?;
- tsconfig.merge(&value);
- self.maybe_config_file = Some(config_file);
- self.maybe_config_uri = Some(config_url);
- if let Some(ignored_options) = maybe_ignored_options {
- // TODO(@kitsonk) turn these into diagnostics that can be sent to the
- // client
- warn!("{}", ignored_options);
- }
+ return Ok(Some((config_file, config_url)));
+ }
+ }
+
+ Ok(None)
+ }
+
+ fn merge_user_tsconfig(
+ &mut self,
+ tsconfig: &mut TsConfig,
+ ) -> Result<(), AnyError> {
+ self.maybe_config_file = None;
+ self.maybe_config_uri = None;
+
+ let maybe_file_and_url = self.get_config_file_and_url()?;
+
+ if let Some((config_file, config_url)) = maybe_file_and_url {
+ let (value, maybe_ignored_options) = config_file.to_compiler_options()?;
+ tsconfig.merge(&value);
+ self.maybe_config_file = Some(config_file);
+ self.maybe_config_uri = Some(config_url);
+ if let Some(ignored_options) = maybe_ignored_options {
+ // TODO(@kitsonk) turn these into diagnostics that can be sent to the
+ // client
+ warn!("{}", ignored_options);
}
}
+
Ok(())
}
@@ -575,20 +591,15 @@ impl Inner {
// TODO(@kitsonk) remove for Deno 1.15
"useUnknownInCatchVariables": false,
}));
- let (maybe_config, maybe_root_uri) = {
- let config = &self.config;
- let workspace_settings = config.get_workspace_settings();
- if workspace_settings.unstable {
- let unstable_libs = json!({
- "lib": ["deno.ns", "deno.window", "deno.unstable"]
- });
- tsconfig.merge(&unstable_libs);
- }
- (workspace_settings.config, config.root_uri.clone())
- };
- if let Err(err) =
- self.merge_user_tsconfig(&maybe_config, &maybe_root_uri, &mut tsconfig)
- {
+ let config = &self.config;
+ let workspace_settings = config.get_workspace_settings();
+ if workspace_settings.unstable {
+ let unstable_libs = json!({
+ "lib": ["deno.ns", "deno.window", "deno.unstable"]
+ });
+ tsconfig.merge(&unstable_libs);
+ }
+ if let Err(err) = self.merge_user_tsconfig(&mut tsconfig) {
self.client.show_message(MessageType::Warning, err).await;
}
let _ok: bool = self
@@ -1015,14 +1026,37 @@ impl Inner {
PathBuf::from(params.text_document.uri.path())
};
+ let maybe_file_and_url = self.get_config_file_and_url().map_err(|err| {
+ error!("Unable to parse configuration file: {}", err);
+ LspError::internal_error()
+ })?;
+
+ let fmt_options = if let Some((config_file, _)) = maybe_file_and_url {
+ config_file
+ .to_fmt_config()
+ .map_err(|err| {
+ error!("Unable to parse fmt configuration: {}", err);
+ LspError::internal_error()
+ })?
+ .unwrap_or_default()
+ } else {
+ Default::default()
+ };
+
let source = document_data.source().clone();
let text_edits = tokio::task::spawn_blocking(move || {
let format_result = match source.module() {
- Some(Ok(parsed_module)) => Ok(format_parsed_module(parsed_module)),
+ Some(Ok(parsed_module)) => {
+ Ok(format_parsed_module(parsed_module, fmt_options.options))
+ }
Some(Err(err)) => Err(err.to_string()),
None => {
// it's not a js/ts file, so attempt to format its contents
- format_file(&file_path, source.text_info().text_str())
+ format_file(
+ &file_path,
+ source.text_info().text_str(),
+ fmt_options.options,
+ )
}
};
diff --git a/cli/main.rs b/cli/main.rs
index 896704eec..55de5a61e 100644
--- a/cli/main.rs
+++ b/cli/main.rs
@@ -805,8 +805,20 @@ async fn format_command(
flags: Flags,
fmt_flags: FmtFlags,
) -> Result<(), AnyError> {
+ let program_state = ProgramState::build(flags.clone()).await?;
+ let maybe_fmt_config =
+ if let Some(config_file) = &program_state.maybe_config_file {
+ config_file.to_fmt_config()?
+ } else {
+ None
+ };
+
if fmt_flags.files.len() == 1 && fmt_flags.files[0].to_string_lossy() == "-" {
- return tools::fmt::format_stdin(fmt_flags.check, fmt_flags.ext);
+ return tools::fmt::format_stdin(
+ fmt_flags.check,
+ fmt_flags.ext,
+ maybe_fmt_config.map(|c| c.options).unwrap_or_default(),
+ );
}
tools::fmt::format(
@@ -814,6 +826,7 @@ async fn format_command(
fmt_flags.ignore,
fmt_flags.check,
flags.watch,
+ maybe_fmt_config,
)
.await?;
Ok(())
diff --git a/cli/tests/integration/fmt_tests.rs b/cli/tests/integration/fmt_tests.rs
index 00565a5d0..2d7451694 100644
--- a/cli/tests/integration/fmt_tests.rs
+++ b/cli/tests/integration/fmt_tests.rs
@@ -129,25 +129,25 @@ fn fmt_ignore_unexplicit_files() {
}
itest!(fmt_check_tests_dir {
- args: "fmt --check ./ --ignore=.test_coverage",
+ args: "fmt --check ./ --ignore=.test_coverage,fmt/fmt_with_config/",
output: "fmt/expected_fmt_check_tests_dir.out",
exit_code: 1,
});
itest!(fmt_quiet_check_fmt_dir {
- args: "fmt --check --quiet fmt/",
+ args: "fmt --check --quiet fmt/regular/",
output_str: Some(""),
exit_code: 0,
});
itest!(fmt_check_formatted_files {
- args: "fmt --check fmt/formatted1.js fmt/formatted2.ts fmt/formatted3.md fmt/formatted4.jsonc",
+ args: "fmt --check fmt/regular/formatted1.js fmt/regular/formatted2.ts fmt/regular/formatted3.md fmt/regular/formatted4.jsonc",
output: "fmt/expected_fmt_check_formatted_files.out",
exit_code: 0,
});
itest!(fmt_check_ignore {
- args: "fmt --check --ignore=fmt/formatted1.js fmt/",
+ args: "fmt --check --ignore=fmt/regular/formatted1.js fmt/regular/",
output: "fmt/expected_fmt_check_ignore.out",
exit_code: 0,
});
@@ -181,3 +181,26 @@ itest!(fmt_stdin_check_not_formatted {
input: Some("const a = 1\n"),
output_str: Some("Not formatted stdin\n"),
});
+
+itest!(fmt_with_config {
+ args: "fmt --config fmt/deno.jsonc fmt/fmt_with_config/",
+ 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",
+ output: "fmt/fmt_with_config_and_flags.out",
+});
+
+itest!(fmt_with_malformed_config {
+ args: "fmt --config fmt/deno.malformed.jsonc",
+ output: "fmt/fmt_with_malformed_config.out",
+ exit_code: 1,
+});
+
+itest!(fmt_with_malformed_config2 {
+ args: "fmt --config fmt/deno.malformed2.jsonc",
+ output: "fmt/fmt_with_malformed_config2.out",
+ exit_code: 1,
+});
diff --git a/cli/tests/integration/lsp_tests.rs b/cli/tests/integration/lsp_tests.rs
index 1b8d35108..3d87c222d 100644
--- a/cli/tests/integration/lsp_tests.rs
+++ b/cli/tests/integration/lsp_tests.rs
@@ -3197,6 +3197,169 @@ fn lsp_format_markdown() {
}
#[test]
+fn lsp_format_with_config() {
+ let temp_dir = TempDir::new().expect("could not create temp dir");
+ let mut params: lsp::InitializeParams =
+ serde_json::from_value(load_fixture("initialize_params.json")).unwrap();
+ let deno_fmt_jsonc =
+ serde_json::to_vec_pretty(&load_fixture("deno.fmt.jsonc")).unwrap();
+ fs::write(temp_dir.path().join("deno.fmt.jsonc"), deno_fmt_jsonc).unwrap();
+
+ params.root_uri = Some(Url::from_file_path(temp_dir.path()).unwrap());
+ if let Some(Value::Object(mut map)) = params.initialization_options {
+ map.insert("config".to_string(), json!("./deno.fmt.jsonc"));
+ params.initialization_options = Some(Value::Object(map));
+ }
+
+ let deno_exe = deno_exe_path();
+ let mut client = LspClient::new(&deno_exe).unwrap();
+ client
+ .write_request::<_, _, Value>("initialize", params)
+ .unwrap();
+
+ client
+ .write_notification(
+ "textDocument/didOpen",
+ json!({
+ "textDocument": {
+ "uri": "file:///a/file.ts",
+ "languageId": "typescript",
+ "version": 1,
+ "text": "export async function someVeryLongFunctionName() {\nconst response = fetch(\"http://localhost:4545/some/non/existent/path.json\");\nconsole.log(response.text());\nconsole.log(\"finished!\")\n}"
+ }
+ }),
+ )
+ .unwrap();
+
+ // The options below should be ignored in favor of configuration from config file.
+ let (maybe_res, maybe_err) = client
+ .write_request::<_, _, Value>(
+ "textDocument/formatting",
+ json!({
+ "textDocument": {
+ "uri": "file:///a/file.ts"
+ },
+ "options": {
+ "tabSize": 2,
+ "insertSpaces": true
+ }
+ }),
+ )
+ .unwrap();
+
+ assert!(maybe_err.is_none());
+ assert_eq!(
+ maybe_res,
+ Some(json!([{
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 0
+ },
+ "end": {
+ "line": 1,
+ "character": 0
+ }
+ },
+ "newText": "\t"
+ },
+ {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 23
+ },
+ "end": {
+ "line": 1,
+ "character": 24
+ }
+ },
+ "newText": "\n\t\t'"
+ },
+ {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 73
+ },
+ "end": {
+ "line": 1,
+ "character": 74
+ }
+ },
+ "newText": "',\n\t"
+ },
+ {
+ "range": {
+ "start": {
+ "line": 2,
+ "character": 0
+ },
+ "end": {
+ "line": 2,
+ "character": 0
+ }
+ },
+ "newText": "\t"
+ },
+ {
+ "range": {
+ "start": {
+ "line": 3,
+ "character": 0
+ },
+ "end": {
+ "line": 3,
+ "character": 0
+ }
+ },
+ "newText": "\t"
+ },
+ {
+ "range": {
+ "start": {
+ "line": 3,
+ "character": 12
+ },
+ "end": {
+ "line": 3,
+ "character": 13
+ }
+ },
+ "newText": "'"
+ },
+ {
+ "range": {
+ "start": {
+ "line": 3,
+ "character": 22
+ },
+ "end": {
+ "line": 3,
+ "character": 24
+ }
+ },
+ "newText": "');"
+ },
+ {
+ "range": {
+ "start": {
+ "line": 4,
+ "character": 1
+ },
+ "end": {
+ "line": 4,
+ "character": 1
+ }
+ },
+ "newText": "\n"
+ }]
+ ))
+ );
+ shutdown(&mut client);
+}
+
+#[test]
fn lsp_markdown_no_diagnostics() {
let mut client = init("initialize_params.json");
client
diff --git a/cli/tests/testdata/fmt/deno.jsonc b/cli/tests/testdata/fmt/deno.jsonc
new file mode 100644
index 000000000..0e682cf36
--- /dev/null
+++ b/cli/tests/testdata/fmt/deno.jsonc
@@ -0,0 +1,15 @@
+{
+ "fmt": {
+ "files": {
+ "include": ["fmt/fmt_with_config/"],
+ "exclude": ["fmt/fmt_with_config/b.ts"]
+ },
+ "options": {
+ "useTabs": true,
+ "lineWidth": 40,
+ "indentWidth": 8,
+ "singleQuote": true,
+ "proseWrap": "always"
+ }
+ }
+}
diff --git a/cli/tests/testdata/fmt/deno.malformed.jsonc b/cli/tests/testdata/fmt/deno.malformed.jsonc
new file mode 100644
index 000000000..667480bdc
--- /dev/null
+++ b/cli/tests/testdata/fmt/deno.malformed.jsonc
@@ -0,0 +1,12 @@
+{
+ "fmt": {
+ "files": {
+ "include": ["fmt/fmt_with_config/"],
+ "exclude": ["fmt/fmt_with_config/b.ts"]
+ },
+ "dont_know_this_field": {},
+ "options": {
+ "useTabs": true
+ }
+ }
+}
diff --git a/cli/tests/testdata/fmt/deno.malformed2.jsonc b/cli/tests/testdata/fmt/deno.malformed2.jsonc
new file mode 100644
index 000000000..2210dc0c7
--- /dev/null
+++ b/cli/tests/testdata/fmt/deno.malformed2.jsonc
@@ -0,0 +1,12 @@
+{
+ "fmt": {
+ "files": {
+ "include": ["fmt/fmt_with_config/"],
+ "exclude": ["fmt/fmt_with_config/b.ts"],
+ "dont_know_this_field": {}
+ },
+ "options": {
+ "useTabs": true
+ }
+ }
+}
diff --git a/cli/tests/testdata/fmt/fmt_with_config.out b/cli/tests/testdata/fmt/fmt_with_config.out
new file mode 100644
index 000000000..158c556c2
--- /dev/null
+++ b/cli/tests/testdata/fmt/fmt_with_config.out
@@ -0,0 +1 @@
+Checked 2 files
diff --git a/cli/tests/testdata/fmt/fmt_with_config/a.ts b/cli/tests/testdata/fmt/fmt_with_config/a.ts
new file mode 100644
index 000000000..e0f32647b
--- /dev/null
+++ b/cli/tests/testdata/fmt/fmt_with_config/a.ts
@@ -0,0 +1,46 @@
+unitTest(
+ { perms: { net: true } },
+ async function responseClone() {
+ const response =
+ await fetch(
+ 'http://localhost:4545/fixture.json',
+ );
+ const response1 =
+ response.clone();
+ assert(
+ response !==
+ response1,
+ );
+ assertEquals(
+ response.status,
+ response1
+ .status,
+ );
+ assertEquals(
+ response.statusText,
+ response1
+ .statusText,
+ );
+ const u8a =
+ new Uint8Array(
+ await response
+ .arrayBuffer(),
+ );
+ const u8a1 =
+ new Uint8Array(
+ await response1
+ .arrayBuffer(),
+ );
+ for (
+ let i = 0;
+ i <
+ u8a.byteLength;
+ i++
+ ) {
+ assertEquals(
+ u8a[i],
+ u8a1[i],
+ );
+ }
+ },
+);
diff --git a/cli/tests/testdata/fmt/fmt_with_config/b.ts b/cli/tests/testdata/fmt/fmt_with_config/b.ts
new file mode 100644
index 000000000..5c37d51d2
--- /dev/null
+++ b/cli/tests/testdata/fmt/fmt_with_config/b.ts
@@ -0,0 +1,15 @@
+// This file should be excluded from formatting
+unitTest(
+ { perms: { net: true } },
+ async function fetchBodyUsedCancelStream() {
+ const response = await fetch(
+ "http://localhost:4545/fixture.json",
+ );
+ assert(response.body !== null);
+
+ assertEquals(response.bodyUsed, false);
+ const promise = response.body.cancel();
+ assertEquals(response.bodyUsed, true);
+ await promise;
+ },
+); \ No newline at end of file
diff --git a/cli/tests/testdata/fmt/fmt_with_config/c.md b/cli/tests/testdata/fmt/fmt_with_config/c.md
new file mode 100644
index 000000000..012f7e3d4
--- /dev/null
+++ b/cli/tests/testdata/fmt/fmt_with_config/c.md
@@ -0,0 +1,17 @@
+## Permissions
+
+Deno is secure by default. Therefore,
+unless you specifically enable it, a
+program run with Deno has no file,
+network, or environment access. Access
+to security sensitive functionality
+requires that permisisons have been
+granted to an executing script through
+command line flags, or a runtime
+permission prompt.
+
+For the following example `mod.ts` has
+been granted read-only access to the
+file system. It cannot write to the file
+system, or perform any other security
+sensitive functions.
diff --git a/cli/tests/testdata/fmt/fmt_with_config_and_flags.out b/cli/tests/testdata/fmt/fmt_with_config_and_flags.out
new file mode 100644
index 000000000..c05ac45a1
--- /dev/null
+++ b/cli/tests/testdata/fmt/fmt_with_config_and_flags.out
@@ -0,0 +1 @@
+Checked 1 file
diff --git a/cli/tests/testdata/fmt/fmt_with_malformed_config.out b/cli/tests/testdata/fmt/fmt_with_malformed_config.out
new file mode 100644
index 000000000..1a55613ef
--- /dev/null
+++ b/cli/tests/testdata/fmt/fmt_with_malformed_config.out
@@ -0,0 +1,4 @@
+error: Failed to parse "fmt" configuration
+
+Caused by:
+ unknown field `dont_know_this_field`, expected `options` or `files`
diff --git a/cli/tests/testdata/fmt/fmt_with_malformed_config2.out b/cli/tests/testdata/fmt/fmt_with_malformed_config2.out
new file mode 100644
index 000000000..948b6b5b8
--- /dev/null
+++ b/cli/tests/testdata/fmt/fmt_with_malformed_config2.out
@@ -0,0 +1,4 @@
+error: Failed to parse "fmt" configuration
+
+Caused by:
+ unknown field `dont_know_this_field`, expected `include` or `exclude`
diff --git a/cli/tests/testdata/fmt/formatted1.js b/cli/tests/testdata/fmt/regular/formatted1.js
index 587aa5b96..587aa5b96 100644
--- a/cli/tests/testdata/fmt/formatted1.js
+++ b/cli/tests/testdata/fmt/regular/formatted1.js
diff --git a/cli/tests/testdata/fmt/formatted2.ts b/cli/tests/testdata/fmt/regular/formatted2.ts
index 4a8036806..4a8036806 100644
--- a/cli/tests/testdata/fmt/formatted2.ts
+++ b/cli/tests/testdata/fmt/regular/formatted2.ts
diff --git a/cli/tests/testdata/fmt/formatted3.md b/cli/tests/testdata/fmt/regular/formatted3.md
index e6e616584..e6e616584 100644
--- a/cli/tests/testdata/fmt/formatted3.md
+++ b/cli/tests/testdata/fmt/regular/formatted3.md
diff --git a/cli/tests/testdata/fmt/formatted4.jsonc b/cli/tests/testdata/fmt/regular/formatted4.jsonc
index f0f72a6ed..f0f72a6ed 100644
--- a/cli/tests/testdata/fmt/formatted4.jsonc
+++ b/cli/tests/testdata/fmt/regular/formatted4.jsonc
diff --git a/cli/tests/testdata/lsp/deno.fmt.jsonc b/cli/tests/testdata/lsp/deno.fmt.jsonc
new file mode 100644
index 000000000..a0a851731
--- /dev/null
+++ b/cli/tests/testdata/lsp/deno.fmt.jsonc
@@ -0,0 +1,11 @@
+{
+ "fmt": {
+ "options": {
+ "useTabs": true,
+ "lineWidth": 40,
+ "indentWidth": 8,
+ "singleQuote": true,
+ "proseWrap": "always"
+ }
+ }
+}
diff --git a/cli/tools/fmt.rs b/cli/tools/fmt.rs
index b3dfc86e8..5f1507d19 100644
--- a/cli/tools/fmt.rs
+++ b/cli/tools/fmt.rs
@@ -8,6 +8,9 @@
//! the same functions as ops available in JS runtime.
use crate::colors;
+use crate::config_file::FmtConfig;
+use crate::config_file::FmtOptionsConfig;
+use crate::config_file::ProseWrap;
use crate::diff::diff;
use crate::file_watcher;
use crate::file_watcher::ResolutionResult;
@@ -35,24 +38,57 @@ pub async fn format(
ignore: Vec<PathBuf>,
check: bool,
watch: bool,
+ maybe_fmt_config: Option<FmtConfig>,
) -> Result<(), AnyError> {
+ // First, prepare final configuration.
+ // Collect included and ignored files. CLI flags take precendence
+ // over config file, ie. if there's `files.ignore` in config file
+ // and `--ignore` CLI flag, only the flag value is taken into account.
+ let mut include_files = args.clone();
+ let mut exclude_files = ignore;
+
+ if let Some(fmt_config) = maybe_fmt_config.as_ref() {
+ if include_files.is_empty() {
+ include_files = fmt_config
+ .files
+ .include
+ .iter()
+ .map(PathBuf::from)
+ .collect::<Vec<PathBuf>>();
+ }
+
+ if exclude_files.is_empty() {
+ exclude_files = fmt_config
+ .files
+ .exclude
+ .iter()
+ .map(PathBuf::from)
+ .collect::<Vec<PathBuf>>();
+ }
+ }
+
+ let fmt_options = maybe_fmt_config.map(|c| c.options).unwrap_or_default();
+
let resolver = |changed: Option<Vec<PathBuf>>| {
let files_changed = changed.is_some();
let result =
- collect_files(&args, &ignore, is_supported_ext_fmt).map(|files| {
- if let Some(paths) = changed {
- files
- .into_iter()
- .filter(|path| paths.contains(path))
- .collect::<Vec<_>>()
- } else {
- files
- }
- });
- let paths_to_watch = args.clone();
+ collect_files(&include_files, &exclude_files, is_supported_ext_fmt).map(
+ |files| {
+ let collected_files = if let Some(paths) = changed {
+ files
+ .into_iter()
+ .filter(|path| paths.contains(path))
+ .collect::<Vec<_>>()
+ } else {
+ files
+ };
+ (collected_files, fmt_options.clone())
+ },
+ );
+ let paths_to_watch = include_files.clone();
async move {
if (files_changed || !watch)
- && matches!(result, Ok(ref files) if files.is_empty())
+ && matches!(result, Ok((ref files, _)) if files.is_empty())
{
ResolutionResult::Ignore
} else {
@@ -63,11 +99,11 @@ pub async fn format(
}
}
};
- let operation = |paths: Vec<PathBuf>| async move {
+ let operation = |(paths, fmt_options): (Vec<PathBuf>, FmtOptionsConfig)| async move {
if check {
- check_source_files(paths).await?;
+ check_source_files(paths, fmt_options).await?;
} else {
- format_source_files(paths).await?;
+ format_source_files(paths, fmt_options).await?;
}
Ok(())
};
@@ -75,13 +111,13 @@ pub async fn format(
if watch {
file_watcher::watch_func(resolver, operation, "Fmt").await?;
} else {
- let files =
+ let (files, fmt_options) =
if let ResolutionResult::Restart { result, .. } = resolver(None).await {
result?
} else {
return Err(generic_error("No target files found."));
};
- operation(files).await?;
+ operation((files, fmt_options)).await?;
}
Ok(())
@@ -89,10 +125,14 @@ pub async fn format(
/// Formats markdown (using <https://github.com/dprint/dprint-plugin-markdown>) and its code blocks
/// (ts/tsx, js/jsx).
-fn format_markdown(file_text: &str) -> Result<String, String> {
+fn format_markdown(
+ file_text: &str,
+ fmt_options: &FmtOptionsConfig,
+) -> Result<String, String> {
+ let markdown_config = get_resolved_markdown_config(fmt_options);
dprint_plugin_markdown::format_text(
file_text,
- &MARKDOWN_CONFIG,
+ &markdown_config,
move |tag, text, line_width| {
let tag = tag.to_lowercase();
if matches!(
@@ -115,13 +155,14 @@ fn format_markdown(file_text: &str) -> Result<String, String> {
};
if matches!(extension, "json" | "jsonc") {
- let mut json_config = JSON_CONFIG.clone();
+ let mut json_config = get_resolved_json_config(fmt_options);
json_config.line_width = line_width;
dprint_plugin_json::format_text(text, &json_config)
} else {
let fake_filename =
PathBuf::from(format!("deno_fmt_stdin.{}", extension));
- let mut codeblock_config = TYPESCRIPT_CONFIG.clone();
+ let mut codeblock_config =
+ get_resolved_typescript_config(fmt_options);
codeblock_config.line_width = line_width;
dprint_plugin_typescript::format_text(
&fake_filename,
@@ -140,32 +181,36 @@ fn format_markdown(file_text: &str) -> Result<String, String> {
/// Formats JSON and JSONC using the rules provided by .deno()
/// of configuration builder of <https://github.com/dprint/dprint-plugin-json>.
/// See <https://git.io/Jt4ht> for configuration.
-fn format_json(file_text: &str) -> Result<String, String> {
- dprint_plugin_json::format_text(file_text, &JSON_CONFIG)
- .map_err(|e| e.to_string())
+fn format_json(
+ file_text: &str,
+ fmt_options: &FmtOptionsConfig,
+) -> Result<String, String> {
+ let config = get_resolved_json_config(fmt_options);
+ dprint_plugin_json::format_text(file_text, &config).map_err(|e| e.to_string())
}
/// Formats a single TS, TSX, JS, JSX, JSONC, JSON, or MD file.
pub fn format_file(
file_path: &Path,
file_text: &str,
+ fmt_options: FmtOptionsConfig,
) -> Result<String, String> {
let ext = get_extension(file_path).unwrap_or_else(String::new);
if ext == "md" {
- format_markdown(file_text)
+ format_markdown(file_text, &fmt_options)
} else if matches!(ext.as_str(), "json" | "jsonc") {
- format_json(file_text)
+ format_json(file_text, &fmt_options)
} else {
- dprint_plugin_typescript::format_text(
- file_path,
- file_text,
- &TYPESCRIPT_CONFIG,
- )
- .map_err(|e| e.to_string())
+ let config = get_resolved_typescript_config(&fmt_options);
+ dprint_plugin_typescript::format_text(file_path, file_text, &config)
+ .map_err(|e| e.to_string())
}
}
-pub fn format_parsed_module(parsed_source: &ParsedSource) -> String {
+pub fn format_parsed_module(
+ parsed_source: &ParsedSource,
+ fmt_options: FmtOptionsConfig,
+) -> String {
dprint_plugin_typescript::format_parsed_file(
&dprint_plugin_typescript::SourceFileInfo {
is_jsx: matches!(
@@ -178,11 +223,14 @@ pub fn format_parsed_module(parsed_source: &ParsedSource) -> String {
module: parsed_source.module(),
tokens: parsed_source.tokens(),
},
- &TYPESCRIPT_CONFIG,
+ &get_resolved_typescript_config(&fmt_options),
)
}
-async fn check_source_files(paths: Vec<PathBuf>) -> Result<(), AnyError> {
+async fn check_source_files(
+ paths: Vec<PathBuf>,
+ fmt_options: FmtOptionsConfig,
+) -> Result<(), AnyError> {
let not_formatted_files_count = Arc::new(AtomicUsize::new(0));
let checked_files_count = Arc::new(AtomicUsize::new(0));
@@ -196,7 +244,7 @@ async fn check_source_files(paths: Vec<PathBuf>) -> Result<(), AnyError> {
checked_files_count.fetch_add(1, Ordering::Relaxed);
let file_text = read_file_contents(&file_path)?.text;
- match format_file(&file_path, &file_text) {
+ match format_file(&file_path, &file_text, fmt_options.clone()) {
Ok(formatted_text) => {
if formatted_text != file_text {
not_formatted_files_count.fetch_add(1, Ordering::Relaxed);
@@ -235,7 +283,10 @@ async fn check_source_files(paths: Vec<PathBuf>) -> Result<(), AnyError> {
}
}
-async fn format_source_files(paths: Vec<PathBuf>) -> Result<(), AnyError> {
+async fn format_source_files(
+ paths: Vec<PathBuf>,
+ fmt_options: FmtOptionsConfig,
+) -> Result<(), AnyError> {
let formatted_files_count = Arc::new(AtomicUsize::new(0));
let checked_files_count = Arc::new(AtomicUsize::new(0));
let output_lock = Arc::new(Mutex::new(0)); // prevent threads outputting at the same time
@@ -247,7 +298,7 @@ async fn format_source_files(paths: Vec<PathBuf>) -> Result<(), AnyError> {
checked_files_count.fetch_add(1, Ordering::Relaxed);
let file_contents = read_file_contents(&file_path)?;
- match format_file(&file_path, &file_contents.text) {
+ match format_file(&file_path, &file_contents.text, fmt_options.clone()) {
Ok(formatted_text) => {
if formatted_text != file_contents.text {
write_file_contents(
@@ -293,14 +344,18 @@ async fn format_source_files(paths: Vec<PathBuf>) -> Result<(), AnyError> {
/// Format stdin and write result to stdout.
/// Treats input as TypeScript or as set by `--ext` flag.
/// Compatible with `--check` flag.
-pub fn format_stdin(check: bool, ext: String) -> Result<(), AnyError> {
+pub fn format_stdin(
+ check: bool,
+ ext: String,
+ fmt_options: FmtOptionsConfig,
+) -> Result<(), AnyError> {
let mut source = String::new();
if stdin().read_to_string(&mut source).is_err() {
return Err(generic_error("Failed to read from stdin"));
}
let file_path = PathBuf::from(format!("_stdin.{}", ext));
- match format_file(&file_path, &source) {
+ match format_file(&file_path, &source, fmt_options) {
Ok(formatted_text) => {
if check {
if formatted_text != source {
@@ -325,18 +380,86 @@ fn files_str(len: usize) -> &'static str {
}
}
-lazy_static::lazy_static! {
- static ref TYPESCRIPT_CONFIG: dprint_plugin_typescript::configuration::Configuration = dprint_plugin_typescript::configuration::ConfigurationBuilder::new()
- .deno()
- .build();
+fn get_resolved_typescript_config(
+ options: &FmtOptionsConfig,
+) -> dprint_plugin_typescript::configuration::Configuration {
+ let mut builder =
+ dprint_plugin_typescript::configuration::ConfigurationBuilder::new();
+ builder.deno();
+
+ if let Some(use_tabs) = options.use_tabs {
+ builder.use_tabs(use_tabs);
+ }
- static ref MARKDOWN_CONFIG: dprint_plugin_markdown::configuration::Configuration = dprint_plugin_markdown::configuration::ConfigurationBuilder::new()
- .deno()
- .build();
+ if let Some(line_width) = options.line_width {
+ builder.line_width(line_width);
+ }
+
+ if let Some(indent_width) = options.indent_width {
+ builder.indent_width(indent_width);
+ }
+
+ if let Some(single_quote) = options.single_quote {
+ if single_quote {
+ builder.quote_style(
+ dprint_plugin_typescript::configuration::QuoteStyle::AlwaysSingle,
+ );
+ }
+ }
+
+ builder.build()
+}
+
+fn get_resolved_markdown_config(
+ options: &FmtOptionsConfig,
+) -> dprint_plugin_markdown::configuration::Configuration {
+ let mut builder =
+ dprint_plugin_markdown::configuration::ConfigurationBuilder::new();
+
+ builder.deno();
+
+ if let Some(line_width) = options.line_width {
+ builder.line_width(line_width);
+ }
+
+ if let Some(prose_wrap) = options.prose_wrap {
+ builder.text_wrap(match prose_wrap {
+ ProseWrap::Always => {
+ dprint_plugin_markdown::configuration::TextWrap::Always
+ }
+ ProseWrap::Never => {
+ dprint_plugin_markdown::configuration::TextWrap::Never
+ }
+ ProseWrap::Preserve => {
+ dprint_plugin_markdown::configuration::TextWrap::Maintain
+ }
+ });
+ }
+
+ builder.build()
+}
+
+fn get_resolved_json_config(
+ options: &FmtOptionsConfig,
+) -> dprint_plugin_json::configuration::Configuration {
+ let mut builder =
+ dprint_plugin_json::configuration::ConfigurationBuilder::new();
+
+ builder.deno();
+
+ if let Some(use_tabs) = options.use_tabs {
+ builder.use_tabs(use_tabs);
+ }
+
+ if let Some(line_width) = options.line_width {
+ builder.line_width(line_width);
+ }
+
+ if let Some(indent_width) = options.indent_width {
+ builder.indent_width(indent_width);
+ }
- static ref JSON_CONFIG: dprint_plugin_json::configuration::Configuration = dprint_plugin_json::configuration::ConfigurationBuilder::new()
- .deno()
- .build();
+ builder.build()
}
struct FileContents {