diff options
author | Bartek IwaĆczuk <biwanczuk@gmail.com> | 2020-01-30 03:16:48 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-01-29 21:16:48 -0500 |
commit | 73a3cc21d0e7ceec1275078db125f57ce97725fb (patch) | |
tree | 17d845d5d125d3ba8ecefa49e57e02e9c088c68b /cli | |
parent | f0a6062012c4e09553877c9feedb3fbc3d4625b9 (diff) |
feat: dprint formatter (#3820)
* rewrite fmt_test in Rust, remove tools/fmt_test.py
* remove //std/prettier
Diffstat (limited to 'cli')
-rw-r--r-- | cli/Cargo.toml | 2 | ||||
-rw-r--r-- | cli/flags.rs | 360 | ||||
-rw-r--r-- | cli/fmt.rs | 162 | ||||
-rw-r--r-- | cli/lib.rs | 8 | ||||
-rw-r--r-- | cli/tests/badly_formatted.js | 4 | ||||
-rw-r--r-- | cli/tests/integration_tests.rs | 40 |
6 files changed, 263 insertions, 313 deletions
diff --git a/cli/Cargo.toml b/cli/Cargo.toml index f37825371..a643e51ba 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -35,7 +35,9 @@ brotli2 = "0.3.2" clap = "2.33.0" dirs = "2.0.2" dlopen = "0.1.8" +dprint-plugin-typescript = "0.3.0-alpha.1" futures = { version = "0.3.1", features = [ "compat", "io-compat" ] } +glob = "0.3.0" http = "0.2.0" indexmap = "1.3.0" lazy_static = "1.4.0" diff --git a/cli/flags.rs b/cli/flags.rs index 10709b780..eaf9b7fc5 100644 --- a/cli/flags.rs +++ b/cli/flags.rs @@ -28,8 +28,6 @@ macro_rules! std_url { }; } -/// Used for `deno fmt <files>...` subcommand -const PRETTIER_URL: &str = std_url!("prettier/main.ts"); /// Used for `deno install...` subcommand const INSTALLER_URL: &str = std_url!("installer/mod.ts"); /// Used for `deno test...` subcommand @@ -41,6 +39,10 @@ pub enum DenoSubcommand { Completions, Eval, Fetch, + Format { + check: bool, + files: Option<Vec<String>>, + }, Help, Info, Install, @@ -230,60 +232,19 @@ fn types_parse(flags: &mut DenoFlags, _matches: &clap::ArgMatches) { } fn fmt_parse(flags: &mut DenoFlags, matches: &clap::ArgMatches) { - flags.subcommand = DenoSubcommand::Run; - flags.allow_read = true; - flags.allow_write = true; - flags.argv.push(PRETTIER_URL.to_string()); - - let files: Vec<String> = matches - .values_of("files") - .unwrap() - .map(String::from) - .collect(); - flags.argv.extend(files); - - if !matches.is_present("stdout") { - // `deno fmt` writes to the files by default - flags.argv.push("--write".to_string()); - } - - let prettier_flags = [ - ["0", "check"], - ["1", "prettierrc"], - ["1", "ignore-path"], - ["1", "print-width"], - ["1", "tab-width"], - ["0", "use-tabs"], - ["0", "no-semi"], - ["0", "single-quote"], - ["1", "quote-props"], - ["0", "jsx-single-quote"], - ["0", "jsx-bracket-same-line"], - ["0", "trailing-comma"], - ["0", "no-bracket-spacing"], - ["1", "arrow-parens"], - ["1", "prose-wrap"], - ["1", "end-of-line"], - ]; - - for opt in &prettier_flags { - let t = opt[0]; - let keyword = opt[1]; - - if matches.is_present(&keyword) { - if t == "0" { - flags.argv.push(format!("--{}", keyword)); - } else { - if keyword == "prettierrc" { - flags.argv.push("--config".to_string()); - } else { - flags.argv.push(format!("--{}", keyword)); - } - flags - .argv - .push(matches.value_of(keyword).unwrap().to_string()); - } + let maybe_files = match matches.values_of("files") { + Some(f) => { + let files: Vec<String> = f.map(String::from).collect(); + Some(files) } + None => None, + }; + + let check = matches.is_present("check"); + + flags.subcommand = DenoSubcommand::Format { + check, + files: maybe_files, } } @@ -556,155 +517,28 @@ The declaration file could be saved and used for typing information.", fn fmt_subcommand<'a, 'b>() -> App<'a, 'b> { SubCommand::with_name("fmt") - .about("Format files") - .long_about( -"Auto-format JavaScript/TypeScript source code using Prettier + .about("Format files") + .long_about( + "Auto-format JavaScript/TypeScript source code -Automatically downloads Prettier dependencies on first run. + deno fmt - deno fmt myfile1.ts myfile2.ts", - ) - .arg( - Arg::with_name("check") - .long("check") - .help("Check if the source files are formatted.") - .takes_value(false), - ) - .arg( - Arg::with_name("prettierrc") - .long("prettierrc") - .value_name("auto|disable|FILE") - .help("Specify the configuration file of the prettier. - auto: Auto detect prettier configuration file in current working dir. - disable: Disable load configuration file. - FILE: Load specified prettier configuration file. support .json/.toml/.js/.ts file - ") - .takes_value(true) - .require_equals(true) - .default_value("auto") - ) - .arg( - Arg::with_name("ignore-path") - .long("ignore-path") - .value_name("auto|disable|FILE") - .help("Path to a file containing patterns that describe files to ignore. - auto: Auto detect .pretierignore file in current working dir. - disable: Disable load .prettierignore file. - FILE: Load specified prettier ignore file. - ") - .takes_value(true) - .require_equals(true) - .default_value("auto") - ) - .arg( - Arg::with_name("stdout") - .long("stdout") - .help("Output formated code to stdout") - .takes_value(false), - ) - .arg( - Arg::with_name("print-width") - .long("print-width") - .value_name("int") - .help("Specify the line length that the printer will wrap on.") - .takes_value(true) - .require_equals(true) - ) - .arg( - Arg::with_name("tab-width") - .long("tab-width") - .value_name("int") - .help("Specify the number of spaces per indentation-level.") - .takes_value(true) - .require_equals(true) - ) - .arg( - Arg::with_name("use-tabs") - .long("use-tabs") - .help("Indent lines with tabs instead of spaces.") - .takes_value(false) - ) - .arg( - Arg::with_name("no-semi") - .long("no-semi") - .help("Print semicolons at the ends of statements.") - .takes_value(false) - ) - .arg( - Arg::with_name("single-quote") - .long("single-quote") - .help("Use single quotes instead of double quotes.") - .takes_value(false) - ) - .arg( - Arg::with_name("quote-props") - .long("quote-props") - .value_name("as-needed|consistent|preserve") - .help("Change when properties in objects are quoted.") - .takes_value(true) - .possible_values(&["as-needed", "consistent", "preserve"]) - .require_equals(true) - ) - .arg( - Arg::with_name("jsx-single-quote") - .long("jsx-single-quote") - .help("Use single quotes instead of double quotes in JSX.") - .takes_value(false) - ) - .arg( - Arg::with_name("jsx-bracket-same-line") - .long("jsx-bracket-same-line") - .help( - "Put the > of a multi-line JSX element at the end of the last line -instead of being alone on the next line (does not apply to self closing elements)." - ) - .takes_value(false) - ) - .arg( - Arg::with_name("trailing-comma") - .long("trailing-comma") - .help("Print trailing commas wherever possible when multi-line.") - .takes_value(false) - ) - .arg( - Arg::with_name("no-bracket-spacing") - .long("no-bracket-spacing") - .help("Print spaces between brackets in object literals.") - .takes_value(false) - ) - .arg( - Arg::with_name("arrow-parens") - .long("arrow-parens") - .value_name("avoid|always") - .help("Include parentheses around a sole arrow function parameter.") - .takes_value(true) - .possible_values(&["avoid", "always"]) - .require_equals(true) - ) - .arg( - Arg::with_name("prose-wrap") - .long("prose-wrap") - .value_name("always|never|preserve") - .help("How to wrap prose.") - .takes_value(true) - .possible_values(&["always", "never", "preserve"]) - .require_equals(true) - ) - .arg( - Arg::with_name("end-of-line") - .long("end-of-line") - .value_name("auto|lf|crlf|cr") - .help("Which end of line characters to apply.") - .takes_value(true) - .possible_values(&["auto", "lf", "crlf", "cr"]) - .require_equals(true) - ) - .arg( - Arg::with_name("files") - .takes_value(true) - .multiple(true) - .required(true), - ) + deno fmt myfile1.ts myfile2.ts + + deno fmt --check", + ) + .arg( + Arg::with_name("check") + .long("check") + .help("Check if the source files are formatted.") + .takes_value(false), + ) + .arg( + Arg::with_name("files") + .takes_value(true) + .multiple(true) + .required(false), + ) } fn repl_subcommand<'a, 'b>() -> App<'a, 'b> { @@ -1439,20 +1273,24 @@ mod tests { assert_eq!( r.unwrap(), DenoFlags { - subcommand: DenoSubcommand::Run, - allow_write: true, - allow_read: true, - argv: svec![ - "deno", - PRETTIER_URL, - "script_1.ts", - "script_2.ts", - "--write", - "--config", - "auto", - "--ignore-path", - "auto" - ], + subcommand: DenoSubcommand::Format { + check: false, + files: Some(svec!["script_1.ts", "script_2.ts"]) + }, + argv: svec!["deno"], + ..DenoFlags::default() + } + ); + + let r = flags_from_vec_safe(svec!["deno", "fmt", "--check"]); + assert_eq!( + r.unwrap(), + DenoFlags { + subcommand: DenoSubcommand::Format { + check: true, + files: None + }, + argv: svec!["deno"], ..DenoFlags::default() } ); @@ -1668,36 +1506,6 @@ mod tests { } #[test] - fn fmt_stdout() { - let r = flags_from_vec_safe(svec![ - "deno", - "fmt", - "--stdout", - "script_1.ts", - "script_2.ts" - ]); - assert_eq!( - r.unwrap(), - DenoFlags { - subcommand: DenoSubcommand::Run, - argv: svec![ - "deno", - PRETTIER_URL, - "script_1.ts", - "script_2.ts", - "--config", - "auto", - "--ignore-path", - "auto" - ], - allow_write: true, - allow_read: true, - ..DenoFlags::default() - } - ); - } - - #[test] fn default_to_run() { let r = flags_from_vec_safe(svec!["deno", "script.ts"]); assert_eq!( @@ -2126,66 +1934,6 @@ mod tests { } #[test] - fn fmt_args() { - let r = flags_from_vec_safe(svec![ - "deno", - "fmt", - "--check", - "--prettierrc=auto", - "--print-width=100", - "--tab-width=4", - "--use-tabs", - "--no-semi", - "--single-quote", - "--arrow-parens=always", - "--prose-wrap=preserve", - "--end-of-line=crlf", - "--quote-props=preserve", - "--jsx-single-quote", - "--jsx-bracket-same-line", - "--ignore-path=.prettier-ignore", - "script.ts" - ]); - assert_eq!( - r.unwrap(), - DenoFlags { - subcommand: DenoSubcommand::Run, - argv: svec![ - "deno", - PRETTIER_URL, - "script.ts", - "--write", - "--check", - "--config", - "auto", - "--ignore-path", - ".prettier-ignore", - "--print-width", - "100", - "--tab-width", - "4", - "--use-tabs", - "--no-semi", - "--single-quote", - "--quote-props", - "preserve", - "--jsx-single-quote", - "--jsx-bracket-same-line", - "--arrow-parens", - "always", - "--prose-wrap", - "preserve", - "--end-of-line", - "crlf" - ], - allow_write: true, - allow_read: true, - ..DenoFlags::default() - } - ); - } - - #[test] fn test_with_exclude() { let r = flags_from_vec_safe(svec. +//! +//! At the moment it is only consumed using CLI but in +//! the future it can be easily extended to provide +//! the same functions as ops available in JS runtime. + +use dprint_plugin_typescript::format_text; +use dprint_plugin_typescript::Configuration; +use dprint_plugin_typescript::ConfigurationBuilder; +use glob; +use regex::Regex; +use std::fs; +use std::path::Path; +use std::path::PathBuf; +use std::time::Instant; + +lazy_static! { + static ref TYPESCRIPT_LIB: Regex = Regex::new(".d.ts$").unwrap(); + static ref TYPESCRIPT: Regex = Regex::new(".tsx?$").unwrap(); + static ref JAVASCRIPT: Regex = Regex::new(".jsx?$").unwrap(); +} + +fn is_supported(path: &Path) -> bool { + let path_str = path.to_string_lossy(); + !TYPESCRIPT_LIB.is_match(&path_str) + && (TYPESCRIPT.is_match(&path_str) || JAVASCRIPT.is_match(&path_str)) +} + +fn get_config() -> Configuration { + ConfigurationBuilder::new() + .line_width(80) + .indent_width(2) + .build() +} + +fn get_supported_files(paths: Vec<PathBuf>) -> Vec<PathBuf> { + let mut files_to_check = vec![]; + + for path in paths { + if is_supported(&path) { + files_to_check.push(path.to_owned()); + } + } + + files_to_check +} + +fn check_source_files(config: Configuration, paths: Vec<PathBuf>) { + let start = Instant::now(); + let mut not_formatted_files = vec![]; + + for file_path in paths { + let file_path_str = file_path.to_string_lossy(); + let file_contents = fs::read_to_string(&file_path).unwrap(); + match format_text(&file_path_str, &file_contents, &config) { + Ok(None) => { + // nothing to format, pass + } + Ok(Some(formatted_text)) => { + if formatted_text != file_contents { + println!("Not formatted {}", file_path_str); + not_formatted_files.push(file_path); + } + } + Err(e) => { + eprintln!("Error checking: {}", &file_path_str); + eprintln!(" {}", e); + } + } + } + + let duration = Instant::now() - start; + + if !not_formatted_files.is_empty() { + let f = if not_formatted_files.len() == 1 { + "file" + } else { + "files" + }; + + eprintln!( + "Found {} not formatted {} in {:?}", + not_formatted_files.len(), + f, + duration + ); + std::process::exit(1); + } +} + +fn format_source_files(config: Configuration, paths: Vec<PathBuf>) { + let start = Instant::now(); + let mut not_formatted_files = vec![]; + + for file_path in paths { + let file_path_str = file_path.to_string_lossy(); + let file_contents = fs::read_to_string(&file_path).unwrap(); + match format_text(&file_path_str, &file_contents, &config) { + Ok(None) => { + // nothing to format, pass + } + Ok(Some(formatted_text)) => { + if formatted_text != file_contents { + println!("Formatting {}", file_path_str); + fs::write(&file_path, formatted_text).unwrap(); + not_formatted_files.push(file_path); + } + } + Err(e) => { + eprintln!("Error formatting: {}", &file_path_str); + eprintln!(" {}", e); + } + } + } + + let duration = Instant::now() - start; + let f = if not_formatted_files.len() == 1 { + "file" + } else { + "files" + }; + eprintln!( + "Formatted {} {} in {:?}", + not_formatted_files.len(), + f, + duration + ); +} + +fn get_matching_files(glob_paths: Vec<String>) -> Vec<PathBuf> { + let mut target_files = Vec::with_capacity(128); + + for path in glob_paths { + let files = glob::glob(&path) + .expect("Failed to execute glob.") + .filter_map(Result::ok); + target_files.extend(files); + } + + target_files +} + +/// Format JavaScript/TypeScript files. +/// +/// First argument supports globs, and if it is `None` +/// then the current directory is recursively walked. +pub fn format_files(maybe_files: Option<Vec<String>>, check: bool) { + // TODO: improve glob to look for tsx?/jsx? files only + let glob_paths = maybe_files.unwrap_or_else(|| vec!["**/*".to_string()]); + let matching_files = get_matching_files(glob_paths); + let matching_files = get_supported_files(matching_files); + let config = get_config(); + + if check { + check_source_files(config, matching_files); + } else { + format_source_files(config, matching_files); + } +} diff --git a/cli/lib.rs b/cli/lib.rs index 56705289f..fe20cd135 100644 --- a/cli/lib.rs +++ b/cli/lib.rs @@ -28,6 +28,7 @@ pub mod diagnostics; mod disk_cache; mod file_fetcher; pub mod flags; +mod fmt; pub mod fmt_errors; mod fs; mod global_state; @@ -422,6 +423,10 @@ fn run_script(flags: DenoFlags) { } } +fn format_command(files: Option<Vec<String>>, check: bool) { + fmt::format_files(files, check); +} + pub fn main() { #[cfg(windows)] ansi_term::enable_ansi_support().ok(); // For Windows 10 @@ -442,11 +447,12 @@ pub fn main() { }; log::set_max_level(log_level.to_level_filter()); - match flags.subcommand { + match flags.clone().subcommand { DenoSubcommand::Bundle => bundle_command(flags), DenoSubcommand::Completions => {} DenoSubcommand::Eval => eval_command(flags), DenoSubcommand::Fetch => fetch_command(flags), + DenoSubcommand::Format { check, files } => format_command(files, check), DenoSubcommand::Info => info_command(flags), DenoSubcommand::Repl => run_repl(flags), DenoSubcommand::Run => run_script(flags), diff --git a/cli/tests/badly_formatted.js b/cli/tests/badly_formatted.js index 17e3e6be0..b646a95a3 100644 --- a/cli/tests/badly_formatted.js +++ b/cli/tests/badly_formatted.js @@ -1,4 +1,4 @@ -console.log( - "Hello World" +console.log("Hello World" + ) diff --git a/cli/tests/integration_tests.rs b/cli/tests/integration_tests.rs index 7f318119a..501ff1713 100644 --- a/cli/tests/integration_tests.rs +++ b/cli/tests/integration_tests.rs @@ -29,12 +29,44 @@ fn fetch_test() { drop(g); } -// TODO(#2933): Rewrite this test in rust. #[test] fn fmt_test() { - let g = util::http_server(); - util::run_python_script("tools/fmt_test.py"); - drop(g); + use tempfile::TempDir; + + let t = TempDir::new().expect("tempdir fail"); + let fixed = util::root_path().join("cli/tests/badly_formatted_fixed.js"); + let badly_formatted_original = + util::root_path().join("cli/tests/badly_formatted.js"); + let badly_formatted = t.path().join("badly_formatted.js"); + let badly_formatted_str = badly_formatted.to_str().unwrap(); + std::fs::copy(&badly_formatted_original, &badly_formatted) + .expect("Failed to copy file"); + + let status = util::deno_cmd() + .current_dir(util::root_path()) + .arg("fmt") + .arg("--check") + .arg(badly_formatted_str) + .spawn() + .expect("Failed to spawn script") + .wait() + .expect("Failed to wait for child process"); + + assert_eq!(Some(1), status.code()); + + let status = util::deno_cmd() + .current_dir(util::root_path()) + .arg("fmt") + .arg(badly_formatted_str) + .spawn() + .expect("Failed to spawn script") + .wait() + .expect("Failed to wait for child process"); + + assert_eq!(Some(0), status.code()); + let expected = std::fs::read_to_string(fixed).unwrap(); + let actual = std::fs::read_to_string(badly_formatted).unwrap(); + assert_eq!(expected, actual); } #[test] |