From 5ae230ea7b1082c919a2cc8fed2208ecd16b1e66 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Fri, 4 Jul 2025 16:52:13 +0200 Subject: [PATCH] Rust implementation of "rescript format" --- rewatch/Cargo.lock | 11 ++++ rewatch/Cargo.toml | 1 + rewatch/src/cli.rs | 16 +++-- rewatch/src/format_cmd.rs | 125 ++++++++++++++++++++++++++++++++++++++ rewatch/src/lib.rs | 1 + rewatch/src/main.rs | 13 ++-- scripts/format.sh | 2 +- scripts/format_check.sh | 2 +- 8 files changed, 159 insertions(+), 12 deletions(-) create mode 100644 rewatch/src/format_cmd.rs diff --git a/rewatch/Cargo.lock b/rewatch/Cargo.lock index 4573c83203..8a1a65c7c2 100644 --- a/rewatch/Cargo.lock +++ b/rewatch/Cargo.lock @@ -626,6 +626,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,6 +773,7 @@ dependencies = [ "log", "notify", "notify-debouncer-mini", + "num_cpus", "rayon", "regex", "serde", diff --git a/rewatch/Cargo.toml b/rewatch/Cargo.toml index 7d0d8b859c..d297177950 100644 --- a/rewatch/Cargo.toml +++ b/rewatch/Cargo.toml @@ -20,6 +20,7 @@ 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" diff --git a/rewatch/src/cli.rs b/rewatch/src/cli.rs index 6053d8cca2..0a1cc760b9 100644 --- a/rewatch/src/cli.rs +++ b/rewatch/src/cli.rs @@ -144,11 +144,19 @@ pub enum Command { #[command(flatten)] snapshot_output: SnapshotOutputArg, }, - /// Alias to `legacy format`. - #[command(disable_help_flag = true)] + /// Formats ReScript files. Format { - #[arg(allow_hyphen_values = true, num_args = 0..)] - format_args: Vec, + /// Read the code from stdin and print the formatted code to stdout. + #[arg(long)] + stdin: Option, + /// Format the whole project. + #[arg(short = 'a', long)] + all: bool, + /// Check formatting for file or the whole project. Use `--all` to check the whole project. + #[arg(short = 'c', long)] + check: bool, + /// Files to format. + files: Vec, }, /// Alias to `legacy dump`. #[command(disable_help_flag = true)] diff --git a/rewatch/src/format_cmd.rs b/rewatch/src/format_cmd.rs new file mode 100644 index 0000000000..bcd5b55d9b --- /dev/null +++ b/rewatch/src/format_cmd.rs @@ -0,0 +1,125 @@ +use crate::helpers; +use anyhow::{Result, bail}; +use num_cpus; +use rayon::prelude::*; +use std::fs; +use std::io::{self, Read, Write}; +use std::path::Path; +use std::process::Command; +use std::sync::atomic::{AtomicUsize, Ordering}; + +pub fn run(stdin_path: Option, all: bool, check: bool, files: Vec) -> Result<()> { + let bsc_path = helpers::get_bsc(); + + if check && stdin_path.is_some() { + bail!("format --stdin cannot be used with --check flag"); + } + + if all { + if stdin_path.is_some() || !files.is_empty() { + bail!("format --all can not be in use with other flags"); + } + format_all(&bsc_path, check)?; + } else if stdin_path.is_some() { + format_stdin(&bsc_path, stdin_path.unwrap())?; + } else { + format_files(&bsc_path, files, check)?; + } + + Ok(()) +} + +fn format_all(bsc_exe: &Path, check: bool) -> Result<()> { + let output = Command::new(std::env::current_exe()?) + .arg("info") + .arg("-list-files") + .output()?; + + if !output.status.success() { + io::stderr().write_all(&output.stderr)?; + bail!("Failed to list files"); + } + + let files_str = String::from_utf8_lossy(&output.stdout); + let files: Vec = files_str + .split('\n') + .filter(|s| !s.trim().is_empty()) + .map(|s| s.trim().to_string()) + .collect(); + + format_files(bsc_exe, files, check)?; + Ok(()) +} + +fn format_stdin(bsc_exe: &Path, stdin_path: String) -> Result<()> { + let mut input = String::new(); + io::stdin().read_to_string(&mut input)?; + + let mut cmd = Command::new(bsc_exe); + cmd.arg("-format").arg(&stdin_path); + cmd.stdin(std::process::Stdio::piped()); + cmd.stdout(std::process::Stdio::piped()); + cmd.stderr(std::process::Stdio::piped()); + + let mut child = cmd.spawn()?; + let mut stdin = child.stdin.take().unwrap(); + std::thread::spawn(move || { + stdin.write_all(input.as_bytes()).unwrap(); + }); + + let output = child.wait_with_output()?; + + if output.status.success() { + io::stdout().write_all(&output.stdout)?; + } else { + io::stderr().write_all(&output.stderr)?; + bail!("bsc exited with an error"); + } + + 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 { + io::stderr().write_all(&output.stderr)?; + bail!("bsc exited with an error for file {}", file); + } + Ok(()) as Result<()> + }) + })?; + + 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..f0fffd1152 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_cmd; pub mod helpers; pub mod lock; pub mod queue; diff --git a/rewatch/src/main.rs b/rewatch/src/main.rs index fb3f6c3516..9a4fd47b0f 100644 --- a/rewatch/src/main.rs +++ b/rewatch/src/main.rs @@ -4,7 +4,7 @@ use log::LevelFilter; use regex::Regex; use std::{io::Write, path::Path}; -use rewatch::{build, cli, cmd, lock, watcher}; +use rewatch::{build, cli, cmd, format_cmd, lock, watcher}; fn main() -> Result<()> { let args = cli::Cli::parse(); @@ -88,11 +88,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_cmd::run(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/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"