Skip to content

Commit eb7da76

Browse files
committed
Rust implementation of "rescript format"
1 parent ae01437 commit eb7da76

File tree

9 files changed

+217
-15
lines changed

9 files changed

+217
-15
lines changed

cli/rescript.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ try {
1818
subcommand === "build" ||
1919
subcommand === "watch" ||
2020
subcommand === "clean" ||
21-
subcommand === "compiler-args"
21+
subcommand === "compiler-args" ||
22+
subcommand === "format"
2223
) {
2324
child_process.execFileSync(
2425
rescript_exe,

rewatch/Cargo.lock

Lines changed: 18 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rewatch/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ log = { version = "0.4.17" }
2020
notify = { version = "5.1.0", features = ["serde"] }
2121
notify-debouncer-mini = { version = "0.2.0" }
2222
rayon = "1.6.1"
23+
num_cpus = "1.17.0"
2324
regex = "1.7.1"
2425
serde = { version = "1.0.152", features = ["derive"] }
2526
serde_derive = "1.0.152"

rewatch/src/cli.rs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -162,11 +162,21 @@ pub enum Command {
162162
#[command(flatten)]
163163
snapshot_output: SnapshotOutputArg,
164164
},
165-
/// Alias to `legacy format`.
166-
#[command(disable_help_flag = true)]
165+
/// Formats ReScript files.
167166
Format {
168-
#[arg(allow_hyphen_values = true, num_args = 0..)]
169-
format_args: Vec<OsString>,
167+
/// Read the code from stdin and print the formatted code to stdout.
168+
#[arg(long)]
169+
stdin: Option<String>,
170+
/// Format the whole project.
171+
#[arg(short = 'a', long)]
172+
all: bool,
173+
/// Check formatting for file or the whole project. Use `--all` to check the whole project.
174+
#[arg(short = 'c', long)]
175+
check: bool,
176+
/// Files to format.
177+
files: Vec<String>,
178+
#[command(flatten)]
179+
bsc_path: BscPathArg,
170180
},
171181
/// Alias to `legacy dump`.
172182
#[command(disable_help_flag = true)]

rewatch/src/format_cmd.rs

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
use std::sync::atomic::{AtomicUsize, Ordering};
2+
use anyhow::{bail, Result};
3+
use std::path::{Path, PathBuf};
4+
use std::process::Command;
5+
use std::io::{self, Read, Write};
6+
use std::fs;
7+
use rayon::prelude::*;
8+
use num_cpus;
9+
10+
pub fn run(
11+
stdin_path: Option<String>,
12+
all: bool,
13+
check: bool,
14+
files: Vec<String>,
15+
bsc_path_arg: Option<PathBuf>,
16+
) -> Result<()> {
17+
let bsc_exe = match bsc_path_arg {
18+
Some(path) => path,
19+
None => find_bsc_exe()?,
20+
};
21+
22+
if check && stdin_path.is_some() {
23+
bail!("format -stdin cannot be used with -check flag");
24+
}
25+
26+
if all {
27+
if stdin_path.is_some() || !files.is_empty() {
28+
bail!("format -all can not be in use with other flags");
29+
}
30+
format_all(&bsc_exe, check)?;
31+
} else if stdin_path.is_some() {
32+
format_stdin(&bsc_exe, stdin_path.unwrap())?;
33+
} else {
34+
format_files(&bsc_exe, files, check)?;
35+
}
36+
37+
Ok(())
38+
}
39+
40+
fn find_bsc_exe() -> Result<PathBuf> {
41+
let current_exe = std::env::current_exe()?;
42+
let mut current_dir = current_exe.parent().unwrap_or_else(|| Path::new("/"));
43+
44+
// Traverse up to find node_modules
45+
let node_modules_path = loop {
46+
let potential_path = current_dir.join("node_modules");
47+
if potential_path.exists() {
48+
break Some(potential_path);
49+
}
50+
if current_dir.parent().is_none() {
51+
break None;
52+
}
53+
current_dir = current_dir.parent().unwrap();
54+
}
55+
.ok_or_else(|| anyhow::anyhow!("Could not find node_modules directory"))?;
56+
57+
let target = format!("{}-{}", std::env::consts::OS, std::env::consts::ARCH);
58+
let bsc_path = node_modules_path
59+
.join("@rescript")
60+
.join(target)
61+
.join("bsc.exe");
62+
63+
if !bsc_path.exists() {
64+
bail!("bsc executable not found at {}", bsc_path.display());
65+
}
66+
Ok(bsc_path)
67+
}
68+
69+
fn format_all(bsc_exe: &Path, check: bool) -> Result<()> {
70+
let output = Command::new(std::env::current_exe()?)
71+
.arg("info")
72+
.arg("-list-files")
73+
.output()?;
74+
75+
if !output.status.success() {
76+
io::stderr().write_all(&output.stderr)?;
77+
bail!("Failed to list files");
78+
}
79+
80+
let files_str = String::from_utf8_lossy(&output.stdout);
81+
let files: Vec<String> = files_str
82+
.split('\n')
83+
.filter(|s| !s.trim().is_empty())
84+
.map(|s| s.trim().to_string())
85+
.collect();
86+
87+
format_files(bsc_exe, files, check)?;
88+
Ok(())
89+
}
90+
91+
fn format_stdin(bsc_exe: &Path, stdin_path: String) -> Result<()> {
92+
let mut input = String::new();
93+
io::stdin().read_to_string(&mut input)?;
94+
95+
let mut cmd = Command::new(bsc_exe);
96+
cmd.arg("-format").arg(&stdin_path);
97+
cmd.stdin(std::process::Stdio::piped());
98+
cmd.stdout(std::process::Stdio::piped());
99+
cmd.stderr(std::process::Stdio::piped());
100+
101+
let mut child = cmd.spawn()?;
102+
let mut stdin = child.stdin.take().unwrap();
103+
std::thread::spawn(move || {
104+
stdin.write_all(input.as_bytes()).unwrap();
105+
});
106+
107+
let output = child.wait_with_output()?;
108+
109+
if output.status.success() {
110+
io::stdout().write_all(&output.stdout)?;
111+
}
112+
else {
113+
io::stderr().write_all(&output.stderr)?;
114+
bail!("bsc exited with an error");
115+
}
116+
117+
Ok(())
118+
}
119+
120+
fn format_files(bsc_exe: &Path, files: Vec<String>, check: bool) -> Result<()> {
121+
let batch_size = 4 * num_cpus::get();
122+
let incorrectly_formatted_files = AtomicUsize::new(0);
123+
124+
files.par_chunks(batch_size).try_for_each(|batch| {
125+
batch.iter().try_for_each(|file| {
126+
let mut cmd = Command::new(bsc_exe);
127+
if check {
128+
cmd.arg("-format").arg(file);
129+
}
130+
else {
131+
cmd.arg("-o").arg(file).arg("-format").arg(file);
132+
}
133+
134+
let output = cmd.output()?;
135+
136+
if output.status.success() {
137+
if check {
138+
let original_content = fs::read_to_string(file)?;
139+
let formatted_content = String::from_utf8_lossy(&output.stdout);
140+
if original_content != formatted_content {
141+
eprintln!("[format check] {}", file);
142+
incorrectly_formatted_files.fetch_add(1, Ordering::SeqCst);
143+
}
144+
}
145+
}
146+
else {
147+
io::stderr().write_all(&output.stderr)?;
148+
bail!("bsc exited with an error for file {}", file);
149+
}
150+
Ok(()) as Result<()>
151+
})
152+
})?;
153+
154+
let count = incorrectly_formatted_files.load(Ordering::SeqCst);
155+
if count > 0 {
156+
if count == 1 {
157+
eprintln!("The file listed above needs formatting");
158+
}
159+
else {
160+
eprintln!(
161+
"The {} files listed above need formatting",
162+
count
163+
);
164+
}
165+
bail!("Formatting check failed");
166+
}
167+
168+
Ok(())
169+
}

rewatch/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ pub mod lock;
77
pub mod queue;
88
pub mod sourcedirs;
99
pub mod watcher;
10+
pub mod format_cmd;

rewatch/src/main.rs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use std::{
77
path::{Path, PathBuf},
88
};
99

10-
use rewatch::{build, cli, cmd, lock, watcher};
10+
use rewatch::{build, cli, cmd, lock, watcher, format_cmd};
1111

1212
fn main() -> Result<()> {
1313
let args = cli::Cli::parse();
@@ -20,7 +20,8 @@ fn main() -> Result<()> {
2020
.target(env_logger::fmt::Target::Stdout)
2121
.init();
2222

23-
let command = args.command.unwrap_or(cli::Command::Build(args.build_args));
23+
24+
let command = args.command.unwrap_or_else(|| cli::Command::Build(args.build_args));
2425

2526
// The 'normal run' mode will show the 'pretty' formatted progress. But if we turn off the log
2627
// level, we should never show that.
@@ -112,11 +113,13 @@ fn main() -> Result<()> {
112113
let code = build::pass_through_legacy(legacy_args);
113114
std::process::exit(code);
114115
}
115-
cli::Command::Format { mut format_args } => {
116-
format_args.insert(0, "format".into());
117-
let code = build::pass_through_legacy(format_args);
118-
std::process::exit(code);
119-
}
116+
cli::Command::Format {
117+
stdin,
118+
all,
119+
check,
120+
files,
121+
bsc_path,
122+
} => format_cmd::run(stdin, all, check, files, bsc_path.as_ref().map(|s| PathBuf::from(s.clone()))),
120123
cli::Command::Dump { mut dump_args } => {
121124
dump_args.insert(0, "dump".into());
122125
let code = build::pass_through_legacy(dump_args);

scripts/format.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@ shopt -s extglob
55
dune build @fmt --auto-promote
66

77
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/*")
8-
./cli/rescript-legacy.js format $files
8+
./cli/rescript.js format $files
99

1010
yarn format

scripts/format_check.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ case "$(uname -s)" in
1818

1919
echo "Checking ReScript code formatting..."
2020
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/*")
21-
if ./cli/rescript-legacy.js format -check $files; then
21+
if ./cli/rescript.js format --check $files; then
2222
printf "${successGreen}✅ ReScript code formatting ok.${reset}\n"
2323
else
2424
printf "${warningYellow}⚠️ ReScript code formatting issues found. Run 'make format' to fix.${reset}\n"

0 commit comments

Comments
 (0)