From 84bf3330310a8203ccb48ed7e1f17e81b6081ff2 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 3 Mar 2024 15:43:49 -0800 Subject: [PATCH] Accept a PEP 440 version specifier for required-version (#10216) ## Summary Allows `required-version` to be set with a version specifier, like `>=0.3.1`. If a single version is provided, falls back to assuming `==0.3.1`, for backwards compatibility. Closes https://github.com/astral-sh/ruff/issues/10192. --- Cargo.lock | 7 - Cargo.toml | 1 - crates/ruff/tests/lint.rs | 154 +++++++++++++++++++++ crates/ruff_linter/Cargo.toml | 1 - crates/ruff_linter/src/settings/types.rs | 42 ++++-- crates/ruff_workspace/src/configuration.rs | 22 +-- crates/ruff_workspace/src/options.rs | 19 ++- ruff.schema.json | 10 +- 8 files changed, 217 insertions(+), 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cd7b0ca22b..ed7b92cdd8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2158,7 +2158,6 @@ dependencies = [ "ruff_text_size", "rustc-hash", "schemars", - "semver", "serde", "serde_json", "similar", @@ -2581,12 +2580,6 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" -[[package]] -name = "semver" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" - [[package]] name = "serde" version = "1.0.197" diff --git a/Cargo.toml b/Cargo.toml index 1ae38f246e..69cfc45e70 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,7 +76,6 @@ result-like = { version = "0.5.0" } rustc-hash = { version = "1.1.0" } schemars = { version = "0.8.16" } seahash = { version = "4.1.0" } -semver = { version = "1.0.22" } serde = { version = "1.0.197", features = ["derive"] } serde-wasm-bindgen = { version = "0.6.4" } serde_json = { version = "1.0.113" } diff --git a/crates/ruff/tests/lint.rs b/crates/ruff/tests/lint.rs index 01e9bc4583..05a93c4d6e 100644 --- a/crates/ruff/tests/lint.rs +++ b/crates/ruff/tests/lint.rs @@ -972,3 +972,157 @@ import os Ok(()) } + +#[test] +fn required_version_exact_mismatch() -> Result<()> { + let version = env!("CARGO_PKG_VERSION"); + + let tempdir = TempDir::new()?; + let ruff_toml = tempdir.path().join("ruff.toml"); + fs::write( + &ruff_toml, + r#" +required-version = "0.1.0" +"#, + )?; + + insta::with_settings!({ + filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/"), (version, "[VERSION]")] + }, { + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .arg("--config") + .arg(&ruff_toml) + .arg("-") + .pass_stdin(r#" +import os +"#), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + ruff failed + Cause: Required version `==0.1.0` does not match the running version `[VERSION]` + "###); + }); + + Ok(()) +} + +#[test] +fn required_version_exact_match() -> Result<()> { + let version = env!("CARGO_PKG_VERSION"); + + let tempdir = TempDir::new()?; + let ruff_toml = tempdir.path().join("ruff.toml"); + fs::write( + &ruff_toml, + format!( + r#" +required-version = "{version}" +"# + ), + )?; + + insta::with_settings!({ + filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/"), (version, "[VERSION]")] + }, { + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .arg("--config") + .arg(&ruff_toml) + .arg("-") + .pass_stdin(r#" +import os +"#), @r###" + success: false + exit_code: 1 + ----- stdout ----- + -:2:8: F401 [*] `os` imported but unused + Found 1 error. + [*] 1 fixable with the `--fix` option. + + ----- stderr ----- + "###); + }); + + Ok(()) +} + +#[test] +fn required_version_bound_mismatch() -> Result<()> { + let version = env!("CARGO_PKG_VERSION"); + + let tempdir = TempDir::new()?; + let ruff_toml = tempdir.path().join("ruff.toml"); + fs::write( + &ruff_toml, + format!( + r#" +required-version = ">{version}" +"# + ), + )?; + + insta::with_settings!({ + filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/"), (version, "[VERSION]")] + }, { + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .arg("--config") + .arg(&ruff_toml) + .arg("-") + .pass_stdin(r#" +import os +"#), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + ruff failed + Cause: Required version `>[VERSION]` does not match the running version `[VERSION]` + "###); + }); + + Ok(()) +} + +#[test] +fn required_version_bound_match() -> Result<()> { + let version = env!("CARGO_PKG_VERSION"); + + let tempdir = TempDir::new()?; + let ruff_toml = tempdir.path().join("ruff.toml"); + fs::write( + &ruff_toml, + r#" +required-version = ">=0.1.0" +"#, + )?; + + insta::with_settings!({ + filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/"), (version, "[VERSION]")] + }, { + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .arg("--config") + .arg(&ruff_toml) + .arg("-") + .pass_stdin(r#" +import os +"#), @r###" + success: false + exit_code: 1 + ----- stdout ----- + -:2:8: F401 [*] `os` imported but unused + Found 1 error. + [*] 1 fixable with the `--fix` option. + + ----- stderr ----- + "###); + }); + + Ok(()) +} diff --git a/crates/ruff_linter/Cargo.toml b/crates/ruff_linter/Cargo.toml index 53cd3a79b8..5be1e804f4 100644 --- a/crates/ruff_linter/Cargo.toml +++ b/crates/ruff_linter/Cargo.toml @@ -60,7 +60,6 @@ regex = { workspace = true } result-like = { workspace = true } rustc-hash = { workspace = true } schemars = { workspace = true, optional = true } -semver = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } similar = { workspace = true } diff --git a/crates/ruff_linter/src/settings/types.rs b/crates/ruff_linter/src/settings/types.rs index f527a00c8d..d89c3844c2 100644 --- a/crates/ruff_linter/src/settings/types.rs +++ b/crates/ruff_linter/src/settings/types.rs @@ -7,7 +7,7 @@ use std::string::ToString; use anyhow::{bail, Result}; use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder}; -use pep440_rs::{Version as Pep440Version, VersionSpecifiers}; +use pep440_rs::{Version as Pep440Version, VersionSpecifier, VersionSpecifiers}; use rustc_hash::FxHashMap; use serde::{de, Deserialize, Deserializer, Serialize}; use strum::IntoEnumIterator; @@ -536,22 +536,44 @@ impl SerializationFormat { #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Hash)] #[serde(try_from = "String")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct Version(String); +pub struct RequiredVersion(VersionSpecifiers); -impl TryFrom for Version { - type Error = semver::Error; +impl TryFrom for RequiredVersion { + type Error = pep440_rs::Pep440Error; fn try_from(value: String) -> Result { - semver::Version::parse(&value).map(|_| Self(value)) + // Treat `0.3.1` as `==0.3.1`, for backwards compatibility. + if let Ok(version) = pep440_rs::Version::from_str(&value) { + Ok(Self(VersionSpecifiers::from( + VersionSpecifier::equals_version(version), + ))) + } else { + Ok(Self(VersionSpecifiers::from_str(&value)?)) + } } } -impl Deref for Version { - type Target = str; +#[cfg(feature = "schemars")] +impl schemars::JsonSchema for RequiredVersion { + fn schema_name() -> String { + "RequiredVersion".to_string() + } - fn deref(&self) -> &Self::Target { - &self.0 + fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + gen.subschema_for::() + } +} + +impl RequiredVersion { + /// Return `true` if the given version is required. + pub fn contains(&self, version: &pep440_rs::Version) -> bool { + self.0.contains(version) + } +} + +impl Display for RequiredVersion { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + Display::fmt(&self.0, f) } } diff --git a/crates/ruff_workspace/src/configuration.rs b/crates/ruff_workspace/src/configuration.rs index beb59ddcad..e6dfea2f37 100644 --- a/crates/ruff_workspace/src/configuration.rs +++ b/crates/ruff_workspace/src/configuration.rs @@ -6,12 +6,12 @@ use std::borrow::Cow; use std::env::VarError; use std::num::{NonZeroU16, NonZeroU8}; use std::path::{Path, PathBuf}; +use std::str::FromStr; use anyhow::{anyhow, Result}; use glob::{glob, GlobError, Paths, PatternError}; use itertools::Itertools; use regex::Regex; -use ruff_linter::settings::fix_safety_table::FixSafetyTable; use rustc_hash::{FxHashMap, FxHashSet}; use shellexpand; use shellexpand::LookupError; @@ -24,10 +24,11 @@ use ruff_linter::registry::RuleNamespace; use ruff_linter::registry::{Rule, RuleSet, INCOMPATIBLE_CODES}; use ruff_linter::rule_selector::{PreviewOptions, Specificity}; use ruff_linter::rules::pycodestyle; +use ruff_linter::settings::fix_safety_table::FixSafetyTable; use ruff_linter::settings::rule_table::RuleTable; use ruff_linter::settings::types::{ ExtensionMapping, FilePattern, FilePatternSet, PerFileIgnore, PerFileIgnores, PreviewMode, - PythonVersion, SerializationFormat, UnsafeFixes, Version, + PythonVersion, RequiredVersion, SerializationFormat, UnsafeFixes, }; use ruff_linter::settings::{LinterSettings, DEFAULT_SELECTORS, DUMMY_VARIABLE_RGX, TASK_TAGS}; use ruff_linter::{ @@ -116,7 +117,7 @@ pub struct Configuration { pub unsafe_fixes: Option, pub output_format: Option, pub preview: Option, - pub required_version: Option, + pub required_version: Option, pub extension: Option, pub show_fixes: Option, @@ -145,10 +146,12 @@ pub struct Configuration { impl Configuration { pub fn into_settings(self, project_root: &Path) -> Result { if let Some(required_version) = &self.required_version { - if &**required_version != RUFF_PKG_VERSION { + let ruff_pkg_version = pep440_rs::Version::from_str(RUFF_PKG_VERSION) + .expect("RUFF_PKG_VERSION is not a valid PEP 440 version specifier"); + if !required_version.contains(&ruff_pkg_version) { return Err(anyhow!( "Required version `{}` does not match the running version `{}`", - &**required_version, + required_version, RUFF_PKG_VERSION )); } @@ -1467,15 +1470,18 @@ fn warn_about_deprecated_top_level_lint_options( #[cfg(test)] mod tests { - use crate::configuration::{LintConfiguration, RuleSelection}; - use crate::options::PydocstyleOptions; + use std::str::FromStr; + use anyhow::Result; + use ruff_linter::codes::{Flake8Copyright, Pycodestyle, Refurb}; use ruff_linter::registry::{Linter, Rule, RuleSet}; use ruff_linter::rule_selector::PreviewOptions; use ruff_linter::settings::types::PreviewMode; use ruff_linter::RuleSelector; - use std::str::FromStr; + + use crate::configuration::{LintConfiguration, RuleSelection}; + use crate::options::PydocstyleOptions; const PREVIEW_RULES: &[Rule] = &[ Rule::IsinstanceTypeNone, diff --git a/crates/ruff_workspace/src/options.rs b/crates/ruff_workspace/src/options.rs index 4d049e344e..4ac62b84f9 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -6,7 +6,6 @@ use rustc_hash::{FxHashMap, FxHashSet}; use serde::{Deserialize, Serialize}; use strum::IntoEnumIterator; -use crate::options_base::{OptionsMetadata, Visit}; use ruff_formatter::IndentStyle; use ruff_linter::line_width::{IndentWidth, LineLength}; use ruff_linter::rules::flake8_pytest_style::settings::SettingsError; @@ -25,12 +24,13 @@ use ruff_linter::rules::{ pycodestyle, pydocstyle, pyflakes, pylint, pyupgrade, }; use ruff_linter::settings::types::{ - IdentifierPattern, PythonVersion, SerializationFormat, Version, + IdentifierPattern, PythonVersion, RequiredVersion, SerializationFormat, }; use ruff_linter::{warn_user_once, RuleSelector}; use ruff_macros::{CombineOptions, OptionsMetadata}; use ruff_python_formatter::{DocstringCodeLineWidth, QuoteStyle}; +use crate::options_base::{OptionsMetadata, Visit}; use crate::settings::LineEnding; #[derive(Clone, Debug, PartialEq, Eq, Default, OptionsMetadata, Serialize, Deserialize)] @@ -135,17 +135,22 @@ pub struct Options { )] pub show_fixes: Option, - /// Require a specific version of Ruff to be running (useful for unifying - /// results across many environments, e.g., with a `pyproject.toml` - /// file). + /// Enforce a requirement on the version of Ruff, to enforce at runtime. + /// If the version of Ruff does not meet the requirement, Ruff will exit + /// with an error. + /// + /// Useful for unifying results across many environments, e.g., with a + /// `pyproject.toml` file. + /// + /// Accepts a PEP 440 specifier, like `==0.3.1` or `>=0.3.1`. #[option( default = "null", value_type = "str", example = r#" - required-version = "0.0.193" + required-version = ">=0.0.193" "# )] - pub required_version: Option, + pub required_version: Option, /// Whether to enable preview mode. When preview mode is enabled, Ruff will /// use unstable rules, fixes, and formatting. diff --git a/ruff.schema.json b/ruff.schema.json index a415b966c0..7509cb026b 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -623,10 +623,10 @@ ] }, "required-version": { - "description": "Require a specific version of Ruff to be running (useful for unifying results across many environments, e.g., with a `pyproject.toml` file).", + "description": "Enforce a requirement on the version of Ruff, to enforce at runtime. If the version of Ruff does not meet the requirement, Ruff will exit with an error.\n\nUseful for unifying results across many environments, e.g., with a `pyproject.toml` file.\n\nAccepts a PEP 440 specifier, like `==0.3.1` or `>=0.3.1`.", "anyOf": [ { - "$ref": "#/definitions/Version" + "$ref": "#/definitions/RequiredVersion" }, { "type": "null" @@ -2590,6 +2590,9 @@ } ] }, + "RequiredVersion": { + "type": "string" + }, "RuleSelector": { "type": "string", "enum": [ @@ -3870,9 +3873,6 @@ ] } ] - }, - "Version": { - "type": "string" } } } \ No newline at end of file