[red-knot] Add support for @final classes (#15070)
Some checks are pending
CI / ecosystem (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / benchmarks (push) Blocked by required conditions

Co-authored-by: Carl Meyer <carl@astral.sh>
This commit is contained in:
Alex Waygood 2024-12-19 21:02:14 +00:00 committed by GitHub
parent bcec5e615b
commit 3aed14935d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 108 additions and 3 deletions

View file

@ -2973,13 +2973,15 @@ pub enum KnownFunction {
RevealType,
/// `builtins.len`
Len,
/// `typing(_extensions).final`
Final,
}
impl KnownFunction {
pub fn constraint_function(self) -> Option<KnownConstraintFunction> {
match self {
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),
),
"len" if definition.is_builtin_definition(db) => Some(KnownFunction::Len),
"final" if definition.is_typing_definition(db) => Some(KnownFunction::Final),
_ => None,
}
}
@ -3149,6 +3152,31 @@ impl<'db> Class<'db> {
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.
/// 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

View file

@ -42,6 +42,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
registry.register_lint(&POSSIBLY_UNBOUND_ATTRIBUTE);
registry.register_lint(&POSSIBLY_UNBOUND_IMPORT);
registry.register_lint(&POSSIBLY_UNRESOLVED_REFERENCE);
registry.register_lint(&SUBCLASS_OF_FINAL_CLASS);
registry.register_lint(&UNDEFINED_REVEAL);
registry.register_lint(&UNRESOLVED_ATTRIBUTE);
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! {
/// ## What it does
/// Checks for calls to `reveal_type` without importing it.

View file

@ -77,6 +77,7 @@ use super::diagnostic::{
report_index_out_of_bounds, report_invalid_exception_caught, report_invalid_exception_cause,
report_invalid_exception_raised, report_non_subscriptable,
report_possibly_unresolved_reference, report_slice_step_size_zero, report_unresolved_reference,
SUBCLASS_OF_FINAL_CLASS,
};
use super::string_annotation::{
parse_string_annotation, BYTE_STRING_TYPE_ANNOTATION, FSTRING_TYPE_ANNOTATION,
@ -568,7 +569,28 @@ impl<'db> TypeInferenceBuilder<'db> {
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() {
match mro_error.reason() {
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()) {
match metaclass_error.reason() {
MetaclassErrorKind::Conflict {