From 03f6b7744048f4571689c701121fb710ecc50afb Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Sat, 17 May 2025 12:37:14 +0200 Subject: [PATCH 01/12] FURB180: add list of base class exceptions --- .../resources/test/fixtures/refurb/FURB180.py | 32 +++++ crates/ruff_linter/src/rules/refurb/mod.rs | 1 + .../rules/refurb/rules/metaclass_abcmeta.rs | 21 ++++ .../ruff_linter/src/rules/refurb/settings.rs | 41 +++++++ ...es__refurb__tests__FURB180_FURB180.py.snap | 116 ++++++++++-------- crates/ruff_linter/src/settings/mod.rs | 4 +- crates/ruff_workspace/src/configuration.rs | 10 +- crates/ruff_workspace/src/options.rs | 58 ++++++++- 8 files changed, 231 insertions(+), 52 deletions(-) create mode 100644 crates/ruff_linter/src/rules/refurb/settings.rs diff --git a/crates/ruff_linter/resources/test/fixtures/refurb/FURB180.py b/crates/ruff_linter/resources/test/fixtures/refurb/FURB180.py index 33576061fc23a..eca6eb12e5b92 100644 --- a/crates/ruff_linter/resources/test/fixtures/refurb/FURB180.py +++ b/crates/ruff_linter/resources/test/fixtures/refurb/FURB180.py @@ -1,5 +1,9 @@ import abc +import typing +import typing_extensions from abc import abstractmethod, ABCMeta +from typing import Protocol as TProtocol +from typing_extensions import Protocol as TEProtocol # Errors @@ -32,6 +36,14 @@ class A3(B0, before_metaclass=1, metaclass=abc.ABCMeta): pass +class Protocol: + pass + + +class C1(Protocol, metaclass=ABCMeta): + pass + + # OK class Meta(type): @@ -56,3 +68,23 @@ def foo(self): pass class A7(B0, abc.ABC, B1): @abstractmethod def foo(self): pass + + +class A8(typing.Protocol, metaclass=ABCMeta): + @abstractmethod + def foo(self): pass + + +class A9(typing_extensions.Protocol, metaclass=ABCMeta): + @abstractmethod + def foo(self): pass + + +class A10(TProtocol, metaclass=ABCMeta): + @abstractmethod + def foo(self): pass + + +class A11(TEProtocol, metaclass=ABCMeta): + @abstractmethod + def foo(self): pass diff --git a/crates/ruff_linter/src/rules/refurb/mod.rs b/crates/ruff_linter/src/rules/refurb/mod.rs index d841bcafe1cb7..f68db0c713b22 100644 --- a/crates/ruff_linter/src/rules/refurb/mod.rs +++ b/crates/ruff_linter/src/rules/refurb/mod.rs @@ -2,6 +2,7 @@ mod helpers; pub(crate) mod rules; +pub mod settings; #[cfg(test)] mod tests { diff --git a/crates/ruff_linter/src/rules/refurb/rules/metaclass_abcmeta.rs b/crates/ruff_linter/src/rules/refurb/rules/metaclass_abcmeta.rs index e71c3e68a2451..445acf7a8859c 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/metaclass_abcmeta.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/metaclass_abcmeta.rs @@ -1,4 +1,5 @@ use itertools::Itertools; +use rustc_hash::FxHashSet; use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{ViolationMetadata, derive_message_formats}; @@ -31,6 +32,9 @@ use crate::importer::ImportRequest; /// pass /// ``` /// +/// ## Options +/// - `lint.refurb.allow-abc-meta-bases` +/// /// ## References /// - [Python documentation: `abc.ABC`](https://docs.python.org/3/library/abc.html#abc.ABC) /// - [Python documentation: `abc.ABCMeta`](https://docs.python.org/3/library/abc.html#abc.ABCMeta) @@ -69,6 +73,23 @@ pub(crate) fn metaclass_abcmeta(checker: &Checker, class_def: &StmtClassDef) { return; } + // Determine if all base classes are in the configured list of exceptions. + let bases: Option> = class_def + .bases() + .iter() + .map(|base| { + checker + .semantic() + .resolve_qualified_name(base) + .map(|qualified_name| qualified_name.to_string()) + }) + .collect(); + if let Some(bases) = &bases { + if !bases.is_empty() && bases.is_subset(&checker.settings.refurb.allow_abc_meta_bases) { + return; + } + } + let mut diagnostic = Diagnostic::new(MetaClassABCMeta, keyword.range); diagnostic.try_set_fix(|| { diff --git a/crates/ruff_linter/src/rules/refurb/settings.rs b/crates/ruff_linter/src/rules/refurb/settings.rs new file mode 100644 index 0000000000000..0bc1ea40d2935 --- /dev/null +++ b/crates/ruff_linter/src/rules/refurb/settings.rs @@ -0,0 +1,41 @@ +//! Settings for the `refurb` plugin. + +use std::fmt; + +use ruff_macros::CacheKey; +use rustc_hash::FxHashSet; + +use crate::display_settings; + +pub fn default_allow_abc_meta_bases() -> FxHashSet { + ["typing.Protocol", "typing_extensions.Protocol"] + .into_iter() + .map(ToString::to_string) + .collect() +} + +#[derive(Debug, Clone, CacheKey)] +pub struct Settings { + pub allow_abc_meta_bases: FxHashSet, +} + +impl Default for Settings { + fn default() -> Self { + Self { + allow_abc_meta_bases: default_allow_abc_meta_bases(), + } + } +} + +impl fmt::Display for Settings { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + display_settings! { + formatter = f, + namespace = "linter.refurb", + fields = [ + self.allow_abc_meta_bases | set + ] + } + Ok(()) + } +} diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB180_FURB180.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB180_FURB180.py.snap index 45df8c8e53cea..5ccaf322bc1ab 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB180_FURB180.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB180_FURB180.py.snap @@ -1,79 +1,97 @@ --- source: crates/ruff_linter/src/rules/refurb/mod.rs --- -FURB180.py:7:10: FURB180 [*] Use of `metaclass=abc.ABCMeta` to define abstract base class - | -5 | # Errors -6 | -7 | class A0(metaclass=abc.ABCMeta): - | ^^^^^^^^^^^^^^^^^^^^^ FURB180 -8 | @abstractmethod -9 | def foo(self): pass - | - = help: Replace with `abc.ABC` +FURB180.py:11:10: FURB180 [*] Use of `metaclass=abc.ABCMeta` to define abstract base class + | + 9 | # Errors +10 | +11 | class A0(metaclass=abc.ABCMeta): + | ^^^^^^^^^^^^^^^^^^^^^ FURB180 +12 | @abstractmethod +13 | def foo(self): pass + | + = help: Replace with `abc.ABC` ℹ Safe fix -4 4 | -5 5 | # Errors -6 6 | -7 |-class A0(metaclass=abc.ABCMeta): - 7 |+class A0(abc.ABC): -8 8 | @abstractmethod -9 9 | def foo(self): pass +8 8 | +9 9 | # Errors 10 10 | +11 |-class A0(metaclass=abc.ABCMeta): + 11 |+class A0(abc.ABC): +12 12 | @abstractmethod +13 13 | def foo(self): pass +14 14 | -FURB180.py:12:10: FURB180 [*] Use of `metaclass=abc.ABCMeta` to define abstract base class +FURB180.py:16:10: FURB180 [*] Use of `metaclass=abc.ABCMeta` to define abstract base class | -12 | class A1(metaclass=ABCMeta): +16 | class A1(metaclass=ABCMeta): | ^^^^^^^^^^^^^^^^^ FURB180 -13 | @abstractmethod -14 | def foo(self): pass +17 | @abstractmethod +18 | def foo(self): pass | = help: Replace with `abc.ABC` ℹ Safe fix -9 9 | def foo(self): pass -10 10 | -11 11 | -12 |-class A1(metaclass=ABCMeta): - 12 |+class A1(abc.ABC): -13 13 | @abstractmethod -14 14 | def foo(self): pass +13 13 | def foo(self): pass +14 14 | 15 15 | +16 |-class A1(metaclass=ABCMeta): + 16 |+class A1(abc.ABC): +17 17 | @abstractmethod +18 18 | def foo(self): pass +19 19 | -FURB180.py:26:18: FURB180 [*] Use of `metaclass=abc.ABCMeta` to define abstract base class +FURB180.py:30:18: FURB180 [*] Use of `metaclass=abc.ABCMeta` to define abstract base class | -26 | class A2(B0, B1, metaclass=ABCMeta): +30 | class A2(B0, B1, metaclass=ABCMeta): | ^^^^^^^^^^^^^^^^^ FURB180 -27 | @abstractmethod -28 | def foo(self): pass +31 | @abstractmethod +32 | def foo(self): pass | = help: Replace with `abc.ABC` ℹ Safe fix -23 23 | pass -24 24 | -25 25 | -26 |-class A2(B0, B1, metaclass=ABCMeta): - 26 |+class A2(B0, B1, abc.ABC): -27 27 | @abstractmethod -28 28 | def foo(self): pass +27 27 | pass +28 28 | 29 29 | +30 |-class A2(B0, B1, metaclass=ABCMeta): + 30 |+class A2(B0, B1, abc.ABC): +31 31 | @abstractmethod +32 32 | def foo(self): pass +33 33 | -FURB180.py:31:34: FURB180 [*] Use of `metaclass=abc.ABCMeta` to define abstract base class +FURB180.py:35:34: FURB180 [*] Use of `metaclass=abc.ABCMeta` to define abstract base class | -31 | class A3(B0, before_metaclass=1, metaclass=abc.ABCMeta): +35 | class A3(B0, before_metaclass=1, metaclass=abc.ABCMeta): | ^^^^^^^^^^^^^^^^^^^^^ FURB180 -32 | pass +36 | pass | = help: Replace with `abc.ABC` ℹ Safe fix -28 28 | def foo(self): pass -29 29 | -30 30 | -31 |-class A3(B0, before_metaclass=1, metaclass=abc.ABCMeta): - 31 |+class A3(B0, abc.ABC, before_metaclass=1): -32 32 | pass +32 32 | def foo(self): pass 33 33 | -34 34 | +34 34 | +35 |-class A3(B0, before_metaclass=1, metaclass=abc.ABCMeta): + 35 |+class A3(B0, abc.ABC, before_metaclass=1): +36 36 | pass +37 37 | +38 38 | + +FURB180.py:43:20: FURB180 [*] Use of `metaclass=abc.ABCMeta` to define abstract base class + | +43 | class C1(Protocol, metaclass=ABCMeta): + | ^^^^^^^^^^^^^^^^^ FURB180 +44 | pass + | + = help: Replace with `abc.ABC` + +ℹ Safe fix +40 40 | pass +41 41 | +42 42 | +43 |-class C1(Protocol, metaclass=ABCMeta): + 43 |+class C1(Protocol, abc.ABC): +44 44 | pass +45 45 | +46 46 | diff --git a/crates/ruff_linter/src/settings/mod.rs b/crates/ruff_linter/src/settings/mod.rs index bfec035a594cd..31f5d62ff0424 100644 --- a/crates/ruff_linter/src/settings/mod.rs +++ b/crates/ruff_linter/src/settings/mod.rs @@ -20,7 +20,7 @@ use crate::rules::{ flake8_comprehensions, flake8_copyright, flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, flake8_import_conventions, flake8_pytest_style, flake8_quotes, flake8_self, flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, isort, mccabe, - pep8_naming, pycodestyle, pydoclint, pydocstyle, pyflakes, pylint, pyupgrade, ruff, + pep8_naming, pycodestyle, pydoclint, pydocstyle, pyflakes, pylint, pyupgrade, refurb, ruff, }; use crate::settings::types::{CompiledPerFileIgnoreList, ExtensionMapping, FilePatternSet}; use crate::{RuleSelector, codes, fs}; @@ -279,6 +279,7 @@ pub struct LinterSettings { pub pyflakes: pyflakes::settings::Settings, pub pylint: pylint::settings::Settings, pub pyupgrade: pyupgrade::settings::Settings, + pub refurb: refurb::settings::Settings, pub ruff: ruff::settings::Settings, } @@ -448,6 +449,7 @@ impl LinterSettings { pyflakes: pyflakes::settings::Settings::default(), pylint: pylint::settings::Settings::default(), pyupgrade: pyupgrade::settings::Settings::default(), + refurb: refurb::settings::Settings::default(), ruff: ruff::settings::Settings::default(), preview: PreviewMode::default(), explicit_preview_rules: false, diff --git a/crates/ruff_workspace/src/configuration.rs b/crates/ruff_workspace/src/configuration.rs index 684a2b434dfa0..e486ea4c5d626 100644 --- a/crates/ruff_workspace/src/configuration.rs +++ b/crates/ruff_workspace/src/configuration.rs @@ -53,7 +53,8 @@ use crate::options::{ Flake8QuotesOptions, Flake8SelfOptions, Flake8TidyImportsOptions, Flake8TypeCheckingOptions, Flake8UnusedArgumentsOptions, FormatOptions, IsortOptions, LintCommonOptions, LintOptions, McCabeOptions, Options, Pep8NamingOptions, PyUpgradeOptions, PycodestyleOptions, - PydoclintOptions, PydocstyleOptions, PyflakesOptions, PylintOptions, RuffOptions, + PydoclintOptions, PydocstyleOptions, PyflakesOptions, PylintOptions, RefurbOptions, + RuffOptions, }; use crate::settings::{ EXCLUDE, FileResolverSettings, FormatterSettings, INCLUDE, LineEnding, Settings, @@ -428,6 +429,10 @@ impl Configuration { .pyupgrade .map(PyUpgradeOptions::into_settings) .unwrap_or_default(), + refurb: lint + .refurb + .map(RefurbOptions::into_settings) + .unwrap_or_default(), ruff: lint .ruff .map(RuffOptions::into_settings) @@ -665,6 +670,7 @@ pub struct LintConfiguration { pub pyflakes: Option, pub pylint: Option, pub pyupgrade: Option, + pub refurb: Option, pub ruff: Option, } @@ -781,6 +787,7 @@ impl LintConfiguration { pyflakes: options.common.pyflakes, pylint: options.common.pylint, pyupgrade: options.common.pyupgrade, + refurb: options.refurb, ruff: options.ruff, }) } @@ -1178,6 +1185,7 @@ impl LintConfiguration { pyflakes: self.pyflakes.combine(config.pyflakes), pylint: self.pylint.combine(config.pylint), pyupgrade: self.pyupgrade.combine(config.pyupgrade), + refurb: self.refurb.combine(config.refurb), ruff: self.ruff.combine(config.ruff), typing_extensions: self.typing_extensions.or(config.typing_extensions), } diff --git a/crates/ruff_workspace/src/options.rs b/crates/ruff_workspace/src/options.rs index b68fc08ea9682..94693566f9a73 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -24,7 +24,7 @@ use ruff_linter::rules::{ flake8_copyright, flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, flake8_import_conventions, flake8_pytest_style, flake8_quotes, flake8_self, flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, isort, mccabe, pep8_naming, - pycodestyle, pydoclint, pydocstyle, pyflakes, pylint, pyupgrade, ruff, + pycodestyle, pydoclint, pydocstyle, pyflakes, pylint, pyupgrade, refurb, ruff, }; use ruff_linter::settings::types::{ IdentifierPattern, OutputFormat, PythonVersion, RequiredVersion, @@ -498,6 +498,10 @@ pub struct LintOptions { #[option_group] pub pydoclint: Option, + /// Options for the `refurb` plugin + #[option_group] + pub refurb: Option, + /// Options for the `ruff` plugin #[option_group] pub ruff: Option, @@ -3384,6 +3388,55 @@ impl PyUpgradeOptions { } } +/// Options for the `refurb` plugin +#[derive( + Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize, OptionsMetadata, CombineOptions, +)] +#[serde(deny_unknown_fields, rename_all = "kebab-case")] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub struct RefurbOptions { + /// Define a list of classes. If a class `C` is a subclass of only classes in this list, + /// `refurb` will allow `C` to explicitly set its metaclass to `abc.ABCMeta`, instead of + /// insisting on inheriting from `abc.ABC` instead. + /// + /// This is useful for classes that validate their base-classes at runtime. + /// + /// For example, if `SpecialBaseClass` is in the list, while `OtherBaseClass` + /// is not: + /// + /// ```python + /// class MyClass1(SpecialBaseClass, metaclass=abc.ABCMeta): # this is allowed + /// pass + /// + /// + /// class MyClass2(OtherBaseClass, metaclass=abc.ABCMeta): # this will be linted + /// pass + /// + /// + /// class MyClass2(OtherBaseClass, abc.ABC): # instead this is the way to go + /// pass + /// ``` + /// + /// Expects to receive a list of fully-qualified names (e.g., `typing.Protocol`, rather than + /// `Protocol`). + #[option( + default = r#"["typing.Protocol", "typing_extensions.Protocol"]"#, + value_type = "list[str]", + example = r#"allow-abc-meta-bases = ["my_package.SpecialBaseClass"]"# + )] + pub allow_abc_meta_bases: Option>, +} + +impl RefurbOptions { + pub fn into_settings(self) -> refurb::settings::Settings { + refurb::settings::Settings { + allow_abc_meta_bases: self + .allow_abc_meta_bases + .unwrap_or_else(refurb::settings::default_allow_abc_meta_bases), + } + } +} + /// Options for the `ruff` plugin #[derive( Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize, OptionsMetadata, CombineOptions, @@ -3892,6 +3945,7 @@ pub struct LintOptionsWire { exclude: Option>, pydoclint: Option, + refurb: Option, ruff: Option, preview: Option, typing_extensions: Option, @@ -3947,6 +4001,7 @@ impl From for LintOptions { extend_per_file_ignores, exclude, pydoclint, + refurb, ruff, preview, typing_extensions, @@ -4003,6 +4058,7 @@ impl From for LintOptions { }, exclude, pydoclint, + refurb, ruff, preview, typing_extensions, From c648794aec54f8b01f107f3c37f846ebe1ab9a97 Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Sat, 17 May 2025 15:58:54 +0200 Subject: [PATCH 02/12] Update schema.json --- ruff.schema.json | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/ruff.schema.json b/ruff.schema.json index 0059b7bb2fbaf..7cd5ae0f70c58 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2428,6 +2428,17 @@ } ] }, + "refurb": { + "description": "Options for the `refurb` plugin", + "anyOf": [ + { + "$ref": "#/definitions/RefurbOptions" + }, + { + "type": "null" + } + ] + }, "ruff": { "description": "Options for the `ruff` plugin", "anyOf": [ @@ -2878,6 +2889,24 @@ "preserve" ] }, + "RefurbOptions": { + "description": "Options for the `refurb` plugin", + "type": "object", + "properties": { + "allow-abc-meta-bases": { + "description": "Define a list of classes. If a class `C` is a subclass of only classes in this list, `refurb` will allow `C` to explicitly set its metaclass to `abc.ABCMeta`, instead of insisting on inheriting from `abc.ABC` instead.\n\nThis is useful for classes that validate their base-classes at runtime.\n\nFor example, if `SpecialBaseClass` is in the list, while `OtherBaseClass` is not:\n\n```python class MyClass1(SpecialBaseClass, metaclass=abc.ABCMeta): # this is allowed pass\n\nclass MyClass2(OtherBaseClass, metaclass=abc.ABCMeta): # this will be linted pass\n\nclass MyClass2(OtherBaseClass, abc.ABC): # instead this is the way to go pass ```\n\nExpects to receive a list of fully-qualified names (e.g., `typing.Protocol`, rather than `Protocol`).", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + }, + "uniqueItems": true + } + }, + "additionalProperties": false + }, "RelativeImportsOrder": { "oneOf": [ { From 540f1acf4af835912edf32bd2e370416df915539 Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Tue, 3 Jun 2025 05:31:50 +0000 Subject: [PATCH 03/12] Improve efficiency - avoid set allocation Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com> --- .../rules/refurb/rules/metaclass_abcmeta.rs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/crates/ruff_linter/src/rules/refurb/rules/metaclass_abcmeta.rs b/crates/ruff_linter/src/rules/refurb/rules/metaclass_abcmeta.rs index b6b5885f5889f..10affea618e9c 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/metaclass_abcmeta.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/metaclass_abcmeta.rs @@ -71,20 +71,18 @@ pub(crate) fn metaclass_abcmeta(checker: &Checker, class_def: &StmtClassDef) { } // Determine if all base classes are in the configured list of exceptions. - let bases: Option> = class_def - .bases() - .iter() - .map(|base| { + let bases = class_def.bases(); + let all_bases_allowed = !bases.is_empty() + && bases.iter().all(|base| { checker .semantic() .resolve_qualified_name(base) .map(|qualified_name| qualified_name.to_string()) - }) - .collect(); - if let Some(bases) = &bases { - if !bases.is_empty() && bases.is_subset(&checker.settings.refurb.allow_abc_meta_bases) { - return; - } + .is_some_and(|name| checker.settings.refurb.allow_abc_meta_bases.contains(&name)) + }); + + if all_bases_allowed { + return; } let mut diagnostic = checker.report_diagnostic(MetaClassABCMeta, keyword.range); From 1ebef76de00e429e0dd7b1f3cbb228d1a46b394b Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Tue, 3 Jun 2025 08:03:57 +0200 Subject: [PATCH 04/12] Add extend-abc-meta-bases config option --- .../src/rules/refurb/rules/metaclass_abcmeta.rs | 13 ++++++++++++- crates/ruff_linter/src/rules/refurb/settings.rs | 2 ++ crates/ruff_workspace/src/options.rs | 12 ++++++++++++ ruff.schema.json | 11 +++++++++++ 4 files changed, 37 insertions(+), 1 deletion(-) diff --git a/crates/ruff_linter/src/rules/refurb/rules/metaclass_abcmeta.rs b/crates/ruff_linter/src/rules/refurb/rules/metaclass_abcmeta.rs index 83b59bd2bdd11..b2bd42340a803 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/metaclass_abcmeta.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/metaclass_abcmeta.rs @@ -37,6 +37,10 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// be validating the class's other base classes (e.g., `typing.Protocol` does this) or otherwise /// alter runtime behavior if more base classes are added. /// +/// ## Options +/// - `lint.refurb.allow-abc-meta-bases` +/// - `lint.refurb.extend-abc-meta-bases` +/// /// ## References /// - [Python documentation: `abc.ABC`](https://docs.python.org/3/library/abc.html#abc.ABC) /// - [Python documentation: `abc.ABCMeta`](https://docs.python.org/3/library/abc.html#abc.ABCMeta) @@ -83,7 +87,14 @@ pub(crate) fn metaclass_abcmeta(checker: &Checker, class_def: &StmtClassDef) { .semantic() .resolve_qualified_name(base) .map(|qualified_name| qualified_name.to_string()) - .is_some_and(|name| checker.settings.refurb.allow_abc_meta_bases.contains(&name)) + .is_some_and(|name| { + checker.settings.refurb.allow_abc_meta_bases.contains(&name) + || checker + .settings + .refurb + .extend_abc_meta_bases + .contains(&name) + }) }); if all_bases_allowed { diff --git a/crates/ruff_linter/src/rules/refurb/settings.rs b/crates/ruff_linter/src/rules/refurb/settings.rs index 0bc1ea40d2935..245845e39813e 100644 --- a/crates/ruff_linter/src/rules/refurb/settings.rs +++ b/crates/ruff_linter/src/rules/refurb/settings.rs @@ -17,12 +17,14 @@ pub fn default_allow_abc_meta_bases() -> FxHashSet { #[derive(Debug, Clone, CacheKey)] pub struct Settings { pub allow_abc_meta_bases: FxHashSet, + pub extend_abc_meta_bases: FxHashSet, } impl Default for Settings { fn default() -> Self { Self { allow_abc_meta_bases: default_allow_abc_meta_bases(), + extend_abc_meta_bases: Default::default(), } } } diff --git a/crates/ruff_workspace/src/options.rs b/crates/ruff_workspace/src/options.rs index 94693566f9a73..97a54268c5db8 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -3425,6 +3425,17 @@ pub struct RefurbOptions { example = r#"allow-abc-meta-bases = ["my_package.SpecialBaseClass"]"# )] pub allow_abc_meta_bases: Option>, + + /// A list of additional classes to [`allow_abc_meta_bases`]. + /// + /// Expects to receive a list of fully-qualified names (e.g., `typing.Protocol`, rather than + /// `Protocol`). + #[option( + default = r#"[]"#, + value_type = "list[str]", + example = r#"extend-abc-meta-bases = ["my_package.SpecialBaseClass"]"# + )] + pub extend_abc_meta_bases: Option>, } impl RefurbOptions { @@ -3433,6 +3444,7 @@ impl RefurbOptions { allow_abc_meta_bases: self .allow_abc_meta_bases .unwrap_or_else(refurb::settings::default_allow_abc_meta_bases), + extend_abc_meta_bases: self.extend_abc_meta_bases.unwrap_or_default(), } } } diff --git a/ruff.schema.json b/ruff.schema.json index 07ad84d686019..3a9277ca1c3ae 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2903,6 +2903,17 @@ "type": "string" }, "uniqueItems": true + }, + "extend-abc-meta-bases": { + "description": "A list of additional classes to [`allow_abc_meta_bases`].\n\nExpects to receive a list of fully-qualified names (e.g., `typing.Protocol`, rather than `Protocol`).", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + }, + "uniqueItems": true } }, "additionalProperties": false From 962ce6081ed7d17d9a53e63bd39e93a2f02b5769 Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Tue, 3 Jun 2025 08:36:24 +0200 Subject: [PATCH 05/12] FURB180: silence if any base is exempt by config --- .../rules/refurb/rules/metaclass_abcmeta.rs | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/crates/ruff_linter/src/rules/refurb/rules/metaclass_abcmeta.rs b/crates/ruff_linter/src/rules/refurb/rules/metaclass_abcmeta.rs index b2bd42340a803..fdade4fbc1980 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/metaclass_abcmeta.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/metaclass_abcmeta.rs @@ -79,25 +79,23 @@ pub(crate) fn metaclass_abcmeta(checker: &Checker, class_def: &StmtClassDef) { return; } - // Determine if all base classes are in the configured list of exceptions. - let bases = class_def.bases(); - let all_bases_allowed = !bases.is_empty() - && bases.iter().all(|base| { - checker - .semantic() - .resolve_qualified_name(base) - .map(|qualified_name| qualified_name.to_string()) - .is_some_and(|name| { - checker.settings.refurb.allow_abc_meta_bases.contains(&name) - || checker - .settings - .refurb - .extend_abc_meta_bases - .contains(&name) - }) - }); + // Determine if base classes contain an exempted class per configuration + let has_exempt_base = class_def.bases().iter().any(|base| { + checker + .semantic() + .resolve_qualified_name(base) + .map(|qualified_name| qualified_name.to_string()) + .is_some_and(|name| { + checker.settings.refurb.allow_abc_meta_bases.contains(&name) + || checker + .settings + .refurb + .extend_abc_meta_bases + .contains(&name) + }) + }); - if all_bases_allowed { + if has_exempt_base { return; } From c369ba7e1a2195be47591ceab36e79426bf61719 Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Tue, 3 Jun 2025 08:37:21 +0200 Subject: [PATCH 06/12] Add tests --- .../fixtures/refurb/FURB180_exceptions.py | 83 +++++++++++++++++++ crates/ruff_linter/src/rules/refurb/mod.rs | 38 +++++++++ ..._tests__FURB180_FURB180_exceptions.py.snap | 62 ++++++++++++++ 3 files changed, 183 insertions(+) create mode 100644 crates/ruff_linter/resources/test/fixtures/refurb/FURB180_exceptions.py create mode 100644 crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB180_FURB180_exceptions.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/refurb/FURB180_exceptions.py b/crates/ruff_linter/resources/test/fixtures/refurb/FURB180_exceptions.py new file mode 100644 index 0000000000000..e915d5b4cb19f --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/refurb/FURB180_exceptions.py @@ -0,0 +1,83 @@ +import abc +import ast +from abc import abstractmethod, ABCMeta +from ast import Name + + +# marked as exempt base class +class A0: + pass + + +# not exempt base class +class A1: + pass + + +# Errors + +class B0(A1, metaclass=abc.ABCMeta): + @abstractmethod + def foo(self): pass + + +class B1(ast.Param, metaclass=abc.ABCMeta): + @abstractmethod + def foo(self): pass + + +class B2(A1, ast.Param, metaclass=abc.ABCMeta): + @abstractmethod + def foo(self): pass + + +# OK + +class C0(A0, metaclass=abc.ABCMeta): + @abstractmethod + def foo(self): pass + + +class C1(ast.Name, metaclass=abc.ABCMeta): + @abstractmethod + def foo(self): pass + + +class C2(Name, metaclass=abc.ABCMeta): + @abstractmethod + def foo(self): pass + + +class C3(ast.Param, A0, metaclass=abc.ABCMeta): + @abstractmethod + def foo(self): pass + + +class C4(A0, ast.Param, metaclass=abc.ABCMeta): + @abstractmethod + def foo(self): pass + + +class C5(ast.Name, A1, metaclass=abc.ABCMeta): + @abstractmethod + def foo(self): pass + + +class C6(A1, ast.Name, metaclass=abc.ABCMeta): + @abstractmethod + def foo(self): pass + + +class C6(Name, A1, metaclass=abc.ABCMeta): + @abstractmethod + def foo(self): pass + + +class C7(A1, Name, metaclass=abc.ABCMeta): + @abstractmethod + def foo(self): pass + + +class C8(A0, Name, metaclass=abc.ABCMeta): + @abstractmethod + def foo(self): pass diff --git a/crates/ruff_linter/src/rules/refurb/mod.rs b/crates/ruff_linter/src/rules/refurb/mod.rs index ec70674630753..135dbdca1aece 100644 --- a/crates/ruff_linter/src/rules/refurb/mod.rs +++ b/crates/ruff_linter/src/rules/refurb/mod.rs @@ -13,6 +13,7 @@ mod tests { use test_case::test_case; use crate::registry::Rule; + use crate::rules::refurb; use crate::test::test_path; use crate::{assert_messages, settings}; @@ -102,4 +103,41 @@ mod tests { assert_messages!(diagnostics); Ok(()) } + + #[test] + fn allow_abc_meta_bases() -> Result<()> { + test_furb180_exemption(refurb::settings::Settings { + allow_abc_meta_bases: ["ast.Name", "FURB180_exceptions.A0"] + .into_iter() + .map(String::from) + .collect(), + ..refurb::settings::Settings::default() + }) + } + + #[test] + fn extend_abc_meta_bases() -> Result<()> { + test_furb180_exemption(refurb::settings::Settings { + extend_abc_meta_bases: ["ast.Name", "FURB180_exceptions.A0"] + .into_iter() + .map(String::from) + .collect(), + ..refurb::settings::Settings::default() + }) + } + + fn test_furb180_exemption(settings: refurb::settings::Settings) -> Result<()> { + let rule_code = Rule::MetaClassABCMeta; + let path = Path::new("FURB180_exceptions.py"); + let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); + let diagnostics = test_path( + Path::new("refurb").join(path).as_path(), + &settings::LinterSettings { + refurb: settings, + ..settings::LinterSettings::for_rule(rule_code) + }, + )?; + assert_messages!(snapshot, diagnostics); + Ok(()) + } } diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB180_FURB180_exceptions.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB180_FURB180_exceptions.py.snap new file mode 100644 index 0000000000000..51d09b4acc202 --- /dev/null +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB180_FURB180_exceptions.py.snap @@ -0,0 +1,62 @@ +--- +source: crates/ruff_linter/src/rules/refurb/mod.rs +assertion_line: 140 +--- +FURB180_exceptions.py:19:14: FURB180 [*] Use of `metaclass=abc.ABCMeta` to define abstract base class + | +17 | # Errors +18 | +19 | class B0(A1, metaclass=abc.ABCMeta): + | ^^^^^^^^^^^^^^^^^^^^^ FURB180 +20 | @abstractmethod +21 | def foo(self): pass + | + = help: Replace with `abc.ABC` + +ℹ Unsafe fix +16 16 | +17 17 | # Errors +18 18 | +19 |-class B0(A1, metaclass=abc.ABCMeta): + 19 |+class B0(A1, abc.ABC): +20 20 | @abstractmethod +21 21 | def foo(self): pass +22 22 | + +FURB180_exceptions.py:24:21: FURB180 [*] Use of `metaclass=abc.ABCMeta` to define abstract base class + | +24 | class B1(ast.Param, metaclass=abc.ABCMeta): + | ^^^^^^^^^^^^^^^^^^^^^ FURB180 +25 | @abstractmethod +26 | def foo(self): pass + | + = help: Replace with `abc.ABC` + +ℹ Unsafe fix +21 21 | def foo(self): pass +22 22 | +23 23 | +24 |-class B1(ast.Param, metaclass=abc.ABCMeta): + 24 |+class B1(ast.Param, abc.ABC): +25 25 | @abstractmethod +26 26 | def foo(self): pass +27 27 | + +FURB180_exceptions.py:29:25: FURB180 [*] Use of `metaclass=abc.ABCMeta` to define abstract base class + | +29 | class B2(A1, ast.Param, metaclass=abc.ABCMeta): + | ^^^^^^^^^^^^^^^^^^^^^ FURB180 +30 | @abstractmethod +31 | def foo(self): pass + | + = help: Replace with `abc.ABC` + +ℹ Unsafe fix +26 26 | def foo(self): pass +27 27 | +28 28 | +29 |-class B2(A1, ast.Param, metaclass=abc.ABCMeta): + 29 |+class B2(A1, ast.Param, abc.ABC): +30 30 | @abstractmethod +31 31 | def foo(self): pass +32 32 | From 915d5ddf4e16d7c60e05ddb729ceaff4ef85e463 Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Tue, 3 Jun 2025 09:47:00 +0200 Subject: [PATCH 07/12] Fix lint & docs link --- crates/ruff_linter/src/rules/refurb/settings.rs | 2 +- crates/ruff_workspace/src/options.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ruff_linter/src/rules/refurb/settings.rs b/crates/ruff_linter/src/rules/refurb/settings.rs index 245845e39813e..44a8024cf12f5 100644 --- a/crates/ruff_linter/src/rules/refurb/settings.rs +++ b/crates/ruff_linter/src/rules/refurb/settings.rs @@ -24,7 +24,7 @@ impl Default for Settings { fn default() -> Self { Self { allow_abc_meta_bases: default_allow_abc_meta_bases(), - extend_abc_meta_bases: Default::default(), + extend_abc_meta_bases: FxHashSet::default(), } } } diff --git a/crates/ruff_workspace/src/options.rs b/crates/ruff_workspace/src/options.rs index 97a54268c5db8..0fa08e751541a 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -3426,7 +3426,7 @@ pub struct RefurbOptions { )] pub allow_abc_meta_bases: Option>, - /// A list of additional classes to [`allow_abc_meta_bases`]. + /// A list of additional classes to [`allow_abc_meta_bases`](#lint_refurb_allow-abc-meta-bases). /// /// Expects to receive a list of fully-qualified names (e.g., `typing.Protocol`, rather than /// `Protocol`). From 40238ac6e4826258fe21065a4c15e03b73405332 Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Tue, 3 Jun 2025 09:58:42 +0200 Subject: [PATCH 08/12] cargo dev generate-all --- ruff.schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ruff.schema.json b/ruff.schema.json index 3a9277ca1c3ae..529301a75ecd1 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2905,7 +2905,7 @@ "uniqueItems": true }, "extend-abc-meta-bases": { - "description": "A list of additional classes to [`allow_abc_meta_bases`].\n\nExpects to receive a list of fully-qualified names (e.g., `typing.Protocol`, rather than `Protocol`).", + "description": "A list of additional classes to [`allow_abc_meta_bases`](#lint_refurb_allow-abc-meta-bases).\n\nExpects to receive a list of fully-qualified names (e.g., `typing.Protocol`, rather than `Protocol`).", "type": [ "array", "null" From cb6f2864c9d94a20173b3ce0d96c921af1847425 Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Tue, 3 Jun 2025 10:01:00 +0200 Subject: [PATCH 09/12] fixup! FURB180: silence if any base is exempt by config --- crates/ruff_workspace/src/options.rs | 2 +- ruff.schema.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ruff_workspace/src/options.rs b/crates/ruff_workspace/src/options.rs index 0fa08e751541a..387b67ef3f891 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -3395,7 +3395,7 @@ impl PyUpgradeOptions { #[serde(deny_unknown_fields, rename_all = "kebab-case")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct RefurbOptions { - /// Define a list of classes. If a class `C` is a subclass of only classes in this list, + /// Define a list of classes. If a class `C` is a subclass of at least one class in this list, /// `refurb` will allow `C` to explicitly set its metaclass to `abc.ABCMeta`, instead of /// insisting on inheriting from `abc.ABC` instead. /// diff --git a/ruff.schema.json b/ruff.schema.json index 529301a75ecd1..b8ebd020791b5 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2894,7 +2894,7 @@ "type": "object", "properties": { "allow-abc-meta-bases": { - "description": "Define a list of classes. If a class `C` is a subclass of only classes in this list, `refurb` will allow `C` to explicitly set its metaclass to `abc.ABCMeta`, instead of insisting on inheriting from `abc.ABC` instead.\n\nThis is useful for classes that validate their base-classes at runtime.\n\nFor example, if `SpecialBaseClass` is in the list, while `OtherBaseClass` is not:\n\n```python class MyClass1(SpecialBaseClass, metaclass=abc.ABCMeta): # this is allowed pass\n\nclass MyClass2(OtherBaseClass, metaclass=abc.ABCMeta): # this will be linted pass\n\nclass MyClass2(OtherBaseClass, abc.ABC): # instead this is the way to go pass ```\n\nExpects to receive a list of fully-qualified names (e.g., `typing.Protocol`, rather than `Protocol`).", + "description": "Define a list of classes. If a class `C` is a subclass of at least one class in this list, `refurb` will allow `C` to explicitly set its metaclass to `abc.ABCMeta`, instead of insisting on inheriting from `abc.ABC` instead.\n\nThis is useful for classes that validate their base-classes at runtime.\n\nFor example, if `SpecialBaseClass` is in the list, while `OtherBaseClass` is not:\n\n```python class MyClass1(SpecialBaseClass, metaclass=abc.ABCMeta): # this is allowed pass\n\nclass MyClass2(OtherBaseClass, metaclass=abc.ABCMeta): # this will be linted pass\n\nclass MyClass2(OtherBaseClass, abc.ABC): # instead this is the way to go pass ```\n\nExpects to receive a list of fully-qualified names (e.g., `typing.Protocol`, rather than `Protocol`).", "type": [ "array", "null" From d2bbe8626cc5bf5f32e32b921371708e8f62199b Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Tue, 3 Jun 2025 18:06:05 +0200 Subject: [PATCH 10/12] Update snapshot files --- .../ruff_linter__rules__refurb__tests__FURB180_FURB180.py.snap | 1 - ...ter__rules__refurb__tests__FURB180_FURB180_exceptions.py.snap | 1 - 2 files changed, 2 deletions(-) diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB180_FURB180.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB180_FURB180.py.snap index 9140c626c2740..186bdaeee290e 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB180_FURB180.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB180_FURB180.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_linter/src/rules/refurb/mod.rs -assertion_line: 62 --- FURB180.py:11:10: FURB180 [*] Use of `metaclass=abc.ABCMeta` to define abstract base class | diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB180_FURB180_exceptions.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB180_FURB180_exceptions.py.snap index 51d09b4acc202..8bacf269e642b 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB180_FURB180_exceptions.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB180_FURB180_exceptions.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_linter/src/rules/refurb/mod.rs -assertion_line: 140 --- FURB180_exceptions.py:19:14: FURB180 [*] Use of `metaclass=abc.ABCMeta` to define abstract base class | From 2f517aeda1dc141dbb88e913ad9d7319b581f560 Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Tue, 3 Jun 2025 18:26:20 +0200 Subject: [PATCH 11/12] Rename new config options and eagerly merge values --- crates/ruff_linter/src/rules/refurb/mod.rs | 28 ++++--------------- .../rules/refurb/rules/metaclass_abcmeta.rs | 11 ++------ .../ruff_linter/src/rules/refurb/settings.rs | 2 -- crates/ruff_workspace/src/options.rs | 10 ++++--- ruff.schema.json | 2 +- 5 files changed, 15 insertions(+), 38 deletions(-) diff --git a/crates/ruff_linter/src/rules/refurb/mod.rs b/crates/ruff_linter/src/rules/refurb/mod.rs index 135dbdca1aece..e0f8453fae911 100644 --- a/crates/ruff_linter/src/rules/refurb/mod.rs +++ b/crates/ruff_linter/src/rules/refurb/mod.rs @@ -106,34 +106,18 @@ mod tests { #[test] fn allow_abc_meta_bases() -> Result<()> { - test_furb180_exemption(refurb::settings::Settings { - allow_abc_meta_bases: ["ast.Name", "FURB180_exceptions.A0"] - .into_iter() - .map(String::from) - .collect(), - ..refurb::settings::Settings::default() - }) - } - - #[test] - fn extend_abc_meta_bases() -> Result<()> { - test_furb180_exemption(refurb::settings::Settings { - extend_abc_meta_bases: ["ast.Name", "FURB180_exceptions.A0"] - .into_iter() - .map(String::from) - .collect(), - ..refurb::settings::Settings::default() - }) - } - - fn test_furb180_exemption(settings: refurb::settings::Settings) -> Result<()> { let rule_code = Rule::MetaClassABCMeta; let path = Path::new("FURB180_exceptions.py"); let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); let diagnostics = test_path( Path::new("refurb").join(path).as_path(), &settings::LinterSettings { - refurb: settings, + refurb: refurb::settings::Settings { + allow_abc_meta_bases: ["ast.Name", "FURB180_exceptions.A0"] + .into_iter() + .map(String::from) + .collect(), + }, ..settings::LinterSettings::for_rule(rule_code) }, )?; diff --git a/crates/ruff_linter/src/rules/refurb/rules/metaclass_abcmeta.rs b/crates/ruff_linter/src/rules/refurb/rules/metaclass_abcmeta.rs index fdade4fbc1980..71d8f99297505 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/metaclass_abcmeta.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/metaclass_abcmeta.rs @@ -39,7 +39,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// /// ## Options /// - `lint.refurb.allow-abc-meta-bases` -/// - `lint.refurb.extend-abc-meta-bases` +/// - `lint.refurb.extend-allow-abc-meta-bases` /// /// ## References /// - [Python documentation: `abc.ABC`](https://docs.python.org/3/library/abc.html#abc.ABC) @@ -85,14 +85,7 @@ pub(crate) fn metaclass_abcmeta(checker: &Checker, class_def: &StmtClassDef) { .semantic() .resolve_qualified_name(base) .map(|qualified_name| qualified_name.to_string()) - .is_some_and(|name| { - checker.settings.refurb.allow_abc_meta_bases.contains(&name) - || checker - .settings - .refurb - .extend_abc_meta_bases - .contains(&name) - }) + .is_some_and(|name| checker.settings.refurb.allow_abc_meta_bases.contains(&name)) }); if has_exempt_base { diff --git a/crates/ruff_linter/src/rules/refurb/settings.rs b/crates/ruff_linter/src/rules/refurb/settings.rs index 44a8024cf12f5..0bc1ea40d2935 100644 --- a/crates/ruff_linter/src/rules/refurb/settings.rs +++ b/crates/ruff_linter/src/rules/refurb/settings.rs @@ -17,14 +17,12 @@ pub fn default_allow_abc_meta_bases() -> FxHashSet { #[derive(Debug, Clone, CacheKey)] pub struct Settings { pub allow_abc_meta_bases: FxHashSet, - pub extend_abc_meta_bases: FxHashSet, } impl Default for Settings { fn default() -> Self { Self { allow_abc_meta_bases: default_allow_abc_meta_bases(), - extend_abc_meta_bases: FxHashSet::default(), } } } diff --git a/crates/ruff_workspace/src/options.rs b/crates/ruff_workspace/src/options.rs index 387b67ef3f891..90a49948db8e0 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -3433,9 +3433,9 @@ pub struct RefurbOptions { #[option( default = r#"[]"#, value_type = "list[str]", - example = r#"extend-abc-meta-bases = ["my_package.SpecialBaseClass"]"# + example = r#"extend-allow-abc-meta-bases = ["my_package.SpecialBaseClass"]"# )] - pub extend_abc_meta_bases: Option>, + pub extend_allow_abc_meta_bases: Option>, } impl RefurbOptions { @@ -3443,8 +3443,10 @@ impl RefurbOptions { refurb::settings::Settings { allow_abc_meta_bases: self .allow_abc_meta_bases - .unwrap_or_else(refurb::settings::default_allow_abc_meta_bases), - extend_abc_meta_bases: self.extend_abc_meta_bases.unwrap_or_default(), + .unwrap_or_else(refurb::settings::default_allow_abc_meta_bases) + .into_iter() + .chain(self.extend_allow_abc_meta_bases.unwrap_or_default()) + .collect(), } } } diff --git a/ruff.schema.json b/ruff.schema.json index b8ebd020791b5..a1612d9395d1b 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2904,7 +2904,7 @@ }, "uniqueItems": true }, - "extend-abc-meta-bases": { + "extend-allow-abc-meta-bases": { "description": "A list of additional classes to [`allow_abc_meta_bases`](#lint_refurb_allow-abc-meta-bases).\n\nExpects to receive a list of fully-qualified names (e.g., `typing.Protocol`, rather than `Protocol`).", "type": [ "array", From 706bf079f0447e14dc5c1fb84e5fd56011003a32 Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Wed, 4 Jun 2025 09:29:46 +0200 Subject: [PATCH 12/12] Rename options `allow...` to `allowed...` --- crates/ruff_linter/src/rules/refurb/mod.rs | 4 ++-- .../rules/refurb/rules/metaclass_abcmeta.rs | 7 ++++--- .../ruff_linter/src/rules/refurb/settings.rs | 8 ++++---- crates/ruff_workspace/src/options.rs | 18 +++++++++--------- ruff.schema.json | 6 +++--- 5 files changed, 22 insertions(+), 21 deletions(-) diff --git a/crates/ruff_linter/src/rules/refurb/mod.rs b/crates/ruff_linter/src/rules/refurb/mod.rs index e0f8453fae911..4f5bd5439f165 100644 --- a/crates/ruff_linter/src/rules/refurb/mod.rs +++ b/crates/ruff_linter/src/rules/refurb/mod.rs @@ -105,7 +105,7 @@ mod tests { } #[test] - fn allow_abc_meta_bases() -> Result<()> { + fn allowed_abc_meta_bases() -> Result<()> { let rule_code = Rule::MetaClassABCMeta; let path = Path::new("FURB180_exceptions.py"); let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); @@ -113,7 +113,7 @@ mod tests { Path::new("refurb").join(path).as_path(), &settings::LinterSettings { refurb: refurb::settings::Settings { - allow_abc_meta_bases: ["ast.Name", "FURB180_exceptions.A0"] + allowed_abc_meta_bases: ["ast.Name", "FURB180_exceptions.A0"] .into_iter() .map(String::from) .collect(), diff --git a/crates/ruff_linter/src/rules/refurb/rules/metaclass_abcmeta.rs b/crates/ruff_linter/src/rules/refurb/rules/metaclass_abcmeta.rs index 71d8f99297505..9b4a1f41e90fa 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/metaclass_abcmeta.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/metaclass_abcmeta.rs @@ -38,8 +38,8 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// alter runtime behavior if more base classes are added. /// /// ## Options -/// - `lint.refurb.allow-abc-meta-bases` -/// - `lint.refurb.extend-allow-abc-meta-bases` +/// - `lint.refurb.allowed-abc-meta-bases` +/// - `lint.refurb.extend-allowed-abc-meta-bases` /// /// ## References /// - [Python documentation: `abc.ABC`](https://docs.python.org/3/library/abc.html#abc.ABC) @@ -80,12 +80,13 @@ pub(crate) fn metaclass_abcmeta(checker: &Checker, class_def: &StmtClassDef) { } // Determine if base classes contain an exempted class per configuration + let allowed_abc_meta_bases = &checker.settings.refurb.allowed_abc_meta_bases; let has_exempt_base = class_def.bases().iter().any(|base| { checker .semantic() .resolve_qualified_name(base) .map(|qualified_name| qualified_name.to_string()) - .is_some_and(|name| checker.settings.refurb.allow_abc_meta_bases.contains(&name)) + .is_some_and(|name| allowed_abc_meta_bases.contains(&name)) }); if has_exempt_base { diff --git a/crates/ruff_linter/src/rules/refurb/settings.rs b/crates/ruff_linter/src/rules/refurb/settings.rs index 0bc1ea40d2935..c5ab06a3524d7 100644 --- a/crates/ruff_linter/src/rules/refurb/settings.rs +++ b/crates/ruff_linter/src/rules/refurb/settings.rs @@ -7,7 +7,7 @@ use rustc_hash::FxHashSet; use crate::display_settings; -pub fn default_allow_abc_meta_bases() -> FxHashSet { +pub fn default_allowed_abc_meta_bases() -> FxHashSet { ["typing.Protocol", "typing_extensions.Protocol"] .into_iter() .map(ToString::to_string) @@ -16,13 +16,13 @@ pub fn default_allow_abc_meta_bases() -> FxHashSet { #[derive(Debug, Clone, CacheKey)] pub struct Settings { - pub allow_abc_meta_bases: FxHashSet, + pub allowed_abc_meta_bases: FxHashSet, } impl Default for Settings { fn default() -> Self { Self { - allow_abc_meta_bases: default_allow_abc_meta_bases(), + allowed_abc_meta_bases: default_allowed_abc_meta_bases(), } } } @@ -33,7 +33,7 @@ impl fmt::Display for Settings { formatter = f, namespace = "linter.refurb", fields = [ - self.allow_abc_meta_bases | set + self.allowed_abc_meta_bases | set ] } Ok(()) diff --git a/crates/ruff_workspace/src/options.rs b/crates/ruff_workspace/src/options.rs index 90a49948db8e0..8712baca52e33 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -3422,30 +3422,30 @@ pub struct RefurbOptions { #[option( default = r#"["typing.Protocol", "typing_extensions.Protocol"]"#, value_type = "list[str]", - example = r#"allow-abc-meta-bases = ["my_package.SpecialBaseClass"]"# + example = r#"allowed-abc-meta-bases = ["my_package.SpecialBaseClass"]"# )] - pub allow_abc_meta_bases: Option>, + pub allowed_abc_meta_bases: Option>, - /// A list of additional classes to [`allow_abc_meta_bases`](#lint_refurb_allow-abc-meta-bases). + /// A list of additional classes to [`allowed_abc_meta_bases`](#lint_refurb_allowed-abc-meta-bases). /// /// Expects to receive a list of fully-qualified names (e.g., `typing.Protocol`, rather than /// `Protocol`). #[option( default = r#"[]"#, value_type = "list[str]", - example = r#"extend-allow-abc-meta-bases = ["my_package.SpecialBaseClass"]"# + example = r#"extend-allowed-abc-meta-bases = ["my_package.SpecialBaseClass"]"# )] - pub extend_allow_abc_meta_bases: Option>, + pub extend_allowed_abc_meta_bases: Option>, } impl RefurbOptions { pub fn into_settings(self) -> refurb::settings::Settings { refurb::settings::Settings { - allow_abc_meta_bases: self - .allow_abc_meta_bases - .unwrap_or_else(refurb::settings::default_allow_abc_meta_bases) + allowed_abc_meta_bases: self + .allowed_abc_meta_bases + .unwrap_or_else(refurb::settings::default_allowed_abc_meta_bases) .into_iter() - .chain(self.extend_allow_abc_meta_bases.unwrap_or_default()) + .chain(self.extend_allowed_abc_meta_bases.unwrap_or_default()) .collect(), } } diff --git a/ruff.schema.json b/ruff.schema.json index a1612d9395d1b..735c86c5da973 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2893,7 +2893,7 @@ "description": "Options for the `refurb` plugin", "type": "object", "properties": { - "allow-abc-meta-bases": { + "allowed-abc-meta-bases": { "description": "Define a list of classes. If a class `C` is a subclass of at least one class in this list, `refurb` will allow `C` to explicitly set its metaclass to `abc.ABCMeta`, instead of insisting on inheriting from `abc.ABC` instead.\n\nThis is useful for classes that validate their base-classes at runtime.\n\nFor example, if `SpecialBaseClass` is in the list, while `OtherBaseClass` is not:\n\n```python class MyClass1(SpecialBaseClass, metaclass=abc.ABCMeta): # this is allowed pass\n\nclass MyClass2(OtherBaseClass, metaclass=abc.ABCMeta): # this will be linted pass\n\nclass MyClass2(OtherBaseClass, abc.ABC): # instead this is the way to go pass ```\n\nExpects to receive a list of fully-qualified names (e.g., `typing.Protocol`, rather than `Protocol`).", "type": [ "array", @@ -2904,8 +2904,8 @@ }, "uniqueItems": true }, - "extend-allow-abc-meta-bases": { - "description": "A list of additional classes to [`allow_abc_meta_bases`](#lint_refurb_allow-abc-meta-bases).\n\nExpects to receive a list of fully-qualified names (e.g., `typing.Protocol`, rather than `Protocol`).", + "extend-allowed-abc-meta-bases": { + "description": "A list of additional classes to [`allowed_abc_meta_bases`](#lint_refurb_allowed-abc-meta-bases).\n\nExpects to receive a list of fully-qualified names (e.g., `typing.Protocol`, rather than `Protocol`).", "type": [ "array", "null"