Improve handling of metaclasses in various linter rules (#12579)

This commit is contained in:
Alex Waygood 2024-07-30 14:48:36 +01:00 committed by GitHub
parent ac1666d6e2
commit 7a4419a2a5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 46 additions and 29 deletions

View file

@ -118,3 +118,9 @@ class Foo(enum.Enum):
@functools.cache @functools.cache
def bar(self, arg: str) -> str: def bar(self, arg: str) -> str:
return f"{self} - {arg}" return f"{self} - {arg}"
class Metaclass(type):
@functools.lru_cache
def lru_cached_instance_method_on_metaclass(cls, x: int):
...

View file

@ -80,4 +80,11 @@ B019.py:106:5: B019 Use of `functools.lru_cache` or `functools.cache` on methods
108 | ... 108 | ...
| |
B019.py:124:5: B019 Use of `functools.lru_cache` or `functools.cache` on methods can lead to memory leaks
|
123 | class Metaclass(type):
124 | @functools.lru_cache
| ^^^^^^^^^^^^^^^^^^^^ B019
125 | def lru_cached_instance_method_on_metaclass(cls, x: int):
126 | ...
|

View file

@ -135,7 +135,7 @@ pub(crate) fn non_self_return_type(
}; };
// PEP 673 forbids the use of `typing(_extensions).Self` in metaclasses. // PEP 673 forbids the use of `typing(_extensions).Self` in metaclasses.
if is_metaclass(class_def, semantic) { if analyze::class::is_metaclass(class_def, semantic) {
return; return;
} }
@ -219,16 +219,6 @@ pub(crate) fn non_self_return_type(
} }
} }
/// Returns `true` if the given class is a metaclass.
fn is_metaclass(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool {
analyze::class::any_qualified_name(class_def, semantic, &|qualified_name| {
matches!(
qualified_name.segments(),
["" | "builtins", "type"] | ["abc", "ABCMeta"] | ["enum", "EnumMeta" | "EnumType"]
)
})
}
/// Returns `true` if the method is an in-place binary operator. /// Returns `true` if the method is an in-place binary operator.
fn is_inplace_bin_op(name: &str) -> bool { fn is_inplace_bin_op(name: &str) -> bool {
matches!( matches!(

View file

@ -5,6 +5,7 @@ use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast as ast; use ruff_python_ast as ast;
use ruff_python_ast::ParameterWithDefault; use ruff_python_ast::ParameterWithDefault;
use ruff_python_codegen::Stylist; use ruff_python_codegen::Stylist;
use ruff_python_semantic::analyze::class::is_metaclass;
use ruff_python_semantic::analyze::function_type; use ruff_python_semantic::analyze::function_type;
use ruff_python_semantic::{Scope, ScopeKind, SemanticModel}; use ruff_python_semantic::{Scope, ScopeKind, SemanticModel};
use ruff_text_size::Ranged; use ruff_text_size::Ranged;
@ -190,22 +191,34 @@ pub(crate) fn invalid_first_argument_name(
panic!("Expected ScopeKind::Function") panic!("Expected ScopeKind::Function")
}; };
let Some(parent) = checker.semantic().first_non_type_parent_scope(scope) else { let semantic = checker.semantic();
let Some(parent_scope) = semantic.first_non_type_parent_scope(scope) else {
return;
};
let ScopeKind::Class(parent) = parent_scope.kind else {
return; return;
}; };
let function_type = match function_type::classify( let function_type = match function_type::classify(
name, name,
decorator_list, decorator_list,
parent, parent_scope,
checker.semantic(), semantic,
&checker.settings.pep8_naming.classmethod_decorators, &checker.settings.pep8_naming.classmethod_decorators,
&checker.settings.pep8_naming.staticmethod_decorators, &checker.settings.pep8_naming.staticmethod_decorators,
) { ) {
function_type::FunctionType::Function | function_type::FunctionType::StaticMethod => { function_type::FunctionType::Function | function_type::FunctionType::StaticMethod => {
return; return;
} }
function_type::FunctionType::Method => FunctionType::Method, function_type::FunctionType::Method => {
if is_metaclass(parent, semantic) {
FunctionType::ClassMethod
} else {
FunctionType::Method
}
}
function_type::FunctionType::ClassMethod => FunctionType::ClassMethod, function_type::FunctionType::ClassMethod => FunctionType::ClassMethod,
}; };
if !checker.enabled(function_type.rule()) { if !checker.enabled(function_type.rule()) {

View file

@ -110,3 +110,13 @@ pub fn is_enumeration(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -
) )
}) })
} }
/// Returns `true` if the given class is a metaclass.
pub fn is_metaclass(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool {
any_qualified_name(class_def, semantic, &|qualified_name| {
matches!(
qualified_name.segments(),
["" | "builtins", "type"] | ["abc", "ABCMeta"] | ["enum", "EnumMeta" | "EnumType"]
)
})
}

View file

@ -3,7 +3,7 @@ use ruff_python_ast::name::{QualifiedName, UnqualifiedName};
use ruff_python_ast::{Decorator, Expr, Stmt, StmtExpr, StmtFunctionDef, StmtRaise}; use ruff_python_ast::{Decorator, Expr, Stmt, StmtExpr, StmtFunctionDef, StmtRaise};
use crate::model::SemanticModel; use crate::model::SemanticModel;
use crate::scope::{Scope, ScopeKind}; use crate::scope::Scope;
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]
pub enum FunctionType { pub enum FunctionType {
@ -17,12 +17,12 @@ pub enum FunctionType {
pub fn classify( pub fn classify(
name: &str, name: &str,
decorator_list: &[Decorator], decorator_list: &[Decorator],
scope: &Scope, parent_scope: &Scope,
semantic: &SemanticModel, semantic: &SemanticModel,
classmethod_decorators: &[String], classmethod_decorators: &[String],
staticmethod_decorators: &[String], staticmethod_decorators: &[String],
) -> FunctionType { ) -> FunctionType {
let ScopeKind::Class(class_def) = &scope.kind else { if !parent_scope.kind.is_class() {
return FunctionType::Function; return FunctionType::Function;
}; };
if decorator_list if decorator_list
@ -30,16 +30,7 @@ pub fn classify(
.any(|decorator| is_static_method(decorator, semantic, staticmethod_decorators)) .any(|decorator| is_static_method(decorator, semantic, staticmethod_decorators))
{ {
FunctionType::StaticMethod FunctionType::StaticMethod
} else if matches!(name, "__new__" | "__init_subclass__" | "__class_getitem__") } else if matches!(name, "__new__" | "__init_subclass__" | "__class_getitem__") // Special-case class method, like `__new__`.
// Special-case class method, like `__new__`.
|| class_def.bases().iter().any(|expr| {
// The class itself extends a known metaclass, so all methods are class methods.
semantic
.resolve_qualified_name(map_callable(expr))
.is_some_and( |qualified_name| {
matches!(qualified_name.segments(), ["" | "builtins", "type"] | ["abc", "ABCMeta"])
})
})
|| decorator_list.iter().any(|decorator| is_class_method(decorator, semantic, classmethod_decorators)) || decorator_list.iter().any(|decorator| is_class_method(decorator, semantic, classmethod_decorators))
{ {
FunctionType::ClassMethod FunctionType::ClassMethod