Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion DEV.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Design

Everything lives in `src/main.rs`. `fastfmt()` pipes a file through `rustfmt --emit stdout` (width and `use_small_heuristics=Max` passed via `--config`, so the project's `disable_all_formatting` guard is overridden), then `compact()` re-joins. `guard_rustfmt()` creates or updates that guard before formatting. Tree-sitter supplies syntax boundaries. `shape` decides how a node may compact: a `Body` joins whole, a `List` packs inside its first and last lines, and a `Run` packs throughout. `compacted` renders one candidate. `compact_round` applies nonoverlapping candidates top-down, repeating until stable. Each accepted edit removes a source newline, so convergence does not need a round limit. After compaction, `format_elses` places `else` branches. README's "Joining rules" section states the rules these functions implement.
Everything lives in `src/main.rs`. `fastfmt()` pipes a file through `rustfmt --emit stdout` (width, `use_small_heuristics=Max` and `use_field_init_shorthand=true` passed via `--config`, so the project's `disable_all_formatting` guard is overridden), then `compact()` re-joins. `guard_rustfmt()` creates or updates that guard before formatting. Tree-sitter supplies syntax boundaries. `shape` decides how a node may compact: a `Body` joins whole, a `List` packs inside its first and last lines, and a `Run` packs throughout. `compacted` renders one candidate. `compact_round` applies nonoverlapping candidates top-down, repeating until stable. Each accepted edit removes a source newline, so convergence does not need a round limit. After compaction, `format_elses` places `else` branches. README's "Joining rules" section states the rules these functions implement.

The tuning benchmark is AnswerDotAI/loopmini: run stable `cargo fmt` on its `src/`, then `cargo-fastfmt`, and diff against the original. `COMPACT_WIDTH` (120) is the body budget, and also the budget for the call or chain enclosing a joined closure body.

Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ Traversal respects `.gitignore`, `.git/info/exclude`, global Git ignores, and `.

`--width N` sets the hard line cap, defaulting to 160. Signatures, imports, lists and expression continuations pack up to that cap. Each joined body must also fit a budget of `min(120, N)` by itself, not counting the signature or condition before it. A closure body joins only when its enclosing call or chain also fits that budget.

On a normal run, fastfmt creates or updates each formatted project's `rustfmt.toml` with `disable_all_formatting = true`, unless that config is ignored. This prevents `cargo fmt` or editor rustfmt runs from undoing the compact formatting. fastfmt overrides the setting for its own pass. No pre-existing config file is needed.
fastfmt runs rustfmt with `use_small_heuristics = "Max"` and `use_field_init_shorthand = true`, which writes `Foo { x: x }` as `Foo { x }`. These settings override the project's `rustfmt.toml`.

On a normal run, fastfmt adds `disable_all_formatting = true` to the `rustfmt.toml` that rustfmt reads for each formatted package: the nearest one from the package directory up to the repository root. If there is none, fastfmt creates one in the package directory. This prevents `cargo fmt` or editor rustfmt runs from undoing the compact formatting. fastfmt overrides the setting for its own pass. No pre-existing config file is needed.

`--check` never writes the config. It reports a missing or disabled guard as a required update.

Expand Down
14 changes: 9 additions & 5 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -377,9 +377,10 @@ fn join_comment_lines(src: &str) -> String {

/// Format `src` with rustfmt (stdin mode, sharing our width cap), then compact.
fn fastfmt(src: &str, edition: &str, width: usize, join_comments: bool) -> Result<String, String> {
let mut child = Command::new("rustfmt")
.args(["--edition", edition, "--emit", "stdout", "--config", &format!("max_width={width},use_small_heuristics=Max,disable_all_formatting=false")])
.stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::piped()).spawn().map_err(|e| format!("failed to run rustfmt: {e}"))?;
let mut child = Command::new("rustfmt").args([
"--edition", edition, "--emit", "stdout", "--config",
&format!("max_width={width},use_small_heuristics=Max,use_field_init_shorthand=true,disable_all_formatting=false"),
]).stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::piped()).spawn().map_err(|e| format!("failed to run rustfmt: {e}"))?;
child.stdin.take().unwrap().write_all(src.as_bytes()).map_err(|e| e.to_string())?;
let out = child.wait_with_output().map_err(|e| e.to_string())?;
if !out.status.success() { return Err(String::from_utf8_lossy(&out.stderr).into_owned()); }
Expand Down Expand Up @@ -458,11 +459,14 @@ fn guarded_config(src: &str) -> String {
out
}

/// The guard config for `file`: `rustfmt.toml` in its package directory, or beside it outside a package.
/// The `rustfmt.toml` that rustfmt reads for `file`'s package, searching up to the repository root, or a new one in the package directory.
fn config_path(file: &Path) -> PathBuf {
let dir = file.parent().unwrap();
let dir = dir.canonicalize().unwrap_or_else(|_| dir.to_path_buf());
package_dir(&dir).unwrap_or(&dir).join("rustfmt.toml")
let package = package_dir(&dir).unwrap_or(&dir);
let repo = package.ancestors().find(|p| p.join(".git").exists()).unwrap_or(package);
let mut configs = package.ancestors().take_while(|p| p.starts_with(repo)).map(|p| p.join("rustfmt.toml"));
configs.find(|p| p.is_file()).unwrap_or_else(|| package.join("rustfmt.toml"))
}

/// Ensure ordinary rustfmt is disabled for each target project. Returns paths which needed an update; in check mode they are reported but left untouched.
Expand Down
7 changes: 6 additions & 1 deletion tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,18 @@ def test_workspace_editions(tmp_path):
subprocess.run(['cargo', 'fastfmt', '--check'], cwd=tmp_path, check=True)


def test_standalone_file_in_virtual_workspace(tmp_path):
def test_virtual_workspace(tmp_path):
subprocess.run(['git', 'init', '-q', str(tmp_path)], check=True)
workspace(tmp_path, ['member'])
(tmp_path/'rustfmt.toml').write_text('max_width = 120\n')
package(tmp_path/'member', 'fn member() {}\n')
script = tmp_path/'script.rs'
script.write_text('async fn main() {\n work().await;\n}\n')
formats_clean(tmp_path)
assert script.read_text() == 'async fn main() { work().await; }\n'
# rustfmt reads the root config for the member as well.
assert (tmp_path/'rustfmt.toml').read_text() == 'max_width = 120\ndisable_all_formatting = true\n'
assert not (tmp_path/'member/rustfmt.toml').exists()


@pytest.mark.parametrize('ws_setting,pkg_setting,enabled', [
Expand Down
Loading