mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-01 14:21:24 +00:00
Add settings for promoting and demoting fixes (#7841)
Adds two configuration-file only settings `extend-safe-fixes` and `extend-unsafe-fixes` which can be used to promote and demote the applicability of fixes for rules. Fixes with `Never` applicability cannot be promoted.
This commit is contained in:
parent
090c1a4a19
commit
739a8aa10e
9 changed files with 261 additions and 5 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -2143,6 +2143,7 @@ name = "ruff_diagnostics"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"is-macro",
|
||||||
"log",
|
"log",
|
||||||
"ruff_text_size",
|
"ruff_text_size",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
#![cfg(not(target_family = "wasm"))]
|
#![cfg(not(target_family = "wasm"))]
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use std::fs::Permissions;
|
use std::fs::Permissions;
|
||||||
|
@ -12,13 +11,13 @@ use std::process::Command;
|
||||||
use std::str;
|
use std::str;
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use anyhow::{Context, Result};
|
use anyhow::Context;
|
||||||
|
use anyhow::Result;
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use insta_cmd::{assert_cmd_snapshot, get_cargo_bin};
|
use insta_cmd::{assert_cmd_snapshot, get_cargo_bin};
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use path_absolutize::path_dedot;
|
use path_absolutize::path_dedot;
|
||||||
#[cfg(unix)]
|
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
|
@ -1208,3 +1207,122 @@ fn diff_only_unsafe_fixes_available() {
|
||||||
"###
|
"###
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_extend_unsafe_fixes() -> Result<()> {
|
||||||
|
let tempdir = TempDir::new()?;
|
||||||
|
let ruff_toml = tempdir.path().join("ruff.toml");
|
||||||
|
fs::write(
|
||||||
|
&ruff_toml,
|
||||||
|
r#"
|
||||||
|
[lint]
|
||||||
|
extend-unsafe-fixes = ["UP034"]
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||||
|
.args(["check", "--config"])
|
||||||
|
.arg(&ruff_toml)
|
||||||
|
.arg("-")
|
||||||
|
.args([
|
||||||
|
"--output-format",
|
||||||
|
"text",
|
||||||
|
"--no-cache",
|
||||||
|
"--select",
|
||||||
|
"F601,UP034",
|
||||||
|
])
|
||||||
|
.pass_stdin("x = {'a': 1, 'a': 1}\nprint(('foo'))\n"),
|
||||||
|
@r###"
|
||||||
|
success: false
|
||||||
|
exit_code: 1
|
||||||
|
----- stdout -----
|
||||||
|
-:1:14: F601 Dictionary key literal `'a'` repeated
|
||||||
|
-:2:7: UP034 Avoid extraneous parentheses
|
||||||
|
Found 2 errors.
|
||||||
|
2 hidden fixes can be enabled with the `--unsafe-fixes` option.
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
"###);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_extend_safe_fixes() -> Result<()> {
|
||||||
|
let tempdir = TempDir::new()?;
|
||||||
|
let ruff_toml = tempdir.path().join("ruff.toml");
|
||||||
|
fs::write(
|
||||||
|
&ruff_toml,
|
||||||
|
r#"
|
||||||
|
[lint]
|
||||||
|
extend-safe-fixes = ["F601"]
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||||
|
.args(["check", "--config"])
|
||||||
|
.arg(&ruff_toml)
|
||||||
|
.arg("-")
|
||||||
|
.args([
|
||||||
|
"--output-format",
|
||||||
|
"text",
|
||||||
|
"--no-cache",
|
||||||
|
"--select",
|
||||||
|
"F601,UP034",
|
||||||
|
])
|
||||||
|
.pass_stdin("x = {'a': 1, 'a': 1}\nprint(('foo'))\n"),
|
||||||
|
@r###"
|
||||||
|
success: false
|
||||||
|
exit_code: 1
|
||||||
|
----- stdout -----
|
||||||
|
-:1:14: F601 [*] Dictionary key literal `'a'` repeated
|
||||||
|
-:2:7: UP034 [*] Avoid extraneous parentheses
|
||||||
|
Found 2 errors.
|
||||||
|
[*] 2 fixable with the `--fix` option.
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
"###);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_extend_unsafe_fixes_conflict_with_extend_safe_fixes() -> Result<()> {
|
||||||
|
// Adding a rule to both options should result in it being treated as unsafe
|
||||||
|
let tempdir = TempDir::new()?;
|
||||||
|
let ruff_toml = tempdir.path().join("ruff.toml");
|
||||||
|
fs::write(
|
||||||
|
&ruff_toml,
|
||||||
|
r#"
|
||||||
|
[lint]
|
||||||
|
extend-unsafe-fixes = ["UP034"]
|
||||||
|
extend-safe-fixes = ["UP034"]
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||||
|
.args(["check", "--config"])
|
||||||
|
.arg(&ruff_toml)
|
||||||
|
.arg("-")
|
||||||
|
.args([
|
||||||
|
"--output-format",
|
||||||
|
"text",
|
||||||
|
"--no-cache",
|
||||||
|
"--select",
|
||||||
|
"F601,UP034",
|
||||||
|
])
|
||||||
|
.pass_stdin("x = {'a': 1, 'a': 1}\nprint(('foo'))\n"),
|
||||||
|
@r###"
|
||||||
|
success: false
|
||||||
|
exit_code: 1
|
||||||
|
----- stdout -----
|
||||||
|
-:1:14: F601 Dictionary key literal `'a'` repeated
|
||||||
|
-:2:7: UP034 Avoid extraneous parentheses
|
||||||
|
Found 2 errors.
|
||||||
|
2 hidden fixes can be enabled with the `--unsafe-fixes` option.
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
"###);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
@ -17,4 +17,5 @@ ruff_text_size = { path = "../ruff_text_size" }
|
||||||
|
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
log = { workspace = true }
|
log = { workspace = true }
|
||||||
|
is-macro = { workspace = true }
|
||||||
serde = { workspace = true, optional = true, features = [] }
|
serde = { workspace = true, optional = true, features = [] }
|
||||||
|
|
|
@ -6,7 +6,7 @@ use ruff_text_size::{Ranged, TextSize};
|
||||||
use crate::edit::Edit;
|
use crate::edit::Edit;
|
||||||
|
|
||||||
/// Indicates if a fix can be applied.
|
/// Indicates if a fix can be applied.
|
||||||
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, is_macro::Is)]
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
|
#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
|
||||||
pub enum Applicability {
|
pub enum Applicability {
|
||||||
|
@ -138,4 +138,11 @@ impl Fix {
|
||||||
pub fn applies(&self, applicability: Applicability) -> bool {
|
pub fn applies(&self, applicability: Applicability) -> bool {
|
||||||
self.applicability >= applicability
|
self.applicability >= applicability
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a new [`Fix`] with the given [`Applicability`].
|
||||||
|
#[must_use]
|
||||||
|
pub fn with_applicability(mut self, applicability: Applicability) -> Self {
|
||||||
|
self.applicability = applicability;
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ use itertools::Itertools;
|
||||||
use log::error;
|
use log::error;
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
|
|
||||||
use ruff_diagnostics::Diagnostic;
|
use ruff_diagnostics::{Applicability, Diagnostic};
|
||||||
use ruff_python_ast::imports::ImportMap;
|
use ruff_python_ast::imports::ImportMap;
|
||||||
use ruff_python_ast::PySourceType;
|
use ruff_python_ast::PySourceType;
|
||||||
use ruff_python_codegen::Stylist;
|
use ruff_python_codegen::Stylist;
|
||||||
|
@ -260,6 +260,29 @@ pub fn check_path(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update fix applicability to account for overrides
|
||||||
|
if !settings.extend_safe_fixes.is_empty() || !settings.extend_unsafe_fixes.is_empty() {
|
||||||
|
for diagnostic in &mut diagnostics {
|
||||||
|
if let Some(fix) = diagnostic.fix.take() {
|
||||||
|
// Enforce demotions over promotions so if someone puts a rule in both we are conservative
|
||||||
|
if fix.applicability().is_safe()
|
||||||
|
&& settings
|
||||||
|
.extend_unsafe_fixes
|
||||||
|
.contains(diagnostic.kind.rule())
|
||||||
|
{
|
||||||
|
diagnostic.set_fix(fix.with_applicability(Applicability::Unsafe));
|
||||||
|
} else if fix.applicability().is_unsafe()
|
||||||
|
&& settings.extend_safe_fixes.contains(diagnostic.kind.rule())
|
||||||
|
{
|
||||||
|
diagnostic.set_fix(fix.with_applicability(Applicability::Safe));
|
||||||
|
} else {
|
||||||
|
// Retain the existing fix (will be dropped from `.take()` otherwise)
|
||||||
|
diagnostic.set_fix(fix);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LinterResult::new((diagnostics, imports), error)
|
LinterResult::new((diagnostics, imports), error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -42,6 +42,8 @@ pub struct LinterSettings {
|
||||||
|
|
||||||
pub rules: RuleTable,
|
pub rules: RuleTable,
|
||||||
pub per_file_ignores: Vec<(GlobMatcher, GlobMatcher, RuleSet)>,
|
pub per_file_ignores: Vec<(GlobMatcher, GlobMatcher, RuleSet)>,
|
||||||
|
pub extend_unsafe_fixes: RuleSet,
|
||||||
|
pub extend_safe_fixes: RuleSet,
|
||||||
|
|
||||||
pub target_version: PythonVersion,
|
pub target_version: PythonVersion,
|
||||||
pub preview: PreviewMode,
|
pub preview: PreviewMode,
|
||||||
|
@ -139,6 +141,8 @@ impl LinterSettings {
|
||||||
namespace_packages: vec![],
|
namespace_packages: vec![],
|
||||||
|
|
||||||
per_file_ignores: vec![],
|
per_file_ignores: vec![],
|
||||||
|
extend_safe_fixes: RuleSet::empty(),
|
||||||
|
extend_unsafe_fixes: RuleSet::empty(),
|
||||||
|
|
||||||
src: vec![path_dedot::CWD.clone()],
|
src: vec![path_dedot::CWD.clone()],
|
||||||
// Needs duplicating
|
// Needs duplicating
|
||||||
|
|
|
@ -226,6 +226,28 @@ impl Configuration {
|
||||||
.chain(lint.extend_per_file_ignores)
|
.chain(lint.extend_per_file_ignores)
|
||||||
.collect(),
|
.collect(),
|
||||||
)?,
|
)?,
|
||||||
|
|
||||||
|
extend_safe_fixes: lint
|
||||||
|
.extend_safe_fixes
|
||||||
|
.iter()
|
||||||
|
.flat_map(|selector| {
|
||||||
|
selector.rules(&PreviewOptions {
|
||||||
|
mode: preview,
|
||||||
|
require_explicit: false,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
extend_unsafe_fixes: lint
|
||||||
|
.extend_unsafe_fixes
|
||||||
|
.iter()
|
||||||
|
.flat_map(|selector| {
|
||||||
|
selector.rules(&PreviewOptions {
|
||||||
|
mode: preview,
|
||||||
|
require_explicit: false,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
|
||||||
src: self.src.unwrap_or_else(|| vec![project_root.to_path_buf()]),
|
src: self.src.unwrap_or_else(|| vec![project_root.to_path_buf()]),
|
||||||
explicit_preview_rules: lint.explicit_preview_rules.unwrap_or_default(),
|
explicit_preview_rules: lint.explicit_preview_rules.unwrap_or_default(),
|
||||||
|
|
||||||
|
@ -494,6 +516,10 @@ pub struct LintConfiguration {
|
||||||
pub rule_selections: Vec<RuleSelection>,
|
pub rule_selections: Vec<RuleSelection>,
|
||||||
pub explicit_preview_rules: Option<bool>,
|
pub explicit_preview_rules: Option<bool>,
|
||||||
|
|
||||||
|
// Fix configuration
|
||||||
|
pub extend_unsafe_fixes: Vec<RuleSelector>,
|
||||||
|
pub extend_safe_fixes: Vec<RuleSelector>,
|
||||||
|
|
||||||
// Global lint settings
|
// Global lint settings
|
||||||
pub allowed_confusables: Option<Vec<char>>,
|
pub allowed_confusables: Option<Vec<char>>,
|
||||||
pub dummy_variable_rgx: Option<Regex>,
|
pub dummy_variable_rgx: Option<Regex>,
|
||||||
|
@ -551,6 +577,8 @@ impl LintConfiguration {
|
||||||
.collect(),
|
.collect(),
|
||||||
extend_fixable: options.extend_fixable.unwrap_or_default(),
|
extend_fixable: options.extend_fixable.unwrap_or_default(),
|
||||||
}],
|
}],
|
||||||
|
extend_safe_fixes: options.extend_safe_fixes.unwrap_or_default(),
|
||||||
|
extend_unsafe_fixes: options.extend_unsafe_fixes.unwrap_or_default(),
|
||||||
allowed_confusables: options.allowed_confusables,
|
allowed_confusables: options.allowed_confusables,
|
||||||
dummy_variable_rgx: options
|
dummy_variable_rgx: options
|
||||||
.dummy_variable_rgx
|
.dummy_variable_rgx
|
||||||
|
@ -847,6 +875,16 @@ impl LintConfiguration {
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.chain(self.rule_selections)
|
.chain(self.rule_selections)
|
||||||
.collect(),
|
.collect(),
|
||||||
|
extend_safe_fixes: config
|
||||||
|
.extend_safe_fixes
|
||||||
|
.into_iter()
|
||||||
|
.chain(self.extend_safe_fixes)
|
||||||
|
.collect(),
|
||||||
|
extend_unsafe_fixes: config
|
||||||
|
.extend_unsafe_fixes
|
||||||
|
.into_iter()
|
||||||
|
.chain(self.extend_unsafe_fixes)
|
||||||
|
.collect(),
|
||||||
allowed_confusables: self.allowed_confusables.or(config.allowed_confusables),
|
allowed_confusables: self.allowed_confusables.or(config.allowed_confusables),
|
||||||
dummy_variable_rgx: self.dummy_variable_rgx.or(config.dummy_variable_rgx),
|
dummy_variable_rgx: self.dummy_variable_rgx.or(config.dummy_variable_rgx),
|
||||||
extend_per_file_ignores: config
|
extend_per_file_ignores: config
|
||||||
|
|
|
@ -523,6 +523,30 @@ pub struct LintOptions {
|
||||||
)]
|
)]
|
||||||
pub ignore: Option<Vec<RuleSelector>>,
|
pub ignore: Option<Vec<RuleSelector>>,
|
||||||
|
|
||||||
|
/// A list of rule codes or prefixes for which unsafe fixes should be considered
|
||||||
|
/// safe.
|
||||||
|
#[option(
|
||||||
|
default = "[]",
|
||||||
|
value_type = "list[RuleSelector]",
|
||||||
|
example = r#"
|
||||||
|
# Allow applying all unsafe fixes in the `E` rules and `F401` without the `--unsafe-fixes` flag
|
||||||
|
extend_safe_fixes = ["E", "F401"]
|
||||||
|
"#
|
||||||
|
)]
|
||||||
|
pub extend_safe_fixes: Option<Vec<RuleSelector>>,
|
||||||
|
|
||||||
|
/// A list of rule codes or prefixes for which safe fixes should be considered
|
||||||
|
/// unsafe.
|
||||||
|
#[option(
|
||||||
|
default = "[]",
|
||||||
|
value_type = "list[RuleSelector]",
|
||||||
|
example = r#"
|
||||||
|
# Require the `--unsafe-fixes` flag when fixing the `E` rules and `F401`
|
||||||
|
extend_unsafe_fixes = ["E", "F401"]
|
||||||
|
"#
|
||||||
|
)]
|
||||||
|
pub extend_unsafe_fixes: Option<Vec<RuleSelector>>,
|
||||||
|
|
||||||
/// Avoid automatically removing unused imports in `__init__.py` files. Such
|
/// Avoid automatically removing unused imports in `__init__.py` files. Such
|
||||||
/// imports will still be flagged, but with a dedicated message suggesting
|
/// imports will still be flagged, but with a dedicated message suggesting
|
||||||
/// that the import is either added to the module's `__all__` symbol, or
|
/// that the import is either added to the module's `__all__` symbol, or
|
||||||
|
|
40
ruff.schema.json
generated
40
ruff.schema.json
generated
|
@ -107,6 +107,16 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"extend-safe-fixes": {
|
||||||
|
"description": "A list of rule codes or prefixes for which unsafe fixes should be considered safe.",
|
||||||
|
"type": [
|
||||||
|
"array",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/RuleSelector"
|
||||||
|
}
|
||||||
|
},
|
||||||
"extend-select": {
|
"extend-select": {
|
||||||
"description": "A list of rule codes or prefixes to enable, in addition to those specified by `select`.",
|
"description": "A list of rule codes or prefixes to enable, in addition to those specified by `select`.",
|
||||||
"type": [
|
"type": [
|
||||||
|
@ -117,6 +127,16 @@
|
||||||
"$ref": "#/definitions/RuleSelector"
|
"$ref": "#/definitions/RuleSelector"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"extend-unsafe-fixes": {
|
||||||
|
"description": "A list of rule codes or prefixes for which safe fixes should be considered unsafe.",
|
||||||
|
"type": [
|
||||||
|
"array",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/RuleSelector"
|
||||||
|
}
|
||||||
|
},
|
||||||
"external": {
|
"external": {
|
||||||
"description": "A list of rule codes that are unsupported by Ruff, but should be preserved when (e.g.) validating `# noqa` directives. Useful for retaining `# noqa` directives that cover plugins not yet implemented by Ruff.",
|
"description": "A list of rule codes that are unsupported by Ruff, but should be preserved when (e.g.) validating `# noqa` directives. Useful for retaining `# noqa` directives that cover plugins not yet implemented by Ruff.",
|
||||||
"type": [
|
"type": [
|
||||||
|
@ -1612,6 +1632,16 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"extend-safe-fixes": {
|
||||||
|
"description": "A list of rule codes or prefixes for which unsafe fixes should be considered safe.",
|
||||||
|
"type": [
|
||||||
|
"array",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/RuleSelector"
|
||||||
|
}
|
||||||
|
},
|
||||||
"extend-select": {
|
"extend-select": {
|
||||||
"description": "A list of rule codes or prefixes to enable, in addition to those specified by `select`.",
|
"description": "A list of rule codes or prefixes to enable, in addition to those specified by `select`.",
|
||||||
"type": [
|
"type": [
|
||||||
|
@ -1622,6 +1652,16 @@
|
||||||
"$ref": "#/definitions/RuleSelector"
|
"$ref": "#/definitions/RuleSelector"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"extend-unsafe-fixes": {
|
||||||
|
"description": "A list of rule codes or prefixes for which safe fixes should be considered unsafe.",
|
||||||
|
"type": [
|
||||||
|
"array",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/RuleSelector"
|
||||||
|
}
|
||||||
|
},
|
||||||
"external": {
|
"external": {
|
||||||
"description": "A list of rule codes that are unsupported by Ruff, but should be preserved when (e.g.) validating `# noqa` directives. Useful for retaining `# noqa` directives that cover plugins not yet implemented by Ruff.",
|
"description": "A list of rule codes that are unsupported by Ruff, but should be preserved when (e.g.) validating `# noqa` directives. Useful for retaining `# noqa` directives that cover plugins not yet implemented by Ruff.",
|
||||||
"type": [
|
"type": [
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue