Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(fmt): add support for configuration file #11944

Merged
merged 15 commits into from
Sep 13, 2021
1 change: 1 addition & 0 deletions .dprint.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
64 changes: 62 additions & 2 deletions cli/config_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
}
Expand All @@ -286,14 +286,40 @@ 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)]
#[serde(rename_all = "camelCase")]
pub struct ConfigFileJson {
pub compiler_options: Option<Value>,
pub lint: Option<Value>,
pub fmt: Option<Value>,
}

#[derive(Clone, Debug)]
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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]
Expand Down
40 changes: 40 additions & 0 deletions cli/flags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -1733,6 +1734,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(),
Expand Down Expand Up @@ -2535,6 +2537,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]
Expand Down
104 changes: 69 additions & 35 deletions cli/lsp/language_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -335,15 +335,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) {
Expand Down Expand Up @@ -373,18 +373,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(())
}

Expand Down Expand Up @@ -566,20 +582,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
Expand Down Expand Up @@ -1006,14 +1017,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,
)
}
};

Expand Down
15 changes: 14 additions & 1 deletion cli/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -806,15 +806,28 @@ 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(
fmt_flags.files,
fmt_flags.ignore,
fmt_flags.check,
flags.watch,
maybe_fmt_config,
)
.await?;
Ok(())
Expand Down
23 changes: 23 additions & 0 deletions cli/tests/integration/fmt_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
Loading