mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-30 13:51:16 +00:00
Add rule deprecation infrastructure (#9689)
Adds a new `Deprecated` rule group in addition to `Stable` and `Preview`. Deprecated rules: - Warn on explicit selection without preview - Error on explicit selection with preview - Are excluded when selected by prefix with preview Deprecates `TRY200`, `ANN101`, and `ANN102` as a proof of concept. We can consider deprecating them separately.
This commit is contained in:
parent
c86e14d1d4
commit
a0ef087e73
10 changed files with 279 additions and 21 deletions
|
@ -1091,6 +1091,156 @@ fn preview_enabled_group_ignore() {
|
|||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deprecated_direct() {
|
||||
// Selection of a deprecated rule without preview enabled should still work
|
||||
// but a warning should be displayed
|
||||
let mut cmd = RuffCheck::default().args(["--select", "TRY200"]).build();
|
||||
assert_cmd_snapshot!(cmd
|
||||
.pass_stdin(r###"
|
||||
def reciprocal(n):
|
||||
try:
|
||||
return 1 / n
|
||||
except ZeroDivisionError:
|
||||
raise ValueError
|
||||
"###), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: Rule `TRY200` is deprecated and will be removed in a future release.
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deprecated_multiple_direct() {
|
||||
let mut cmd = RuffCheck::default()
|
||||
.args(["--select", "ANN101", "--select", "ANN102"])
|
||||
.build();
|
||||
assert_cmd_snapshot!(cmd
|
||||
.pass_stdin(r###"
|
||||
class Foo:
|
||||
def a(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def b(cls):
|
||||
pass
|
||||
"###), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:3:11: ANN101 Missing type annotation for `self` in method
|
||||
-:7:11: ANN102 Missing type annotation for `cls` in classmethod
|
||||
Found 2 errors.
|
||||
|
||||
----- stderr -----
|
||||
warning: Rule `ANN102` is deprecated and will be removed in a future release.
|
||||
warning: Rule `ANN101` is deprecated and will be removed in a future release.
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deprecated_indirect() {
|
||||
// `ANN` includes deprecated rules `ANN101` and `ANN102` but should not warn
|
||||
// since it is not a "direct" selection
|
||||
let mut cmd = RuffCheck::default().args(["--select", "ANN1"]).build();
|
||||
assert_cmd_snapshot!(cmd
|
||||
.pass_stdin(r###"
|
||||
class Foo:
|
||||
def a(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def b(cls):
|
||||
pass
|
||||
"###), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:3:11: ANN101 Missing type annotation for `self` in method
|
||||
-:7:11: ANN102 Missing type annotation for `cls` in classmethod
|
||||
Found 2 errors.
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deprecated_direct_preview_enabled() {
|
||||
// Direct selection of a deprecated rule in preview should fail
|
||||
let mut cmd = RuffCheck::default()
|
||||
.args(["--select", "TRY200", "--preview"])
|
||||
.build();
|
||||
assert_cmd_snapshot!(cmd
|
||||
.pass_stdin(r###"
|
||||
def reciprocal(n):
|
||||
try:
|
||||
return 1 / n
|
||||
except ZeroDivisionError:
|
||||
raise ValueError
|
||||
"###), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
ruff failed
|
||||
Cause: Selection of deprecated rule `TRY200` is not allowed when preview mode is enabled.
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deprecated_indirect_preview_enabled() {
|
||||
// `TRY200` is deprecated and should be off by default in preview.
|
||||
let mut cmd = RuffCheck::default()
|
||||
.args(["--select", "TRY", "--preview"])
|
||||
.build();
|
||||
assert_cmd_snapshot!(cmd
|
||||
.pass_stdin(r###"
|
||||
def reciprocal(n):
|
||||
try:
|
||||
return 1 / n
|
||||
except ZeroDivisionError:
|
||||
raise ValueError
|
||||
"###), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deprecated_multiple_direct_preview_enabled() {
|
||||
// Direct selection of the deprecated rules in preview should fail with
|
||||
// a message listing all of the rule codes
|
||||
let mut cmd = RuffCheck::default()
|
||||
.args(["--select", "ANN101", "--select", "ANN102", "--preview"])
|
||||
.build();
|
||||
assert_cmd_snapshot!(cmd
|
||||
.pass_stdin(r###"
|
||||
def reciprocal(n):
|
||||
try:
|
||||
return 1 / n
|
||||
except ZeroDivisionError:
|
||||
raise ValueError
|
||||
"###), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
ruff failed
|
||||
Cause: Selection of deprecated rules is not allowed when preview mode is enabled. Remove selection of:
|
||||
- ANN102
|
||||
- ANN101
|
||||
|
||||
"###);
|
||||
}
|
||||
|
||||
/// An unreadable pyproject.toml in non-isolated mode causes ruff to hard-error trying to build up
|
||||
/// configuration globs
|
||||
#[cfg(unix)]
|
||||
|
|
|
@ -26,9 +26,9 @@ pub(crate) fn main(args: &Args) -> Result<()> {
|
|||
for rule in Rule::iter() {
|
||||
if let Some(explanation) = rule.explanation() {
|
||||
let mut output = String::new();
|
||||
|
||||
output.push_str(&format!("# {} ({})", rule.as_ref(), rule.noqa_code()));
|
||||
output.push('\n');
|
||||
output.push('\n');
|
||||
|
||||
let (linter, _) = Linter::parse_code(&rule.noqa_code().to_string()).unwrap();
|
||||
if linter.url().is_some() {
|
||||
|
@ -37,6 +37,14 @@ pub(crate) fn main(args: &Args) -> Result<()> {
|
|||
output.push('\n');
|
||||
}
|
||||
|
||||
if rule.is_deprecated() {
|
||||
output.push_str(
|
||||
r"**Warning: This rule is deprecated and will be removed in a future release.**",
|
||||
);
|
||||
output.push('\n');
|
||||
output.push('\n');
|
||||
}
|
||||
|
||||
let fix_availability = rule.fixable();
|
||||
if matches!(
|
||||
fix_availability,
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
//! Used for <https://docs.astral.sh/ruff/rules/>.
|
||||
|
||||
use itertools::Itertools;
|
||||
use ruff_linter::codes::RuleGroup;
|
||||
use std::borrow::Cow;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
|
@ -14,6 +15,9 @@ use ruff_workspace::options_base::OptionsMetadata;
|
|||
|
||||
const FIX_SYMBOL: &str = "🛠️";
|
||||
const PREVIEW_SYMBOL: &str = "🧪";
|
||||
const WARNING_SYMBOL: &str = "⚠️";
|
||||
const STABLE_SYMBOL: &str = "✔️";
|
||||
const SPACER: &str = " ";
|
||||
|
||||
fn generate_table(table_out: &mut String, rules: impl IntoIterator<Item = Rule>, linter: &Linter) {
|
||||
table_out.push_str("| Code | Name | Message | |");
|
||||
|
@ -21,20 +25,30 @@ fn generate_table(table_out: &mut String, rules: impl IntoIterator<Item = Rule>,
|
|||
table_out.push_str("| ---- | ---- | ------- | ------: |");
|
||||
table_out.push('\n');
|
||||
for rule in rules {
|
||||
let status_token = match rule.group() {
|
||||
RuleGroup::Deprecated => {
|
||||
format!("<span title='Rule has been deprecated'>{WARNING_SYMBOL}</span>")
|
||||
}
|
||||
#[allow(deprecated)]
|
||||
RuleGroup::Preview | RuleGroup::Nursery => {
|
||||
format!("<span title='Rule is in preview'>{PREVIEW_SYMBOL}</span>")
|
||||
}
|
||||
RuleGroup::Stable => {
|
||||
// A full opacity checkmark is a bit aggressive for indicating stable
|
||||
format!("<span title='Rule is stable' style='opacity: 0.6'>{STABLE_SYMBOL}</span>")
|
||||
}
|
||||
};
|
||||
|
||||
let fix_token = match rule.fixable() {
|
||||
FixAvailability::Always | FixAvailability::Sometimes => {
|
||||
format!("<span title='Automatic fix available'>{FIX_SYMBOL}</span>")
|
||||
}
|
||||
FixAvailability::None => {
|
||||
format!("<span style='opacity: 0.1' aria-hidden='true'>{FIX_SYMBOL}</span>")
|
||||
format!("<span title='Automatic fix not available' style='opacity: 0.1' aria-hidden='true'>{FIX_SYMBOL}</span>")
|
||||
}
|
||||
};
|
||||
let preview_token = if rule.is_preview() || rule.is_nursery() {
|
||||
format!("<span title='Rule is in preview'>{PREVIEW_SYMBOL}</span>")
|
||||
} else {
|
||||
format!("<span style='opacity: 0.1' aria-hidden='true'>{PREVIEW_SYMBOL}</span>")
|
||||
};
|
||||
let status_token = format!("{fix_token} {preview_token}");
|
||||
|
||||
let tokens = format!("{status_token} {fix_token}");
|
||||
|
||||
let rule_name = rule.as_ref();
|
||||
|
||||
|
@ -58,7 +72,7 @@ fn generate_table(table_out: &mut String, rules: impl IntoIterator<Item = Rule>,
|
|||
.then_some(format_args!("[{rule_name}](rules/{rule_name}.md)"))
|
||||
.unwrap_or(format_args!("{rule_name}")),
|
||||
message,
|
||||
status_token,
|
||||
tokens,
|
||||
));
|
||||
table_out.push('\n');
|
||||
}
|
||||
|
@ -69,15 +83,28 @@ pub(crate) fn generate() -> String {
|
|||
// Generate the table string.
|
||||
let mut table_out = String::new();
|
||||
|
||||
table_out.push_str(&format!(
|
||||
"The {FIX_SYMBOL} emoji indicates that a rule is automatically fixable by the `--fix` command-line option."));
|
||||
table_out.push('\n');
|
||||
table_out.push_str("### Legend");
|
||||
table_out.push('\n');
|
||||
|
||||
table_out.push_str(&format!(
|
||||
"The {PREVIEW_SYMBOL} emoji indicates that a rule is in [\"preview\"](faq.md#what-is-preview)."
|
||||
"{SPACER}{STABLE_SYMBOL}{SPACER} The rule is stable."
|
||||
));
|
||||
table_out.push('\n');
|
||||
table_out.push_str("<br />");
|
||||
|
||||
table_out.push_str(&format!(
|
||||
"{SPACER}{PREVIEW_SYMBOL}{SPACER} The rule is unstable and is in [\"preview\"](faq.md#what-is-preview)."
|
||||
));
|
||||
table_out.push_str("<br />");
|
||||
|
||||
table_out.push_str(&format!(
|
||||
"{SPACER}{WARNING_SYMBOL}{SPACER} The rule has been deprecated and will be removed in a future release."
|
||||
));
|
||||
table_out.push_str("<br />");
|
||||
|
||||
table_out.push_str(&format!(
|
||||
"{SPACER}{FIX_SYMBOL}{SPACER} The rule is automatically fixable by the `--fix` command-line option."
|
||||
));
|
||||
table_out.push_str("<br />");
|
||||
table_out.push('\n');
|
||||
|
||||
for linter in Linter::iter() {
|
||||
|
|
|
@ -52,6 +52,9 @@ pub enum RuleGroup {
|
|||
Stable,
|
||||
/// The rule is unstable, and preview mode must be enabled for usage.
|
||||
Preview,
|
||||
/// The rule has been deprecated, warnings will be displayed during selection in stable
|
||||
/// and errors will be raised if used with preview mode enabled.
|
||||
Deprecated,
|
||||
/// Legacy category for unstable rules, supports backwards compatible selection.
|
||||
#[deprecated(note = "Use `RuleGroup::Preview` for new rules instead")]
|
||||
Nursery,
|
||||
|
@ -424,8 +427,8 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
|||
(Flake8Annotations, "001") => (RuleGroup::Stable, rules::flake8_annotations::rules::MissingTypeFunctionArgument),
|
||||
(Flake8Annotations, "002") => (RuleGroup::Stable, rules::flake8_annotations::rules::MissingTypeArgs),
|
||||
(Flake8Annotations, "003") => (RuleGroup::Stable, rules::flake8_annotations::rules::MissingTypeKwargs),
|
||||
(Flake8Annotations, "101") => (RuleGroup::Stable, rules::flake8_annotations::rules::MissingTypeSelf),
|
||||
(Flake8Annotations, "102") => (RuleGroup::Stable, rules::flake8_annotations::rules::MissingTypeCls),
|
||||
(Flake8Annotations, "101") => (RuleGroup::Deprecated, rules::flake8_annotations::rules::MissingTypeSelf),
|
||||
(Flake8Annotations, "102") => (RuleGroup::Deprecated, rules::flake8_annotations::rules::MissingTypeCls),
|
||||
(Flake8Annotations, "201") => (RuleGroup::Stable, rules::flake8_annotations::rules::MissingReturnTypeUndocumentedPublicFunction),
|
||||
(Flake8Annotations, "202") => (RuleGroup::Stable, rules::flake8_annotations::rules::MissingReturnTypePrivateFunction),
|
||||
(Flake8Annotations, "204") => (RuleGroup::Stable, rules::flake8_annotations::rules::MissingReturnTypeSpecialMethod),
|
||||
|
@ -842,7 +845,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
|||
(Tryceratops, "002") => (RuleGroup::Stable, rules::tryceratops::rules::RaiseVanillaClass),
|
||||
(Tryceratops, "003") => (RuleGroup::Stable, rules::tryceratops::rules::RaiseVanillaArgs),
|
||||
(Tryceratops, "004") => (RuleGroup::Stable, rules::tryceratops::rules::TypeCheckWithoutTypeError),
|
||||
(Tryceratops, "200") => (RuleGroup::Stable, rules::tryceratops::rules::ReraiseNoCause),
|
||||
(Tryceratops, "200") => (RuleGroup::Deprecated, rules::tryceratops::rules::ReraiseNoCause),
|
||||
(Tryceratops, "201") => (RuleGroup::Stable, rules::tryceratops::rules::VerboseRaise),
|
||||
(Tryceratops, "300") => (RuleGroup::Stable, rules::tryceratops::rules::TryConsiderElse),
|
||||
(Tryceratops, "301") => (RuleGroup::Stable, rules::tryceratops::rules::RaiseWithinTry),
|
||||
|
|
|
@ -204,13 +204,15 @@ impl RuleSelector {
|
|||
let preview_require_explicit = preview.require_explicit;
|
||||
#[allow(deprecated)]
|
||||
self.all_rules().filter(move |rule| {
|
||||
// Always include rules that are not in preview or the nursery
|
||||
!(rule.is_preview() || rule.is_nursery())
|
||||
// Always include stable rules
|
||||
rule.is_stable()
|
||||
// Backwards compatibility allows selection of nursery rules by exact code or dedicated group
|
||||
|| ((matches!(self, RuleSelector::Rule { .. }) || matches!(self, RuleSelector::Nursery { .. })) && rule.is_nursery())
|
||||
// Enabling preview includes all preview or nursery rules unless explicit selection
|
||||
// is turned on
|
||||
|| (preview_enabled && (matches!(self, RuleSelector::Rule { .. }) || !preview_require_explicit))
|
||||
// Deprecated rules are excluded in preview mode unless explicitly selected
|
||||
|| (rule.is_deprecated() && (!preview_enabled || matches!(self, RuleSelector::Rule { .. })))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -111,6 +111,10 @@ impl Violation for MissingTypeKwargs {
|
|||
}
|
||||
}
|
||||
|
||||
/// ## Deprecation
|
||||
/// This rule is commonly disabled because type checkers can infer this type without annotation.
|
||||
/// It will be removed in a future release.
|
||||
///
|
||||
/// ## What it does
|
||||
/// Checks that instance method `self` arguments have type annotations.
|
||||
///
|
||||
|
@ -148,6 +152,10 @@ impl Violation for MissingTypeSelf {
|
|||
}
|
||||
}
|
||||
|
||||
/// ## Deprecation
|
||||
/// This rule is commonly disabled because type checkers can infer this type without annotation.
|
||||
/// It will be removed in a future release.
|
||||
///
|
||||
/// ## What it does
|
||||
/// Checks that class method `cls` arguments have type annotations.
|
||||
///
|
||||
|
|
|
@ -7,6 +7,9 @@ use ruff_python_ast::statement_visitor::StatementVisitor;
|
|||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## Deprecation
|
||||
/// This rule is identical to [B904] which should be used instead.
|
||||
///
|
||||
/// ## What it does
|
||||
/// Checks for exceptions that are re-raised without specifying the cause via
|
||||
/// the `from` keyword.
|
||||
|
@ -36,6 +39,8 @@ use crate::checkers::ast::Checker;
|
|||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: Exception context](https://docs.python.org/3/library/exceptions.html#exception-context)
|
||||
///
|
||||
/// [B904]: https://docs.astral.sh/ruff/rules/raise-without-from-inside-except/
|
||||
#[violation]
|
||||
pub struct ReraiseNoCause;
|
||||
|
||||
|
|
|
@ -317,10 +317,18 @@ See also https://github.com/astral-sh/ruff/issues/2186.
|
|||
matches!(self.group(), RuleGroup::Preview)
|
||||
}
|
||||
|
||||
pub fn is_stable(&self) -> bool {
|
||||
matches!(self.group(), RuleGroup::Stable)
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
pub fn is_nursery(&self) -> bool {
|
||||
matches!(self.group(), RuleGroup::Nursery)
|
||||
}
|
||||
|
||||
pub fn is_deprecated(&self) -> bool {
|
||||
matches!(self.group(), RuleGroup::Deprecated)
|
||||
}
|
||||
}
|
||||
|
||||
impl Linter {
|
||||
|
|
|
@ -755,6 +755,7 @@ impl LintConfiguration {
|
|||
// Store selectors for displaying warnings
|
||||
let mut redirects = FxHashMap::default();
|
||||
let mut deprecated_nursery_selectors = FxHashSet::default();
|
||||
let mut deprecated_selectors = FxHashSet::default();
|
||||
let mut ignored_preview_selectors = FxHashSet::default();
|
||||
|
||||
// Track which docstring rules are specifically enabled
|
||||
|
@ -895,8 +896,10 @@ impl LintConfiguration {
|
|||
return Err(anyhow!("The `NURSERY` selector was removed.{suggestion}"));
|
||||
};
|
||||
|
||||
// Only warn for the following selectors if used to enable rules
|
||||
// e.g. use with `--ignore` or `--fixable` is okay
|
||||
// Some of these checks are only for `Kind::Enable` which means only `--select` will warn
|
||||
// and use with, e.g., `--ignore` or `--fixable` is okay
|
||||
|
||||
// Unstable rules
|
||||
if preview.mode.is_disabled() && kind.is_enable() {
|
||||
if let RuleSelector::Rule { prefix, .. } = selector {
|
||||
if prefix.rules().any(|rule| rule.is_nursery()) {
|
||||
|
@ -910,6 +913,16 @@ impl LintConfiguration {
|
|||
}
|
||||
}
|
||||
|
||||
// Deprecated rules
|
||||
if kind.is_enable() {
|
||||
if let RuleSelector::Rule { prefix, .. } = selector {
|
||||
if prefix.rules().any(|rule| rule.is_deprecated()) {
|
||||
deprecated_selectors.insert(selector);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Redirected rules
|
||||
if let RuleSelector::Prefix {
|
||||
prefix,
|
||||
redirected_from: Some(redirect_from),
|
||||
|
@ -950,6 +963,35 @@ impl LintConfiguration {
|
|||
}
|
||||
}
|
||||
|
||||
if preview.mode.is_disabled() {
|
||||
for selection in deprecated_selectors {
|
||||
let (prefix, code) = selection.prefix_and_code();
|
||||
warn_user!(
|
||||
"Rule `{prefix}{code}` is deprecated and will be removed in a future release.",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
let deprecated_selectors = deprecated_selectors.iter().collect::<Vec<_>>();
|
||||
match deprecated_selectors.as_slice() {
|
||||
[] => (),
|
||||
[selection] => {
|
||||
let (prefix, code) = selection.prefix_and_code();
|
||||
return Err(anyhow!("Selection of deprecated rule `{prefix}{code}` is not allowed when preview mode is enabled."));
|
||||
}
|
||||
[..] => {
|
||||
let mut message = "Selection of deprecated rules is not allowed when preview mode is enabled. Remove selection of:".to_string();
|
||||
for selection in deprecated_selectors {
|
||||
let (prefix, code) = selection.prefix_and_code();
|
||||
message.push_str("\n\t- ");
|
||||
message.push_str(prefix);
|
||||
message.push_str(code);
|
||||
}
|
||||
message.push('\n');
|
||||
return Err(anyhow!(message));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for selection in ignored_preview_selectors {
|
||||
let (prefix, code) = selection.prefix_and_code();
|
||||
warn_user!(
|
||||
|
|
|
@ -143,3 +143,8 @@ In our previous example, `--select` with `ALL` `HYP`, `HYP0`, or `HYP00` would n
|
|||
rule will need to be selected with its exact code, e.g. `--select ALL,HYP001`.
|
||||
|
||||
If preview mode is not enabled, this setting has no effect.
|
||||
|
||||
## Deprecated rules
|
||||
|
||||
When preview mode is enabled, deprecated rules will be disabled. If a deprecated rule is selected explicitly, an
|
||||
error will be raised. Deprecated rules will not be included if selected via a rule category or prefix.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue