diff --git a/CHANGELOG.md b/CHANGELOG.md index 8af60f60dc6..12cc2db51af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,7 +39,7 @@ use std::num::{NonZeroU8, NonZeroU16, NonZeroU32, NonZeroU64}; ``` [style guide's version sorting algorithm]: https://doc.rust-lang.org/nightly/style-guide/#sorting -- When parsing rustfmt configurations fails, rustfmt will now include the path to the toml file in the erorr message [#6302](https://github.com/rust-lang/rustfmt/issues/6302) +- When parsing rustfmt configurations fails, rustfmt will now include the path to the toml file in the error message [#6302](https://github.com/rust-lang/rustfmt/issues/6302) ### Added - rustfmt now formats trailing where clauses in type aliases [#5887](https://github.com/rust-lang/rustfmt/pull/5887) @@ -133,7 +133,7 @@ ### Changed - `hide_parse_errors` has been soft deprecated and it's been renamed to `show_parse_errors` [#5961](https://github.com/rust-lang/rustfmt/pull/5961). -- The diff output produced by `rustfmt --check` is more compatable with editors that support navigating directly to line numbers [#5971](https://github.com/rust-lang/rustfmt/pull/5971) +- The diff output produced by `rustfmt --check` is more compatible with editors that support navigating directly to line numbers [#5971](https://github.com/rust-lang/rustfmt/pull/5971) - When using `version=Two`, the `trace!` macro from the [log crate] is now formatted similarly to `debug!`, `info!`, `warn!`, and `error!` [#5987](https://github.com/rust-lang/rustfmt/issues/5987). [log crate]: https://crates.io/crates/log diff --git a/Cargo.lock b/Cargo.lock index e2ceb668ebd..e3fc87feca4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,12 +13,12 @@ dependencies = [ [[package]] name = "annotate-snippets" -version = "0.9.1" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3b9d411ecbaf79885c6df4d75fff75858d5995ff25385657a28af47e82f9c36" +checksum = "24e35ed54e5ea7997c14ed4c70ba043478db1112e98263b3b035907aa197d991" dependencies = [ + "anstyle", "unicode-width", - "yansi-term", ] [[package]] @@ -37,9 +37,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.3" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b84bf0a05bbb2a83e5eb6fa36bb6e87baa08193c35ff52bbf6b38d8af2890e46" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anstyle-parse" @@ -426,18 +426,18 @@ checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" [[package]] name = "proc-macro2" -version = "1.0.63" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.26" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -514,6 +514,7 @@ dependencies = [ "itertools", "regex", "rustfmt-config_proc_macro", + "semver", "serde", "serde_json", "term", @@ -549,27 +550,27 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.7" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d65bd28f48be7196d222d95b9243287f48d27aca604e08497513019ff0502cc4" +checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0" dependencies = [ "serde", ] [[package]] name = "serde" -version = "1.0.160" +version = "1.0.196" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c" +checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.160" +version = "1.0.196" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df" +checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" dependencies = [ "proc-macro2", "quote", @@ -619,9 +620,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" -version = "2.0.14" +version = "2.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcf316d5356ed6847742d036f8a39c3b8435cac10bd528a4bd461928a6ab34d5" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" dependencies = [ "proc-macro2", "quote", @@ -772,9 +773,9 @@ checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" [[package]] name = "unicode-width" -version = "0.1.10" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "utf8parse" @@ -910,12 +911,3 @@ checksum = "ca0ace3845f0d96209f0375e6d367e3eb87eb65d27d445bdc9f1843a26f39448" dependencies = [ "memchr", ] - -[[package]] -name = "yansi-term" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe5c30ade05e61656247b2e334a031dfd0cc466fadef865bdcdea8d537951bf1" -dependencies = [ - "winapi", -] diff --git a/Cargo.toml b/Cargo.toml index e497b792342..07bc74a5127 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ rustfmt-format-diff = [] generic-simd = ["bytecount/generic-simd"] [dependencies] -annotate-snippets = { version = "0.9", features = ["color"] } +annotate-snippets = { version = "0.11" } anyhow = "1.0" bytecount = "0.6.8" cargo_metadata = "0.18" @@ -57,6 +57,7 @@ unicode-width = "0.1" unicode-properties = { version = "0.1", default-features = false, features = ["general-category"] } rustfmt-config_proc_macro = { version = "0.3", path = "config_proc_macro" } +semver = "1.0.21" # Rustc dependencies are loaded from the sysroot, Cargo doesn't know about them. diff --git a/Configurations.md b/Configurations.md index b1f54060392..67debec1ddb 100644 --- a/Configurations.md +++ b/Configurations.md @@ -537,14 +537,17 @@ Specifies which edition is used by the parser. - **Possible values**: `"2015"`, `"2018"`, `"2021"`, `"2024"` - **Stable**: Yes -Rustfmt is able to pick up the edition used by reading the `Cargo.toml` file if executed -through the Cargo's formatting tool `cargo fmt`. Otherwise, the edition needs to be specified -in your config file: +The `edition` option determines the Rust language edition used for parsing the code. This is important for syntax compatibility but does not directly control formatting behavior (see [style_edition](#style_edition)). + +When running `cargo fmt`, the `edition` is automatically read from the `Cargo.toml` file. However, when running `rustfmt` directly the `edition` defaults to 2015 if not explicitly configured. For consistent parsing between rustfmt and `cargo fmt` you should configure the `edition`. +For example in your `rustfmt.toml` file: ```toml edition = "2018" ``` +Alternatively, you can use the `--edition` flag when running `rustfmt` directly. + ## `empty_item_single_line` Put empty-body functions and impls on a single line @@ -1256,6 +1259,56 @@ Control the case of the letters in hexadecimal literal values - **Possible values**: `Preserve`, `Upper`, `Lower` - **Stable**: No (tracking issue: [#5081](https://github.com/rust-lang/rustfmt/issues/5081)) +## `float_literal_trailing_zero` + +Control the presence of trailing zero in floating-point literal values + +- **Default value**: `Preserve` +- **Possible values**: `Preserve`, `Always`, `IfNoPostfix`, `Never` +- **Stable**: No (tracking issue: [#6471](https://github.com/rust-lang/rustfmt/issues/6471)) + +#### `Preserve` (default): + +Leave the literal as-is. + +```rust +fn main() { + let values = [1.0, 2., 3.0e10, 4f32]; +} +``` + +#### `Always`: + +Add a trailing zero to the literal: + +```rust +fn main() { + let values = [1.0, 2.0, 3.0e10, 4.0f32]; +} +``` + +#### `IfNoPostfix`: + +Add a trailing zero by default. If the literal contains an exponent or a suffix, the zero +and the preceding period are removed: + +```rust +fn main() { + let values = [1.0, 2.0, 3e10, 4f32]; +} +``` + +#### `Never`: + +Remove the trailing zero. If the literal contains an exponent or a suffix, the preceding +period is also removed: + +```rust +fn main() { + let values = [1., 2., 3e10, 4f32]; +} +``` + ## `hide_parse_errors` This option is deprecated and has been renamed to `show_parse_errors` to avoid confusion around the double negative default of `hide_parse_errors=false`. @@ -2362,9 +2415,62 @@ Require a specific version of rustfmt. If you want to make sure that the specific version of rustfmt is used in your CI, use this option. - **Default value**: `CARGO_PKG_VERSION` -- **Possible values**: any published version (e.g. `"0.3.8"`) +- **Possible values**: `semver` compliant values, such as defined on [semver.org](https://semver.org/). - **Stable**: No (tracking issue: [#3386](https://github.com/rust-lang/rustfmt/issues/3386)) +#### Match on exact version: + +```toml +required_version="1.0.0" +``` + +#### Higher or equal to: + +```toml +required_version=">=1.0.0" +``` + +#### Lower or equal to: + +```toml +required_version="<=1.0.0" +``` + +#### New minor or patch versions: + +```toml +required_version="^1.0.0" +``` + +#### New patch versions: + +```toml +required_version="~1.0.0" +``` + +#### Wildcard: + +```toml +required_version="*" # matches any version. +required_version="1.*" # matches any version with the same major version +required_version="1.0.*" # matches any version with the same major and minor version +``` + +#### Multiple versions to match: + +A comma separated list of version requirements. +The match succeeds when the current rustfmt version matches all version requirements. + +The one notable exception is that a wildcard matching any version cannot be used in the list. +For example, `*, <1.0.0` will always fail. + +Additionally, the version match will always fail if any of the version requirements contradict themselves. +Some examples of contradictory requirements are `1.*, >2.0.0`, `1.0.*, >2.0.0` and `<1.5.0, >1.10.*`. + +```toml +required_version=">=1.0.0, <2.0.0" +``` + ## `short_array_element_width_threshold` The width threshold for an array element to be considered "short". @@ -2700,6 +2806,20 @@ Controls the edition of the [Rust Style Guide] to use for formatting ([RFC 3338] - **Possible values**: `"2015"`, `"2018"`, `"2021"`, `"2024"` (unstable variant) - **Stable**: No +This option is inferred from the [`edition`](#edition) if not specified. + +See [Rust Style Editions] for details on formatting differences between style editions. +rustfmt has a default style edition of `2015` while `cargo fmt` infers the style edition from the `edition` set in `Cargo.toml`. This can lead to inconsistencies between `rustfmt` and `cargo fmt` if the style edition is not explicitly configured. + +To ensure consistent formatting, it is recommended to specify the `style_edition` in a `rustfmt.toml` configuration file. For example: + +```toml +style_edition = "2024" +``` + +Alternatively, you can use the `--style-edition` flag when running `rustfmt` directly. + +[Rust Style Editions]: https://doc.rust-lang.org/nightly/style-guide/editions.html?highlight=editions#rust-style-editions [Rust Style Guide]: https://doc.rust-lang.org/nightly/style-guide/ [RFC 3338]: https://rust-lang.github.io/rfcs/3338-style-evolution.html @@ -3062,7 +3182,9 @@ fn main() { ## `version` -This option is deprecated and has been replaced by [`style_edition`](#style_edition) +This option is deprecated and has been replaced by [`style_edition`](#style_edition). +`version = "One"` is equivalent to `style_edition = "(2015|2018|2021)"` and +`version = "Two"` is equivalent to `style_edition = "2024"` - **Default value**: `One` - **Possible values**: `One`, `Two` @@ -3112,7 +3234,7 @@ Break comments to fit on the line Note that no wrapping will happen if: 1. The comment is the start of a markdown header doc comment -2. An URL was found in the comment +2. A URL was found in the comment - **Default value**: `false` - **Possible values**: `true`, `false` diff --git a/README.md b/README.md index b68a942e463..0fdd2a6b60a 100644 --- a/README.md +++ b/README.md @@ -170,12 +170,36 @@ See [GitHub page](https://rust-lang.github.io/rustfmt/) for details. ### Rust's Editions -Rustfmt is able to pick up the edition used by reading the `Cargo.toml` file if -executed through the Cargo's formatting tool `cargo fmt`. Otherwise, the edition -needs to be specified in `rustfmt.toml`, e.g., with `edition = "2018"`. +The `edition` option determines the Rust language edition used for parsing the code. This is important for syntax compatibility but does not directly control formatting behavior (see [Style Editions](#style-editions)). + +When running `cargo fmt`, the `edition` is automatically read from the `Cargo.toml` file. However, when running `rustfmt` directly the `edition` defaults to 2015 if not explicitly configured. For consistent parsing between rustfmt and `cargo fmt` you should configure the `edition`. +For example in your `rustfmt.toml` file: + +```toml +edition = "2018" +``` + +### Style Editions + +This option is inferred from the [`edition`](#rusts-editions) if not specified. + +See [Rust Style Editions] for details on formatting differences between style editions. +rustfmt has a default style edition of `2015` while `cargo fmt` infers the style edition from the `edition` set in `Cargo.toml`. This can lead to inconsistencies between `rustfmt` and `cargo fmt` if the style edition is not explicitly configured. + +To ensure consistent formatting, it is recommended to specify the `style_edition` in a `rustfmt.toml` configuration file. For example: + +```toml +style_edition = "2024" +``` +[Rust Style Editions]: https://doc.rust-lang.org/nightly/style-guide/editions.html?highlight=editions#rust-style-editions +[Rust Style Guide]: https://doc.rust-lang.org/nightly/style-guide/ +[RFC 3338]: https://rust-lang.github.io/rfcs/3338-style-evolution.html ## Tips +* To ensure consistent parsing between `cargo fmt` and `rustfmt`, you should configure the [`edition`](#rusts-editions) in your `rustfmt.toml` file. +* To ensure consistent formatting between `cargo fmt` and `rustfmt`, you should configure the [`style_edition`](#style-editions) in your `rustfmt.toml` file. + * For things you do not want rustfmt to mangle, use `#[rustfmt::skip]` * To prevent rustfmt from formatting a macro or an attribute, use `#[rustfmt::skip::macros(target_macro_name)]` or diff --git a/check_diff/Cargo.lock b/check_diff/Cargo.lock index 2abf5af2f98..95bdd32567b 100644 --- a/check_diff/Cargo.lock +++ b/check_diff/Cargo.lock @@ -77,9 +77,11 @@ name = "check_diff" version = "0.1.0" dependencies = [ "clap", + "diffy", "tempfile", "tracing", "tracing-subscriber", + "walkdir", ] [[package]] @@ -128,6 +130,15 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" +[[package]] +name = "diffy" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d3041965b7a63e70447ec818a46b1e5297f7fcae3058356d226c02750c4e6cb" +dependencies = [ + "nu-ansi-term 0.50.1", +] + [[package]] name = "errno" version = "0.3.9" @@ -205,6 +216,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "nu-ansi-term" +version = "0.50.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" +dependencies = [ + "windows-sys", +] + [[package]] name = "once_cell" version = "1.19.0" @@ -298,6 +318,15 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -402,7 +431,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ "matchers", - "nu-ansi-term", + "nu-ansi-term 0.46.0", "once_cell", "regex", "sharded-slab", @@ -431,6 +460,16 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "winapi" version = "0.3.9" @@ -447,6 +486,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/check_diff/Cargo.toml b/check_diff/Cargo.toml index 4ae8a5f1f3a..877735e4e39 100644 --- a/check_diff/Cargo.toml +++ b/check_diff/Cargo.toml @@ -9,5 +9,6 @@ edition = "2021" clap = { version = "4.4.2", features = ["derive"] } tracing = "0.1.37" tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } -[dev-dependencies] tempfile = "3" +walkdir = "2.5.0" +diffy = "0.4.0" diff --git a/check_diff/src/lib.rs b/check_diff/src/lib.rs index b83d67c8b6e..90dbd5cc729 100644 --- a/check_diff/src/lib.rs +++ b/check_diff/src/lib.rs @@ -1,11 +1,54 @@ +use diffy; use std::env; -use std::io; -use std::path::Path; -use std::process::Command; +use std::fmt::{Debug, Display}; +use std::io::{self, Write}; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; +use std::str::Utf8Error; use tracing::info; +use walkdir::WalkDir; +#[derive(Debug)] +pub enum CheckDiffError { + /// Git related errors + FailedGit(GitError), + /// Error for generic commands + FailedCommand(&'static str), + /// UTF8 related errors + FailedUtf8(Utf8Error), + /// Error for building rustfmt from source + FailedSourceBuild(&'static str), + /// Error when obtaining binary version + FailedBinaryVersioning(PathBuf), + /// Error when obtaining cargo version + FailedCargoVersion(&'static str), + IO(std::io::Error), +} + +impl From for CheckDiffError { + fn from(error: io::Error) -> Self { + CheckDiffError::IO(error) + } +} + +impl From for CheckDiffError { + fn from(error: GitError) -> Self { + CheckDiffError::FailedGit(error) + } +} + +impl From for CheckDiffError { + fn from(error: Utf8Error) -> Self { + CheckDiffError::FailedUtf8(error) + } +} + +#[derive(Debug)] pub enum GitError { FailedClone { stdout: Vec, stderr: Vec }, + FailedRemoteAdd { stdout: Vec, stderr: Vec }, + FailedFetch { stdout: Vec, stderr: Vec }, + FailedSwitch { stdout: Vec, stderr: Vec }, IO(std::io::Error), } @@ -15,6 +58,142 @@ impl From for GitError { } } +pub struct Diff { + src_format: String, + feature_format: String, +} + +impl Display for Diff { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let patch = diffy::create_patch(self.src_format.as_str(), self.feature_format.as_str()); + write!(f, "{}", patch) + } +} + +impl Diff { + pub fn is_empty(&self) -> bool { + let patch = diffy::create_patch(self.src_format.as_str(), self.feature_format.as_str()); + patch.hunks().is_empty() + } +} + +pub struct CheckDiffRunners { + feature_runner: F, + src_runner: S, +} + +pub trait CodeFormatter { + fn format_code<'a>( + &self, + code: &'a str, + config: &Option>, + ) -> Result; +} + +pub struct RustfmtRunner { + ld_library_path: String, + binary_path: PathBuf, +} + +impl CheckDiffRunners { + pub fn new(feature_runner: F, src_runner: S) -> Self { + Self { + feature_runner, + src_runner, + } + } +} + +impl CheckDiffRunners +where + F: CodeFormatter, + S: CodeFormatter, +{ + /// Creates a diff generated by running the source and feature binaries on the same file path + pub fn create_diff( + &self, + path: &Path, + additional_configs: &Option>, + ) -> Result { + let code = std::fs::read_to_string(path)?; + let src_format = self.src_runner.format_code(&code, additional_configs)?; + let feature_format = self.feature_runner.format_code(&code, additional_configs)?; + Ok(Diff { + src_format, + feature_format, + }) + } +} + +impl RustfmtRunner { + fn get_binary_version(&self) -> Result { + let Ok(command) = Command::new(&self.binary_path) + .env("LD_LIBRARY_PATH", &self.ld_library_path) + .args(["--version"]) + .output() + else { + return Err(CheckDiffError::FailedBinaryVersioning( + self.binary_path.clone(), + )); + }; + + let binary_version = std::str::from_utf8(&command.stdout)?.trim(); + return Ok(binary_version.to_string()); + } +} + +impl CodeFormatter for RustfmtRunner { + // Run rusfmt to see if a diff is produced. Runs on the code specified + // + // Parameters: + // code: Code to run the binary on + // config: Any additional configuration options to pass to rustfmt + // + fn format_code<'a>( + &self, + code: &'a str, + config: &Option>, + ) -> Result { + let config = create_config_arg(config); + let mut command = Command::new(&self.binary_path) + .env("LD_LIBRARY_PATH", &self.ld_library_path) + .args([ + "--unstable-features", + "--skip-children", + "--emit=stdout", + config.as_str(), + ]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn()?; + + command.stdin.as_mut().unwrap().write_all(code.as_bytes())?; + let output = command.wait_with_output()?; + Ok(std::str::from_utf8(&output.stdout)?.to_string()) + } +} + +/// Creates a configuration in the following form: +/// =, =, ... +fn create_config_arg(config: &Option>) -> String { + let config_arg: String = match config { + Some(configs) => { + let mut result = String::new(); + for arg in configs.iter() { + result.push(','); + result.push_str(arg.as_str()); + } + result + } + None => String::new(), + }; + let config = format!( + "--config=error_on_line_overflow=false,error_on_unformatted=false{}", + config_arg.as_str() + ); + config +} /// Clone a git repository /// /// Parameters: @@ -47,6 +226,62 @@ pub fn clone_git_repo(url: &str, dest: &Path) -> Result<(), GitError> { return Ok(()); } +pub fn git_remote_add(url: &str) -> Result<(), GitError> { + let git_cmd = Command::new("git") + .args(["remote", "add", "feature", url]) + .output()?; + + // if the git command does not return successfully, + // any command on the repo will fail. So fail fast. + if !git_cmd.status.success() { + let error = GitError::FailedRemoteAdd { + stdout: git_cmd.stdout, + stderr: git_cmd.stderr, + }; + return Err(error); + } + + info!("Successfully added remote: {url}"); + return Ok(()); +} + +pub fn git_fetch(branch_name: &str) -> Result<(), GitError> { + let git_cmd = Command::new("git") + .args(["fetch", "feature", branch_name]) + .output()?; + + // if the git command does not return successfully, + // any command on the repo will fail. So fail fast. + if !git_cmd.status.success() { + let error = GitError::FailedFetch { + stdout: git_cmd.stdout, + stderr: git_cmd.stderr, + }; + return Err(error); + } + + info!("Successfully fetched: {branch_name}"); + return Ok(()); +} + +pub fn git_switch(git_ref: &str, should_detach: bool) -> Result<(), GitError> { + let detach_arg = if should_detach { "--detach" } else { "" }; + let args = ["switch", git_ref, detach_arg]; + let output = Command::new("git") + .args(args.iter().filter(|arg| !arg.is_empty())) + .output()?; + if !output.status.success() { + tracing::error!("Git switch failed: {output:?}"); + let error = GitError::FailedSwitch { + stdout: output.stdout, + stderr: output.stderr, + }; + return Err(error); + } + info!("Successfully switched to {git_ref}"); + return Ok(()); +} + pub fn change_directory_to_path(dest: &Path) -> io::Result<()> { let dest_path = Path::new(&dest); env::set_current_dir(&dest_path)?; @@ -56,3 +291,147 @@ pub fn change_directory_to_path(dest: &Path) -> io::Result<()> { ); return Ok(()); } + +pub fn get_ld_library_path(dir: &Path) -> Result { + let Ok(command) = Command::new("rustc") + .current_dir(dir) + .args(["--print", "sysroot"]) + .output() + else { + return Err(CheckDiffError::FailedCommand("Error getting sysroot")); + }; + let sysroot = std::str::from_utf8(&command.stdout)?.trim_end(); + let ld_lib_path = format!("{}/lib", sysroot); + return Ok(ld_lib_path); +} + +pub fn get_cargo_version() -> Result { + let Ok(command) = Command::new("cargo").args(["--version"]).output() else { + return Err(CheckDiffError::FailedCargoVersion( + "Failed to obtain cargo version", + )); + }; + + let cargo_version = std::str::from_utf8(&command.stdout)?.trim_end(); + return Ok(cargo_version.to_string()); +} + +/// Obtains the ld_lib path and then builds rustfmt from source +/// If that operation succeeds, the source is then copied to the output path specified +pub fn build_rustfmt_from_src( + binary_path: PathBuf, + dir: &Path, +) -> Result { + //Because we're building standalone binaries we need to set `LD_LIBRARY_PATH` so each + // binary can find it's runtime dependencies. + // See https://github.com/rust-lang/rustfmt/issues/5675 + // This will prepend the `LD_LIBRARY_PATH` for the master rustfmt binary + let ld_lib_path = get_ld_library_path(&dir)?; + + info!("Building rustfmt from source"); + let Ok(_) = Command::new("cargo") + .current_dir(dir) + .args(["build", "-q", "--release", "--bin", "rustfmt"]) + .output() + else { + return Err(CheckDiffError::FailedSourceBuild( + "Error building rustfmt from source", + )); + }; + + std::fs::copy(dir.join("target/release/rustfmt"), &binary_path)?; + + return Ok(RustfmtRunner { + ld_library_path: ld_lib_path, + binary_path, + }); +} + +// Compiles and produces two rustfmt binaries. +// One for the current master, and another for the feature branch +// Parameters: +// dest: Directory where rustfmt will be cloned +pub fn compile_rustfmt( + dest: &Path, + remote_repo_url: String, + feature_branch: String, + commit_hash: Option, +) -> Result, CheckDiffError> { + const RUSTFMT_REPO: &str = "https://github.com/rust-lang/rustfmt.git"; + + clone_git_repo(RUSTFMT_REPO, dest)?; + change_directory_to_path(dest)?; + git_remote_add(remote_repo_url.as_str())?; + git_fetch(feature_branch.as_str())?; + + let cargo_version = get_cargo_version()?; + info!("Compiling with {}", cargo_version); + let src_runner = build_rustfmt_from_src(dest.join("src_rustfmt"), dest)?; + let should_detach = commit_hash.is_some(); + git_switch( + commit_hash.unwrap_or(feature_branch).as_str(), + should_detach, + )?; + + let feature_runner = build_rustfmt_from_src(dest.join("feature_rustfmt"), dest)?; + info!("RUSFMT_BIN {}", src_runner.get_binary_version()?); + info!( + "Runtime dependencies for (src) rustfmt -- LD_LIBRARY_PATH: {}", + src_runner.ld_library_path + ); + info!("FEATURE_BIN {}", feature_runner.get_binary_version()?); + info!( + "Runtime dependencies for (feature) rustfmt -- LD_LIBRARY_PATH: {}", + feature_runner.ld_library_path + ); + + return Ok(CheckDiffRunners { + src_runner, + feature_runner, + }); +} + +/// Searches for rust files in the particular path and returns an iterator to them. +pub fn search_for_rs_files(repo: &Path) -> impl Iterator { + return WalkDir::new(repo).into_iter().filter_map(|e| match e.ok() { + Some(entry) => { + let path = entry.path(); + if path.is_file() && path.extension().map_or(false, |ext| ext == "rs") { + return Some(entry.into_path()); + } + return None; + } + None => None, + }); +} + +/// Calculates the number of errors when running the compiled binary and the feature binary on the +/// repo specified with the specific configs. +pub fn check_diff( + config: Option>, + runners: CheckDiffRunners, + repo: &Path, +) -> i32 { + let mut errors = 0; + let iter = search_for_rs_files(repo); + for file in iter { + match runners.create_diff(file.as_path(), &config) { + Ok(diff) => { + if !diff.is_empty() { + eprint!("{diff}"); + errors += 1; + } + } + Err(e) => { + eprintln!( + "Error creating diff for {:?}: {:?}", + file.as_path().display(), + e + ); + errors += 1; + } + } + } + + return errors; +} diff --git a/check_diff/src/main.rs b/check_diff/src/main.rs index 01c5926c490..f4ce3572faa 100644 --- a/check_diff/src/main.rs +++ b/check_diff/src/main.rs @@ -1,4 +1,7 @@ +use check_diff::{CheckDiffError, check_diff, compile_rustfmt}; use clap::Parser; +use tempfile::Builder; +use tracing::info; /// Inputs for the check_diff script #[derive(Parser)] @@ -16,6 +19,22 @@ struct CliInputs { rustfmt_config: Option>, } -fn main() { - let _args = CliInputs::parse(); +fn main() -> Result<(), CheckDiffError> { + tracing_subscriber::fmt() + .with_env_filter(tracing_subscriber::EnvFilter::from_env("CHECK_DIFF_LOG")) + .init(); + let args = CliInputs::parse(); + let tmp_dir = Builder::new().tempdir_in("").unwrap(); + info!("Created tmp_dir {:?}", tmp_dir); + let check_diff_runners = compile_rustfmt( + tmp_dir.path(), + args.remote_repo_url, + args.feature_branch, + args.commit_hash, + )?; + + // TODO: currently using same tmp dir path for sake of compilation + let _ = check_diff(args.rustfmt_config, check_diff_runners, tmp_dir.path()); + + Ok(()) } diff --git a/check_diff/tests/check_diff.rs b/check_diff/tests/check_diff.rs new file mode 100644 index 00000000000..17297c13043 --- /dev/null +++ b/check_diff/tests/check_diff.rs @@ -0,0 +1,96 @@ +use check_diff::{ + CheckDiffError, CheckDiffRunners, CodeFormatter, check_diff, search_for_rs_files, +}; +use std::fs::File; +use tempfile::Builder; + +struct DoNothingFormatter; + +impl CodeFormatter for DoNothingFormatter { + fn format_code<'a>( + &self, + _code: &'a str, + _config: &Option>, + ) -> Result { + Ok(String::new()) + } +} + +/// Formatter that adds a white space to the end of the codd +struct AddWhiteSpaceFormatter; + +impl CodeFormatter for AddWhiteSpaceFormatter { + fn format_code<'a>( + &self, + code: &'a str, + _config: &Option>, + ) -> Result { + let result = code.to_string() + " "; + Ok(result) + } +} + +#[test] +fn search_for_files_correctly_non_nested() -> Result<(), Box> { + let dir = Builder::new().tempdir_in("").unwrap(); + let file_path = dir.path().join("test.rs"); + let _tmp_file = File::create(file_path)?; + + let iter = search_for_rs_files(dir.path()); + + let mut count = 0; + for _ in iter { + count += 1; + } + + assert_eq!(count, 1); + + Ok(()) +} + +#[test] +fn search_for_files_correctly_nested() -> Result<(), Box> { + let dir = Builder::new().tempdir_in("").unwrap(); + let file_path = dir.path().join("test.rs"); + let _tmp_file = File::create(file_path)?; + + let nested_dir = Builder::new().tempdir_in(dir.path()).unwrap(); + let nested_file_path = nested_dir.path().join("nested.rs"); + let _ = File::create(nested_file_path)?; + + let iter = search_for_rs_files(dir.path()); + + let mut count = 0; + for _ in iter { + count += 1; + } + + assert_eq!(count, 2); + + Ok(()) +} + +#[test] +fn check_diff_test_no_formatting_difference() -> Result<(), CheckDiffError> { + let runners = CheckDiffRunners::new(DoNothingFormatter, DoNothingFormatter); + + let dir = Builder::new().tempdir_in("").unwrap(); + let file_path = dir.path().join("test.rs"); + let _tmp_file = File::create(file_path)?; + + let errors = check_diff(None, runners, dir.path()); + assert_eq!(errors, 0); + Ok(()) +} + +#[test] +fn check_diff_test_formatting_difference() -> Result<(), CheckDiffError> { + let runners = CheckDiffRunners::new(DoNothingFormatter, AddWhiteSpaceFormatter); + let dir = Builder::new().tempdir_in("").unwrap(); + let file_path = dir.path().join("test.rs"); + let _tmp_file = File::create(file_path)?; + + let errors = check_diff(None, runners, dir.path()); + assert_ne!(errors, 0); + Ok(()) +} diff --git a/docs/index.html b/docs/index.html index 5e588d1db54..38a978b8251 100644 --- a/docs/index.html +++ b/docs/index.html @@ -108,7 +108,6 @@