diff --git a/DEV.md b/DEV.md index aaa1b48..077b664 100644 --- a/DEV.md +++ b/DEV.md @@ -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. diff --git a/README.md b/README.md index 6c2a513..dcd7dda 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/src/main.rs b/src/main.rs index 7a3be80..628c555 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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 { - 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()); } @@ -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. diff --git a/tests/test_cli.py b/tests/test_cli.py index 5ea4fed..d971214 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -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', [