Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/refurb/FURB180.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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):
Expand All @@ -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
Original file line number Diff line number Diff line change
@@ -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
23 changes: 23 additions & 0 deletions crates/ruff_linter/src/rules/refurb/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

mod helpers;
pub(crate) mod rules;
pub mod settings;

#[cfg(test)]
mod tests {
Expand All @@ -12,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};

Expand Down Expand Up @@ -101,4 +103,25 @@ mod tests {
assert_messages!(diagnostics);
Ok(())
}

#[test]
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());
let diagnostics = test_path(
Path::new("refurb").join(path).as_path(),
&settings::LinterSettings {
refurb: refurb::settings::Settings {
allowed_abc_meta_bases: ["ast.Name", "FURB180_exceptions.A0"]
.into_iter()
.map(String::from)
.collect(),
},
..settings::LinterSettings::for_rule(rule_code)
},
)?;
assert_messages!(snapshot, diagnostics);
Ok(())
}
}
18 changes: 18 additions & 0 deletions crates/ruff_linter/src/rules/refurb/rules/metaclass_abcmeta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.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)
/// - [Python documentation: `abc.ABCMeta`](https://docs.python.org/3/library/abc.html#abc.ABCMeta)
Expand Down Expand Up @@ -75,6 +79,20 @@ pub(crate) fn metaclass_abcmeta(checker: &Checker, class_def: &StmtClassDef) {
return;
}

// 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| allowed_abc_meta_bases.contains(&name))
});

if has_exempt_base {
return;
}

let applicability = if class_def.bases().is_empty() {
Applicability::Safe
} else {
Expand Down
41 changes: 41 additions & 0 deletions crates/ruff_linter/src/rules/refurb/settings.rs
Original file line number Diff line number Diff line change
@@ -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_allowed_abc_meta_bases() -> FxHashSet<String> {
["typing.Protocol", "typing_extensions.Protocol"]
.into_iter()
.map(ToString::to_string)
.collect()
}

#[derive(Debug, Clone, CacheKey)]
pub struct Settings {
pub allowed_abc_meta_bases: FxHashSet<String>,
}

impl Default for Settings {
fn default() -> Self {
Self {
allowed_abc_meta_bases: default_allowed_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.allowed_abc_meta_bases | set
]
}
Ok(())
}
}
Original file line number Diff line number Diff line change
@@ -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`

ℹ Unsafe 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`

ℹ Unsafe 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`

ℹ Unsafe 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 |
Loading
Loading