Skip to content

Commit 12b5c3a

Browse files
[flake8-bugbear] Ignore enum classes in cached-instance-method (B019) (#11312)
## Summary While I was here, I also updated the rule to use `function_type::classify` rather than hard-coding `staticmethod` and friends. Per Carl: > Enum instances are already referred to by the class, forming a cycle that won't get collected until the class itself does. At which point the `lru_cache` itself would be collected, too. Closes #9912.
1 parent a73b8c8 commit 12b5c3a

File tree

3 files changed

+56
-32
lines changed

3 files changed

+56
-32
lines changed

crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B019.py

+12
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,15 @@ def called_lru_cached_instance_method(self, y):
106106
@lru_cache()
107107
def another_called_lru_cached_instance_method(self, y):
108108
...
109+
110+
111+
import enum
112+
113+
114+
class Foo(enum.Enum):
115+
ONE = enum.auto()
116+
TWO = enum.auto()
117+
118+
@functools.cache
119+
def bar(self, arg: str) -> str:
120+
return f"{self} - {arg}"

crates/ruff_linter/src/checkers/ast/analyze/statement.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
203203
}
204204
}
205205
if checker.enabled(Rule::CachedInstanceMethod) {
206-
flake8_bugbear::rules::cached_instance_method(checker, decorator_list);
206+
flake8_bugbear::rules::cached_instance_method(checker, function_def);
207207
}
208208
if checker.enabled(Rule::MutableArgumentDefault) {
209209
flake8_bugbear::rules::mutable_argument_default(checker, function_def);

crates/ruff_linter/src/rules/flake8_bugbear/rules/cached_instance_method.rs

+43-31
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
use ruff_python_ast::{self as ast, Decorator, Expr};
2-
31
use ruff_diagnostics::{Diagnostic, Violation};
42
use ruff_macros::{derive_message_formats, violation};
5-
use ruff_python_semantic::SemanticModel;
3+
use ruff_python_ast::helpers::map_callable;
4+
use ruff_python_ast::{self as ast, Expr};
5+
use ruff_python_semantic::analyze::{class, function_type};
6+
use ruff_python_semantic::{ScopeKind, SemanticModel};
67
use ruff_text_size::Ranged;
78

89
use crate::checkers::ast::Checker;
@@ -20,6 +21,9 @@ use crate::checkers::ast::Checker;
2021
/// instance of the class, or use the `@lru_cache` decorator on a function
2122
/// outside of the class.
2223
///
24+
/// This rule ignores instance methods on enumeration classes, as enum members
25+
/// are singletons.
26+
///
2327
/// ## Example
2428
/// ```python
2529
/// from functools import lru_cache
@@ -70,42 +74,50 @@ impl Violation for CachedInstanceMethod {
7074
}
7175
}
7276

73-
fn is_cache_func(expr: &Expr, semantic: &SemanticModel) -> bool {
74-
semantic
75-
.resolve_qualified_name(expr)
76-
.is_some_and(|qualified_name| {
77-
matches!(
78-
qualified_name.segments(),
79-
["functools", "lru_cache" | "cache"]
80-
)
81-
})
82-
}
83-
8477
/// B019
85-
pub(crate) fn cached_instance_method(checker: &mut Checker, decorator_list: &[Decorator]) {
86-
if !checker.semantic().current_scope().kind.is_class() {
78+
pub(crate) fn cached_instance_method(checker: &mut Checker, function_def: &ast::StmtFunctionDef) {
79+
let scope = checker.semantic().current_scope();
80+
81+
// Parent scope _must_ be a class.
82+
let ScopeKind::Class(class_def) = scope.kind else {
83+
return;
84+
};
85+
86+
// The function must be an _instance_ method.
87+
let type_ = function_type::classify(
88+
&function_def.name,
89+
&function_def.decorator_list,
90+
scope,
91+
checker.semantic(),
92+
&checker.settings.pep8_naming.classmethod_decorators,
93+
&checker.settings.pep8_naming.staticmethod_decorators,
94+
);
95+
if !matches!(type_, function_type::FunctionType::Method) {
8796
return;
8897
}
89-
for decorator in decorator_list {
90-
// TODO(charlie): This should take into account `classmethod-decorators` and
91-
// `staticmethod-decorators`.
92-
if let Expr::Name(ast::ExprName { id, .. }) = &decorator.expression {
93-
if id == "classmethod" || id == "staticmethod" {
98+
99+
for decorator in &function_def.decorator_list {
100+
if is_cache_func(map_callable(&decorator.expression), checker.semantic()) {
101+
// If we found a cached instance method, validate (lazily) that the class is not an enum.
102+
if class::is_enumeration(class_def, checker.semantic()) {
94103
return;
95104
}
96-
}
97-
}
98-
for decorator in decorator_list {
99-
if is_cache_func(
100-
match &decorator.expression {
101-
Expr::Call(ast::ExprCall { func, .. }) => func,
102-
_ => &decorator.expression,
103-
},
104-
checker.semantic(),
105-
) {
105+
106106
checker
107107
.diagnostics
108108
.push(Diagnostic::new(CachedInstanceMethod, decorator.range()));
109109
}
110110
}
111111
}
112+
113+
/// Returns `true` if the given expression is a call to `functools.lru_cache` or `functools.cache`.
114+
fn is_cache_func(expr: &Expr, semantic: &SemanticModel) -> bool {
115+
semantic
116+
.resolve_qualified_name(expr)
117+
.is_some_and(|qualified_name| {
118+
matches!(
119+
qualified_name.segments(),
120+
["functools", "lru_cache" | "cache"]
121+
)
122+
})
123+
}

0 commit comments

Comments
 (0)