mirror of
https://github.com/denoland/deno.git
synced 2025-01-21 13:00:36 -05:00
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" } } }
This commit is contained in:
parent
a655a0f3e4
commit
0dbeb774ba
23 changed files with 687 additions and 92 deletions
|
@ -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",
|
||||
|
|
|
@ -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]
|
||||
|
|
40
cli/flags.rs
40
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]
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
|
|
15
cli/main.rs
15
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(())
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -3196,6 +3196,169 @@ fn lsp_format_markdown() {
|
|||
shutdown(&mut client);
|
||||
}
|
||||
|
||||
#[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");
|
||||
|
|
15
cli/tests/testdata/fmt/deno.jsonc
vendored
Normal file
15
cli/tests/testdata/fmt/deno.jsonc
vendored
Normal file
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
12
cli/tests/testdata/fmt/deno.malformed.jsonc
vendored
Normal file
12
cli/tests/testdata/fmt/deno.malformed.jsonc
vendored
Normal file
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
12
cli/tests/testdata/fmt/deno.malformed2.jsonc
vendored
Normal file
12
cli/tests/testdata/fmt/deno.malformed2.jsonc
vendored
Normal file
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
1
cli/tests/testdata/fmt/fmt_with_config.out
vendored
Normal file
1
cli/tests/testdata/fmt/fmt_with_config.out
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
Checked 2 files
|
46
cli/tests/testdata/fmt/fmt_with_config/a.ts
vendored
Normal file
46
cli/tests/testdata/fmt/fmt_with_config/a.ts
vendored
Normal file
|
@ -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],
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
15
cli/tests/testdata/fmt/fmt_with_config/b.ts
vendored
Normal file
15
cli/tests/testdata/fmt/fmt_with_config/b.ts
vendored
Normal file
|
@ -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;
|
||||
},
|
||||
);
|
17
cli/tests/testdata/fmt/fmt_with_config/c.md
vendored
Normal file
17
cli/tests/testdata/fmt/fmt_with_config/c.md
vendored
Normal file
|
@ -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.
|
1
cli/tests/testdata/fmt/fmt_with_config_and_flags.out
vendored
Normal file
1
cli/tests/testdata/fmt/fmt_with_config_and_flags.out
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
Checked 1 file
|
4
cli/tests/testdata/fmt/fmt_with_malformed_config.out
vendored
Normal file
4
cli/tests/testdata/fmt/fmt_with_malformed_config.out
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
error: Failed to parse "fmt" configuration
|
||||
|
||||
Caused by:
|
||||
unknown field `dont_know_this_field`, expected `options` or `files`
|
4
cli/tests/testdata/fmt/fmt_with_malformed_config2.out
vendored
Normal file
4
cli/tests/testdata/fmt/fmt_with_malformed_config2.out
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
error: Failed to parse "fmt" configuration
|
||||
|
||||
Caused by:
|
||||
unknown field `dont_know_this_field`, expected `include` or `exclude`
|
11
cli/tests/testdata/lsp/deno.fmt.jsonc
vendored
Normal file
11
cli/tests/testdata/lsp/deno.fmt.jsonc
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"fmt": {
|
||||
"options": {
|
||||
"useTabs": true,
|
||||
"lineWidth": 40,
|
||||
"indentWidth": 8,
|
||||
"singleQuote": true,
|
||||
"proseWrap": "always"
|
||||
}
|
||||
}
|
||||
}
|
223
cli/tools/fmt.rs
223
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();
|
||||
|
||||
static ref MARKDOWN_CONFIG: dprint_plugin_markdown::configuration::Configuration = dprint_plugin_markdown::configuration::ConfigurationBuilder::new()
|
||||
.deno()
|
||||
.build();
|
||||
if let Some(use_tabs) = options.use_tabs {
|
||||
builder.use_tabs(use_tabs);
|
||||
}
|
||||
|
||||
static ref JSON_CONFIG: dprint_plugin_json::configuration::Configuration = dprint_plugin_json::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);
|
||||
}
|
||||
|
||||
builder.build()
|
||||
}
|
||||
|
||||
struct FileContents {
|
||||
|
|
Loading…
Add table
Reference in a new issue