mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-01 17:32:25 +00:00
[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
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:
parent
bcec5e615b
commit
3aed14935d
4 changed files with 108 additions and 3 deletions
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue