From 1268640c6628cb6e3d3eef160f000aeb2492f4d7 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Fri, 4 Jul 2025 16:52:13 +0200 Subject: [PATCH 1/2] Rust implementation of "rescript format" --- CHANGELOG.md | 4 ++ rewatch/Cargo.lock | 60 ++++++++++++++++++ rewatch/Cargo.toml | 2 + rewatch/src/cli.rs | 36 +++++++++-- rewatch/src/format.rs | 127 ++++++++++++++++++++++++++++++++++++++ rewatch/src/lib.rs | 1 + rewatch/src/main.rs | 13 ++-- rewatch/tests/format.sh | 43 +++++++++++++ rewatch/tests/suite-ci.sh | 2 +- scripts/format.sh | 2 +- scripts/format_check.sh | 2 +- 11 files changed, 279 insertions(+), 13 deletions(-) create mode 100644 rewatch/src/format.rs create mode 100755 rewatch/tests/format.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index cfc49f2fe0..a9bc066a59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ # 12.0.0-beta.2 (Unreleased) +#### :boom: Breaking Change + +- Rust implementation of the `rescript format` command. Command line options changed from `-all`, `-check` and `-stdin` to `--all`, `--check` and `--stdin` compared to the legacy implementation. https://github.com/rescript-lang/rescript/pull/7603 + #### :nail_care: Polish - Add missing backtick and spaces to `Belt.Map.map` doc comment. https://github.com/rescript-lang/rescript/pull/7632 diff --git a/rewatch/Cargo.lock b/rewatch/Cargo.lock index 4573c83203..c6d897b25a 100644 --- a/rewatch/Cargo.lock +++ b/rewatch/Cargo.lock @@ -303,6 +303,22 @@ dependencies = [ "termcolor", ] +[[package]] +name = "errno" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "filetime" version = "0.2.25" @@ -552,6 +568,12 @@ dependencies = [ "redox_syscall", ] +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + [[package]] name = "log" version = "0.4.27" @@ -626,6 +648,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "number_prefix" version = "0.4.0" @@ -763,12 +795,27 @@ dependencies = [ "log", "notify", "notify-debouncer-mini", + "num_cpus", "rayon", "regex", "serde", "serde_derive", "serde_json", "sysinfo", + "tempfile", +] + +[[package]] +name = "rustix" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +dependencies = [ + "bitflags 2.9.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", ] [[package]] @@ -862,6 +909,19 @@ dependencies = [ "winapi", ] +[[package]] +name = "tempfile" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + [[package]] name = "termcolor" version = "1.4.1" diff --git a/rewatch/Cargo.toml b/rewatch/Cargo.toml index 7d0d8b859c..193b7a01aa 100644 --- a/rewatch/Cargo.toml +++ b/rewatch/Cargo.toml @@ -20,11 +20,13 @@ log = { version = "0.4.17" } notify = { version = "5.1.0", features = ["serde"] } notify-debouncer-mini = { version = "0.2.0" } rayon = "1.6.1" +num_cpus = "1.17.0" regex = "1.7.1" serde = { version = "1.0.152", features = ["derive"] } serde_derive = "1.0.152" serde_json = { version = "1.0.93" } sysinfo = "0.29.10" +tempfile = "3.10.1" [profile.release] diff --git a/rewatch/src/cli.rs b/rewatch/src/cli.rs index f74c676714..ff23ce284e 100644 --- a/rewatch/src/cli.rs +++ b/rewatch/src/cli.rs @@ -8,6 +8,16 @@ fn parse_regex(s: &str) -> Result { Regex::new(s) } +use clap::ValueEnum; + +#[derive(Debug, Clone, ValueEnum)] +pub enum FileExtension { + #[value(name = ".res")] + Res, + #[value(name = ".resi")] + Resi, +} + /// ReScript - Fast, Simple, Fully Typed JavaScript from the Future #[derive(Parser, Debug)] #[command(version)] @@ -169,11 +179,29 @@ pub enum Command { #[command(flatten)] dev: DevArg, }, - /// Alias to `legacy format`. - #[command(disable_help_flag = true)] + /// Formats ReScript files. Format { - #[arg(allow_hyphen_values = true, num_args = 0..)] - format_args: Vec, + /// Format the whole project. + #[arg(short, long, group = "format_input_mode")] + all: bool, + + /// Check formatting status without applying changes. + #[arg(short, long)] + check: bool, + + /// Read the code from stdin and print the formatted code to stdout. + #[arg( + short, + long, + group = "format_input_mode", + value_enum, + conflicts_with = "check" + )] + stdin: Option, + + /// Files to format. + #[arg(group = "format_input_mode")] + files: Vec, }, /// Alias to `legacy dump`. #[command(disable_help_flag = true)] diff --git a/rewatch/src/format.rs b/rewatch/src/format.rs new file mode 100644 index 0000000000..2c64247907 --- /dev/null +++ b/rewatch/src/format.rs @@ -0,0 +1,127 @@ +use crate::helpers; +use anyhow::{Result, bail}; +use num_cpus; +use rayon::prelude::*; +use std::fs; +use std::io::{self, Write}; +use std::path::Path; +use std::process::Command; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use crate::build::packages; +use crate::cli::FileExtension; +use clap::ValueEnum; + +pub fn format( + stdin_extension: Option, + all: bool, + check: bool, + files: Vec, +) -> Result<()> { + let bsc_path = helpers::get_bsc(); + + match stdin_extension { + Some(extension) => { + format_stdin(&bsc_path, extension)?; + } + None => { + let files = if all { get_all_files()? } else { files }; + format_files(&bsc_path, files, check)?; + } + } + + Ok(()) +} + +fn get_all_files() -> Result> { + let current_dir = std::env::current_dir()?; + let project_root = helpers::get_abs_path(¤t_dir); + let workspace_root_option = helpers::get_workspace_root(&project_root); + + let build_state = packages::make(&None, &project_root, &workspace_root_option, false, false)?; + let mut files: Vec = Vec::new(); + + for (_package_name, package) in build_state { + if let Some(source_files) = package.source_files { + for (path, _metadata) in source_files { + if let Some(extension) = path.extension() { + if extension == "res" || extension == "resi" { + files.push(package.path.join(path).to_string_lossy().into_owned()); + } + } + } + } + } + Ok(files) +} + +fn format_stdin(bsc_exe: &Path, extension: FileExtension) -> Result<()> { + let extension_value = extension + .to_possible_value() + .ok_or(anyhow::anyhow!("Could not get extension arg value"))?; + + let mut temp_file = tempfile::Builder::new() + .suffix(extension_value.get_name()) + .tempfile()?; + io::copy(&mut io::stdin(), &mut temp_file)?; + let temp_path = temp_file.path(); + + let mut cmd = Command::new(bsc_exe); + cmd.arg("-format").arg(temp_path); + + let output = cmd.output()?; + + if output.status.success() { + io::stdout().write_all(&output.stdout)?; + } else { + let stderr_str = String::from_utf8_lossy(&output.stderr); + bail!("Error formatting stdin: {}", stderr_str); + } + + Ok(()) +} + +fn format_files(bsc_exe: &Path, files: Vec, check: bool) -> Result<()> { + let batch_size = 4 * num_cpus::get(); + let incorrectly_formatted_files = AtomicUsize::new(0); + + files.par_chunks(batch_size).try_for_each(|batch| { + batch.iter().try_for_each(|file| { + let mut cmd = Command::new(bsc_exe); + if check { + cmd.arg("-format").arg(file); + } else { + cmd.arg("-o").arg(file).arg("-format").arg(file); + } + + let output = cmd.output()?; + + if output.status.success() { + if check { + let original_content = fs::read_to_string(file)?; + let formatted_content = String::from_utf8_lossy(&output.stdout); + if original_content != formatted_content { + eprintln!("[format check] {}", file); + incorrectly_formatted_files.fetch_add(1, Ordering::SeqCst); + } + } + } else { + let stderr_str = String::from_utf8_lossy(&output.stderr); + bail!("Error formatting {}: {}", file, stderr_str); + } + Ok(()) + }) + })?; + + let count = incorrectly_formatted_files.load(Ordering::SeqCst); + if count > 0 { + if count == 1 { + eprintln!("The file listed above needs formatting"); + } else { + eprintln!("The {} files listed above need formatting", count); + } + bail!("Formatting check failed"); + } + + Ok(()) +} diff --git a/rewatch/src/lib.rs b/rewatch/src/lib.rs index 2df92a48f3..5b24c38886 100644 --- a/rewatch/src/lib.rs +++ b/rewatch/src/lib.rs @@ -2,6 +2,7 @@ pub mod build; pub mod cli; pub mod cmd; pub mod config; +pub mod format; pub mod helpers; pub mod lock; pub mod queue; diff --git a/rewatch/src/main.rs b/rewatch/src/main.rs index 827ab7be5f..cb9a57fcd2 100644 --- a/rewatch/src/main.rs +++ b/rewatch/src/main.rs @@ -3,7 +3,7 @@ use clap::Parser; use log::LevelFilter; use std::{io::Write, path::Path}; -use rewatch::{build, cli, cmd, lock, watcher}; +use rewatch::{build, cli, cmd, format, lock, watcher}; fn main() -> Result<()> { let args = cli::Cli::parse(); @@ -91,11 +91,12 @@ fn main() -> Result<()> { let code = build::pass_through_legacy(legacy_args); std::process::exit(code); } - cli::Command::Format { mut format_args } => { - format_args.insert(0, "format".into()); - let code = build::pass_through_legacy(format_args); - std::process::exit(code); - } + cli::Command::Format { + stdin, + all, + check, + files, + } => format::format(stdin, all, check, files), cli::Command::Dump { mut dump_args } => { dump_args.insert(0, "dump".into()); let code = build::pass_through_legacy(dump_args); diff --git a/rewatch/tests/format.sh b/rewatch/tests/format.sh new file mode 100755 index 0000000000..84d328dce4 --- /dev/null +++ b/rewatch/tests/format.sh @@ -0,0 +1,43 @@ +source "./utils.sh" +cd ../testrepo + +bold "Test: It should format all files" + +git diff --name-only ./ +error_output=$("$REWATCH_EXECUTABLE" format --all) +git_diff_file_count=$(git diff --name-only ./ | wc -l | xargs) +if [ $? -eq 0 ] && [ $git_diff_file_count -eq 4 ]; +then + success "Test package formatted. Got $git_diff_file_count changed files." + git restore . +else + error "Error formatting test package" + echo $error_output + exit 1 +fi + +bold "Test: It should format a single file" + +error_output=$("$REWATCH_EXECUTABLE" format packages/dep01/src/Dep01.res) +git_diff_file_count=$(git diff --name-only ./ | wc -l | xargs) +if [ $? -eq 0 ] && [ $git_diff_file_count -eq 1 ]; +then + success "Single file formatted successfully" + git restore . +else + error "Error formatting single file" + echo $error_output + exit 1 +fi + +bold "Test: It should format from stdin" + +error_output=$(echo "let x = 1" | "$REWATCH_EXECUTABLE" format --stdin .res) +if [ $? -eq 0 ]; +then + success "Stdin formatted successfully" +else + error "Error formatting from stdin" + echo $error_output + exit 1 +fi diff --git a/rewatch/tests/suite-ci.sh b/rewatch/tests/suite-ci.sh index 24ae688424..692000357e 100755 --- a/rewatch/tests/suite-ci.sh +++ b/rewatch/tests/suite-ci.sh @@ -40,4 +40,4 @@ else exit 1 fi -./compile.sh && ./watch.sh && ./lock.sh && ./suffix.sh && ./legacy.sh +./compile.sh && ./watch.sh && ./lock.sh && ./suffix.sh && ./legacy.sh && ./format.sh diff --git a/scripts/format.sh b/scripts/format.sh index 0b5ab33ac3..6bab80eed3 100755 --- a/scripts/format.sh +++ b/scripts/format.sh @@ -7,7 +7,7 @@ dune build @fmt --auto-promote echo Formatting ReScript code... files=$(find runtime tests -type f \( -name "*.res" -o -name "*.resi" \) ! -name "syntaxErrors*" ! -name "generated_mocha_test.res" ! -path "tests/syntax_tests*" ! -path "tests/analysis_tests/tests*" ! -path "*/node_modules/*") -./cli/rescript-legacy.js format $files +./cli/rescript.js format $files echo Formatting JS code... yarn format diff --git a/scripts/format_check.sh b/scripts/format_check.sh index 313ed72cfb..2c59497e92 100755 --- a/scripts/format_check.sh +++ b/scripts/format_check.sh @@ -18,7 +18,7 @@ case "$(uname -s)" in echo "Checking ReScript code formatting..." files=$(find runtime tests -type f \( -name "*.res" -o -name "*.resi" \) ! -name "syntaxErrors*" ! -name "generated_mocha_test.res" ! -path "tests/syntax_tests*" ! -path "tests/analysis_tests/tests*" ! -path "*/node_modules/*") - if ./cli/rescript-legacy.js format -check $files; then + if ./cli/rescript.js format --check $files; then printf "${successGreen}✅ ReScript code formatting ok.${reset}\n" else printf "${warningYellow}⚠️ ReScript code formatting issues found. Run 'make format' to fix.${reset}\n" From 8f2970bc48a2c60003592e3fc3b24f06a5f0fca3 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Thu, 17 Jul 2025 08:49:31 +0200 Subject: [PATCH 2/2] Files must be present unless --all or --stdin is set --- rewatch/src/cli.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rewatch/src/cli.rs b/rewatch/src/cli.rs index ff23ce284e..5967ffc9a7 100644 --- a/rewatch/src/cli.rs +++ b/rewatch/src/cli.rs @@ -200,7 +200,7 @@ pub enum Command { stdin: Option, /// Files to format. - #[arg(group = "format_input_mode")] + #[arg(group = "format_input_mode", required_unless_present_any = ["format_input_mode"])] files: Vec, }, /// Alias to `legacy dump`.