diff --git a/crates/ruff_linter/resources/test/fixtures/pep8_naming/N805.py b/crates/ruff_linter/resources/test/fixtures/pep8_naming/N805.py index 8f228c1028..d350a39c12 100644 --- a/crates/ruff_linter/resources/test/fixtures/pep8_naming/N805.py +++ b/crates/ruff_linter/resources/test/fixtures/pep8_naming/N805.py @@ -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): ... diff --git a/crates/ruff_linter/src/rules/flake8_django/rules/helpers.rs b/crates/ruff_linter/src/rules/flake8_django/rules/helpers.rs index 2a8198021a..67c12bbbfb 100644 --- a/crates/ruff_linter/src/rules/flake8_django/rules/helpers.rs +++ b/crates/ruff_linter/src/rules/flake8_django/rules/helpers.rs @@ -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"] diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/non_self_return_type.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/non_self_return_type.rs index b02cb555b5..42c7399e61 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/non_self_return_type.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/non_self_return_type.rs @@ -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"] diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/helpers.rs b/crates/ruff_linter/src/rules/flake8_type_checking/helpers.rs index 2d24a47aa2..fb85d48c22 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/helpers.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/helpers.rs @@ -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) diff --git a/crates/ruff_linter/src/rules/pep8_naming/helpers.rs b/crates/ruff_linter/src/rules/pep8_naming/helpers.rs index e94ace527c..41598653b3 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/helpers.rs +++ b/crates/ruff_linter/src/rules/pep8_naming/helpers.rs @@ -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") }) } diff --git a/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__N805_N805.py.snap b/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__N805_N805.py.snap index dfc03bc7ba..da5c1b1686 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__N805_N805.py.snap +++ b/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__N805_N805.py.snap @@ -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 diff --git a/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__classmethod_decorators.snap b/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__classmethod_decorators.snap index 33143ab760..b631ea1792 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__classmethod_decorators.snap +++ b/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__classmethod_decorators.snap @@ -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 diff --git a/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__staticmethod_decorators.snap b/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__staticmethod_decorators.snap index cac0d909d9..f253d2c666 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__staticmethod_decorators.snap +++ b/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__staticmethod_decorators.snap @@ -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 diff --git a/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs b/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs index d8ed49d455..30d3ed6e1c 100644 --- a/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs +++ b/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs @@ -531,7 +531,7 @@ struct BodyEntries<'a> { struct BodyVisitor<'a> { returns: Vec, yields: Vec, - currently_suspended_exceptions: Option<&'a Expr>, + currently_suspended_exceptions: Option<&'a ast::Expr>, raised_exceptions: Vec>, semantic: &'a SemanticModel<'a>, } diff --git a/crates/ruff_linter/src/rules/ruff/rules/helpers.rs b/crates/ruff_linter/src/rules/ruff/rules/helpers.rs index e151a5bb16..07b96af1f0 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/helpers.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/helpers.rs @@ -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"] diff --git a/crates/ruff_python_semantic/src/analyze/class.rs b/crates/ruff_python_semantic/src/analyze/class.rs index 4ea0d3cb08..eac8de4605 100644 --- a/crates/ruff_python_semantic/src/analyze/class.rs +++ b/crates/ruff_python_semantic/src/analyze/class.rs @@ -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, ) -> 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"] + ) + }), }) }