mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-01 06:11:43 +00:00
[red-knot] Add support for @final
classes (#15070)
Co-authored-by: Carl Meyer <carl@astral.sh>
This commit is contained in:
parent
bcec5e615b
commit
3aed14935d
4 changed files with 108 additions and 3 deletions
31
crates/red_knot_python_semantic/resources/mdtest/final.md
Normal file
31
crates/red_knot_python_semantic/resources/mdtest/final.md
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
# Tests for the `@typing(_extensions).final` decorator
|
||||||
|
|
||||||
|
## Cannot subclass
|
||||||
|
|
||||||
|
Don't do this:
|
||||||
|
|
||||||
|
```py
|
||||||
|
import typing_extensions
|
||||||
|
from typing import final
|
||||||
|
|
||||||
|
@final
|
||||||
|
class A: ...
|
||||||
|
|
||||||
|
class B(A): ... # error: 9 [subclass-of-final-class] "Class `B` cannot inherit from final class `A`"
|
||||||
|
|
||||||
|
@typing_extensions.final
|
||||||
|
class C: ...
|
||||||
|
|
||||||
|
class D(C): ... # error: [subclass-of-final-class]
|
||||||
|
class E: ...
|
||||||
|
class F: ...
|
||||||
|
class G: ...
|
||||||
|
|
||||||
|
# fmt: off
|
||||||
|
class H(
|
||||||
|
E,
|
||||||
|
F,
|
||||||
|
A, # error: [subclass-of-final-class]
|
||||||
|
G,
|
||||||
|
): ...
|
||||||
|
```
|
|
@ -2973,13 +2973,15 @@ pub enum KnownFunction {
|
||||||
RevealType,
|
RevealType,
|
||||||
/// `builtins.len`
|
/// `builtins.len`
|
||||||
Len,
|
Len,
|
||||||
|
/// `typing(_extensions).final`
|
||||||
|
Final,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl KnownFunction {
|
impl KnownFunction {
|
||||||
pub fn constraint_function(self) -> Option<KnownConstraintFunction> {
|
pub fn constraint_function(self) -> Option<KnownConstraintFunction> {
|
||||||
match self {
|
match self {
|
||||||
Self::ConstraintFunction(f) => Some(f),
|
Self::ConstraintFunction(f) => Some(f),
|
||||||
Self::RevealType | Self::Len => None,
|
Self::RevealType | Self::Len | Self::Final => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2997,6 +2999,7 @@ impl KnownFunction {
|
||||||
KnownFunction::ConstraintFunction(KnownConstraintFunction::IsSubclass),
|
KnownFunction::ConstraintFunction(KnownConstraintFunction::IsSubclass),
|
||||||
),
|
),
|
||||||
"len" if definition.is_builtin_definition(db) => Some(KnownFunction::Len),
|
"len" if definition.is_builtin_definition(db) => Some(KnownFunction::Len),
|
||||||
|
"final" if definition.is_typing_definition(db) => Some(KnownFunction::Final),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3149,6 +3152,31 @@ impl<'db> Class<'db> {
|
||||||
self.body_scope(db).node(db).expect_class()
|
self.body_scope(db).node(db).expect_class()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return the types of the decorators on this class
|
||||||
|
#[salsa::tracked(return_ref)]
|
||||||
|
fn decorators(self, db: &'db dyn Db) -> Box<[Type<'db>]> {
|
||||||
|
let class_stmt = self.node(db);
|
||||||
|
if class_stmt.decorator_list.is_empty() {
|
||||||
|
return Box::new([]);
|
||||||
|
}
|
||||||
|
let class_definition = semantic_index(db, self.file(db)).definition(class_stmt);
|
||||||
|
class_stmt
|
||||||
|
.decorator_list
|
||||||
|
.iter()
|
||||||
|
.map(|decorator_node| {
|
||||||
|
definition_expression_ty(db, class_definition, &decorator_node.expression)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Is this class final?
|
||||||
|
fn is_final(self, db: &'db dyn Db) -> bool {
|
||||||
|
self.decorators(db)
|
||||||
|
.iter()
|
||||||
|
.filter_map(|deco| deco.into_function_literal())
|
||||||
|
.any(|decorator| decorator.is_known(db, KnownFunction::Final))
|
||||||
|
}
|
||||||
|
|
||||||
/// Attempt to resolve the [method resolution order] ("MRO") for this class.
|
/// Attempt to resolve the [method resolution order] ("MRO") for this class.
|
||||||
/// If the MRO is unresolvable, return an error indicating why the class's MRO
|
/// If the MRO is unresolvable, return an error indicating why the class's MRO
|
||||||
/// cannot be accurately determined. The error returned contains a fallback MRO
|
/// cannot be accurately determined. The error returned contains a fallback MRO
|
||||||
|
|
|
@ -42,6 +42,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
|
||||||
registry.register_lint(&POSSIBLY_UNBOUND_ATTRIBUTE);
|
registry.register_lint(&POSSIBLY_UNBOUND_ATTRIBUTE);
|
||||||
registry.register_lint(&POSSIBLY_UNBOUND_IMPORT);
|
registry.register_lint(&POSSIBLY_UNBOUND_IMPORT);
|
||||||
registry.register_lint(&POSSIBLY_UNRESOLVED_REFERENCE);
|
registry.register_lint(&POSSIBLY_UNRESOLVED_REFERENCE);
|
||||||
|
registry.register_lint(&SUBCLASS_OF_FINAL_CLASS);
|
||||||
registry.register_lint(&UNDEFINED_REVEAL);
|
registry.register_lint(&UNDEFINED_REVEAL);
|
||||||
registry.register_lint(&UNRESOLVED_ATTRIBUTE);
|
registry.register_lint(&UNRESOLVED_ATTRIBUTE);
|
||||||
registry.register_lint(&UNRESOLVED_IMPORT);
|
registry.register_lint(&UNRESOLVED_IMPORT);
|
||||||
|
@ -395,6 +396,29 @@ declare_lint! {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare_lint! {
|
||||||
|
/// ## What it does
|
||||||
|
/// Checks for classes that subclass final classes.
|
||||||
|
///
|
||||||
|
/// ## Why is this bad?
|
||||||
|
/// Decorating a class with `@final` declares to the type checker that it should not be subclassed.
|
||||||
|
///
|
||||||
|
/// ## Example
|
||||||
|
///
|
||||||
|
/// ```python
|
||||||
|
/// from typing import final
|
||||||
|
///
|
||||||
|
/// @final
|
||||||
|
/// class A: ...
|
||||||
|
/// class B(A): ... # Error raised here
|
||||||
|
/// ```
|
||||||
|
pub(crate) static SUBCLASS_OF_FINAL_CLASS = {
|
||||||
|
summary: "detects subclasses of final classes",
|
||||||
|
status: LintStatus::preview("1.0.0"),
|
||||||
|
default_level: Level::Error,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
declare_lint! {
|
declare_lint! {
|
||||||
/// ## What it does
|
/// ## What it does
|
||||||
/// Checks for calls to `reveal_type` without importing it.
|
/// Checks for calls to `reveal_type` without importing it.
|
||||||
|
|
|
@ -77,6 +77,7 @@ use super::diagnostic::{
|
||||||
report_index_out_of_bounds, report_invalid_exception_caught, report_invalid_exception_cause,
|
report_index_out_of_bounds, report_invalid_exception_caught, report_invalid_exception_cause,
|
||||||
report_invalid_exception_raised, report_non_subscriptable,
|
report_invalid_exception_raised, report_non_subscriptable,
|
||||||
report_possibly_unresolved_reference, report_slice_step_size_zero, report_unresolved_reference,
|
report_possibly_unresolved_reference, report_slice_step_size_zero, report_unresolved_reference,
|
||||||
|
SUBCLASS_OF_FINAL_CLASS,
|
||||||
};
|
};
|
||||||
use super::string_annotation::{
|
use super::string_annotation::{
|
||||||
parse_string_annotation, BYTE_STRING_TYPE_ANNOTATION, FSTRING_TYPE_ANNOTATION,
|
parse_string_annotation, BYTE_STRING_TYPE_ANNOTATION, FSTRING_TYPE_ANNOTATION,
|
||||||
|
@ -568,7 +569,28 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// (2) Check that the class's MRO is resolvable
|
// (2) Check for classes that inherit from `@final` classes
|
||||||
|
for (i, base_class) in class.explicit_bases(self.db()).iter().enumerate() {
|
||||||
|
// dynamic/unknown bases are never `@final`
|
||||||
|
let Some(ClassLiteralType { class: base_class }) = base_class.into_class_literal()
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if !base_class.is_final(self.db()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
self.context.report_lint(
|
||||||
|
&SUBCLASS_OF_FINAL_CLASS,
|
||||||
|
(&class_node.bases()[i]).into(),
|
||||||
|
format_args!(
|
||||||
|
"Class `{}` cannot inherit from final class `{}`",
|
||||||
|
class.name(self.db()),
|
||||||
|
base_class.name(self.db()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// (3) Check that the class's MRO is resolvable
|
||||||
if let Err(mro_error) = class.try_mro(self.db()).as_ref() {
|
if let Err(mro_error) = class.try_mro(self.db()).as_ref() {
|
||||||
match mro_error.reason() {
|
match mro_error.reason() {
|
||||||
MroErrorKind::DuplicateBases(duplicates) => {
|
MroErrorKind::DuplicateBases(duplicates) => {
|
||||||
|
@ -606,7 +628,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// (3) Check that the class's metaclass can be determined without error.
|
// (4) Check that the class's metaclass can be determined without error.
|
||||||
if let Err(metaclass_error) = class.try_metaclass(self.db()) {
|
if let Err(metaclass_error) = class.try_metaclass(self.db()) {
|
||||||
match metaclass_error.reason() {
|
match metaclass_error.reason() {
|
||||||
MetaclassErrorKind::Conflict {
|
MetaclassErrorKind::Conflict {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue