From 2c8348c4fef5261f528e67234b4f77f3eac4a2c0 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Wed, 9 Sep 2026 13:16:33 +1000 Subject: [PATCH] Resolve Rust editions through Cargo workspace metadata --- Cargo.toml | 1 + DEV.md | 5 +++++ src/main.rs | 32 ++++++++++++++++++++++++++++---- tests/test_cli.py | 32 +++++++++++++++++++++++++++++++- 4 files changed, 65 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a2402fc..3715ac8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ name = "cargo-fastfmt" path = "src/main.rs" [dependencies] +serde_json = "1.0.151" tree-sitter = "0.25" tree-sitter-rust = "0.24" diff --git a/DEV.md b/DEV.md index ad6b679..bca1813 100644 --- a/DEV.md +++ b/DEV.md @@ -8,6 +8,11 @@ The tuning benchmark is AnswerDotAI/loopmini: run stable `cargo fmt` on its `src ## Commands +Edition lookup uses offline `cargo metadata --no-deps`, caching the resolved +editions for all workspace packages. This respects workspace inheritance, member +overrides, and Cargo's default edition without reimplementing manifest parsing. +Standalone Rust files outside a Cargo package, including files under a virtual workspace root, retain the 2021 fallback. + ```bash maturin develop && pytest -q ship-rs-build diff --git a/src/main.rs b/src/main.rs index fb14ce3..2a2caeb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ //! width cap, and small (code blocks: 1 expression, fn/impl: 1 item, //! comma-separated bodies: 3 items). Joins run innermost-first to a fixpoint, so //! nested one-liners collapse fully. +use std::collections::HashMap; use std::io::Write; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; @@ -158,8 +159,7 @@ fn fastfmt(src: &str, edition: &str, width: usize) -> Result { Ok(compact(&String::from_utf8_lossy(&out.stdout), width)) } -/// The value at `key` in the nearest `file` at or above `path`, e.g. the edition -/// from Cargo.toml or max_width from rustfmt.toml. +/// The value at `key` in the nearest config file at or above `path`. fn toml_lookup(path: &Path, file: &str, key: &str) -> Option { for dir in path.canonicalize().unwrap_or_else(|_| path.to_path_buf()).ancestors() { if let Ok(t) = std::fs::read_to_string(dir.join(file)) { @@ -169,7 +169,30 @@ fn toml_lookup(path: &Path, file: &str, key: &str) -> Option { None } -fn edition_for(path: &Path) -> String { toml_lookup(path, "Cargo.toml", "edition").unwrap_or_else(|| "2021".into()) } +/// Let Cargo resolve editions, including workspace inheritance and its 2015 default. +/// Cache every package from the response so a workspace only needs one lookup. +fn edition_for(path: &Path, editions: &mut HashMap) -> Result { + let path = path.canonicalize().map_err(|e| e.to_string())?; + let Some(dir) = path.ancestors().find(|p| p.join("Cargo.toml").is_file()) else { return Ok("2021".into()) }; + let manifest = dir.join("Cargo.toml"); + if let Some(edition) = editions.get(&manifest) { return Ok(edition.clone()); } + let out = Command::new("cargo") + .args(["metadata", "--no-deps", "--offline", "--format-version", "1", "--manifest-path"]) + .arg(&manifest) + .output() + .map_err(|e| format!("failed to run cargo metadata: {e}"))?; + if !out.status.success() { return Err(String::from_utf8_lossy(&out.stderr).into_owned()); } + let metadata: serde_json::Value = serde_json::from_slice(&out.stdout).map_err(|e| format!("invalid cargo metadata: {e}"))?; + let packages = metadata["packages"].as_array().ok_or("cargo metadata has no packages")?; + for package in packages { + let manifest = package["manifest_path"].as_str().ok_or("cargo package has no manifest_path")?; + let edition = package["edition"].as_str().ok_or("cargo package has no edition")?; + let manifest = Path::new(manifest).canonicalize().map_err(|e| e.to_string())?; + editions.insert(manifest, edition.into()); + } + // A virtual workspace manifest owns no package: this is a standalone file. + Ok(editions.get(&manifest).cloned().unwrap_or_else(|| "2021".into())) +} /// `--width` beats the nearest rustfmt.toml's max_width, which beats the default. fn width_for(path: &Path, flag: Option) -> usize { @@ -263,6 +286,7 @@ fn main() { for p in &paths { rs_files(p, &mut files) } files.sort(); let mut dirty = vec![]; + let mut editions = HashMap::new(); for f in &files { let src = match std::fs::read_to_string(f) { Ok(s) => s, @@ -271,7 +295,7 @@ fn main() { std::process::exit(2) } }; - match fastfmt(&src, &edition_for(f), width_for(f, width_flag)) { + match edition_for(f, &mut editions).and_then(|edition| fastfmt(&src, &edition, width_for(f, width_flag))) { Ok(new) if new != src => { if check { dirty.push(f) } else if let Err(e) = std::fs::write(f, &new) { eprintln!("{}: {e}", f.display()); diff --git a/tests/test_cli.py b/tests/test_cli.py index 5fdf9a6..5f1a1c7 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,5 +1,5 @@ "End-to-end check of the installed binary: rustfmt runs, then compaction joins." -import subprocess +import subprocess, pytest BROKEN ="""fn time() -> f64 { 42.0 @@ -68,3 +68,33 @@ def test_fastfmt_binary(tmp_path): assert f.read_text() == BROKEN r = subprocess.run(['cargo-fastfmt', '--check'], cwd=tmp_path, capture_output=True, text=True) assert r.returncode == 1 and 'main.rs' in r.stdout + + +@pytest.mark.parametrize('edition,source', [ + ('edition.workspace = true', 'fn f(x: Option) { if let Some(x) = x && x > 0 { drop(x); } }'), + ('edition = { workspace = true }', 'fn f(x: Option) { if let Some(x) = x && x > 0 { drop(x); } }'), + ("edition = '2021' # member override", 'fn gen() {}'), + ('', 'fn async() {}'), # Cargo defaults to 2015, not the workspace edition +]) +def test_workspace_editions(tmp_path, edition, source): + (tmp_path/'Cargo.toml').write_text('[workspace]\nmembers = ["py"]\nresolver = "2"\n' + '[workspace.package]\nedition = "2024"\n') + member = tmp_path/'py' + (member/'src').mkdir(parents=True) + (member/'Cargo.toml').write_text('[package]\nname = "bindings"\nversion = "0.1.0"\n' + edition + '\n') + (member/'src/lib.rs').write_text(source + '\n') + subprocess.run(['cargo', 'fastfmt'], cwd=tmp_path, check=True) + subprocess.run(['cargo', 'fastfmt', '--check'], cwd=tmp_path, check=True) + + +def test_standalone_file_in_virtual_workspace(tmp_path): + (tmp_path/'Cargo.toml').write_text('[workspace]\nmembers = ["member"]\nresolver = "2"\n') + src = tmp_path/'member/src' + src.mkdir(parents=True) + (src.parent/'Cargo.toml').write_text('[package]\nname = "member"\nversion = "0.1.0"\nedition = "2021"\n') + (src/'lib.rs').write_text('fn member() {}\n') + script = tmp_path/'script.rs' + script.write_text('async fn main() {\n work().await;\n}\n') + subprocess.run(['cargo-fastfmt'], cwd=tmp_path, check=True) + assert script.read_text() == 'async fn main() { work().await; }\n' + subprocess.run(['cargo-fastfmt', '--check'], cwd=tmp_path, check=True)