Merge remote-tracking branch 'origin/main' into dcreager/callable-return
* origin/main: [ty] Reachability constraints: minor documentation fixes (#21774) [ty] Fix non-determinism in `ConstraintSet.specialize_constrained` (#21744) [ty] Improve `@override`, `@final` and Liskov checks in cases where there are multiple reachable definitions (#21767) [ty] Extend `invalid-explicit-override` to also cover properties decorated with `@override` that do not override anything (#21756) [ty] Enable LRU collection for parsed module (#21749) [ty] Support typevar-specialized dynamic types in generic type aliases (#21730) Add token based `parenthesized_ranges` implementation (#21738) [ty] Default-specialization of generic type aliases (#21765) [ty] Suppress false positives when `dataclasses.dataclass(...)(cls)` is called imperatively (#21729) [syntax-error] Default type parameter followed by non-default type parameter (#21657)
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
use std::fmt::Debug;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::parsed::ParsedModuleRef;
|
||||
use ruff_python_ast::{AnyNodeRef, NodeIndex};
|
||||
use ruff_python_ast::{AnyRootNodeRef, HasNodeIndex};
|
||||
@@ -44,7 +46,7 @@ pub struct AstNodeRef<T> {
|
||||
// cannot implement `Eq`, as indices are only unique within a given instance of the
|
||||
// AST.
|
||||
#[cfg(debug_assertions)]
|
||||
module_addr: usize,
|
||||
file: File,
|
||||
|
||||
_node: PhantomData<T>,
|
||||
}
|
||||
@@ -72,7 +74,7 @@ where
|
||||
Self {
|
||||
index,
|
||||
#[cfg(debug_assertions)]
|
||||
module_addr: module_ref.module().addr(),
|
||||
file: module_ref.module().file(),
|
||||
#[cfg(debug_assertions)]
|
||||
kind: AnyNodeRef::from(node).kind(),
|
||||
#[cfg(debug_assertions)]
|
||||
@@ -88,8 +90,7 @@ where
|
||||
#[track_caller]
|
||||
pub fn node<'ast>(&self, module_ref: &'ast ParsedModuleRef) -> &'ast T {
|
||||
#[cfg(debug_assertions)]
|
||||
assert_eq!(module_ref.module().addr(), self.module_addr);
|
||||
|
||||
assert_eq!(module_ref.module().file(), self.file);
|
||||
// The user guarantees that the module is from the same file and Salsa
|
||||
// revision, so the file contents cannot have changed.
|
||||
module_ref
|
||||
|
||||
@@ -120,7 +120,7 @@ impl std::fmt::Debug for Module<'_> {
|
||||
}
|
||||
|
||||
#[allow(clippy::ref_option)]
|
||||
#[salsa::tracked(returns(ref))]
|
||||
#[salsa::tracked(returns(ref), heap_size=ruff_memory_usage::heap_size)]
|
||||
fn all_submodule_names_for_package<'db>(
|
||||
db: &'db dyn Db,
|
||||
module: Module<'db>,
|
||||
|
||||
@@ -459,7 +459,7 @@ fn core_module_scope(db: &dyn Db, core_module: KnownModule) -> Option<ScopeId<'_
|
||||
pub(super) fn place_from_bindings<'db>(
|
||||
db: &'db dyn Db,
|
||||
bindings_with_constraints: BindingWithConstraintsIterator<'_, 'db>,
|
||||
) -> Place<'db> {
|
||||
) -> PlaceWithDefinition<'db> {
|
||||
place_from_bindings_impl(db, bindings_with_constraints, RequiresExplicitReExport::No)
|
||||
}
|
||||
|
||||
@@ -487,20 +487,21 @@ type DeclaredTypeAndConflictingTypes<'db> = (
|
||||
pub(crate) struct PlaceFromDeclarationsResult<'db> {
|
||||
place_and_quals: PlaceAndQualifiers<'db>,
|
||||
conflicting_types: Option<Box<indexmap::set::Slice<Type<'db>>>>,
|
||||
/// Contains `Some(declaration)` if the declared type originates from exactly one declaration.
|
||||
/// Contains the first reachable declaration for this place, if any.
|
||||
/// This field is used for backreferences in diagnostics.
|
||||
pub(crate) single_declaration: Option<Definition<'db>>,
|
||||
pub(crate) first_declaration: Option<Definition<'db>>,
|
||||
}
|
||||
|
||||
impl<'db> PlaceFromDeclarationsResult<'db> {
|
||||
fn conflict(
|
||||
place_and_quals: PlaceAndQualifiers<'db>,
|
||||
conflicting_types: Box<indexmap::set::Slice<Type<'db>>>,
|
||||
first_declaration: Option<Definition<'db>>,
|
||||
) -> Self {
|
||||
PlaceFromDeclarationsResult {
|
||||
place_and_quals,
|
||||
conflicting_types: Some(conflicting_types),
|
||||
single_declaration: None,
|
||||
first_declaration,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -798,6 +799,7 @@ pub(crate) fn place_by_id<'db>(
|
||||
if let Some(qualifiers) = declared.is_bare_final() {
|
||||
let bindings = all_considered_bindings();
|
||||
return place_from_bindings_impl(db, bindings, requires_explicit_reexport)
|
||||
.place
|
||||
.with_qualifiers(qualifiers);
|
||||
}
|
||||
|
||||
@@ -809,7 +811,7 @@ pub(crate) fn place_by_id<'db>(
|
||||
qualifiers,
|
||||
} if qualifiers.contains(TypeQualifiers::CLASS_VAR) => {
|
||||
let bindings = all_considered_bindings();
|
||||
match place_from_bindings_impl(db, bindings, requires_explicit_reexport) {
|
||||
match place_from_bindings_impl(db, bindings, requires_explicit_reexport).place {
|
||||
Place::Defined(inferred, origin, boundness) => Place::Defined(
|
||||
UnionType::from_elements(db, [Type::unknown(), inferred]),
|
||||
origin,
|
||||
@@ -835,7 +837,7 @@ pub(crate) fn place_by_id<'db>(
|
||||
let boundness_analysis = bindings.boundness_analysis;
|
||||
let inferred = place_from_bindings_impl(db, bindings, requires_explicit_reexport);
|
||||
|
||||
let place = match inferred {
|
||||
let place = match inferred.place {
|
||||
// Place is possibly undeclared and definitely unbound
|
||||
Place::Undefined => {
|
||||
// TODO: We probably don't want to report `AlwaysDefined` here. This requires a bit of
|
||||
@@ -864,7 +866,8 @@ pub(crate) fn place_by_id<'db>(
|
||||
} => {
|
||||
let bindings = all_considered_bindings();
|
||||
let boundness_analysis = bindings.boundness_analysis;
|
||||
let mut inferred = place_from_bindings_impl(db, bindings, requires_explicit_reexport);
|
||||
let mut inferred =
|
||||
place_from_bindings_impl(db, bindings, requires_explicit_reexport).place;
|
||||
|
||||
if boundness_analysis == BoundnessAnalysis::AssumeBound {
|
||||
if let Place::Defined(ty, origin, Definedness::PossiblyUndefined) = inferred {
|
||||
@@ -1010,7 +1013,7 @@ fn place_from_bindings_impl<'db>(
|
||||
db: &'db dyn Db,
|
||||
bindings_with_constraints: BindingWithConstraintsIterator<'_, 'db>,
|
||||
requires_explicit_reexport: RequiresExplicitReExport,
|
||||
) -> Place<'db> {
|
||||
) -> PlaceWithDefinition<'db> {
|
||||
let predicates = bindings_with_constraints.predicates;
|
||||
let reachability_constraints = bindings_with_constraints.reachability_constraints;
|
||||
let boundness_analysis = bindings_with_constraints.boundness_analysis;
|
||||
@@ -1039,6 +1042,8 @@ fn place_from_bindings_impl<'db>(
|
||||
})
|
||||
};
|
||||
|
||||
let mut first_definition = None;
|
||||
|
||||
let mut types = bindings_with_constraints.filter_map(
|
||||
|BindingWithConstraints {
|
||||
binding,
|
||||
@@ -1119,12 +1124,13 @@ fn place_from_bindings_impl<'db>(
|
||||
return None;
|
||||
}
|
||||
|
||||
first_definition.get_or_insert(binding);
|
||||
let binding_ty = binding_type(db, binding);
|
||||
Some(narrowing_constraint.narrow(db, binding_ty, binding.place(db)))
|
||||
},
|
||||
);
|
||||
|
||||
if let Some(first) = types.next() {
|
||||
let place = if let Some(first) = types.next() {
|
||||
let ty = if let Some(second) = types.next() {
|
||||
let mut builder = PublicTypeBuilder::new(db);
|
||||
builder.add(first);
|
||||
@@ -1161,9 +1167,19 @@ fn place_from_bindings_impl<'db>(
|
||||
}
|
||||
} else {
|
||||
Place::Undefined
|
||||
};
|
||||
|
||||
PlaceWithDefinition {
|
||||
place,
|
||||
first_definition,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct PlaceWithDefinition<'db> {
|
||||
pub(super) place: Place<'db>,
|
||||
pub(super) first_definition: Option<Definition<'db>>,
|
||||
}
|
||||
|
||||
/// Accumulates types from multiple bindings or declarations, and eventually builds a
|
||||
/// union type from them.
|
||||
///
|
||||
@@ -1294,7 +1310,6 @@ fn place_from_declarations_impl<'db>(
|
||||
let boundness_analysis = declarations.boundness_analysis;
|
||||
let mut declarations = declarations.peekable();
|
||||
let mut first_declaration = None;
|
||||
let mut exactly_one_declaration = false;
|
||||
|
||||
let is_non_exported = |declaration: Definition<'db>| {
|
||||
requires_explicit_reexport.is_yes() && !is_reexported(db, declaration)
|
||||
@@ -1325,12 +1340,7 @@ fn place_from_declarations_impl<'db>(
|
||||
return None;
|
||||
}
|
||||
|
||||
if first_declaration.is_none() {
|
||||
first_declaration = Some(declaration);
|
||||
exactly_one_declaration = true;
|
||||
} else {
|
||||
exactly_one_declaration = false;
|
||||
}
|
||||
first_declaration.get_or_insert(declaration);
|
||||
|
||||
let static_reachability =
|
||||
reachability_constraints.evaluate(db, predicates, reachability_constraint);
|
||||
@@ -1387,19 +1397,19 @@ fn place_from_declarations_impl<'db>(
|
||||
.with_qualifiers(declared.qualifiers());
|
||||
|
||||
if let Some(conflicting) = conflicting {
|
||||
PlaceFromDeclarationsResult::conflict(place_and_quals, conflicting)
|
||||
PlaceFromDeclarationsResult::conflict(place_and_quals, conflicting, first_declaration)
|
||||
} else {
|
||||
PlaceFromDeclarationsResult {
|
||||
place_and_quals,
|
||||
conflicting_types: None,
|
||||
single_declaration: first_declaration.filter(|_| exactly_one_declaration),
|
||||
first_declaration,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
PlaceFromDeclarationsResult {
|
||||
place_and_quals: Place::Undefined.into(),
|
||||
conflicting_types: None,
|
||||
single_declaration: None,
|
||||
first_declaration: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,10 +166,10 @@ impl<'db> PatternPredicate<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A "placeholder predicate" that is used to model the fact that the boundness of a
|
||||
/// (possible) definition or declaration caused by a `*` import cannot be fully determined
|
||||
/// until type-inference time. This is essentially the same as a standard reachability constraint,
|
||||
/// so we reuse the [`Predicate`] infrastructure to model it.
|
||||
/// A "placeholder predicate" that is used to model the fact that the boundness of a (possible)
|
||||
/// definition or declaration caused by a `*` import cannot be fully determined until type-
|
||||
/// inference time. This is essentially the same as a standard reachability constraint, so we reuse
|
||||
/// the [`Predicate`] infrastructure to model it.
|
||||
///
|
||||
/// To illustrate, say we have a module `exporter.py` like so:
|
||||
///
|
||||
@@ -183,14 +183,14 @@ impl<'db> PatternPredicate<'db> {
|
||||
/// ```py
|
||||
/// A = 1
|
||||
///
|
||||
/// from importer import *
|
||||
/// from exporter import *
|
||||
/// ```
|
||||
///
|
||||
/// Since we cannot know whether or not <condition> is true at semantic-index time,
|
||||
/// we record a definition for `A` in `b.py` as a result of the `from a import *`
|
||||
/// statement, but place a predicate on it to record the fact that we don't yet
|
||||
/// know whether this definition will be visible from all control-flow paths or not.
|
||||
/// Essentially, we model `b.py` as something similar to this:
|
||||
/// Since we cannot know whether or not <condition> is true at semantic-index time, we record
|
||||
/// a definition for `A` in `importer.py` as a result of the `from exporter import *` statement,
|
||||
/// but place a predicate on it to record the fact that we don't yet know whether this definition
|
||||
/// will be visible from all control-flow paths or not. Essentially, we model `importer.py` as
|
||||
/// something similar to this:
|
||||
///
|
||||
/// ```py
|
||||
/// A = 1
|
||||
@@ -199,8 +199,8 @@ impl<'db> PatternPredicate<'db> {
|
||||
/// from a import A
|
||||
/// ```
|
||||
///
|
||||
/// At type-check time, the placeholder predicate for the `A` definition is evaluated by
|
||||
/// attempting to resolve the `A` symbol in `a.py`'s global namespace:
|
||||
/// At type-check time, the placeholder predicate for the `A` definition is evaluated by attempting
|
||||
/// to resolve the `A` symbol in `exporter.py`'s global namespace:
|
||||
/// - If it resolves to a definitely bound symbol, then the predicate resolves to [`Truthiness::AlwaysTrue`]
|
||||
/// - If it resolves to an unbound symbol, then the predicate resolves to [`Truthiness::AlwaysFalse`]
|
||||
/// - If it resolves to a possibly bound symbol, then the predicate resolves to [`Truthiness::Ambiguous`]
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! During semantic index building, we record so-called reachability constraints that keep track
|
||||
//! of a set of conditions that need to apply in order for a certain statement or expression to
|
||||
//! be reachable from the start of the scope. As an example, consider the following situation where
|
||||
//! we have just processed two `if`-statements:
|
||||
//! we have just processed an `if`-statement:
|
||||
//! ```py
|
||||
//! if test:
|
||||
//! <is this reachable?>
|
||||
@@ -101,13 +101,13 @@
|
||||
//! <is this reachable?>
|
||||
//! ```
|
||||
//! If we would not record any constraints at the branching point, we would have an `always-true`
|
||||
//! reachability for the no-loop branch, and a `always-false` reachability for the branch which enters
|
||||
//! the loop. Merging those would lead to a reachability of `always-true OR always-false = always-true`,
|
||||
//! reachability for the no-loop branch, and a `always-true` reachability for the branch which enters
|
||||
//! the loop. Merging those would lead to a reachability of `always-true OR always-true = always-true`,
|
||||
//! i.e. we would consider the end of the scope to be unconditionally reachable, which is not correct.
|
||||
//!
|
||||
//! Recording an ambiguous constraint at the branching point modifies the constraints in both branches to
|
||||
//! `always-true AND ambiguous = ambiguous` and `always-false AND ambiguous = always-false`, respectively.
|
||||
//! Merging these two using OR correctly leads to `ambiguous` for the end-of-scope reachability.
|
||||
//! `always-true AND ambiguous = ambiguous`. Merging these two using OR correctly leads to `ambiguous` for
|
||||
//! the end-of-scope reachability.
|
||||
//!
|
||||
//!
|
||||
//! ## Reachability constraints and bindings
|
||||
|
||||
@@ -82,7 +82,7 @@ impl<'db> SemanticModel<'db> {
|
||||
memberdef.member.name,
|
||||
MemberDefinition {
|
||||
ty: memberdef.member.ty,
|
||||
definition: memberdef.definition,
|
||||
first_reachable_definition: memberdef.first_reachable_definition,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -328,11 +328,11 @@ impl<'db> SemanticModel<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The type and definition (if available) of a symbol.
|
||||
/// The type and definition of a symbol.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MemberDefinition<'db> {
|
||||
pub ty: Type<'db>,
|
||||
pub definition: Option<Definition<'db>>,
|
||||
pub first_reachable_definition: Definition<'db>,
|
||||
}
|
||||
|
||||
/// A classification of symbol names.
|
||||
|
||||
@@ -763,7 +763,7 @@ impl<'db> DataclassParams<'db> {
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)]
|
||||
pub enum Type<'db> {
|
||||
/// The dynamic type: a statically unknown set of values
|
||||
Dynamic(DynamicType),
|
||||
Dynamic(DynamicType<'db>),
|
||||
/// The empty set of values
|
||||
Never,
|
||||
/// A specific function object
|
||||
@@ -889,7 +889,10 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
|
||||
pub const fn is_unknown(&self) -> bool {
|
||||
matches!(self, Type::Dynamic(DynamicType::Unknown))
|
||||
matches!(
|
||||
self,
|
||||
Type::Dynamic(DynamicType::Unknown | DynamicType::UnknownGeneric(_))
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) const fn is_never(&self) -> bool {
|
||||
@@ -975,7 +978,10 @@ impl<'db> Type<'db> {
|
||||
|
||||
pub(crate) fn is_todo(&self) -> bool {
|
||||
self.as_dynamic().is_some_and(|dynamic| match dynamic {
|
||||
DynamicType::Any | DynamicType::Unknown | DynamicType::Divergent(_) => false,
|
||||
DynamicType::Any
|
||||
| DynamicType::Unknown
|
||||
| DynamicType::UnknownGeneric(_)
|
||||
| DynamicType::Divergent(_) => false,
|
||||
DynamicType::Todo(_) | DynamicType::TodoStarredExpression | DynamicType::TodoUnpack => {
|
||||
true
|
||||
}
|
||||
@@ -1162,7 +1168,7 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn as_dynamic(self) -> Option<DynamicType> {
|
||||
pub(crate) const fn as_dynamic(self) -> Option<DynamicType<'db>> {
|
||||
match self {
|
||||
Type::Dynamic(dynamic_type) => Some(dynamic_type),
|
||||
_ => None,
|
||||
@@ -1176,7 +1182,7 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn expect_dynamic(self) -> DynamicType {
|
||||
pub(crate) const fn expect_dynamic(self) -> DynamicType<'db> {
|
||||
self.as_dynamic().expect("Expected a Type::Dynamic variant")
|
||||
}
|
||||
|
||||
@@ -6274,11 +6280,25 @@ impl<'db> Type<'db> {
|
||||
),
|
||||
|
||||
Type::Intersection(_) => {
|
||||
Binding::single(self, Signature::todo("Type::Intersection.call()")).into()
|
||||
Binding::single(self, Signature::todo("Type::Intersection.call")).into()
|
||||
}
|
||||
|
||||
// TODO: this is actually callable
|
||||
Type::DataclassDecorator(_) => CallableBinding::not_callable(self).into(),
|
||||
Type::DataclassDecorator(_) => {
|
||||
let typevar = BoundTypeVarInstance::synthetic(db, "T", TypeVarVariance::Invariant);
|
||||
let typevar_meta = SubclassOfType::from(db, typevar);
|
||||
let context = GenericContext::from_typevar_instances(db, [typevar]);
|
||||
let parameters = [Parameter::positional_only(Some(Name::new_static("cls")))
|
||||
.with_annotated_type(typevar_meta)];
|
||||
// Intersect with `Any` for the return type to reflect the fact that the `dataclass()`
|
||||
// decorator adds methods to the class
|
||||
let returns = IntersectionType::from_elements(db, [typevar_meta, Type::any()]);
|
||||
let signature = Signature::new_generic(
|
||||
Some(context),
|
||||
Parameters::new(db, parameters),
|
||||
Some(returns),
|
||||
);
|
||||
Binding::single(self, signature).into()
|
||||
}
|
||||
|
||||
// TODO: some `SpecialForm`s are callable (e.g. TypedDicts)
|
||||
Type::SpecialForm(_) => CallableBinding::not_callable(self).into(),
|
||||
@@ -7885,14 +7905,18 @@ impl<'db> Type<'db> {
|
||||
typevars: &mut FxOrderSet<BoundTypeVarInstance<'db>>,
|
||||
visitor: &FindLegacyTypeVarsVisitor<'db>,
|
||||
) {
|
||||
let is_matching_typevar = |bound_typevar: &BoundTypeVarInstance<'db>| {
|
||||
matches!(
|
||||
bound_typevar.typevar(db).kind(db),
|
||||
TypeVarKind::Legacy | TypeVarKind::TypingSelf | TypeVarKind::ParamSpec
|
||||
) && binding_context.is_none_or(|binding_context| {
|
||||
bound_typevar.binding_context(db) == BindingContext::Definition(binding_context)
|
||||
})
|
||||
};
|
||||
|
||||
match self {
|
||||
Type::TypeVar(bound_typevar) => {
|
||||
if matches!(
|
||||
bound_typevar.typevar(db).kind(db),
|
||||
TypeVarKind::Legacy | TypeVarKind::TypingSelf | TypeVarKind::ParamSpec
|
||||
) && binding_context.is_none_or(|binding_context| {
|
||||
bound_typevar.binding_context(db) == BindingContext::Definition(binding_context)
|
||||
}) {
|
||||
if is_matching_typevar(&bound_typevar) {
|
||||
typevars.insert(bound_typevar);
|
||||
}
|
||||
}
|
||||
@@ -8032,6 +8056,14 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
},
|
||||
|
||||
Type::Dynamic(DynamicType::UnknownGeneric(generic_context)) => {
|
||||
for variable in generic_context.variables(db) {
|
||||
if is_matching_typevar(&variable) {
|
||||
typevars.insert(variable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Type::Dynamic(_)
|
||||
| Type::Never
|
||||
| Type::AlwaysTruthy
|
||||
@@ -8063,6 +8095,26 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Bind all unbound legacy type variables to the given context and then
|
||||
/// add all legacy typevars to the provided set.
|
||||
pub(crate) fn bind_and_find_all_legacy_typevars(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
binding_context: Option<Definition<'db>>,
|
||||
variables: &mut FxOrderSet<BoundTypeVarInstance<'db>>,
|
||||
) {
|
||||
self.apply_type_mapping(
|
||||
db,
|
||||
&TypeMapping::BindLegacyTypevars(
|
||||
binding_context
|
||||
.map(BindingContext::Definition)
|
||||
.unwrap_or(BindingContext::Synthetic),
|
||||
),
|
||||
TypeContext::default(),
|
||||
)
|
||||
.find_legacy_typevars(db, None, variables);
|
||||
}
|
||||
|
||||
/// Replace default types in parameters of callables with `Unknown`.
|
||||
pub(crate) fn replace_parameter_defaults(self, db: &'db dyn Db) -> Type<'db> {
|
||||
self.apply_type_mapping(
|
||||
@@ -8211,7 +8263,7 @@ impl<'db> Type<'db> {
|
||||
Self::SpecialForm(special_form) => special_form.definition(db),
|
||||
Self::Never => Type::SpecialForm(SpecialFormType::Never).definition(db),
|
||||
Self::Dynamic(DynamicType::Any) => Type::SpecialForm(SpecialFormType::Any).definition(db),
|
||||
Self::Dynamic(DynamicType::Unknown) => Type::SpecialForm(SpecialFormType::Unknown).definition(db),
|
||||
Self::Dynamic(DynamicType::Unknown | DynamicType::UnknownGeneric(_)) => Type::SpecialForm(SpecialFormType::Unknown).definition(db),
|
||||
Self::AlwaysTruthy => Type::SpecialForm(SpecialFormType::AlwaysTruthy).definition(db),
|
||||
Self::AlwaysFalsy => Type::SpecialForm(SpecialFormType::AlwaysFalsy).definition(db),
|
||||
|
||||
@@ -8297,6 +8349,16 @@ impl<'db> Type<'db> {
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Default-specialize all legacy typevars in this type.
|
||||
///
|
||||
/// This is used when an implicit type alias is referenced without explicitly specializing it.
|
||||
pub(crate) fn default_specialize(self, db: &'db dyn Db) -> Type<'db> {
|
||||
let mut variables = FxOrderSet::default();
|
||||
self.find_legacy_typevars(db, None, &mut variables);
|
||||
let generic_context = GenericContext::from_typevar_instances(db, variables);
|
||||
self.apply_specialization(db, generic_context.default_specialization(db, None))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> From<&Type<'db>> for Type<'db> {
|
||||
@@ -8863,11 +8925,18 @@ pub struct DivergentType {
|
||||
impl get_size2::GetSize for DivergentType {}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, salsa::Update, get_size2::GetSize)]
|
||||
pub enum DynamicType {
|
||||
pub enum DynamicType<'db> {
|
||||
/// An explicitly annotated `typing.Any`
|
||||
Any,
|
||||
/// An unannotated value, or a dynamic type resulting from an error
|
||||
Unknown,
|
||||
/// Similar to `Unknown`, this represents a dynamic type that has been explicitly specialized
|
||||
/// with legacy typevars, e.g. `UnknownClass[T]`, where `T` is a legacy typevar. We keep track
|
||||
/// of the type variables in the generic context in case this type is later specialized again.
|
||||
///
|
||||
/// TODO: Once we implement <https://github.com/astral-sh/ty/issues/1711>, this variant might
|
||||
/// not be needed anymore.
|
||||
UnknownGeneric(GenericContext<'db>),
|
||||
/// Temporary type for symbols that can't be inferred yet because of missing implementations.
|
||||
///
|
||||
/// This variant should eventually be removed once ty is spec-compliant.
|
||||
@@ -8886,7 +8955,7 @@ pub enum DynamicType {
|
||||
Divergent(DivergentType),
|
||||
}
|
||||
|
||||
impl DynamicType {
|
||||
impl DynamicType<'_> {
|
||||
fn normalized(self) -> Self {
|
||||
if matches!(self, Self::Divergent(_)) {
|
||||
self
|
||||
@@ -8904,11 +8973,11 @@ impl DynamicType {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for DynamicType {
|
||||
impl std::fmt::Display for DynamicType<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
DynamicType::Any => f.write_str("Any"),
|
||||
DynamicType::Unknown => f.write_str("Unknown"),
|
||||
DynamicType::Unknown | DynamicType::UnknownGeneric(_) => f.write_str("Unknown"),
|
||||
// `DynamicType::Todo`'s display should be explicit that is not a valid display of
|
||||
// any other type
|
||||
DynamicType::Todo(todo) => write!(f, "@Todo{todo}"),
|
||||
|
||||
@@ -172,7 +172,7 @@ impl<'db> BoundSuperError<'db> {
|
||||
|
||||
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, get_size2::GetSize)]
|
||||
pub enum SuperOwnerKind<'db> {
|
||||
Dynamic(DynamicType),
|
||||
Dynamic(DynamicType<'db>),
|
||||
Class(ClassType<'db>),
|
||||
Instance(NominalInstanceType<'db>),
|
||||
}
|
||||
|
||||
@@ -1377,9 +1377,9 @@ pub(crate) struct Field<'db> {
|
||||
pub(crate) declared_ty: Type<'db>,
|
||||
/// Kind-specific metadata for this field
|
||||
pub(crate) kind: FieldKind<'db>,
|
||||
/// The original declaration of this field, if there is exactly one.
|
||||
/// The first declaration of this field.
|
||||
/// This field is used for backreferences in diagnostics.
|
||||
pub(crate) single_declaration: Option<Definition<'db>>,
|
||||
pub(crate) first_declaration: Option<Definition<'db>>,
|
||||
}
|
||||
|
||||
impl Field<'_> {
|
||||
@@ -3041,7 +3041,7 @@ impl<'db> ClassLiteral<'db> {
|
||||
let symbol = table.symbol(symbol_id);
|
||||
|
||||
let result = place_from_declarations(db, declarations.clone());
|
||||
let single_declaration = result.single_declaration;
|
||||
let first_declaration = result.first_declaration;
|
||||
let attr = result.ignore_conflicting_declarations();
|
||||
if attr.is_class_var() {
|
||||
continue;
|
||||
@@ -3049,7 +3049,9 @@ impl<'db> ClassLiteral<'db> {
|
||||
|
||||
if let Some(attr_ty) = attr.place.ignore_possibly_undefined() {
|
||||
let bindings = use_def.end_of_scope_symbol_bindings(symbol_id);
|
||||
let mut default_ty = place_from_bindings(db, bindings).ignore_possibly_undefined();
|
||||
let mut default_ty = place_from_bindings(db, bindings)
|
||||
.place
|
||||
.ignore_possibly_undefined();
|
||||
|
||||
default_ty =
|
||||
default_ty.map(|ty| ty.apply_optional_specialization(db, specialization));
|
||||
@@ -3107,7 +3109,7 @@ impl<'db> ClassLiteral<'db> {
|
||||
let mut field = Field {
|
||||
declared_ty: attr_ty.apply_optional_specialization(db, specialization),
|
||||
kind,
|
||||
single_declaration,
|
||||
first_declaration,
|
||||
};
|
||||
|
||||
// Check if this is a KW_ONLY sentinel and mark subsequent fields as keyword-only
|
||||
@@ -3590,7 +3592,7 @@ impl<'db> ClassLiteral<'db> {
|
||||
// The attribute is declared in the class body.
|
||||
|
||||
let bindings = use_def.end_of_scope_symbol_bindings(symbol_id);
|
||||
let inferred = place_from_bindings(db, bindings);
|
||||
let inferred = place_from_bindings(db, bindings).place;
|
||||
let has_binding = !inferred.is_undefined();
|
||||
|
||||
if has_binding {
|
||||
@@ -3833,7 +3835,9 @@ impl<'db> VarianceInferable<'db> for ClassLiteral<'db> {
|
||||
(symbol_id, place_and_qual)
|
||||
})
|
||||
.chain(use_def_map.all_end_of_scope_symbol_bindings().map(
|
||||
|(symbol_id, bindings)| (symbol_id, place_from_bindings(db, bindings).into()),
|
||||
|(symbol_id, bindings)| {
|
||||
(symbol_id, place_from_bindings(db, bindings).place.into())
|
||||
},
|
||||
))
|
||||
.filter_map(|(symbol_id, place_and_qual)| {
|
||||
if let Some(name) = table.place(symbol_id).as_symbol().map(Symbol::name) {
|
||||
|
||||
@@ -18,7 +18,7 @@ use crate::types::{
|
||||
/// automatically construct the default specialization for that class.
|
||||
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)]
|
||||
pub enum ClassBase<'db> {
|
||||
Dynamic(DynamicType),
|
||||
Dynamic(DynamicType<'db>),
|
||||
Class(ClassType<'db>),
|
||||
/// Although `Protocol` is not a class in typeshed's stubs, it is at runtime,
|
||||
/// and can appear in the MRO of a class.
|
||||
@@ -62,7 +62,7 @@ impl<'db> ClassBase<'db> {
|
||||
match self {
|
||||
ClassBase::Class(class) => class.name(db),
|
||||
ClassBase::Dynamic(DynamicType::Any) => "Any",
|
||||
ClassBase::Dynamic(DynamicType::Unknown) => "Unknown",
|
||||
ClassBase::Dynamic(DynamicType::Unknown | DynamicType::UnknownGeneric(_)) => "Unknown",
|
||||
ClassBase::Dynamic(
|
||||
DynamicType::Todo(_) | DynamicType::TodoUnpack | DynamicType::TodoStarredExpression,
|
||||
) => "@Todo",
|
||||
|
||||
@@ -51,6 +51,19 @@
|
||||
//! the constraint says that the typevar must specialize to that _exact_ type, not to a subtype or
|
||||
//! supertype of it.
|
||||
//!
|
||||
//! ### Tracing
|
||||
//!
|
||||
//! This module is instrumented with debug- and trace-level `tracing` messages. You can set the
|
||||
//! `TY_LOG` environment variable to see this output when testing locally. `tracing` log messages
|
||||
//! typically have a `target` field, which is the name of the module the message appears in — in
|
||||
//! this case, `ty_python_semantic::types::constraints`. We add additional detail to these targets,
|
||||
//! in case you only want to debug parts of the implementation. For instance, if you want to debug
|
||||
//! how we construct sequent maps, you could use
|
||||
//!
|
||||
//! ```sh
|
||||
//! env TY_LOG=ty_python_semantic::types::constraints::SequentMap=trace ty check ...
|
||||
//! ```
|
||||
//!
|
||||
//! [bdd]: https://en.wikipedia.org/wiki/Binary_decision_diagram
|
||||
|
||||
use std::cell::RefCell;
|
||||
@@ -754,9 +767,10 @@ impl<'db> ConstrainedTypeVar<'db> {
|
||||
/// Terminal nodes (`false` and `true`) have their own dedicated enum variants. The
|
||||
/// [`Interior`][InteriorNode] variant represents interior nodes.
|
||||
///
|
||||
/// BDD nodes are _reduced_, which means that there are no duplicate nodes (which we handle via
|
||||
/// Salsa interning), and that there are no redundant nodes, with `if_true` and `if_false` edges
|
||||
/// that point at the same node.
|
||||
/// BDD nodes are _quasi-reduced_, which means that there are no duplicate nodes (which we handle
|
||||
/// via Salsa interning). Unlike the typical BDD representation, which is (fully) reduced, we do
|
||||
/// allow redundant nodes, with `if_true` and `if_false` edges that point at the same node. That
|
||||
/// means that our BDDs "remember" all of the individual constraints that they were created with.
|
||||
///
|
||||
/// BDD nodes are also _ordered_, meaning that every path from the root of a BDD to a terminal node
|
||||
/// visits variables in the same order. [`ConstrainedTypeVar::ordering`] defines the variable
|
||||
@@ -769,7 +783,7 @@ enum Node<'db> {
|
||||
}
|
||||
|
||||
impl<'db> Node<'db> {
|
||||
/// Creates a new BDD node, ensuring that it is fully reduced.
|
||||
/// Creates a new BDD node, ensuring that it is quasi-reduced.
|
||||
fn new(
|
||||
db: &'db dyn Db,
|
||||
constraint: ConstrainedTypeVar<'db>,
|
||||
@@ -784,9 +798,6 @@ impl<'db> Node<'db> {
|
||||
root_constraint.ordering(db) > constraint.ordering(db)
|
||||
})
|
||||
);
|
||||
if if_true == if_false {
|
||||
return if_true;
|
||||
}
|
||||
Self::Interior(InteriorNode::new(db, constraint, if_true, if_false))
|
||||
}
|
||||
|
||||
@@ -854,10 +865,10 @@ impl<'db> Node<'db> {
|
||||
Node::AlwaysFalse => {}
|
||||
Node::Interior(interior) => {
|
||||
let constraint = interior.constraint(db);
|
||||
path.walk_edge(map, constraint.when_true(), |path, _| {
|
||||
path.walk_edge(db, map, constraint.when_true(), |path, _| {
|
||||
interior.if_true(db).for_each_path_inner(db, f, map, path);
|
||||
});
|
||||
path.walk_edge(map, constraint.when_false(), |path, _| {
|
||||
path.walk_edge(db, map, constraint.when_false(), |path, _| {
|
||||
interior.if_false(db).for_each_path_inner(db, f, map, path);
|
||||
});
|
||||
}
|
||||
@@ -892,7 +903,7 @@ impl<'db> Node<'db> {
|
||||
// impossible paths, and so we treat them as passing the "always satisfied" check.
|
||||
let constraint = interior.constraint(db);
|
||||
let true_always_satisfied = path
|
||||
.walk_edge(map, constraint.when_true(), |path, _| {
|
||||
.walk_edge(db, map, constraint.when_true(), |path, _| {
|
||||
interior
|
||||
.if_true(db)
|
||||
.is_always_satisfied_inner(db, map, path)
|
||||
@@ -903,7 +914,7 @@ impl<'db> Node<'db> {
|
||||
}
|
||||
|
||||
// Ditto for the if_false branch
|
||||
path.walk_edge(map, constraint.when_false(), |path, _| {
|
||||
path.walk_edge(db, map, constraint.when_false(), |path, _| {
|
||||
interior
|
||||
.if_false(db)
|
||||
.is_always_satisfied_inner(db, map, path)
|
||||
@@ -941,7 +952,7 @@ impl<'db> Node<'db> {
|
||||
// impossible paths, and so we treat them as passing the "never satisfied" check.
|
||||
let constraint = interior.constraint(db);
|
||||
let true_never_satisfied = path
|
||||
.walk_edge(map, constraint.when_true(), |path, _| {
|
||||
.walk_edge(db, map, constraint.when_true(), |path, _| {
|
||||
interior.if_true(db).is_never_satisfied_inner(db, map, path)
|
||||
})
|
||||
.unwrap_or(true);
|
||||
@@ -950,7 +961,7 @@ impl<'db> Node<'db> {
|
||||
}
|
||||
|
||||
// Ditto for the if_false branch
|
||||
path.walk_edge(map, constraint.when_false(), |path, _| {
|
||||
path.walk_edge(db, map, constraint.when_false(), |path, _| {
|
||||
interior
|
||||
.if_false(db)
|
||||
.is_never_satisfied_inner(db, map, path)
|
||||
@@ -972,8 +983,15 @@ impl<'db> Node<'db> {
|
||||
/// Returns the `or` or union of two BDDs.
|
||||
fn or(self, db: &'db dyn Db, other: Self) -> Self {
|
||||
match (self, other) {
|
||||
(Node::AlwaysTrue, _) | (_, Node::AlwaysTrue) => Node::AlwaysTrue,
|
||||
(Node::AlwaysTrue, Node::AlwaysTrue) => Node::AlwaysTrue,
|
||||
(Node::AlwaysFalse, other) | (other, Node::AlwaysFalse) => other,
|
||||
(Node::AlwaysTrue, Node::Interior(interior))
|
||||
| (Node::Interior(interior), Node::AlwaysTrue) => Node::new(
|
||||
db,
|
||||
interior.constraint(db),
|
||||
Node::AlwaysTrue,
|
||||
Node::AlwaysTrue,
|
||||
),
|
||||
(Node::Interior(a), Node::Interior(b)) => {
|
||||
// OR is commutative, which lets us halve the cache requirements
|
||||
let (a, b) = if b.0 < a.0 { (b, a) } else { (a, b) };
|
||||
@@ -985,8 +1003,15 @@ impl<'db> Node<'db> {
|
||||
/// Returns the `and` or intersection of two BDDs.
|
||||
fn and(self, db: &'db dyn Db, other: Self) -> Self {
|
||||
match (self, other) {
|
||||
(Node::AlwaysFalse, _) | (_, Node::AlwaysFalse) => Node::AlwaysFalse,
|
||||
(Node::AlwaysFalse, Node::AlwaysFalse) => Node::AlwaysFalse,
|
||||
(Node::AlwaysTrue, other) | (other, Node::AlwaysTrue) => other,
|
||||
(Node::AlwaysFalse, Node::Interior(interior))
|
||||
| (Node::Interior(interior), Node::AlwaysFalse) => Node::new(
|
||||
db,
|
||||
interior.constraint(db),
|
||||
Node::AlwaysFalse,
|
||||
Node::AlwaysFalse,
|
||||
),
|
||||
(Node::Interior(a), Node::Interior(b)) => {
|
||||
// AND is commutative, which lets us halve the cache requirements
|
||||
let (a, b) = if b.0 < a.0 { (b, a) } else { (a, b) };
|
||||
@@ -1765,7 +1790,7 @@ impl<'db> InteriorNode<'db> {
|
||||
// we're about to remove. If so, we need to "remember" them by AND-ing them in with the
|
||||
// corresponding branch.
|
||||
let if_true = path
|
||||
.walk_edge(map, self_constraint.when_true(), |path, new_range| {
|
||||
.walk_edge(db, map, self_constraint.when_true(), |path, new_range| {
|
||||
let branch = self
|
||||
.if_true(db)
|
||||
.abstract_one_inner(db, should_remove, map, path);
|
||||
@@ -1782,7 +1807,7 @@ impl<'db> InteriorNode<'db> {
|
||||
})
|
||||
.unwrap_or(Node::AlwaysFalse);
|
||||
let if_false = path
|
||||
.walk_edge(map, self_constraint.when_false(), |path, new_range| {
|
||||
.walk_edge(db, map, self_constraint.when_false(), |path, new_range| {
|
||||
let branch = self
|
||||
.if_false(db)
|
||||
.abstract_one_inner(db, should_remove, map, path);
|
||||
@@ -1802,13 +1827,13 @@ impl<'db> InteriorNode<'db> {
|
||||
} else {
|
||||
// Otherwise, we abstract the if_false/if_true edges recursively.
|
||||
let if_true = path
|
||||
.walk_edge(map, self_constraint.when_true(), |path, _| {
|
||||
.walk_edge(db, map, self_constraint.when_true(), |path, _| {
|
||||
self.if_true(db)
|
||||
.abstract_one_inner(db, should_remove, map, path)
|
||||
})
|
||||
.unwrap_or(Node::AlwaysFalse);
|
||||
let if_false = path
|
||||
.walk_edge(map, self_constraint.when_false(), |path, _| {
|
||||
.walk_edge(db, map, self_constraint.when_false(), |path, _| {
|
||||
self.if_false(db)
|
||||
.abstract_one_inner(db, should_remove, map, path)
|
||||
})
|
||||
@@ -1858,6 +1883,11 @@ impl<'db> InteriorNode<'db> {
|
||||
heap_size=ruff_memory_usage::heap_size,
|
||||
)]
|
||||
fn sequent_map(self, db: &'db dyn Db) -> SequentMap<'db> {
|
||||
tracing::debug!(
|
||||
target: "ty_python_semantic::types::constraints::SequentMap",
|
||||
constraints = %Node::Interior(self).display(db),
|
||||
"create sequent map",
|
||||
);
|
||||
let mut map = SequentMap::default();
|
||||
Node::Interior(self).for_each_constraint(db, &mut |constraint| {
|
||||
map.add(db, constraint);
|
||||
@@ -2263,10 +2293,7 @@ impl<'db> ConstraintAssignment<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
// Keep this for future debugging needs, even though it's not currently used when rendering
|
||||
// constraint sets.
|
||||
#[expect(dead_code)]
|
||||
pub(crate) fn display(self, db: &'db dyn Db) -> impl Display {
|
||||
fn display(self, db: &'db dyn Db) -> impl Display {
|
||||
struct DisplayConstraintAssignment<'db> {
|
||||
constraint: ConstraintAssignment<'db>,
|
||||
db: &'db dyn Db,
|
||||
@@ -2342,18 +2369,29 @@ impl<'db> SequentMap<'db> {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Otherwise, check this constraint against all of the other ones we've seen so far, seeing
|
||||
// First see if we can create any sequents from the constraint on its own.
|
||||
tracing::trace!(
|
||||
target: "ty_python_semantic::types::constraints::SequentMap",
|
||||
constraint = %constraint.display(db),
|
||||
"add sequents for constraint",
|
||||
);
|
||||
self.add_sequents_for_single(db, constraint);
|
||||
|
||||
// Then check this constraint against all of the other ones we've seen so far, seeing
|
||||
// if they're related to each other.
|
||||
let processed = std::mem::take(&mut self.processed);
|
||||
for other in &processed {
|
||||
if constraint != *other {
|
||||
tracing::trace!(
|
||||
target: "ty_python_semantic::types::constraints::SequentMap",
|
||||
left = %constraint.display(db),
|
||||
right = %other.display(db),
|
||||
"add sequents for constraint pair",
|
||||
);
|
||||
self.add_sequents_for_pair(db, constraint, *other);
|
||||
}
|
||||
}
|
||||
self.processed = processed;
|
||||
|
||||
// And see if we can create any sequents from the constraint on its own.
|
||||
self.add_sequents_for_single(db, constraint);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2377,8 +2415,14 @@ impl<'db> SequentMap<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
fn add_single_tautology(&mut self, ante: ConstrainedTypeVar<'db>) {
|
||||
self.single_tautologies.insert(ante);
|
||||
fn add_single_tautology(&mut self, db: &'db dyn Db, ante: ConstrainedTypeVar<'db>) {
|
||||
if self.single_tautologies.insert(ante) {
|
||||
tracing::debug!(
|
||||
target: "ty_python_semantic::types::constraints::SequentMap",
|
||||
sequent = %format_args!("¬{} → false", ante.display(db)),
|
||||
"add sequent",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn add_pair_impossibility(
|
||||
@@ -2387,8 +2431,16 @@ impl<'db> SequentMap<'db> {
|
||||
ante1: ConstrainedTypeVar<'db>,
|
||||
ante2: ConstrainedTypeVar<'db>,
|
||||
) {
|
||||
self.pair_impossibilities
|
||||
.insert(Self::pair_key(db, ante1, ante2));
|
||||
if self
|
||||
.pair_impossibilities
|
||||
.insert(Self::pair_key(db, ante1, ante2))
|
||||
{
|
||||
tracing::debug!(
|
||||
target: "ty_python_semantic::types::constraints::SequentMap",
|
||||
sequent = %format_args!("{} ∧ {} → false", ante1.display(db), ante2.display(db)),
|
||||
"add sequent",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn add_pair_implication(
|
||||
@@ -2402,24 +2454,50 @@ impl<'db> SequentMap<'db> {
|
||||
if ante1.implies(db, post) || ante2.implies(db, post) {
|
||||
return;
|
||||
}
|
||||
self.pair_implications
|
||||
if self
|
||||
.pair_implications
|
||||
.entry(Self::pair_key(db, ante1, ante2))
|
||||
.or_default()
|
||||
.insert(post);
|
||||
.insert(post)
|
||||
{
|
||||
tracing::debug!(
|
||||
target: "ty_python_semantic::types::constraints::SequentMap",
|
||||
sequent = %format_args!(
|
||||
"{} ∧ {} → {}",
|
||||
ante1.display(db),
|
||||
ante2.display(db),
|
||||
post.display(db),
|
||||
),
|
||||
"add sequent",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn add_single_implication(
|
||||
&mut self,
|
||||
db: &'db dyn Db,
|
||||
ante: ConstrainedTypeVar<'db>,
|
||||
post: ConstrainedTypeVar<'db>,
|
||||
) {
|
||||
if ante == post {
|
||||
return;
|
||||
}
|
||||
self.single_implications
|
||||
if self
|
||||
.single_implications
|
||||
.entry(ante)
|
||||
.or_default()
|
||||
.insert(post);
|
||||
.insert(post)
|
||||
{
|
||||
tracing::debug!(
|
||||
target: "ty_python_semantic::types::constraints::SequentMap",
|
||||
sequent = %format_args!(
|
||||
"{} → {}",
|
||||
ante.display(db),
|
||||
post.display(db),
|
||||
),
|
||||
"add sequent",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn add_sequents_for_single(&mut self, db: &'db dyn Db, constraint: ConstrainedTypeVar<'db>) {
|
||||
@@ -2428,7 +2506,7 @@ impl<'db> SequentMap<'db> {
|
||||
let lower = constraint.lower(db);
|
||||
let upper = constraint.upper(db);
|
||||
if lower.is_never() && upper.is_object() {
|
||||
self.add_single_tautology(constraint);
|
||||
self.add_single_tautology(db, constraint);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2465,7 +2543,7 @@ impl<'db> SequentMap<'db> {
|
||||
_ => return,
|
||||
};
|
||||
|
||||
self.add_single_implication(constraint, post_constraint);
|
||||
self.add_single_implication(db, constraint, post_constraint);
|
||||
self.enqueue_constraint(post_constraint);
|
||||
}
|
||||
|
||||
@@ -2660,25 +2738,50 @@ impl<'db> SequentMap<'db> {
|
||||
// elements. (For instance, when processing `T ≤ τ₁ & τ₂` and `T ≤ τ₂ & τ₁`, these clauses
|
||||
// would add sequents for `(T ≤ τ₁ & τ₂) → (T ≤ τ₂ & τ₁)` and vice versa.)
|
||||
if left_constraint.implies(db, right_constraint) {
|
||||
self.add_single_implication(left_constraint, right_constraint);
|
||||
tracing::debug!(
|
||||
target: "ty_python_semantic::types::constraints::SequentMap",
|
||||
left = %left_constraint.display(db),
|
||||
right = %right_constraint.display(db),
|
||||
"left implies right",
|
||||
);
|
||||
self.add_single_implication(db, left_constraint, right_constraint);
|
||||
}
|
||||
if right_constraint.implies(db, left_constraint) {
|
||||
self.add_single_implication(right_constraint, left_constraint);
|
||||
tracing::debug!(
|
||||
target: "ty_python_semantic::types::constraints::SequentMap",
|
||||
left = %left_constraint.display(db),
|
||||
right = %right_constraint.display(db),
|
||||
"right implies left",
|
||||
);
|
||||
self.add_single_implication(db, right_constraint, left_constraint);
|
||||
}
|
||||
|
||||
match left_constraint.intersect(db, right_constraint) {
|
||||
Some(intersection_constraint) => {
|
||||
tracing::debug!(
|
||||
target: "ty_python_semantic::types::constraints::SequentMap",
|
||||
left = %left_constraint.display(db),
|
||||
right = %right_constraint.display(db),
|
||||
intersection = %intersection_constraint.display(db),
|
||||
"left and right overlap",
|
||||
);
|
||||
self.add_pair_implication(
|
||||
db,
|
||||
left_constraint,
|
||||
right_constraint,
|
||||
intersection_constraint,
|
||||
);
|
||||
self.add_single_implication(intersection_constraint, left_constraint);
|
||||
self.add_single_implication(intersection_constraint, right_constraint);
|
||||
self.add_single_implication(db, intersection_constraint, left_constraint);
|
||||
self.add_single_implication(db, intersection_constraint, right_constraint);
|
||||
self.enqueue_constraint(intersection_constraint);
|
||||
}
|
||||
None => {
|
||||
tracing::debug!(
|
||||
target: "ty_python_semantic::types::constraints::SequentMap",
|
||||
left = %left_constraint.display(db),
|
||||
right = %right_constraint.display(db),
|
||||
"left and right are disjoint",
|
||||
);
|
||||
self.add_pair_impossibility(db, left_constraint, right_constraint);
|
||||
}
|
||||
}
|
||||
@@ -2781,6 +2884,7 @@ impl<'db> PathAssignments<'db> {
|
||||
/// the path we're on.
|
||||
fn walk_edge<R>(
|
||||
&mut self,
|
||||
db: &'db dyn Db,
|
||||
map: &SequentMap<'db>,
|
||||
assignment: ConstraintAssignment<'db>,
|
||||
f: impl FnOnce(&mut Self, Range<usize>) -> R,
|
||||
@@ -2791,7 +2895,17 @@ impl<'db> PathAssignments<'db> {
|
||||
let start = self.assignments.len();
|
||||
|
||||
// Add the new assignment and anything we can derive from it.
|
||||
let result = if self.add_assignment(map, assignment).is_err() {
|
||||
tracing::trace!(
|
||||
target: "ty_python_semantic::types::constraints::PathAssignment",
|
||||
before = %format_args!(
|
||||
"[{}]",
|
||||
self.assignments[..start].iter().map(|assignment| assignment.display(db)).format(", "),
|
||||
),
|
||||
edge = %assignment.display(db),
|
||||
"walk edge",
|
||||
);
|
||||
let found_conflict = self.add_assignment(db, map, assignment);
|
||||
let result = if found_conflict.is_err() {
|
||||
// If that results in the path now being impossible due to a contradiction, return
|
||||
// without invoking the callback.
|
||||
None
|
||||
@@ -2801,6 +2915,14 @@ impl<'db> PathAssignments<'db> {
|
||||
// if that happens, `start..end` will mark the assignments that were added by the
|
||||
// `add_assignment` call above — that is, the new assignment for this edge along with
|
||||
// the derived information we inferred from it.
|
||||
tracing::trace!(
|
||||
target: "ty_python_semantic::types::constraints::PathAssignment",
|
||||
new = %format_args!(
|
||||
"[{}]",
|
||||
self.assignments[start..].iter().map(|assignment| assignment.display(db)).format(", "),
|
||||
),
|
||||
"new assignments",
|
||||
);
|
||||
let end = self.assignments.len();
|
||||
Some(f(self, start..end))
|
||||
};
|
||||
@@ -2831,12 +2953,22 @@ impl<'db> PathAssignments<'db> {
|
||||
/// to become invalid, due to a contradiction, returns a [`PathAssignmentConflict`] error.
|
||||
fn add_assignment(
|
||||
&mut self,
|
||||
db: &'db dyn Db,
|
||||
map: &SequentMap<'db>,
|
||||
assignment: ConstraintAssignment<'db>,
|
||||
) -> Result<(), PathAssignmentConflict> {
|
||||
// First add this assignment. If it causes a conflict, return that as an error. If we've
|
||||
// already know this assignment holds, just return.
|
||||
if self.assignments.contains(&assignment.negated()) {
|
||||
tracing::trace!(
|
||||
target: "ty_python_semantic::types::constraints::PathAssignment",
|
||||
assignment = %assignment.display(db),
|
||||
facts = %format_args!(
|
||||
"[{}]",
|
||||
self.assignments.iter().map(|assignment| assignment.display(db)).format(", "),
|
||||
),
|
||||
"found contradiction",
|
||||
);
|
||||
return Err(PathAssignmentConflict);
|
||||
}
|
||||
if !self.assignments.insert(assignment) {
|
||||
@@ -2852,6 +2984,15 @@ impl<'db> PathAssignments<'db> {
|
||||
if self.assignment_holds(ante.when_false()) {
|
||||
// The sequent map says (ante1) is always true, and the current path asserts that
|
||||
// it's false.
|
||||
tracing::trace!(
|
||||
target: "ty_python_semantic::types::constraints::PathAssignment",
|
||||
ante = %ante.display(db),
|
||||
facts = %format_args!(
|
||||
"[{}]",
|
||||
self.assignments.iter().map(|assignment| assignment.display(db)).format(", "),
|
||||
),
|
||||
"found contradiction",
|
||||
);
|
||||
return Err(PathAssignmentConflict);
|
||||
}
|
||||
}
|
||||
@@ -2861,6 +3002,16 @@ impl<'db> PathAssignments<'db> {
|
||||
{
|
||||
// The sequent map says (ante1 ∧ ante2) is an impossible combination, and the
|
||||
// current path asserts that both are true.
|
||||
tracing::trace!(
|
||||
target: "ty_python_semantic::types::constraints::PathAssignment",
|
||||
ante1 = %ante1.display(db),
|
||||
ante2 = %ante2.display(db),
|
||||
facts = %format_args!(
|
||||
"[{}]",
|
||||
self.assignments.iter().map(|assignment| assignment.display(db)).format(", "),
|
||||
),
|
||||
"found contradiction",
|
||||
);
|
||||
return Err(PathAssignmentConflict);
|
||||
}
|
||||
}
|
||||
@@ -2870,7 +3021,7 @@ impl<'db> PathAssignments<'db> {
|
||||
if self.assignment_holds(ante1.when_true())
|
||||
&& self.assignment_holds(ante2.when_true())
|
||||
{
|
||||
self.add_assignment(map, post.when_true())?;
|
||||
self.add_assignment(db, map, post.when_true())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2878,7 +3029,7 @@ impl<'db> PathAssignments<'db> {
|
||||
for (ante, posts) in &map.single_implications {
|
||||
for post in posts {
|
||||
if self.assignment_holds(ante.when_true()) {
|
||||
self.add_assignment(map, post.when_true())?;
|
||||
self.add_assignment(db, map, post.when_true())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2967,10 +3118,13 @@ impl<'db> SatisfiedClause<'db> {
|
||||
}
|
||||
|
||||
fn display(&self, db: &'db dyn Db) -> String {
|
||||
if self.constraints.is_empty() {
|
||||
return String::from("always");
|
||||
}
|
||||
|
||||
// This is a bit heavy-handed, but we need to output the constraints in a consistent order
|
||||
// even though Salsa IDs are assigned non-deterministically. This Display output is only
|
||||
// used in test cases, so we don't need to over-optimize it.
|
||||
|
||||
let mut constraints: Vec<_> = self
|
||||
.constraints
|
||||
.iter()
|
||||
@@ -3097,7 +3251,7 @@ impl<'db> SatisfiedClauses<'db> {
|
||||
// used in test cases, so we don't need to over-optimize it.
|
||||
|
||||
if self.clauses.is_empty() {
|
||||
return String::from("always");
|
||||
return String::from("never");
|
||||
}
|
||||
let mut clauses: Vec<_> = self
|
||||
.clauses
|
||||
@@ -3202,8 +3356,20 @@ impl<'db> GenericContext<'db> {
|
||||
db: &'db dyn Db,
|
||||
constraints: ConstraintSet<'db>,
|
||||
) -> Result<Specialization<'db>, ()> {
|
||||
tracing::debug!(
|
||||
target: "ty_python_semantic::types::constraints::specialize_constrained",
|
||||
generic_context = %self.display_full(db),
|
||||
constraints = %constraints.node.display(db),
|
||||
"create specialization for constraint set",
|
||||
);
|
||||
|
||||
// If the constraint set is cyclic, don't even try to construct a specialization.
|
||||
if constraints.is_cyclic(db) {
|
||||
tracing::error!(
|
||||
target: "ty_python_semantic::types::constraints::specialize_constrained",
|
||||
constraints = %constraints.node.display(db),
|
||||
"constraint set is cyclic",
|
||||
);
|
||||
// TODO: Better error
|
||||
return Err(());
|
||||
}
|
||||
@@ -3216,6 +3382,11 @@ impl<'db> GenericContext<'db> {
|
||||
.fold(constraints.node, |constraints, bound_typevar| {
|
||||
constraints.and(db, bound_typevar.valid_specializations(db))
|
||||
});
|
||||
tracing::debug!(
|
||||
target: "ty_python_semantic::types::constraints::specialize_constrained",
|
||||
valid = %abstracted.display(db),
|
||||
"limited to valid specializations",
|
||||
);
|
||||
|
||||
// Then we find all of the "representative types" for each typevar in the constraint set.
|
||||
let mut error_occurred = false;
|
||||
@@ -3234,10 +3405,24 @@ impl<'db> GenericContext<'db> {
|
||||
let mut unconstrained = false;
|
||||
let mut greatest_lower_bound = Type::Never;
|
||||
let mut least_upper_bound = Type::object();
|
||||
abstracted.find_representative_types(db, bound_typevar.identity(db), |bounds| {
|
||||
let identity = bound_typevar.identity(db);
|
||||
tracing::trace!(
|
||||
target: "ty_python_semantic::types::constraints::specialize_constrained",
|
||||
bound_typevar = %identity.display(db),
|
||||
abstracted = %abstracted.retain_one(db, identity).display(db),
|
||||
"find specialization for typevar",
|
||||
);
|
||||
abstracted.find_representative_types(db, identity, |bounds| {
|
||||
satisfied = true;
|
||||
match bounds {
|
||||
Some((lower_bound, upper_bound)) => {
|
||||
tracing::trace!(
|
||||
target: "ty_python_semantic::types::constraints::specialize_constrained",
|
||||
bound_typevar = %identity.display(db),
|
||||
lower_bound = %lower_bound.display(db),
|
||||
upper_bound = %upper_bound.display(db),
|
||||
"found representative type",
|
||||
);
|
||||
greatest_lower_bound =
|
||||
UnionType::from_elements(db, [greatest_lower_bound, lower_bound]);
|
||||
least_upper_bound =
|
||||
@@ -3253,6 +3438,11 @@ impl<'db> GenericContext<'db> {
|
||||
// for this constraint set.
|
||||
if !satisfied {
|
||||
// TODO: Construct a useful error here
|
||||
tracing::debug!(
|
||||
target: "ty_python_semantic::types::constraints::specialize_constrained",
|
||||
bound_typevar = %identity.display(db),
|
||||
"typevar cannot be satisfied",
|
||||
);
|
||||
error_occurred = true;
|
||||
return None;
|
||||
}
|
||||
@@ -3260,18 +3450,36 @@ impl<'db> GenericContext<'db> {
|
||||
// The BDD is satisfiable, but the typevar is unconstrained, then we use `None` to tell
|
||||
// specialize_recursive to fall back on the typevar's default.
|
||||
if unconstrained {
|
||||
tracing::debug!(
|
||||
target: "ty_python_semantic::types::constraints::specialize_constrained",
|
||||
bound_typevar = %identity.display(db),
|
||||
"typevar is unconstrained",
|
||||
);
|
||||
return None;
|
||||
}
|
||||
|
||||
// If `lower ≰ upper`, then there is no type that satisfies all of the paths in the
|
||||
// BDD. That's an ambiguous specialization, as described above.
|
||||
if !greatest_lower_bound.is_subtype_of(db, least_upper_bound) {
|
||||
tracing::debug!(
|
||||
target: "ty_python_semantic::types::constraints::specialize_constrained",
|
||||
bound_typevar = %identity.display(db),
|
||||
greatest_lower_bound = %greatest_lower_bound.display(db),
|
||||
least_upper_bound = %least_upper_bound.display(db),
|
||||
"typevar bounds are incompatible",
|
||||
);
|
||||
error_occurred = true;
|
||||
return None;
|
||||
}
|
||||
|
||||
// Of all of the types that satisfy all of the paths in the BDD, we choose the
|
||||
// "largest" one (i.e., "closest to `object`") as the specialization.
|
||||
tracing::debug!(
|
||||
target: "ty_python_semantic::types::constraints::specialize_constrained",
|
||||
bound_typevar = %identity.display(db),
|
||||
specialization = %least_upper_bound.display(db),
|
||||
"found specialization for typevar",
|
||||
);
|
||||
Some(least_upper_bound)
|
||||
});
|
||||
|
||||
@@ -3297,18 +3505,32 @@ mod tests {
|
||||
fn test_display_graph_output() {
|
||||
let expected = indoc! {r#"
|
||||
(T = str)
|
||||
┡━₁ (U = str)
|
||||
│ ┡━₁ always
|
||||
│ └─₀ (U = bool)
|
||||
│ ┡━₁ always
|
||||
│ └─₀ never
|
||||
┡━₁ (T = bool)
|
||||
│ ┡━₁ (U = str)
|
||||
│ │ ┡━₁ (U = bool)
|
||||
│ │ │ ┡━₁ always
|
||||
│ │ │ └─₀ always
|
||||
│ │ └─₀ (U = bool)
|
||||
│ │ ┡━₁ always
|
||||
│ │ └─₀ never
|
||||
│ └─₀ (U = str)
|
||||
│ ┡━₁ (U = bool)
|
||||
│ │ ┡━₁ always
|
||||
│ │ └─₀ always
|
||||
│ └─₀ (U = bool)
|
||||
│ ┡━₁ always
|
||||
│ └─₀ never
|
||||
└─₀ (T = bool)
|
||||
┡━₁ (U = str)
|
||||
│ ┡━₁ always
|
||||
│ ┡━₁ (U = bool)
|
||||
│ │ ┡━₁ always
|
||||
│ │ └─₀ always
|
||||
│ └─₀ (U = bool)
|
||||
│ ┡━₁ always
|
||||
│ └─₀ never
|
||||
└─₀ never
|
||||
└─₀ (U = str)
|
||||
┡━₁ never
|
||||
└─₀ never
|
||||
"#}
|
||||
.trim_end();
|
||||
|
||||
|
||||
@@ -37,13 +37,11 @@ use itertools::Itertools;
|
||||
use ruff_db::{
|
||||
diagnostic::{Annotation, Diagnostic, Span, SubDiagnostic, SubDiagnosticSeverity},
|
||||
parsed::parsed_module,
|
||||
source::source_text,
|
||||
};
|
||||
use ruff_diagnostics::{Edit, Fix};
|
||||
use ruff_python_ast::name::Name;
|
||||
use ruff_python_ast::parenthesize::parentheses_iterator;
|
||||
use ruff_python_ast::token::parentheses_iterator;
|
||||
use ruff_python_ast::{self as ast, AnyNodeRef, StringFlags};
|
||||
use ruff_python_trivia::CommentRanges;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
use rustc_hash::FxHashSet;
|
||||
use std::fmt::{self, Formatter};
|
||||
@@ -2423,9 +2421,7 @@ pub(super) fn report_invalid_assignment<'db>(
|
||||
// ) # ty: ignore <- or here
|
||||
// ```
|
||||
|
||||
let comment_ranges = CommentRanges::from(context.module().tokens());
|
||||
let source = source_text(context.db(), context.file());
|
||||
parentheses_iterator(value_node.into(), None, &comment_ranges, &source)
|
||||
parentheses_iterator(value_node.into(), None, context.module().tokens())
|
||||
.last()
|
||||
.unwrap_or(value_node.range())
|
||||
} else {
|
||||
|
||||
@@ -77,7 +77,7 @@ pub(crate) fn enum_metadata<'db>(
|
||||
|
||||
let ignored_names: Option<Vec<&str>> = if let Some(ignore) = table.symbol_id("_ignore_") {
|
||||
let ignore_bindings = use_def_map.all_reachable_symbol_bindings(ignore);
|
||||
let ignore_place = place_from_bindings(db, ignore_bindings);
|
||||
let ignore_place = place_from_bindings(db, ignore_bindings).place;
|
||||
|
||||
match ignore_place {
|
||||
Place::Defined(Type::StringLiteral(ignored_names), _, _) => {
|
||||
@@ -111,7 +111,7 @@ pub(crate) fn enum_metadata<'db>(
|
||||
return None;
|
||||
}
|
||||
|
||||
let inferred = place_from_bindings(db, bindings);
|
||||
let inferred = place_from_bindings(db, bindings).place;
|
||||
|
||||
let value_ty = match inferred {
|
||||
Place::Undefined => {
|
||||
|
||||
@@ -373,7 +373,7 @@ impl<'db> OverloadLiteral<'db> {
|
||||
.scoped_use_id(db, scope);
|
||||
|
||||
let Place::Defined(Type::FunctionLiteral(previous_type), _, Definedness::AlwaysDefined) =
|
||||
place_from_bindings(db, use_def.bindings_at_use(use_id))
|
||||
place_from_bindings(db, use_def.bindings_at_use(use_id)).place
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
|
||||
@@ -634,7 +634,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
&self.context,
|
||||
class,
|
||||
&field_name,
|
||||
field.single_declaration,
|
||||
field.first_declaration,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -645,13 +645,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
}
|
||||
) {
|
||||
field_with_default_encountered =
|
||||
Some((field_name, field.single_declaration));
|
||||
Some((field_name, field.first_declaration));
|
||||
} else if let Some(field_with_default) = field_with_default_encountered.as_ref()
|
||||
{
|
||||
report_namedtuple_field_without_default_after_field_with_default(
|
||||
&self.context,
|
||||
class,
|
||||
(&field_name, field.single_declaration),
|
||||
(&field_name, field.first_declaration),
|
||||
field_with_default,
|
||||
);
|
||||
}
|
||||
@@ -1034,6 +1034,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
self.db(),
|
||||
use_def.end_of_scope_symbol_bindings(place.as_symbol().unwrap()),
|
||||
)
|
||||
.place
|
||||
{
|
||||
if function.file(self.db()) != self.file() {
|
||||
// If the function is not in this file, we don't need to check it.
|
||||
@@ -1727,6 +1728,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
let prior_bindings = use_def.bindings_at_definition(declaration);
|
||||
// unbound_ty is Never because for this check we don't care about unbound
|
||||
let inferred_ty = place_from_bindings(self.db(), prior_bindings)
|
||||
.place
|
||||
.with_qualifiers(TypeQualifiers::empty())
|
||||
.or_fall_back_to(self.db(), || {
|
||||
// Fallback to bindings declared on `types.ModuleType` if it's a global symbol
|
||||
@@ -8673,7 +8675,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
// If we're inferring types of deferred expressions, look them up from end-of-scope.
|
||||
if self.is_deferred() {
|
||||
let place = if let Some(place_id) = place_table.place_id(expr) {
|
||||
place_from_bindings(db, use_def.all_reachable_bindings(place_id))
|
||||
place_from_bindings(db, use_def.all_reachable_bindings(place_id)).place
|
||||
} else {
|
||||
assert!(
|
||||
self.deferred_state.in_string_annotation(),
|
||||
@@ -8691,7 +8693,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
}
|
||||
|
||||
let use_id = expr_ref.scoped_use_id(db, scope);
|
||||
let place = place_from_bindings(db, use_def.bindings_at_use(use_id));
|
||||
let place = place_from_bindings(db, use_def.bindings_at_use(use_id)).place;
|
||||
|
||||
(place, Some(use_id))
|
||||
}
|
||||
@@ -8832,7 +8834,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
}
|
||||
}
|
||||
EnclosingSnapshotResult::FoundBindings(bindings) => {
|
||||
let place = place_from_bindings(db, bindings).map_type(|ty| {
|
||||
let place = place_from_bindings(db, bindings).place.map_type(|ty| {
|
||||
self.narrow_place_with_applicable_constraints(
|
||||
place_expr,
|
||||
ty,
|
||||
@@ -8952,13 +8954,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
return Place::Undefined.into();
|
||||
}
|
||||
EnclosingSnapshotResult::FoundBindings(bindings) => {
|
||||
let place = place_from_bindings(db, bindings).map_type(|ty| {
|
||||
self.narrow_place_with_applicable_constraints(
|
||||
place_expr,
|
||||
ty,
|
||||
&constraint_keys,
|
||||
)
|
||||
});
|
||||
let place =
|
||||
place_from_bindings(db, bindings).place.map_type(|ty| {
|
||||
self.narrow_place_with_applicable_constraints(
|
||||
place_expr,
|
||||
ty,
|
||||
&constraint_keys,
|
||||
)
|
||||
});
|
||||
constraint_keys.push((
|
||||
FileScopeId::global(),
|
||||
ConstraintKey::NestedScope(file_scope_id),
|
||||
@@ -9571,6 +9574,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
(unknown @ Type::Dynamic(DynamicType::Unknown), _, _)
|
||||
| (_, unknown @ Type::Dynamic(DynamicType::Unknown), _) => Some(unknown),
|
||||
|
||||
(unknown @ Type::Dynamic(DynamicType::UnknownGeneric(_)), _, _)
|
||||
| (_, unknown @ Type::Dynamic(DynamicType::UnknownGeneric(_)), _) => Some(unknown),
|
||||
|
||||
(
|
||||
todo @ Type::Dynamic(
|
||||
DynamicType::Todo(_)
|
||||
@@ -10969,6 +10975,19 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
);
|
||||
}
|
||||
}
|
||||
Type::KnownInstance(KnownInstanceType::TypeAliasType(TypeAliasType::ManualPEP695(
|
||||
_,
|
||||
))) => {
|
||||
let slice_ty = self.infer_expression(slice, TypeContext::default());
|
||||
let mut variables = FxOrderSet::default();
|
||||
slice_ty.bind_and_find_all_legacy_typevars(
|
||||
self.db(),
|
||||
self.typevar_binding_context,
|
||||
&mut variables,
|
||||
);
|
||||
let generic_context = GenericContext::from_typevar_instances(self.db(), variables);
|
||||
return Type::Dynamic(DynamicType::UnknownGeneric(generic_context));
|
||||
}
|
||||
Type::KnownInstance(KnownInstanceType::TypeAliasType(type_alias)) => {
|
||||
if let Some(generic_context) = type_alias.generic_context(self.db()) {
|
||||
return self.infer_explicit_type_alias_type_specialization(
|
||||
@@ -11107,33 +11126,74 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
));
|
||||
}
|
||||
Type::SpecialForm(SpecialFormType::Callable) => {
|
||||
// TODO: Remove this once we support ParamSpec properly. This is necessary to avoid
|
||||
// a lot of false positives downstream, because we can't represent the specialized
|
||||
// `Callable[P, _]` type yet.
|
||||
if let Some(first_arg) = subscript
|
||||
.slice
|
||||
.as_ref()
|
||||
.as_tuple_expr()
|
||||
.and_then(|args| args.elts.first())
|
||||
&& first_arg.is_name_expr()
|
||||
{
|
||||
let first_arg_ty = self.infer_expression(first_arg, TypeContext::default());
|
||||
let arguments = if let ast::Expr::Tuple(tuple) = &*subscript.slice {
|
||||
&*tuple.elts
|
||||
} else {
|
||||
std::slice::from_ref(&*subscript.slice)
|
||||
};
|
||||
|
||||
if let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) = first_arg_ty
|
||||
&& typevar.kind(self.db()).is_paramspec()
|
||||
{
|
||||
return todo_type!("Callable[..] specialized with ParamSpec");
|
||||
}
|
||||
// TODO: Remove this once we support ParamSpec and Concatenate properly. This is necessary
|
||||
// to avoid a lot of false positives downstream, because we can't represent the typevar-
|
||||
// specialized `Callable` types yet.
|
||||
let num_arguments = arguments.len();
|
||||
if num_arguments == 2 {
|
||||
let first_arg = &arguments[0];
|
||||
let second_arg = &arguments[1];
|
||||
|
||||
if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) {
|
||||
builder.into_diagnostic(format_args!(
|
||||
"The first argument to `Callable` must be either a list of types, \
|
||||
ParamSpec, Concatenate, or `...`",
|
||||
if first_arg.is_name_expr() {
|
||||
let first_arg_ty = self.infer_expression(first_arg, TypeContext::default());
|
||||
if let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) =
|
||||
first_arg_ty
|
||||
&& typevar.kind(self.db()).is_paramspec()
|
||||
{
|
||||
return todo_type!("Callable[..] specialized with ParamSpec");
|
||||
}
|
||||
|
||||
if let Some(builder) =
|
||||
self.context.report_lint(&INVALID_TYPE_FORM, subscript)
|
||||
{
|
||||
builder.into_diagnostic(format_args!(
|
||||
"The first argument to `Callable` must be either a list of types, \
|
||||
ParamSpec, Concatenate, or `...`",
|
||||
));
|
||||
}
|
||||
return Type::KnownInstance(KnownInstanceType::Callable(
|
||||
CallableType::unknown(self.db()),
|
||||
));
|
||||
} else if first_arg.is_subscript_expr() {
|
||||
let first_arg_ty = self.infer_expression(first_arg, TypeContext::default());
|
||||
if let Type::Dynamic(DynamicType::UnknownGeneric(generic_context)) =
|
||||
first_arg_ty
|
||||
{
|
||||
let mut variables = generic_context
|
||||
.variables(self.db())
|
||||
.collect::<FxOrderSet<_>>();
|
||||
|
||||
let return_ty =
|
||||
self.infer_expression(second_arg, TypeContext::default());
|
||||
return_ty.bind_and_find_all_legacy_typevars(
|
||||
self.db(),
|
||||
self.typevar_binding_context,
|
||||
&mut variables,
|
||||
);
|
||||
|
||||
let generic_context =
|
||||
GenericContext::from_typevar_instances(self.db(), variables);
|
||||
return Type::Dynamic(DynamicType::UnknownGeneric(generic_context));
|
||||
}
|
||||
|
||||
if let Some(builder) =
|
||||
self.context.report_lint(&INVALID_TYPE_FORM, subscript)
|
||||
{
|
||||
builder.into_diagnostic(format_args!(
|
||||
"The first argument to `Callable` must be either a list of types, \
|
||||
ParamSpec, Concatenate, or `...`",
|
||||
));
|
||||
}
|
||||
return Type::KnownInstance(KnownInstanceType::Callable(
|
||||
CallableType::unknown(self.db()),
|
||||
));
|
||||
}
|
||||
return Type::KnownInstance(KnownInstanceType::Callable(
|
||||
CallableType::unknown(self.db()),
|
||||
));
|
||||
}
|
||||
|
||||
let callable = self
|
||||
@@ -11240,6 +11300,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
) => {
|
||||
return self.infer_explicit_type_alias_specialization(subscript, value_ty, false);
|
||||
}
|
||||
Type::Dynamic(DynamicType::Unknown) => {
|
||||
let slice_ty = self.infer_expression(slice, TypeContext::default());
|
||||
let mut variables = FxOrderSet::default();
|
||||
slice_ty.bind_and_find_all_legacy_typevars(
|
||||
self.db(),
|
||||
self.typevar_binding_context,
|
||||
&mut variables,
|
||||
);
|
||||
let generic_context = GenericContext::from_typevar_instances(self.db(), variables);
|
||||
return Type::Dynamic(DynamicType::UnknownGeneric(generic_context));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -11696,6 +11767,18 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
Some(Type::Dynamic(DynamicType::TodoUnpack))
|
||||
}
|
||||
|
||||
(Type::SpecialForm(SpecialFormType::Concatenate), _) => {
|
||||
// TODO: Add proper support for `Concatenate`
|
||||
let mut variables = FxOrderSet::default();
|
||||
slice_ty.bind_and_find_all_legacy_typevars(
|
||||
db,
|
||||
self.typevar_binding_context,
|
||||
&mut variables,
|
||||
);
|
||||
let generic_context = GenericContext::from_typevar_instances(self.db(), variables);
|
||||
Some(Type::Dynamic(DynamicType::UnknownGeneric(generic_context)))
|
||||
}
|
||||
|
||||
(Type::SpecialForm(special_form), _) if special_form.class().is_special_form() => {
|
||||
Some(todo_type!("Inference of subscript on special form"))
|
||||
}
|
||||
|
||||
@@ -144,18 +144,19 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||
)
|
||||
}
|
||||
_ => TypeAndQualifiers::declared(
|
||||
ty.in_type_expression(
|
||||
builder.db(),
|
||||
builder.scope(),
|
||||
builder.typevar_binding_context,
|
||||
)
|
||||
.unwrap_or_else(|error| {
|
||||
error.into_fallback_type(
|
||||
&builder.context,
|
||||
annotation,
|
||||
builder.is_reachable(annotation),
|
||||
ty.default_specialize(builder.db())
|
||||
.in_type_expression(
|
||||
builder.db(),
|
||||
builder.scope(),
|
||||
builder.typevar_binding_context,
|
||||
)
|
||||
}),
|
||||
.unwrap_or_else(|error| {
|
||||
error.into_fallback_type(
|
||||
&builder.context,
|
||||
annotation,
|
||||
builder.is_reachable(annotation),
|
||||
)
|
||||
}),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||
ast::Expr::Name(name) => match name.ctx {
|
||||
ast::ExprContext::Load => self
|
||||
.infer_name_expression(name)
|
||||
.default_specialize(self.db())
|
||||
.in_type_expression(self.db(), self.scope(), self.typevar_binding_context)
|
||||
.unwrap_or_else(|error| {
|
||||
error.into_fallback_type(
|
||||
@@ -108,6 +109,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||
ast::Expr::Attribute(attribute_expression) => match attribute_expression.ctx {
|
||||
ast::ExprContext::Load => self
|
||||
.infer_attribute_expression(attribute_expression)
|
||||
.default_specialize(self.db())
|
||||
.in_type_expression(self.db(), self.scope(), self.typevar_binding_context)
|
||||
.unwrap_or_else(|error| {
|
||||
error.into_fallback_type(
|
||||
@@ -944,8 +946,17 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||
}
|
||||
}
|
||||
KnownInstanceType::TypeAliasType(TypeAliasType::ManualPEP695(_)) => {
|
||||
self.infer_type_expression(slice);
|
||||
todo_type!("Generic manual PEP-695 type alias")
|
||||
// TODO: support generic "manual" PEP 695 type aliases
|
||||
let slice_ty = self.infer_expression(slice, TypeContext::default());
|
||||
let mut variables = FxOrderSet::default();
|
||||
slice_ty.bind_and_find_all_legacy_typevars(
|
||||
self.db(),
|
||||
self.typevar_binding_context,
|
||||
&mut variables,
|
||||
);
|
||||
let generic_context =
|
||||
GenericContext::from_typevar_instances(self.db(), variables);
|
||||
Type::Dynamic(DynamicType::UnknownGeneric(generic_context))
|
||||
}
|
||||
KnownInstanceType::LiteralStringAlias(_) => {
|
||||
self.infer_type_expression(slice);
|
||||
@@ -982,6 +993,9 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||
Type::unknown()
|
||||
}
|
||||
},
|
||||
Type::Dynamic(DynamicType::UnknownGeneric(_)) => {
|
||||
self.infer_explicit_type_alias_specialization(subscript, value_ty, true)
|
||||
}
|
||||
Type::Dynamic(_) => {
|
||||
// Infer slice as a value expression to avoid false-positive
|
||||
// `invalid-type-form` diagnostics, when we have e.g.
|
||||
|
||||
@@ -12,7 +12,9 @@ use rustc_hash::FxHashSet;
|
||||
|
||||
use crate::{
|
||||
Db, NameKind,
|
||||
place::{Place, imported_symbol, place_from_bindings, place_from_declarations},
|
||||
place::{
|
||||
Place, PlaceWithDefinition, imported_symbol, place_from_bindings, place_from_declarations,
|
||||
},
|
||||
semantic_index::{
|
||||
attribute_scopes, definition::Definition, global_scope, place_table, scope::ScopeId,
|
||||
semantic_index, use_def_map,
|
||||
@@ -35,48 +37,40 @@ pub(crate) fn all_members_of_scope<'db>(
|
||||
.all_end_of_scope_symbol_declarations()
|
||||
.filter_map(move |(symbol_id, declarations)| {
|
||||
let place_result = place_from_declarations(db, declarations);
|
||||
let definition = place_result.single_declaration;
|
||||
place_result
|
||||
let first_reachable_definition = place_result.first_declaration?;
|
||||
let ty = place_result
|
||||
.ignore_conflicting_declarations()
|
||||
.place
|
||||
.ignore_possibly_undefined()
|
||||
.map(|ty| {
|
||||
let symbol = table.symbol(symbol_id);
|
||||
let member = Member {
|
||||
name: symbol.name().clone(),
|
||||
ty,
|
||||
};
|
||||
MemberWithDefinition { member, definition }
|
||||
})
|
||||
.ignore_possibly_undefined()?;
|
||||
let symbol = table.symbol(symbol_id);
|
||||
let member = Member {
|
||||
name: symbol.name().clone(),
|
||||
ty,
|
||||
};
|
||||
Some(MemberWithDefinition {
|
||||
member,
|
||||
first_reachable_definition,
|
||||
})
|
||||
})
|
||||
.chain(use_def_map.all_end_of_scope_symbol_bindings().filter_map(
|
||||
move |(symbol_id, bindings)| {
|
||||
// It's not clear to AG how to using a bindings
|
||||
// iterator here to get the correct definition for
|
||||
// this binding. Below, we look through all bindings
|
||||
// with a definition and only take one if there is
|
||||
// exactly one. I don't think this can be wrong, but
|
||||
// it's probably omitting definitions in some cases.
|
||||
let mut definition = None;
|
||||
for binding in bindings.clone() {
|
||||
if let Some(def) = binding.binding.definition() {
|
||||
if definition.is_some() {
|
||||
definition = None;
|
||||
break;
|
||||
}
|
||||
definition = Some(def);
|
||||
}
|
||||
}
|
||||
place_from_bindings(db, bindings)
|
||||
.ignore_possibly_undefined()
|
||||
.map(|ty| {
|
||||
let symbol = table.symbol(symbol_id);
|
||||
let member = Member {
|
||||
name: symbol.name().clone(),
|
||||
ty,
|
||||
};
|
||||
MemberWithDefinition { member, definition }
|
||||
})
|
||||
let PlaceWithDefinition {
|
||||
place,
|
||||
first_definition,
|
||||
} = place_from_bindings(db, bindings);
|
||||
|
||||
let first_reachable_definition = first_definition?;
|
||||
let ty = place.ignore_possibly_undefined()?;
|
||||
|
||||
let symbol = table.symbol(symbol_id);
|
||||
let member = Member {
|
||||
name: symbol.name().clone(),
|
||||
ty,
|
||||
};
|
||||
Some(MemberWithDefinition {
|
||||
member,
|
||||
first_reachable_definition,
|
||||
})
|
||||
},
|
||||
))
|
||||
}
|
||||
@@ -457,11 +451,11 @@ impl<'db> AllMembers<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A member of a type or scope, with an optional definition.
|
||||
/// A member of a type or scope, with the first reachable definition of that member.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
||||
pub struct MemberWithDefinition<'db> {
|
||||
pub member: Member<'db>,
|
||||
pub definition: Option<Definition<'db>>,
|
||||
pub first_reachable_definition: Definition<'db>,
|
||||
}
|
||||
|
||||
/// A member of a type or scope.
|
||||
|
||||
@@ -75,7 +75,7 @@ pub(super) fn class_member<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str
|
||||
// Otherwise, we need to check if the symbol has bindings
|
||||
let use_def = use_def_map(db, scope);
|
||||
let bindings = use_def.end_of_scope_symbol_bindings(symbol_id);
|
||||
let inferred = place_from_bindings(db, bindings);
|
||||
let inferred = place_from_bindings(db, bindings).place;
|
||||
|
||||
// TODO: we should not need to calculate inferred type second time. This is a temporary
|
||||
// solution until the notion of Boundness and Declaredness is split. See #16036, #16264
|
||||
|
||||
@@ -12,8 +12,8 @@ use crate::{
|
||||
lint::LintId,
|
||||
place::Place,
|
||||
semantic_index::{
|
||||
definition::DefinitionKind, place_table, scope::ScopeId, symbol::ScopedSymbolId,
|
||||
use_def_map,
|
||||
definition::DefinitionKind, place::ScopedPlaceId, place_table, scope::ScopeId,
|
||||
symbol::ScopedSymbolId, use_def_map,
|
||||
},
|
||||
types::{
|
||||
ClassBase, ClassLiteral, ClassType, KnownClass, Type,
|
||||
@@ -25,7 +25,7 @@ use crate::{
|
||||
report_overridden_final_method,
|
||||
},
|
||||
function::{FunctionDecorators, FunctionType, KnownFunction},
|
||||
list_members::{MemberWithDefinition, all_members_of_scope},
|
||||
list_members::{Member, MemberWithDefinition, all_members_of_scope},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -53,10 +53,11 @@ pub(super) fn check_class<'db>(context: &InferContext<'db, '_>, class: ClassLite
|
||||
}
|
||||
|
||||
let class_specialized = class.identity_specialization(db);
|
||||
let own_class_members: FxHashSet<_> = all_members_of_scope(db, class.body_scope(db)).collect();
|
||||
let scope = class.body_scope(db);
|
||||
let own_class_members: FxHashSet<_> = all_members_of_scope(db, scope).collect();
|
||||
|
||||
for member in own_class_members {
|
||||
check_class_declaration(context, configuration, class_specialized, &member);
|
||||
check_class_declaration(context, configuration, class_specialized, scope, &member);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +65,7 @@ fn check_class_declaration<'db>(
|
||||
context: &InferContext<'db, '_>,
|
||||
configuration: OverrideRulesConfig,
|
||||
class: ClassType<'db>,
|
||||
class_scope: ScopeId<'db>,
|
||||
member: &MemberWithDefinition<'db>,
|
||||
) {
|
||||
/// Salsa-tracked query to check whether any of the definitions of a symbol
|
||||
@@ -101,40 +103,12 @@ fn check_class_declaration<'db>(
|
||||
.any(|definition| definition.kind(db).is_function_def())
|
||||
}
|
||||
|
||||
fn extract_underlying_functions<'db>(
|
||||
db: &'db dyn Db,
|
||||
ty: Type<'db>,
|
||||
) -> Option<smallvec::SmallVec<[FunctionType<'db>; 1]>> {
|
||||
match ty {
|
||||
Type::FunctionLiteral(function) => Some(smallvec::smallvec_inline![function]),
|
||||
Type::BoundMethod(method) => Some(smallvec::smallvec_inline![method.function(db)]),
|
||||
Type::PropertyInstance(property) => {
|
||||
extract_underlying_functions(db, property.getter(db)?)
|
||||
}
|
||||
Type::Union(union) => {
|
||||
let mut functions = smallvec::smallvec![];
|
||||
for member in union.elements(db) {
|
||||
if let Some(mut member_functions) = extract_underlying_functions(db, *member) {
|
||||
functions.append(&mut member_functions);
|
||||
}
|
||||
}
|
||||
if functions.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(functions)
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
let db = context.db();
|
||||
|
||||
let MemberWithDefinition { member, definition } = member;
|
||||
|
||||
let Some(definition) = definition else {
|
||||
return;
|
||||
};
|
||||
let MemberWithDefinition {
|
||||
member,
|
||||
first_reachable_definition,
|
||||
} = member;
|
||||
|
||||
let Place::Defined(type_on_subclass_instance, _, _) =
|
||||
Type::instance(db, class).member(db, &member.name).place
|
||||
@@ -153,10 +127,14 @@ fn check_class_declaration<'db>(
|
||||
if class_kind == Some(CodeGeneratorKind::NamedTuple)
|
||||
&& configuration.check_prohibited_named_tuple_attrs()
|
||||
&& PROHIBITED_NAMEDTUPLE_ATTRS.contains(&member.name.as_str())
|
||||
&& !matches!(definition.kind(db), DefinitionKind::AnnotatedAssignment(_))
|
||||
&& let Some(symbol_id) = place_table(db, class_scope).symbol_id(&member.name)
|
||||
&& let Some(bad_definition) = use_def_map(db, class_scope)
|
||||
.all_reachable_bindings(ScopedPlaceId::Symbol(symbol_id))
|
||||
.filter_map(|binding| binding.binding.definition())
|
||||
.find(|def| !matches!(def.kind(db), DefinitionKind::AnnotatedAssignment(_)))
|
||||
&& let Some(builder) = context.report_lint(
|
||||
&INVALID_NAMED_TUPLE,
|
||||
definition.focus_range(db, context.module()),
|
||||
bad_definition.focus_range(db, context.module()),
|
||||
)
|
||||
{
|
||||
let mut diagnostic = builder.into_diagnostic(format_args!(
|
||||
@@ -212,8 +190,6 @@ fn check_class_declaration<'db>(
|
||||
.unwrap_or_default();
|
||||
}
|
||||
|
||||
subclass_overrides_superclass_declaration = true;
|
||||
|
||||
let Place::Defined(superclass_type, _, _) = Type::instance(db, superclass)
|
||||
.member(db, &member.name)
|
||||
.place
|
||||
@@ -222,6 +198,8 @@ fn check_class_declaration<'db>(
|
||||
break;
|
||||
};
|
||||
|
||||
subclass_overrides_superclass_declaration = true;
|
||||
|
||||
if configuration.check_final_method_overridden() {
|
||||
overridden_final_method = overridden_final_method.or_else(|| {
|
||||
let superclass_symbol_id = superclass_symbol_id?;
|
||||
@@ -297,7 +275,7 @@ fn check_class_declaration<'db>(
|
||||
context,
|
||||
&member.name,
|
||||
class,
|
||||
*definition,
|
||||
*first_reachable_definition,
|
||||
subclass_function,
|
||||
superclass,
|
||||
superclass_type,
|
||||
@@ -331,42 +309,18 @@ fn check_class_declaration<'db>(
|
||||
|
||||
if !subclass_overrides_superclass_declaration
|
||||
&& !has_dynamic_superclass
|
||||
&& definition.kind(db).is_function_def()
|
||||
&& let Type::FunctionLiteral(function) = member.ty
|
||||
&& function.has_known_decorator(db, FunctionDecorators::OVERRIDE)
|
||||
// accessing `.kind()` here is fine as `definition`
|
||||
// will always be a definition in the file currently being checked
|
||||
&& first_reachable_definition.kind(db).is_function_def()
|
||||
{
|
||||
let function_literal = if context.in_stub() {
|
||||
function.first_overload_or_implementation(db)
|
||||
} else {
|
||||
function.literal(db).last_definition(db)
|
||||
};
|
||||
|
||||
if let Some(builder) = context.report_lint(
|
||||
&INVALID_EXPLICIT_OVERRIDE,
|
||||
function_literal.focus_range(db, context.module()),
|
||||
) {
|
||||
let mut diagnostic = builder.into_diagnostic(format_args!(
|
||||
"Method `{}` is decorated with `@override` but does not override anything",
|
||||
member.name
|
||||
));
|
||||
if let Some(decorator_span) =
|
||||
function_literal.find_known_decorator_span(db, KnownFunction::Override)
|
||||
{
|
||||
diagnostic.annotate(Annotation::secondary(decorator_span));
|
||||
}
|
||||
diagnostic.info(format_args!(
|
||||
"No `{member}` definitions were found on any superclasses of `{class}`",
|
||||
member = &member.name,
|
||||
class = class.name(db)
|
||||
));
|
||||
}
|
||||
check_explicit_overrides(context, member, class);
|
||||
}
|
||||
|
||||
if let Some((superclass, superclass_method)) = overridden_final_method {
|
||||
report_overridden_final_method(
|
||||
context,
|
||||
&member.name,
|
||||
*definition,
|
||||
*first_reachable_definition,
|
||||
member.ty,
|
||||
superclass,
|
||||
class,
|
||||
@@ -434,3 +388,72 @@ impl OverrideRulesConfig {
|
||||
self.contains(OverrideRulesConfig::PROHIBITED_NAMED_TUPLE_ATTR)
|
||||
}
|
||||
}
|
||||
|
||||
fn check_explicit_overrides<'db>(
|
||||
context: &InferContext<'db, '_>,
|
||||
member: &Member<'db>,
|
||||
class: ClassType<'db>,
|
||||
) {
|
||||
let db = context.db();
|
||||
let underlying_functions = extract_underlying_functions(db, member.ty);
|
||||
let Some(functions) = underlying_functions else {
|
||||
return;
|
||||
};
|
||||
let Some(decorated_function) = functions
|
||||
.iter()
|
||||
.find(|function| function.has_known_decorator(db, FunctionDecorators::OVERRIDE))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let function_literal = if context.in_stub() {
|
||||
decorated_function.first_overload_or_implementation(db)
|
||||
} else {
|
||||
decorated_function.literal(db).last_definition(db)
|
||||
};
|
||||
|
||||
let Some(builder) = context.report_lint(
|
||||
&INVALID_EXPLICIT_OVERRIDE,
|
||||
function_literal.focus_range(db, context.module()),
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
let mut diagnostic = builder.into_diagnostic(format_args!(
|
||||
"Method `{}` is decorated with `@override` but does not override anything",
|
||||
member.name
|
||||
));
|
||||
if let Some(decorator_span) =
|
||||
function_literal.find_known_decorator_span(db, KnownFunction::Override)
|
||||
{
|
||||
diagnostic.annotate(Annotation::secondary(decorator_span));
|
||||
}
|
||||
diagnostic.info(format_args!(
|
||||
"No `{member}` definitions were found on any superclasses of `{class}`",
|
||||
member = &member.name,
|
||||
class = class.name(db)
|
||||
));
|
||||
}
|
||||
|
||||
fn extract_underlying_functions<'db>(
|
||||
db: &'db dyn Db,
|
||||
ty: Type<'db>,
|
||||
) -> Option<smallvec::SmallVec<[FunctionType<'db>; 1]>> {
|
||||
match ty {
|
||||
Type::FunctionLiteral(function) => Some(smallvec::smallvec_inline![function]),
|
||||
Type::BoundMethod(method) => Some(smallvec::smallvec_inline![method.function(db)]),
|
||||
Type::PropertyInstance(property) => extract_underlying_functions(db, property.getter(db)?),
|
||||
Type::Union(union) => {
|
||||
let mut functions = smallvec::smallvec![];
|
||||
for member in union.elements(db) {
|
||||
if let Some(mut member_functions) = extract_underlying_functions(db, *member) {
|
||||
functions.append(&mut member_functions);
|
||||
}
|
||||
}
|
||||
if functions.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(functions)
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -896,7 +896,10 @@ fn cached_protocol_interface<'db>(
|
||||
// type narrowing that uses `isinstance()` or `issubclass()` with
|
||||
// runtime-checkable protocols.
|
||||
for (symbol_id, bindings) in use_def_map.all_end_of_scope_symbol_bindings() {
|
||||
let Some(ty) = place_from_bindings(db, bindings).ignore_possibly_undefined() else {
|
||||
let Some(ty) = place_from_bindings(db, bindings)
|
||||
.place
|
||||
.ignore_possibly_undefined()
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
direct_members.insert(
|
||||
|
||||
@@ -322,7 +322,7 @@ impl<'db> VarianceInferable<'db> for SubclassOfType<'db> {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)]
|
||||
pub(crate) enum SubclassOfInner<'db> {
|
||||
Class(ClassType<'db>),
|
||||
Dynamic(DynamicType),
|
||||
Dynamic(DynamicType<'db>),
|
||||
TypeVar(BoundTypeVarInstance<'db>),
|
||||
}
|
||||
|
||||
@@ -362,7 +362,7 @@ impl<'db> SubclassOfInner<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn into_dynamic(self) -> Option<DynamicType> {
|
||||
pub(crate) const fn into_dynamic(self) -> Option<DynamicType<'db>> {
|
||||
match self {
|
||||
Self::Class(_) | Self::TypeVar(_) => None,
|
||||
Self::Dynamic(dynamic) => Some(dynamic),
|
||||
@@ -465,8 +465,8 @@ impl<'db> From<ClassType<'db>> for SubclassOfInner<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DynamicType> for SubclassOfInner<'_> {
|
||||
fn from(value: DynamicType) -> Self {
|
||||
impl<'db> From<DynamicType<'db>> for SubclassOfInner<'db> {
|
||||
fn from(value: DynamicType<'db>) -> Self {
|
||||
SubclassOfInner::Dynamic(value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,6 +265,9 @@ fn dynamic_elements_ordering(left: DynamicType, right: DynamicType) -> Ordering
|
||||
(DynamicType::Unknown, _) => Ordering::Less,
|
||||
(_, DynamicType::Unknown) => Ordering::Greater,
|
||||
|
||||
(DynamicType::UnknownGeneric(_), _) => Ordering::Less,
|
||||
(_, DynamicType::UnknownGeneric(_)) => Ordering::Greater,
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
(DynamicType::Todo(TodoType(left)), DynamicType::Todo(TodoType(right))) => left.cmp(right),
|
||||
|
||||
|
||||
@@ -365,7 +365,7 @@ pub(super) fn validate_typed_dict_key_assignment<'db, 'ast>(
|
||||
};
|
||||
|
||||
let add_item_definition_subdiagnostic = |diagnostic: &mut Diagnostic, message| {
|
||||
if let Some(declaration) = item.single_declaration {
|
||||
if let Some(declaration) = item.first_declaration {
|
||||
let file = declaration.file(db);
|
||||
let module = parsed_module(db, file).load(db);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user