[ty] Extend Liskov checks to also cover classmethods and staticmethods (#21598)
## Summary Building on https://github.com/astral-sh/ruff/pull/21436. There's nothing conceptually more complicated about this, it just requires its own set of tests and its own subdiagnostic hint. I also uncovered another inconsistency between mypy/pyright/pyrefly, which is fun. In this case, I suggest we go with pyright's behaviour. ## Test Plan mdtests/snapshots
This commit is contained in:
@@ -1107,13 +1107,6 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn as_bound_method(self) -> Option<BoundMethodType<'db>> {
|
||||
match self {
|
||||
Type::BoundMethod(bound_method) => Some(bound_method),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub(crate) const fn expect_class_literal(self) -> ClassLiteral<'db> {
|
||||
self.as_class_literal()
|
||||
|
||||
@@ -1264,6 +1264,14 @@ impl MethodDecorator {
|
||||
(false, false) => Ok(Self::None),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn description(self) -> &'static str {
|
||||
match self {
|
||||
MethodDecorator::None => "an instance method",
|
||||
MethodDecorator::ClassMethod => "a classmethod",
|
||||
MethodDecorator::StaticMethod => "a staticmethod",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Kind-specific metadata for different types of fields
|
||||
|
||||
@@ -8,13 +8,13 @@ use super::{
|
||||
use crate::diagnostic::did_you_mean;
|
||||
use crate::diagnostic::format_enumeration;
|
||||
use crate::lint::{Level, LintRegistryBuilder, LintStatus};
|
||||
use crate::place::Place;
|
||||
use crate::semantic_index::definition::{Definition, DefinitionKind};
|
||||
use crate::semantic_index::place::{PlaceTable, ScopedPlaceId};
|
||||
use crate::semantic_index::{global_scope, place_table, use_def_map};
|
||||
use crate::suppression::FileSuppressionId;
|
||||
use crate::types::KnownInstanceType;
|
||||
use crate::types::call::CallError;
|
||||
use crate::types::class::{DisjointBase, DisjointBaseKind, Field};
|
||||
use crate::types::class::{DisjointBase, DisjointBaseKind, Field, MethodDecorator};
|
||||
use crate::types::function::{FunctionType, KnownFunction};
|
||||
use crate::types::liskov::{MethodKind, SynthesizedMethodKind};
|
||||
use crate::types::string_annotation::{
|
||||
@@ -27,6 +27,7 @@ use crate::types::{
|
||||
ProtocolInstanceType, SpecialFormType, SubclassOfInner, Type, TypeContext, binding_type,
|
||||
infer_isolated_expression, protocol_class::ProtocolClass,
|
||||
};
|
||||
use crate::types::{KnownInstanceType, MemberLookupPolicy};
|
||||
use crate::{Db, DisplaySettings, FxIndexMap, Module, ModuleName, Program, declare_lint};
|
||||
use itertools::Itertools;
|
||||
use ruff_db::{
|
||||
@@ -3519,6 +3520,27 @@ pub(super) fn report_invalid_method_override<'db>(
|
||||
"Definition is incompatible with `{overridden_method}`"
|
||||
));
|
||||
|
||||
let class_member = |cls: ClassType<'db>| {
|
||||
cls.class_member(db, member, MemberLookupPolicy::default())
|
||||
.place
|
||||
};
|
||||
|
||||
if let Place::Defined(Type::FunctionLiteral(subclass_function), _, _) = class_member(subclass)
|
||||
&& let Place::Defined(Type::FunctionLiteral(superclass_function), _, _) =
|
||||
class_member(superclass)
|
||||
&& let Ok(superclass_function_kind) =
|
||||
MethodDecorator::try_from_fn_type(db, superclass_function)
|
||||
&& let Ok(subclass_function_kind) = MethodDecorator::try_from_fn_type(db, subclass_function)
|
||||
&& superclass_function_kind != subclass_function_kind
|
||||
{
|
||||
diagnostic.info(format_args!(
|
||||
"`{class_name}.{member}` is {subclass_function_kind} \
|
||||
but `{overridden_method}` is {superclass_function_kind}",
|
||||
superclass_function_kind = superclass_function_kind.description(),
|
||||
subclass_function_kind = subclass_function_kind.description(),
|
||||
));
|
||||
}
|
||||
|
||||
diagnostic.info("This violates the Liskov Substitution Principle");
|
||||
|
||||
if !subclass_definition_kind.is_function_def()
|
||||
@@ -3545,9 +3567,11 @@ pub(super) fn report_invalid_method_override<'db>(
|
||||
.full_range(db, &parsed_module(db, superclass_scope.file(db)).load(db)),
|
||||
);
|
||||
|
||||
let superclass_function_span = superclass_type
|
||||
.as_bound_method()
|
||||
.and_then(|method| signature_span(method.function(db)));
|
||||
let superclass_function_span = match superclass_type {
|
||||
Type::FunctionLiteral(function) => signature_span(function),
|
||||
Type::BoundMethod(method) => signature_span(method.function(db)),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let superclass_definition_kind = definition.kind(db);
|
||||
|
||||
|
||||
@@ -49,11 +49,6 @@ fn check_class_declaration<'db>(
|
||||
return;
|
||||
};
|
||||
|
||||
// TODO: classmethods and staticmethods
|
||||
if function.is_classmethod(db) || function.is_staticmethod(db) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Constructor methods are not checked for Liskov compliance
|
||||
if matches!(
|
||||
&*member.name,
|
||||
|
||||
Reference in New Issue
Block a user