From 328f453c2c0d431581b02bf2261f3fd037e48cef Mon Sep 17 00:00:00 2001 From: ologbonowiwi Date: Tue, 25 Feb 2025 22:52:18 -0300 Subject: [PATCH] feat: use `semver` to match required version (#6066) implement `semver` checks for rustfmt's `required_version` config --- Cargo.lock | 25 +-- Cargo.toml | 1 + Configurations.md | 55 +++++- src/config/mod.rs | 428 +++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 492 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6486468a2fa..e3fc87feca4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index f119cde5257..07bc74a5127 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 e27c40ff0df..8e04ddd325e 100644 --- a/Configurations.md +++ b/Configurations.md @@ -2412,9 +2412,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". diff --git a/src/config/mod.rs b/src/config/mod.rs index 439a5765ff9..742b99c15ef 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -236,6 +236,43 @@ impl PartialConfig { } } +fn check_semver_version(range_requirement: &str, actual: &str) -> bool { + let mut version_req = match semver::VersionReq::parse(range_requirement) { + Ok(r) => r, + Err(e) => { + eprintln!("Error: failed to parse required version {range_requirement:?}: {e}"); + return false; + } + }; + let actual_version = match semver::Version::parse(actual) { + Ok(v) => v, + Err(e) => { + eprintln!("Error: failed to parse current version {actual:?}: {e}"); + return false; + } + }; + + range_requirement + .split(',') + .enumerate() + .for_each(|(i, label)| { + // the label refers to the current comparator + let Some(comparator) = version_req.comparators.get_mut(i) else { + return; + }; + + // semver crate handles "1.0.0" as "^1.0.0", and we want to treat it as "=1.0.0" + // because of this, we need to iterate over the comparators, and change each one + // that has "default caret operator" to an exact operator + // this condition overrides the "default caret operator" of semver create. + if !label.starts_with('^') && comparator.op == semver::Op::Caret { + comparator.op = semver::Op::Exact; + } + }); + + version_req.matches(&actual_version) +} + impl Config { pub fn default_for_possible_style_edition( style_edition: Option, @@ -265,10 +302,10 @@ impl Config { if self.was_set().required_version() { let version = env!("CARGO_PKG_VERSION"); let required_version = self.required_version(); - if version != required_version { - println!( - "Error: rustfmt version ({version}) doesn't match the required version \ -({required_version})" + if !check_semver_version(&required_version, version) { + eprintln!( + "Error: rustfmt version ({}) doesn't match the required version ({})", + version, required_version ); return false; } @@ -1287,4 +1324,387 @@ make_backup = false ]) ); } + + #[cfg(test)] + mod required_version { + use super::*; + + #[allow(dead_code)] // Only used in tests + fn get_current_version() -> semver::Version { + semver::Version::parse(env!("CARGO_PKG_VERSION")).unwrap() + } + + #[nightly_only_test] + #[test] + fn test_required_version_default() { + let config = Config::default(); + assert!(config.version_meets_requirement()); + } + + #[nightly_only_test] + #[test] + fn test_current_required_version() { + let toml = format!("required_version=\"{}\"", env!("CARGO_PKG_VERSION")); + let config = Config::from_toml(&toml, Path::new("./rustfmt.toml")).unwrap(); + + assert!(config.version_meets_requirement()); + } + + #[nightly_only_test] + #[test] + fn test_required_version_above() { + let toml = "required_version=\"1000.0.0\""; + let config = Config::from_toml(toml, Path::new("./rustfmt.toml")).unwrap(); + + assert!(!config.version_meets_requirement()); + } + + #[nightly_only_test] + #[test] + fn test_required_version_below() { + let versions = vec!["0.0.0", "0.0.1", "0.1.0"]; + + for version in versions { + let toml = format!("required_version=\"{}\"", version.to_string()); + let config = Config::from_toml(&toml, Path::new("./rustfmt.toml")).unwrap(); + + assert!(!config.version_meets_requirement()); + } + } + + #[nightly_only_test] + #[test] + fn test_required_version_tilde() { + let toml = format!("required_version=\"~{}\"", env!("CARGO_PKG_VERSION")); + let config = Config::from_toml(&toml, Path::new("./rustfmt.toml")).unwrap(); + + assert!(config.version_meets_requirement()); + } + + #[nightly_only_test] + #[test] + fn test_required_version_caret() { + let current_version = get_current_version(); + + for minor in current_version.minor..0 { + let toml = format!( + "required_version=\"^{}.{}.0\"", + current_version.major.to_string(), + minor.to_string() + ); + let config = Config::from_toml(&toml, Path::new("./rustfmt.toml")).unwrap(); + + assert!(!config.version_meets_requirement()); + } + } + + #[nightly_only_test] + #[test] + fn test_required_version_greater_than() { + let toml = "required_version=\">1.0.0\""; + let config = Config::from_toml(toml, Path::new("./rustfmt.toml")).unwrap(); + + assert!(config.version_meets_requirement()); + } + + #[nightly_only_test] + #[test] + fn test_required_version_less_than() { + let toml = "required_version=\"<1.0.0\""; + let config = Config::from_toml(toml, Path::new("./rustfmt.toml")).unwrap(); + + assert!(!config.version_meets_requirement()); + } + + #[nightly_only_test] + #[test] + fn test_required_version_range() { + let current_version = get_current_version(); + + let toml = format!( + "required_version=\">={}.0.0, <{}.0.0\"", + current_version.major, + current_version.major + 1 + ); + let config = Config::from_toml(&toml, Path::new("./rustfmt.toml")).unwrap(); + + assert!(config.version_meets_requirement()); + } + + #[nightly_only_test] + #[test] + fn test_required_version_exact_boundary() { + let toml = format!("required_version=\"{}\"", get_current_version().to_string()); + let config = Config::from_toml(&toml, Path::new("./rustfmt.toml")).unwrap(); + + assert!(config.version_meets_requirement()); + } + + #[nightly_only_test] + #[test] + fn test_required_version_pre_release() { + let toml = format!( + "required_version=\"^{}-alpha\"", + get_current_version().to_string() + ); + let config = Config::from_toml(&toml, Path::new("./rustfmt.toml")).unwrap(); + + assert!(config.version_meets_requirement()); + } + + #[nightly_only_test] + #[test] + fn test_required_version_with_build_metadata() { + let toml = format!( + "required_version=\"{}+build.1\"", + get_current_version().to_string() + ); + + let config = Config::from_toml(&toml, Path::new("./rustfmt.toml")).unwrap(); + + assert!(config.version_meets_requirement()); + } + + #[nightly_only_test] + #[test] + fn test_required_version_invalid_specification() { + let toml = "required_version=\"not.a.version\""; + let config = Config::from_toml(toml, Path::new("./rustfmt.toml")).unwrap(); + + assert!(!config.version_meets_requirement()) + } + + #[nightly_only_test] + #[test] + fn test_required_version_complex_range() { + let current_version = get_current_version(); + + let toml = format!( + "required_version=\">={}.0.0, <{}.0.0, ~{}.{}.0\"", + current_version.major, + current_version.major + 1, + current_version.major, + current_version.minor + ); + let config = Config::from_toml(&toml, Path::new("./rustfmt.toml")).unwrap(); + + assert!(config.version_meets_requirement()); + } + + #[nightly_only_test] + #[test] + fn test_required_version_wildcard_major() { + let toml = "required_version=\"1.x\""; + let config = Config::from_toml(toml, Path::new("./rustfmt.toml")).unwrap(); + + assert!(config.version_meets_requirement()); + } + + #[nightly_only_test] + #[test] + fn test_required_version_wildcard_any() { + let toml = "required_version=\"*\""; + let config = Config::from_toml(toml, Path::new("./rustfmt.toml")).unwrap(); + + assert!(config.version_meets_requirement()); + } + + #[nightly_only_test] + #[test] + fn test_required_version_major_version_zero() { + let toml = "required_version=\"0.1.0\""; + let config = Config::from_toml(toml, Path::new("./rustfmt.toml")).unwrap(); + + assert!(!config.version_meets_requirement()); + } + + #[nightly_only_test] + #[test] + fn test_required_version_future_major_version() { + let toml = "required_version=\"3.0.0\""; + let config = Config::from_toml(toml, Path::new("./rustfmt.toml")).unwrap(); + + assert!(!config.version_meets_requirement()); + } + + #[nightly_only_test] + #[test] + fn test_required_version_fail_different_operator() { + // != is not supported + let toml = "required_version=\"!=1.0.0\""; + let config = Config::from_toml(toml, Path::new("./rustfmt.toml")).unwrap(); + + assert!(!config.version_meets_requirement()); + } + } + + #[cfg(test)] + mod check_semver_version { + use super::*; + + #[test] + fn test_exact_version_match() { + assert!(check_semver_version("1.0.0", "1.0.0")); + assert!(!check_semver_version("1.0.0", "1.1.0")); + assert!(!check_semver_version("1.0.0", "1.0.1")); + assert!(!check_semver_version("1.0.0", "2.1.0")); + assert!(!check_semver_version("1.0.0", "0.1.0")); + assert!(!check_semver_version("1.0.0", "0.0.1")); + } + + #[test] + fn test_version_mismatch() { + assert!(!check_semver_version("2.0.0", "1.0.0")); + } + + #[test] + fn test_patch_version_greater() { + assert!(check_semver_version("^1.0.0", "1.0.1")); + } + + #[test] + fn test_minor_version_greater() { + assert!(check_semver_version("^1.0.0", "1.1.0")); + } + + #[test] + fn test_major_version_less() { + assert!(!check_semver_version("1.0.0", "0.9.0")); + } + + #[test] + fn test_prerelease_less_than_release() { + assert!(!check_semver_version("1.0.0", "1.0.0-alpha")); + } + + #[test] + fn test_prerelease_version_specific_match() { + assert!(check_semver_version("1.0.0-alpha", "1.0.0-alpha")); + } + + #[test] + fn test_build_metadata_ignored() { + assert!(check_semver_version("1.0.0", "1.0.0+build.1")); + } + + #[test] + fn test_greater_than_requirement() { + assert!(check_semver_version(">1.0.0", "1.1.0")); + } + + #[test] + fn test_less_than_requirement_fails_when_greater() { + assert!(!check_semver_version("<1.0.0", "1.1.0")); + } + + #[test] + fn test_caret_requirement_matches_minor_update() { + assert!(check_semver_version("^1.1.0", "1.2.0")); + } + + #[test] + fn test_tilde_requirement_matches_patch_update() { + assert!(check_semver_version("~1.0.0", "1.0.1")); + } + + #[test] + fn test_range_requirement_inclusive() { + assert!(check_semver_version(">=1.0.0, <2.0.0", "1.5.0")); + } + + #[test] + fn test_pre_release_specific_match() { + assert!(check_semver_version("1.0.0-alpha.1", "1.0.0-alpha.1")); + } + + #[test] + fn test_pre_release_non_match_when_requiring_release() { + assert!(!check_semver_version("1.0.0", "1.0.0-alpha.1")); + } + + // That's not our choice. `semver` does not support `||` operator. + // Only asserting here to ensure this behavior (which match our docs). + #[test] + fn test_invalid_or() { + assert!(!check_semver_version("1.0.0 || 2.0.0", "1.0.0")); + assert!(!check_semver_version("1.0.0 || 2.0.0", "2.0.0")); + assert!(!check_semver_version("1.0.0 || 2.0.0", "3.0.0")); + } + + #[test] + fn test_wildcard_match_minor() { + assert!(check_semver_version("1.*", "1.1.0")); + assert!(check_semver_version("1.*, <2.0.0", "1.1.0")); + } + + #[test] + fn test_wildcard_mismatch() { + assert!(!check_semver_version("1.*, <2.0.0", "2.1.0")); + assert!(!check_semver_version("1.*, <2.0.0", "2.0.0")); + assert!(!check_semver_version("1.*, <2.*", "2.1.0")); + assert!(!check_semver_version("1.*, <2.*", "2.0.0")); + + assert!(!check_semver_version("1.*, >2.0.0", "1.1.0")); + assert!(!check_semver_version("1.*, >2.0.0", "1.0.0")); + assert!(!check_semver_version("1.*, >2.*", "1.1.0")); + assert!(!check_semver_version("1.*, >2.*", "1.0.0")); + + assert!(!check_semver_version("<1.5.0, >1.10.*", "1.6.0")); + } + + #[test] + fn test_wildcard_match_major() { + assert!(check_semver_version("2.*", "2.0.0")); + } + + #[test] + fn test_wildcard_match_patch() { + assert!(check_semver_version("1.0.*", "1.0.1")); + } + + #[test] + fn test_invalid_inputs() { + assert!(!check_semver_version("not.a.requirement", "1.0.0")); + assert!(!check_semver_version("1.0.0", "not.a.version")); + } + + #[test] + fn test_version_with_pre_release_and_build() { + assert!(check_semver_version("1.0.0-alpha", "1.0.0-alpha+001")); + } + + // Demonstrates precedence of numeric identifiers over alphanumeric in pre-releases + #[test] + fn test_pre_release_numeric_vs_alphanumeric() { + assert!(!check_semver_version("^1.0.0-alpha.beta", "1.0.0-alpha.1")); + assert!(check_semver_version("^1.0.0-alpha.1", "1.0.0-alpha.beta")); + } + + // Any version is allowed when * is used + #[test] + fn test_wildcard_any() { + assert!(check_semver_version("*", "1.0.0")); + assert!(check_semver_version("*", "1.0.0+build")); + } + + // Demonstrates lexicographic ordering of alphanumeric identifiers in pre-releases + #[test] + fn test_pre_release_lexicographic_ordering() { + assert!(check_semver_version( + "^1.0.0-alpha.alpha", + "1.0.0-alpha.beta", + )); + assert!(!check_semver_version( + "^1.0.0-alpha.beta", + "1.0.0-alpha.alpha", + )); + } + + // These are not allowed. '*' can't be used with other version specifiers. + #[test] + fn test_wildcard_any_with_range() { + assert!(!check_semver_version("*, <2.0.0", "1.0.0")); + assert!(!check_semver_version("*, 1.0.0", "1.5.0")); + } + } }