mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-04 18:58:04 +00:00
Treat type(Protocol)
et al as metaclass base (#12770)
## Summary Closes https://github.com/astral-sh/ruff/issues/12736.
This commit is contained in:
parent
37b9bac403
commit
69e1c567d4
11 changed files with 68 additions and 22 deletions
|
@ -123,3 +123,14 @@ class RenamingInMethodBodyClass:
|
|||
class RenamingWithNFKC:
|
||||
def formula(household):
|
||||
hºusehold(1)
|
||||
|
||||
|
||||
from typing import Protocol
|
||||
|
||||
|
||||
class MyMeta(type):
|
||||
def __subclasscheck__(cls, other): ...
|
||||
|
||||
|
||||
class MyProtocolMeta(type(Protocol)):
|
||||
def __subclasscheck__(cls, other): ...
|
||||
|
|
|
@ -4,7 +4,7 @@ use ruff_python_semantic::{analyze, SemanticModel};
|
|||
|
||||
/// Return `true` if a Python class appears to be a Django model, based on its base classes.
|
||||
pub(super) fn is_model(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool {
|
||||
analyze::class::any_qualified_name(class_def, semantic, &|qualified_name| {
|
||||
analyze::class::any_qualified_base_class(class_def, semantic, &|qualified_name| {
|
||||
matches!(
|
||||
qualified_name.segments(),
|
||||
["django", "db", "models", "Model"]
|
||||
|
@ -14,7 +14,7 @@ pub(super) fn is_model(class_def: &ast::StmtClassDef, semantic: &SemanticModel)
|
|||
|
||||
/// Return `true` if a Python class appears to be a Django model form, based on its base classes.
|
||||
pub(super) fn is_model_form(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool {
|
||||
analyze::class::any_qualified_name(class_def, semantic, &|qualified_name| {
|
||||
analyze::class::any_qualified_base_class(class_def, semantic, &|qualified_name| {
|
||||
matches!(
|
||||
qualified_name.segments(),
|
||||
["django", "forms", "ModelForm"] | ["django", "forms", "models", "ModelForm"]
|
||||
|
|
|
@ -254,7 +254,7 @@ fn is_self(expr: &Expr, semantic: &SemanticModel) -> bool {
|
|||
|
||||
/// Return `true` if the given class extends `collections.abc.Iterator`.
|
||||
fn subclasses_iterator(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool {
|
||||
analyze::class::any_qualified_name(class_def, semantic, &|qualified_name| {
|
||||
analyze::class::any_qualified_base_class(class_def, semantic, &|qualified_name| {
|
||||
matches!(
|
||||
qualified_name.segments(),
|
||||
["typing", "Iterator"] | ["collections", "abc", "Iterator"]
|
||||
|
@ -277,7 +277,7 @@ fn is_iterable_or_iterator(expr: &Expr, semantic: &SemanticModel) -> bool {
|
|||
|
||||
/// Return `true` if the given class extends `collections.abc.AsyncIterator`.
|
||||
fn subclasses_async_iterator(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool {
|
||||
analyze::class::any_qualified_name(class_def, semantic, &|qualified_name| {
|
||||
analyze::class::any_qualified_base_class(class_def, semantic, &|qualified_name| {
|
||||
matches!(
|
||||
qualified_name.segments(),
|
||||
["typing", "AsyncIterator"] | ["collections", "abc", "AsyncIterator"]
|
||||
|
|
|
@ -78,7 +78,7 @@ fn runtime_required_base_class(
|
|||
base_classes: &[String],
|
||||
semantic: &SemanticModel,
|
||||
) -> bool {
|
||||
analyze::class::any_qualified_name(class_def, semantic, &|qualified_name| {
|
||||
analyze::class::any_qualified_base_class(class_def, semantic, &|qualified_name| {
|
||||
base_classes
|
||||
.iter()
|
||||
.any(|base_class| QualifiedName::from_dotted_name(base_class) == qualified_name)
|
||||
|
|
|
@ -91,7 +91,7 @@ pub(super) fn is_typed_dict_class(class_def: &ast::StmtClassDef, semantic: &Sema
|
|||
return false;
|
||||
}
|
||||
|
||||
analyze::class::any_qualified_name(class_def, semantic, &|qualified_name| {
|
||||
analyze::class::any_qualified_base_class(class_def, semantic, &|qualified_name| {
|
||||
semantic.match_typing_qualified_name(&qualified_name, "TypedDict")
|
||||
})
|
||||
}
|
||||
|
|
|
@ -286,3 +286,6 @@ N805.py:124:17: N805 [*] First argument of a method should be named `self`
|
|||
125 |- hºusehold(1)
|
||||
124 |+ def formula(self):
|
||||
125 |+ self(1)
|
||||
126 126 |
|
||||
127 127 |
|
||||
128 128 | from typing import Protocol
|
||||
|
|
|
@ -229,3 +229,6 @@ N805.py:124:17: N805 [*] First argument of a method should be named `self`
|
|||
125 |- hºusehold(1)
|
||||
124 |+ def formula(self):
|
||||
125 |+ self(1)
|
||||
126 126 |
|
||||
127 127 |
|
||||
128 128 | from typing import Protocol
|
||||
|
|
|
@ -267,3 +267,6 @@ N805.py:124:17: N805 [*] First argument of a method should be named `self`
|
|||
125 |- hºusehold(1)
|
||||
124 |+ def formula(self):
|
||||
125 |+ self(1)
|
||||
126 126 |
|
||||
127 127 |
|
||||
128 128 | from typing import Protocol
|
||||
|
|
|
@ -531,7 +531,7 @@ struct BodyEntries<'a> {
|
|||
struct BodyVisitor<'a> {
|
||||
returns: Vec<Entry>,
|
||||
yields: Vec<Entry>,
|
||||
currently_suspended_exceptions: Option<&'a Expr>,
|
||||
currently_suspended_exceptions: Option<&'a ast::Expr>,
|
||||
raised_exceptions: Vec<ExceptionEntry<'a>>,
|
||||
semantic: &'a SemanticModel<'a>,
|
||||
}
|
||||
|
|
|
@ -74,7 +74,7 @@ pub(super) fn has_default_copy_semantics(
|
|||
class_def: &ast::StmtClassDef,
|
||||
semantic: &SemanticModel,
|
||||
) -> bool {
|
||||
analyze::class::any_qualified_name(class_def, semantic, &|qualified_name| {
|
||||
analyze::class::any_qualified_base_class(class_def, semantic, &|qualified_name| {
|
||||
matches!(
|
||||
qualified_name.segments(),
|
||||
["pydantic", "BaseModel" | "BaseSettings" | "BaseConfig"]
|
||||
|
|
|
@ -1,30 +1,40 @@
|
|||
use rustc_hash::FxHashSet;
|
||||
|
||||
use crate::{BindingId, SemanticModel};
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_ast::helpers::map_subscript;
|
||||
use ruff_python_ast::name::QualifiedName;
|
||||
|
||||
use crate::{BindingId, SemanticModel};
|
||||
use ruff_python_ast::Expr;
|
||||
|
||||
/// Return `true` if any base class matches a [`QualifiedName`] predicate.
|
||||
pub fn any_qualified_name(
|
||||
pub fn any_qualified_base_class(
|
||||
class_def: &ast::StmtClassDef,
|
||||
semantic: &SemanticModel,
|
||||
func: &dyn Fn(QualifiedName) -> bool,
|
||||
) -> bool {
|
||||
any_base_class(class_def, semantic, &|expr| {
|
||||
semantic
|
||||
.resolve_qualified_name(map_subscript(expr))
|
||||
.is_some_and(func)
|
||||
})
|
||||
}
|
||||
|
||||
/// Return `true` if any base class matches an [`Expr`] predicate.
|
||||
pub fn any_base_class(
|
||||
class_def: &ast::StmtClassDef,
|
||||
semantic: &SemanticModel,
|
||||
func: &dyn Fn(&Expr) -> bool,
|
||||
) -> bool {
|
||||
fn inner(
|
||||
class_def: &ast::StmtClassDef,
|
||||
semantic: &SemanticModel,
|
||||
func: &dyn Fn(QualifiedName) -> bool,
|
||||
func: &dyn Fn(&Expr) -> bool,
|
||||
seen: &mut FxHashSet<BindingId>,
|
||||
) -> bool {
|
||||
class_def.bases().iter().any(|expr| {
|
||||
// If the base class itself matches the pattern, then this does too.
|
||||
// Ex) `class Foo(BaseModel): ...`
|
||||
if semantic
|
||||
.resolve_qualified_name(map_subscript(expr))
|
||||
.is_some_and(func)
|
||||
{
|
||||
if func(expr) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -100,7 +110,7 @@ pub fn any_super_class(
|
|||
|
||||
/// Return `true` if `class_def` is a class that has one or more enum classes in its mro
|
||||
pub fn is_enumeration(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool {
|
||||
any_qualified_name(class_def, semantic, &|qualified_name| {
|
||||
any_qualified_base_class(class_def, semantic, &|qualified_name| {
|
||||
matches!(
|
||||
qualified_name.segments(),
|
||||
[
|
||||
|
@ -113,10 +123,26 @@ 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"]
|
||||
)
|
||||
any_base_class(class_def, semantic, &|expr| match expr {
|
||||
Expr::Call(ast::ExprCall {
|
||||
func, arguments, ..
|
||||
}) => {
|
||||
// Ex) `class Foo(type(Protocol)): ...`
|
||||
arguments.len() == 1 && semantic.match_builtin_expr(func.as_ref(), "type")
|
||||
}
|
||||
Expr::Subscript(ast::ExprSubscript { value, .. }) => {
|
||||
// Ex) `class Foo(type[int]): ...`
|
||||
semantic.match_builtin_expr(value.as_ref(), "type")
|
||||
}
|
||||
_ => semantic
|
||||
.resolve_qualified_name(expr)
|
||||
.is_some_and(|qualified_name| {
|
||||
matches!(
|
||||
qualified_name.segments(),
|
||||
["" | "builtins", "type"]
|
||||
| ["abc", "ABCMeta"]
|
||||
| ["enum", "EnumMeta" | "EnumType"]
|
||||
)
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue