mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-18 17:41:12 +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:
|
class RenamingWithNFKC:
|
||||||
def formula(household):
|
def formula(household):
|
||||||
hºusehold(1)
|
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.
|
/// 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 {
|
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!(
|
matches!(
|
||||||
qualified_name.segments(),
|
qualified_name.segments(),
|
||||||
["django", "db", "models", "Model"]
|
["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.
|
/// 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 {
|
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!(
|
matches!(
|
||||||
qualified_name.segments(),
|
qualified_name.segments(),
|
||||||
["django", "forms", "ModelForm"] | ["django", "forms", "models", "ModelForm"]
|
["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`.
|
/// Return `true` if the given class extends `collections.abc.Iterator`.
|
||||||
fn subclasses_iterator(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool {
|
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!(
|
matches!(
|
||||||
qualified_name.segments(),
|
qualified_name.segments(),
|
||||||
["typing", "Iterator"] | ["collections", "abc", "Iterator"]
|
["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`.
|
/// Return `true` if the given class extends `collections.abc.AsyncIterator`.
|
||||||
fn subclasses_async_iterator(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool {
|
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!(
|
matches!(
|
||||||
qualified_name.segments(),
|
qualified_name.segments(),
|
||||||
["typing", "AsyncIterator"] | ["collections", "abc", "AsyncIterator"]
|
["typing", "AsyncIterator"] | ["collections", "abc", "AsyncIterator"]
|
||||||
|
|
|
@ -78,7 +78,7 @@ fn runtime_required_base_class(
|
||||||
base_classes: &[String],
|
base_classes: &[String],
|
||||||
semantic: &SemanticModel,
|
semantic: &SemanticModel,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
analyze::class::any_qualified_name(class_def, semantic, &|qualified_name| {
|
analyze::class::any_qualified_base_class(class_def, semantic, &|qualified_name| {
|
||||||
base_classes
|
base_classes
|
||||||
.iter()
|
.iter()
|
||||||
.any(|base_class| QualifiedName::from_dotted_name(base_class) == qualified_name)
|
.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;
|
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")
|
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)
|
125 |- hºusehold(1)
|
||||||
124 |+ def formula(self):
|
124 |+ def formula(self):
|
||||||
125 |+ self(1)
|
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)
|
125 |- hºusehold(1)
|
||||||
124 |+ def formula(self):
|
124 |+ def formula(self):
|
||||||
125 |+ self(1)
|
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)
|
125 |- hºusehold(1)
|
||||||
124 |+ def formula(self):
|
124 |+ def formula(self):
|
||||||
125 |+ self(1)
|
125 |+ self(1)
|
||||||
|
126 126 |
|
||||||
|
127 127 |
|
||||||
|
128 128 | from typing import Protocol
|
||||||
|
|
|
@ -531,7 +531,7 @@ struct BodyEntries<'a> {
|
||||||
struct BodyVisitor<'a> {
|
struct BodyVisitor<'a> {
|
||||||
returns: Vec<Entry>,
|
returns: Vec<Entry>,
|
||||||
yields: Vec<Entry>,
|
yields: Vec<Entry>,
|
||||||
currently_suspended_exceptions: Option<&'a Expr>,
|
currently_suspended_exceptions: Option<&'a ast::Expr>,
|
||||||
raised_exceptions: Vec<ExceptionEntry<'a>>,
|
raised_exceptions: Vec<ExceptionEntry<'a>>,
|
||||||
semantic: &'a SemanticModel<'a>,
|
semantic: &'a SemanticModel<'a>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -74,7 +74,7 @@ pub(super) fn has_default_copy_semantics(
|
||||||
class_def: &ast::StmtClassDef,
|
class_def: &ast::StmtClassDef,
|
||||||
semantic: &SemanticModel,
|
semantic: &SemanticModel,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
analyze::class::any_qualified_name(class_def, semantic, &|qualified_name| {
|
analyze::class::any_qualified_base_class(class_def, semantic, &|qualified_name| {
|
||||||
matches!(
|
matches!(
|
||||||
qualified_name.segments(),
|
qualified_name.segments(),
|
||||||
["pydantic", "BaseModel" | "BaseSettings" | "BaseConfig"]
|
["pydantic", "BaseModel" | "BaseSettings" | "BaseConfig"]
|
||||||
|
|
|
@ -1,30 +1,40 @@
|
||||||
use rustc_hash::FxHashSet;
|
use rustc_hash::FxHashSet;
|
||||||
|
|
||||||
|
use crate::{BindingId, SemanticModel};
|
||||||
use ruff_python_ast as ast;
|
use ruff_python_ast as ast;
|
||||||
use ruff_python_ast::helpers::map_subscript;
|
use ruff_python_ast::helpers::map_subscript;
|
||||||
use ruff_python_ast::name::QualifiedName;
|
use ruff_python_ast::name::QualifiedName;
|
||||||
|
use ruff_python_ast::Expr;
|
||||||
use crate::{BindingId, SemanticModel};
|
|
||||||
|
|
||||||
/// Return `true` if any base class matches a [`QualifiedName`] predicate.
|
/// 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,
|
class_def: &ast::StmtClassDef,
|
||||||
semantic: &SemanticModel,
|
semantic: &SemanticModel,
|
||||||
func: &dyn Fn(QualifiedName) -> bool,
|
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 {
|
) -> bool {
|
||||||
fn inner(
|
fn inner(
|
||||||
class_def: &ast::StmtClassDef,
|
class_def: &ast::StmtClassDef,
|
||||||
semantic: &SemanticModel,
|
semantic: &SemanticModel,
|
||||||
func: &dyn Fn(QualifiedName) -> bool,
|
func: &dyn Fn(&Expr) -> bool,
|
||||||
seen: &mut FxHashSet<BindingId>,
|
seen: &mut FxHashSet<BindingId>,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
class_def.bases().iter().any(|expr| {
|
class_def.bases().iter().any(|expr| {
|
||||||
// If the base class itself matches the pattern, then this does too.
|
// If the base class itself matches the pattern, then this does too.
|
||||||
// Ex) `class Foo(BaseModel): ...`
|
// Ex) `class Foo(BaseModel): ...`
|
||||||
if semantic
|
if func(expr) {
|
||||||
.resolve_qualified_name(map_subscript(expr))
|
|
||||||
.is_some_and(func)
|
|
||||||
{
|
|
||||||
return true;
|
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
|
/// 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 {
|
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!(
|
matches!(
|
||||||
qualified_name.segments(),
|
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.
|
/// Returns `true` if the given class is a metaclass.
|
||||||
pub fn is_metaclass(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool {
|
pub fn is_metaclass(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool {
|
||||||
any_qualified_name(class_def, semantic, &|qualified_name| {
|
any_base_class(class_def, semantic, &|expr| match expr {
|
||||||
matches!(
|
Expr::Call(ast::ExprCall {
|
||||||
qualified_name.segments(),
|
func, arguments, ..
|
||||||
["" | "builtins", "type"] | ["abc", "ABCMeta"] | ["enum", "EnumMeta" | "EnumType"]
|
}) => {
|
||||||
)
|
// 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