Compare commits

..

25 Commits

Author SHA1 Message Date
David Peter
1ab975b142 Make try_call a query 2025-02-20 20:35:57 +01:00
David Peter
92416e1f85 Make try_call_dunder_get a query 2025-02-20 19:13:27 +01:00
David Peter
5ecea4e81f Use or_fall_back_to 2025-02-20 19:13:27 +01:00
David Peter
b3a0353bf2 Use __kwdefaults__ instead of __module__ 2025-02-20 19:13:27 +01:00
David Peter
9953dede9e Attempt to model getattr_static on gradual types 2025-02-20 19:13:27 +01:00
David Peter
fed67170ec Model fallback MethodType => FunctionType 2025-02-20 19:13:27 +01:00
David Peter
cc5270ae9c Remove incorrect __class__ lookup branch in static_member 2025-02-20 19:13:27 +01:00
David Peter
c0fc2796a2 Add doc comment for try_call_dunder_get 2025-02-20 19:13:27 +01:00
David Peter
c5224316c0 Add TODO for builtins.tuple attribute lookups 2025-02-20 19:13:27 +01:00
David Peter
67087c0417 Add FunctionLiteral and BoundMethod to property tests 2025-02-20 19:13:27 +01:00
David Peter
72fe5525ab Wording 2025-02-20 19:13:27 +01:00
David Peter
ff290172d7 Add TODO: Type::member should return Result 2025-02-20 19:13:27 +01:00
David Peter
7673b7265d Return a Result from try_call_dunder_get 2025-02-20 19:13:27 +01:00
David Peter
caca1874ae Add reference to 'Functions and methods' section in the descriptor guide 2025-02-20 19:13:27 +01:00
David Peter
08f4c60660 Properly catch errors to known function calls 2025-02-20 19:13:27 +01:00
David Peter
e86c21e90a Wording and typos 2025-02-20 19:13:27 +01:00
David Peter
c84f1e0c72 Introduce CallArguments::none() 2025-02-20 19:13:27 +01:00
David Peter
d6ae12c05f Fix two typos 2025-02-20 19:13:27 +01:00
David Peter
0743c21811 Remove memoryview as a KnownClass 2025-02-20 19:13:27 +01:00
David Peter
c322baaaef Fix clippy suggestion 2025-02-20 19:13:27 +01:00
David Peter
ce3dcb066c Handle errors in __get__ calls 2025-02-20 19:13:27 +01:00
David Peter
f406835639 Use write!(…) 2025-02-20 19:13:27 +01:00
David Peter
30383d4855 Patch is_assignable_to to add partial support for SupportsIndex 2025-02-20 19:13:27 +01:00
David Peter
c7d97c3cd5 [red-knot] Method calls and descriptor protocol 2025-02-20 19:13:26 +01:00
Micha Reiser
470f852f04 [red-knot] Prevent cross-module query dependencies in own_instance_member (#16268) 2025-02-20 18:46:45 +01:00
10 changed files with 627 additions and 502 deletions

View File

@@ -346,12 +346,14 @@ impl<'db> SemanticIndexBuilder<'db> {
// SAFETY: `definition_node` is guaranteed to be a child of `self.module`
let kind = unsafe { definition_node.into_owned(self.module.clone()) };
let category = kind.category();
let is_reexported = kind.is_reexported();
let definition = Definition::new(
self.db,
self.file,
self.current_scope(),
symbol,
kind,
is_reexported,
countme::Count::default(),
);

View File

@@ -33,11 +33,16 @@ pub struct Definition<'db> {
/// The symbol defined.
pub(crate) symbol: ScopedSymbolId,
/// WARNING: Only access this field when doing type inference for the same
/// file as where `Definition` is defined to avoid cross-file query dependencies.
#[no_eq]
#[return_ref]
#[tracked]
pub(crate) kind: DefinitionKind<'db>,
/// This is a dedicated field to avoid accessing `kind` to compute this value.
pub(crate) is_reexported: bool,
count: countme::Count<Definition<'static>>,
}
@@ -45,22 +50,6 @@ impl<'db> Definition<'db> {
pub(crate) fn scope(self, db: &'db dyn Db) -> ScopeId<'db> {
self.file_scope(db).to_scope_id(db, self.file(db))
}
pub(crate) fn category(self, db: &'db dyn Db) -> DefinitionCategory {
self.kind(db).category()
}
pub(crate) fn is_declaration(self, db: &'db dyn Db) -> bool {
self.kind(db).category().is_declaration()
}
pub(crate) fn is_binding(self, db: &'db dyn Db) -> bool {
self.kind(db).category().is_binding()
}
pub(crate) fn is_reexported(self, db: &'db dyn Db) -> bool {
self.kind(db).is_reexported()
}
}
#[derive(Copy, Clone, Debug)]

View File

@@ -293,7 +293,7 @@ fn core_module_scope(db: &dyn Db, core_module: KnownModule) -> Option<ScopeId<'_
/// together with boundness information in a [`Symbol`].
///
/// The type will be a union if there are multiple bindings with different types.
pub(crate) fn symbol_from_bindings<'db>(
pub(super) fn symbol_from_bindings<'db>(
db: &'db dyn Db,
bindings_with_constraints: BindingWithConstraintsIterator<'_, 'db>,
) -> Symbol<'db> {
@@ -479,6 +479,10 @@ fn symbol_impl<'db>(
}
/// Implementation of [`symbol_from_bindings`].
///
/// ## Implementation Note
/// This function gets called cross-module. It, therefore, shouldn't
/// access any AST nodes from the file containing the declarations.
fn symbol_from_bindings_impl<'db>(
db: &'db dyn Db,
bindings_with_constraints: BindingWithConstraintsIterator<'_, 'db>,
@@ -562,6 +566,10 @@ fn symbol_from_bindings_impl<'db>(
}
/// Implementation of [`symbol_from_declarations`].
///
/// ## Implementation Note
/// This function gets called cross-module. It, therefore, shouldn't
/// access any AST nodes from the file containing the declarations.
fn symbol_from_declarations_impl<'db>(
db: &'db dyn Db,
declarations: DeclarationsIterator<'_, 'db>,

View File

@@ -16,7 +16,8 @@ pub(crate) use self::diagnostic::register_lints;
pub use self::diagnostic::{TypeCheckDiagnostic, TypeCheckDiagnostics};
pub(crate) use self::display::TypeArrayDisplay;
pub(crate) use self::infer::{
infer_deferred_types, infer_definition_types, infer_expression_types, infer_scope_types,
infer_deferred_types, infer_definition_types, infer_expression_type, infer_expression_types,
infer_scope_types,
};
pub use self::narrow::KnownConstraintFunction;
pub(crate) use self::signatures::Signature;
@@ -26,7 +27,6 @@ use crate::module_resolver::{file_to_module, resolve_module, KnownModule};
use crate::semantic_index::ast_ids::HasScopedExpressionId;
use crate::semantic_index::attribute_assignment::AttributeAssignment;
use crate::semantic_index::definition::Definition;
use crate::semantic_index::expression::Expression;
use crate::semantic_index::symbol::ScopeId;
use crate::semantic_index::{
attribute_assignments, imported_modules, semantic_index, symbol_table, use_def_map,
@@ -1586,7 +1586,10 @@ impl<'db> Type<'db> {
.ignore_possibly_unbound()?
.try_call(
db,
&CallArguments::positional([instance.unwrap_or(Type::none(db)), owner]),
CallArguments::positional(
db,
[instance.unwrap_or(Type::none(db)), owner],
),
)
.map(|outcome| Some(outcome.return_type(db)))
.unwrap_or(None)
@@ -1710,7 +1713,7 @@ impl<'db> Type<'db> {
// and a subclass could add a `__bool__` method.
if let Ok(Type::BooleanLiteral(bool_val)) = self
.try_call_dunder(db, "__bool__", &CallArguments::none())
.try_call_dunder(db, "__bool__", CallArguments::none(db))
.map(|outcome| outcome.return_type(db))
{
bool_val.into()
@@ -1783,7 +1786,7 @@ impl<'db> Type<'db> {
return usize_len.try_into().ok().map(Type::IntLiteral);
}
let return_ty = match self.try_call_dunder(db, "__len__", &CallArguments::none()) {
let return_ty = match self.try_call_dunder(db, "__len__", CallArguments::none(db)) {
Ok(outcome) | Err(CallDunderError::PossiblyUnbound(outcome)) => outcome.return_type(db),
// TODO: emit a diagnostic
@@ -1799,359 +1802,377 @@ impl<'db> Type<'db> {
fn try_call(
self,
db: &'db dyn Db,
arguments: &CallArguments<'_, 'db>,
arguments: CallArguments<'db>,
) -> Result<CallOutcome<'db>, CallError<'db>> {
match self {
Type::Callable(CallableType::BoundMethod(bound_method)) => {
let instance = bound_method.self_instance(db);
let arguments = arguments.with_self(instance);
#[salsa::tracked]
fn try_call_query<'db>(
db: &'db dyn Db,
ty_self: Type<'db>,
arguments: CallArguments<'db>,
) -> Result<CallOutcome<'db>, CallError<'db>> {
match ty_self {
Type::Callable(CallableType::BoundMethod(bound_method)) => {
let instance = bound_method.self_instance(db);
let arguments = arguments.with_self(db, instance);
let binding = bind_call(
db,
&arguments,
bound_method.function(db).signature(db),
self,
);
let binding = bind_call(
db,
arguments,
bound_method.function(db).signature(db),
ty_self,
);
if binding.has_binding_errors() {
Err(CallError::BindingError { binding })
} else {
Ok(CallOutcome::Single(binding))
if binding.has_binding_errors() {
Err(CallError::BindingError { binding })
} else {
Ok(CallOutcome::Single(binding))
}
}
}
Type::Callable(CallableType::MethodWrapperDunderGet(function)) => {
// Here, we dynamically model the overloaded function signature of `types.FunctionType.__get__`.
// This is required because we need to return more precise types than what the signature in
// typeshed provides:
//
// ```py
// class FunctionType:
// # ...
// @overload
// def __get__(self, instance: None, owner: type, /) -> FunctionType: ...
// @overload
// def __get__(self, instance: object, owner: type | None = None, /) -> MethodType: ...
// ```
Type::Callable(CallableType::MethodWrapperDunderGet(function)) => {
// Here, we dynamically model the overloaded function signature of `types.FunctionType.__get__`.
// This is required because we need to return more precise types than what the signature in
// typeshed provides:
//
// ```py
// class FunctionType:
// # ...
// @overload
// def __get__(self, instance: None, owner: type, /) -> FunctionType: ...
// @overload
// def __get__(self, instance: object, owner: type | None = None, /) -> MethodType: ...
// ```
let first_argument_is_none =
arguments.first_argument().is_some_and(|ty| ty.is_none(db));
let first_argument_is_none = arguments
.first_argument(db)
.is_some_and(|ty| ty.is_none(db));
let signature = Signature::new(
Parameters::new([
Parameter::new(
Some("instance".into()),
Some(Type::object(db)),
ParameterKind::PositionalOnly { default_ty: None },
),
if first_argument_is_none {
let signature = Signature::new(
Parameters::new([
Parameter::new(
Some("owner".into()),
Some(KnownClass::Type.to_instance(db)),
Some("instance".into()),
Some(Type::object(db)),
ParameterKind::PositionalOnly { default_ty: None },
)
} else {
Parameter::new(
Some("owner".into()),
Some(UnionType::from_elements(
db,
[KnownClass::Type.to_instance(db), Type::none(db)],
)),
ParameterKind::PositionalOnly {
default_ty: Some(Type::none(db)),
},
)
},
]),
Some(match arguments.first_argument() {
Some(ty) if ty.is_none(db) => Type::FunctionLiteral(function),
Some(instance) => Type::Callable(CallableType::BoundMethod(
BoundMethodType::new(db, function, instance),
)),
_ => Type::unknown(),
}),
);
let binding = bind_call(db, arguments, &signature, self);
if binding.has_binding_errors() {
Err(CallError::BindingError { binding })
} else {
Ok(CallOutcome::Single(binding))
}
}
Type::Callable(CallableType::WrapperDescriptorDunderGet) => {
// Here, we also model `types.FunctionType.__get__`, but now we consider a call to
// this as a function, i.e. we also expect the `self` argument to be passed in.
let second_argument_is_none =
arguments.second_argument().is_some_and(|ty| ty.is_none(db));
let signature = Signature::new(
Parameters::new([
Parameter::new(
Some("self".into()),
Some(KnownClass::FunctionType.to_instance(db)),
ParameterKind::PositionalOnly { default_ty: None },
),
Parameter::new(
Some("instance".into()),
Some(Type::object(db)),
ParameterKind::PositionalOnly { default_ty: None },
),
if second_argument_is_none {
Parameter::new(
Some("owner".into()),
Some(KnownClass::Type.to_instance(db)),
ParameterKind::PositionalOnly { default_ty: None },
)
} else {
Parameter::new(
Some("owner".into()),
Some(UnionType::from_elements(
db,
[KnownClass::Type.to_instance(db), Type::none(db)],
)),
ParameterKind::PositionalOnly {
default_ty: Some(Type::none(db)),
},
)
},
]),
Some(
match (arguments.first_argument(), arguments.second_argument()) {
(Some(function @ Type::FunctionLiteral(_)), Some(instance))
if instance.is_none(db) =>
{
function
}
(Some(Type::FunctionLiteral(function)), Some(instance)) => {
Type::Callable(CallableType::BoundMethod(BoundMethodType::new(
db, function, instance,
)))
}
),
if first_argument_is_none {
Parameter::new(
Some("owner".into()),
Some(KnownClass::Type.to_instance(db)),
ParameterKind::PositionalOnly { default_ty: None },
)
} else {
Parameter::new(
Some("owner".into()),
Some(UnionType::from_elements(
db,
[KnownClass::Type.to_instance(db), Type::none(db)],
)),
ParameterKind::PositionalOnly {
default_ty: Some(Type::none(db)),
},
)
},
]),
Some(match arguments.first_argument(db) {
Some(ty) if ty.is_none(db) => Type::FunctionLiteral(function),
Some(instance) => Type::Callable(CallableType::BoundMethod(
BoundMethodType::new(db, function, instance),
)),
_ => Type::unknown(),
},
),
);
}),
);
let binding = bind_call(db, arguments, &signature, self);
let binding = bind_call(db, arguments, &signature, ty_self);
if binding.has_binding_errors() {
Err(CallError::BindingError { binding })
} else {
Ok(CallOutcome::Single(binding))
if binding.has_binding_errors() {
Err(CallError::BindingError { binding })
} else {
Ok(CallOutcome::Single(binding))
}
}
}
Type::FunctionLiteral(function_type) => {
let mut binding = bind_call(db, arguments, function_type.signature(db), self);
Type::Callable(CallableType::WrapperDescriptorDunderGet) => {
// Here, we also model `types.FunctionType.__get__`, but now we consider a call to
// this as a function, i.e. we also expect the `self` argument to be passed in.
if binding.has_binding_errors() {
return Err(CallError::BindingError { binding });
let second_argument_is_none = arguments
.second_argument(db)
.is_some_and(|ty| ty.is_none(db));
let signature = Signature::new(
Parameters::new([
Parameter::new(
Some("self".into()),
Some(KnownClass::FunctionType.to_instance(db)),
ParameterKind::PositionalOnly { default_ty: None },
),
Parameter::new(
Some("instance".into()),
Some(Type::object(db)),
ParameterKind::PositionalOnly { default_ty: None },
),
if second_argument_is_none {
Parameter::new(
Some("owner".into()),
Some(KnownClass::Type.to_instance(db)),
ParameterKind::PositionalOnly { default_ty: None },
)
} else {
Parameter::new(
Some("owner".into()),
Some(UnionType::from_elements(
db,
[KnownClass::Type.to_instance(db), Type::none(db)],
)),
ParameterKind::PositionalOnly {
default_ty: Some(Type::none(db)),
},
)
},
]),
Some(
match (arguments.first_argument(db), arguments.second_argument(db)) {
(Some(function @ Type::FunctionLiteral(_)), Some(instance))
if instance.is_none(db) =>
{
function
}
(Some(Type::FunctionLiteral(function)), Some(instance)) => {
Type::Callable(CallableType::BoundMethod(BoundMethodType::new(
db, function, instance,
)))
}
_ => Type::unknown(),
},
),
);
let binding = bind_call(db, arguments, &signature, ty_self);
if binding.has_binding_errors() {
Err(CallError::BindingError { binding })
} else {
Ok(CallOutcome::Single(binding))
}
}
Type::FunctionLiteral(function_type) => {
let mut binding =
bind_call(db, arguments, function_type.signature(db), ty_self);
match function_type.known(db) {
Some(KnownFunction::IsEquivalentTo) => {
let (ty_a, ty_b) = binding
.two_parameter_types()
.unwrap_or((Type::unknown(), Type::unknown()));
binding
.set_return_type(Type::BooleanLiteral(ty_a.is_equivalent_to(db, ty_b)));
}
Some(KnownFunction::IsSubtypeOf) => {
let (ty_a, ty_b) = binding
.two_parameter_types()
.unwrap_or((Type::unknown(), Type::unknown()));
binding.set_return_type(Type::BooleanLiteral(ty_a.is_subtype_of(db, ty_b)));
}
Some(KnownFunction::IsAssignableTo) => {
let (ty_a, ty_b) = binding
.two_parameter_types()
.unwrap_or((Type::unknown(), Type::unknown()));
binding
.set_return_type(Type::BooleanLiteral(ty_a.is_assignable_to(db, ty_b)));
}
Some(KnownFunction::IsDisjointFrom) => {
let (ty_a, ty_b) = binding
.two_parameter_types()
.unwrap_or((Type::unknown(), Type::unknown()));
binding
.set_return_type(Type::BooleanLiteral(ty_a.is_disjoint_from(db, ty_b)));
}
Some(KnownFunction::IsGradualEquivalentTo) => {
let (ty_a, ty_b) = binding
.two_parameter_types()
.unwrap_or((Type::unknown(), Type::unknown()));
binding.set_return_type(Type::BooleanLiteral(
ty_a.is_gradual_equivalent_to(db, ty_b),
));
}
Some(KnownFunction::IsFullyStatic) => {
let ty = binding.one_parameter_type().unwrap_or(Type::unknown());
binding.set_return_type(Type::BooleanLiteral(ty.is_fully_static(db)));
}
Some(KnownFunction::IsSingleton) => {
let ty = binding.one_parameter_type().unwrap_or(Type::unknown());
binding.set_return_type(Type::BooleanLiteral(ty.is_singleton(db)));
}
Some(KnownFunction::IsSingleValued) => {
let ty = binding.one_parameter_type().unwrap_or(Type::unknown());
binding.set_return_type(Type::BooleanLiteral(ty.is_single_valued(db)));
if binding.has_binding_errors() {
return Err(CallError::BindingError { binding });
}
Some(KnownFunction::Len) => {
if let Some(first_arg) = binding.one_parameter_type() {
if let Some(len_ty) = first_arg.len(db) {
binding.set_return_type(len_ty);
}
};
}
match function_type.known(db) {
Some(KnownFunction::IsEquivalentTo) => {
let (ty_a, ty_b) = binding
.two_parameter_types()
.unwrap_or((Type::unknown(), Type::unknown()));
binding.set_return_type(Type::BooleanLiteral(
ty_a.is_equivalent_to(db, ty_b),
));
}
Some(KnownFunction::IsSubtypeOf) => {
let (ty_a, ty_b) = binding
.two_parameter_types()
.unwrap_or((Type::unknown(), Type::unknown()));
binding.set_return_type(Type::BooleanLiteral(
ty_a.is_subtype_of(db, ty_b),
));
}
Some(KnownFunction::IsAssignableTo) => {
let (ty_a, ty_b) = binding
.two_parameter_types()
.unwrap_or((Type::unknown(), Type::unknown()));
binding.set_return_type(Type::BooleanLiteral(
ty_a.is_assignable_to(db, ty_b),
));
}
Some(KnownFunction::IsDisjointFrom) => {
let (ty_a, ty_b) = binding
.two_parameter_types()
.unwrap_or((Type::unknown(), Type::unknown()));
binding.set_return_type(Type::BooleanLiteral(
ty_a.is_disjoint_from(db, ty_b),
));
}
Some(KnownFunction::IsGradualEquivalentTo) => {
let (ty_a, ty_b) = binding
.two_parameter_types()
.unwrap_or((Type::unknown(), Type::unknown()));
binding.set_return_type(Type::BooleanLiteral(
ty_a.is_gradual_equivalent_to(db, ty_b),
));
}
Some(KnownFunction::IsFullyStatic) => {
let ty = binding.one_parameter_type().unwrap_or(Type::unknown());
binding.set_return_type(Type::BooleanLiteral(ty.is_fully_static(db)));
}
Some(KnownFunction::IsSingleton) => {
let ty = binding.one_parameter_type().unwrap_or(Type::unknown());
binding.set_return_type(Type::BooleanLiteral(ty.is_singleton(db)));
}
Some(KnownFunction::IsSingleValued) => {
let ty = binding.one_parameter_type().unwrap_or(Type::unknown());
binding.set_return_type(Type::BooleanLiteral(ty.is_single_valued(db)));
}
Some(KnownFunction::Repr) => {
if let Some(first_arg) = binding.one_parameter_type() {
binding.set_return_type(first_arg.repr(db));
};
}
Some(KnownFunction::Len) => {
if let Some(first_arg) = binding.one_parameter_type() {
if let Some(len_ty) = first_arg.len(db) {
binding.set_return_type(len_ty);
}
};
}
Some(KnownFunction::Cast) => {
// TODO: Use `.two_parameter_tys()` exclusively
// when overloads are supported.
if let Some(casted_ty) = arguments.first_argument() {
if binding.two_parameter_types().is_some() {
binding.set_return_type(casted_ty);
}
};
}
Some(KnownFunction::Repr) => {
if let Some(first_arg) = binding.one_parameter_type() {
binding.set_return_type(first_arg.repr(db));
};
}
Some(KnownFunction::GetattrStatic) => {
let Some((instance_ty, attr_name, default)) =
binding.three_parameter_types()
else {
return Ok(CallOutcome::Single(binding));
};
Some(KnownFunction::Cast) => {
// TODO: Use `.two_parameter_tys()` exclusively
// when overloads are supported.
if let Some(casted_ty) = arguments.first_argument(db) {
if binding.two_parameter_types().is_some() {
binding.set_return_type(casted_ty);
}
};
}
let Some(attr_name) = attr_name.into_string_literal() else {
return Ok(CallOutcome::Single(binding));
};
Some(KnownFunction::GetattrStatic) => {
let Some((instance_ty, attr_name, default)) =
binding.three_parameter_types()
else {
return Ok(CallOutcome::Single(binding));
};
let default = if default.is_unknown() {
Type::Never
} else {
default
};
let Some(attr_name) = attr_name.into_string_literal() else {
return Ok(CallOutcome::Single(binding));
};
let union_with_default = |ty| UnionType::from_elements(db, [ty, default]);
let default = if default.is_unknown() {
Type::Never
} else {
default
};
// TODO: we could emit a diagnostic here (if default is not set)
binding.set_return_type(
match instance_ty.static_member(db, attr_name.value(db)) {
Symbol::Type(ty, Boundness::Bound) => {
if instance_ty.is_fully_static(db) {
ty
} else {
// Here, we attempt to model the fact that an attribute lookup on
// a non-fully static type could fail. This is an approximation,
// as there are gradual types like `tuple[Any]`, on which a lookup
// of (e.g. of the `index` method) would always succeed.
let union_with_default =
|ty| UnionType::from_elements(db, [ty, default]);
// TODO: we could emit a diagnostic here (if default is not set)
binding.set_return_type(
match instance_ty.static_member(db, attr_name.value(db)) {
Symbol::Type(ty, Boundness::Bound) => {
if instance_ty.is_fully_static(db) {
ty
} else {
// Here, we attempt to model the fact that an attribute lookup on
// a non-fully static type could fail. This is an approximation,
// as there are gradual types like `tuple[Any]`, on which a lookup
// of (e.g. of the `index` method) would always succeed.
union_with_default(ty)
}
}
Symbol::Type(ty, Boundness::PossiblyUnbound) => {
union_with_default(ty)
}
}
Symbol::Type(ty, Boundness::PossiblyUnbound) => {
union_with_default(ty)
}
Symbol::Unbound => default,
},
);
Symbol::Unbound => default,
},
);
}
_ => {}
};
if binding.has_binding_errors() {
Err(CallError::BindingError { binding })
} else {
Ok(CallOutcome::Single(binding))
}
_ => {}
};
if binding.has_binding_errors() {
Err(CallError::BindingError { binding })
} else {
Ok(CallOutcome::Single(binding))
}
}
// TODO annotated return type on `__new__` or metaclass `__call__`
// TODO check call vs signatures of `__new__` and/or `__init__`
Type::ClassLiteral(ClassLiteralType { class }) => {
Ok(CallOutcome::Single(CallBinding::from_return_type(
match class.known(db) {
// If the class is the builtin-bool class (for example `bool(1)`), we try to
// return the specific truthiness value of the input arg, `Literal[True]` for
// the example above.
Some(KnownClass::Bool) => arguments
.first_argument()
.map(|arg| arg.bool(db).into_type(db))
.unwrap_or(Type::BooleanLiteral(false)),
// TODO annotated return type on `__new__` or metaclass `__call__`
// TODO check call vs signatures of `__new__` and/or `__init__`
Type::ClassLiteral(ClassLiteralType { class }) => {
Ok(CallOutcome::Single(CallBinding::from_return_type(
match class.known(db) {
// If the class is the builtin-bool class (for example `bool(1)`), we try to
// return the specific truthiness value of the input arg, `Literal[True]` for
// the example above.
Some(KnownClass::Bool) => arguments
.first_argument(db)
.map(|arg| arg.bool(db).into_type(db))
.unwrap_or(Type::BooleanLiteral(false)),
// TODO: Don't ignore the second and third arguments to `str`
// https://github.com/astral-sh/ruff/pull/16161#discussion_r1958425568
Some(KnownClass::Str) => arguments
.first_argument()
.map(|arg| arg.str(db))
.unwrap_or(Type::string_literal(db, "")),
// TODO: Don't ignore the second and third arguments to `str`
// https://github.com/astral-sh/ruff/pull/16161#discussion_r1958425568
Some(KnownClass::Str) => arguments
.first_argument(db)
.map(|arg| arg.str(db))
.unwrap_or(Type::string_literal(db, "")),
_ => Type::Instance(InstanceType { class }),
},
)))
}
instance_ty @ Type::Instance(_) => {
instance_ty
.try_call_dunder(db, "__call__", arguments)
.map_err(|err| match err {
CallDunderError::Call(CallError::NotCallable { .. }) => {
// Turn "`<type of illegal '__call__'>` not callable" into
// "`X` not callable"
CallError::NotCallable {
not_callable_ty: self,
}
}
CallDunderError::Call(CallError::Union {
called_ty: _,
bindings,
errors,
}) => CallError::Union {
called_ty: self,
bindings,
errors,
_ => Type::Instance(InstanceType { class }),
},
CallDunderError::Call(error) => error,
// Turn "possibly unbound object of type `Literal['__call__']`"
// into "`X` not callable (possibly unbound `__call__` method)"
CallDunderError::PossiblyUnbound(outcome) => {
CallError::PossiblyUnboundDunderCall {
called_type: self,
outcome: Box::new(outcome),
)))
}
instance_ty @ Type::Instance(_) => {
instance_ty
.try_call_dunder(db, "__call__", arguments)
.map_err(|err| match err {
CallDunderError::Call(CallError::NotCallable { .. }) => {
// Turn "`<type of illegal '__call__'>` not callable" into
// "`X` not callable"
CallError::NotCallable {
not_callable_ty: ty_self,
}
}
}
CallDunderError::MethodNotAvailable => {
// Turn "`X.__call__` unbound" into "`X` not callable"
CallError::NotCallable {
not_callable_ty: self,
CallDunderError::Call(CallError::Union {
called_ty: _,
bindings,
errors,
}) => CallError::Union {
called_ty: ty_self,
bindings,
errors,
},
CallDunderError::Call(error) => error,
// Turn "possibly unbound object of type `Literal['__call__']`"
// into "`X` not callable (possibly unbound `__call__` method)"
CallDunderError::PossiblyUnbound(outcome) => {
CallError::PossiblyUnboundDunderCall {
called_type: ty_self,
outcome: Box::new(outcome),
}
}
}
})
CallDunderError::MethodNotAvailable => {
// Turn "`X.__call__` unbound" into "`X` not callable"
CallError::NotCallable {
not_callable_ty: ty_self,
}
}
})
}
// Dynamic types are callable, and the return type is the same dynamic type
Type::Dynamic(_) => Ok(CallOutcome::Single(CallBinding::from_return_type(ty_self))),
Type::Union(union) => CallOutcome::try_call_union(db, union, |element| {
element.try_call(db, arguments)
}),
Type::Intersection(_) => Ok(CallOutcome::Single(CallBinding::from_return_type(
todo_type!("Type::Intersection.call()"),
))),
_ => Err(CallError::NotCallable {
not_callable_ty: ty_self,
}),
}
// Dynamic types are callable, and the return type is the same dynamic type
Type::Dynamic(_) => Ok(CallOutcome::Single(CallBinding::from_return_type(self))),
Type::Union(union) => {
CallOutcome::try_call_union(db, union, |element| element.try_call(db, arguments))
}
Type::Intersection(_) => Ok(CallOutcome::Single(CallBinding::from_return_type(
todo_type!("Type::Intersection.call()"),
))),
_ => Err(CallError::NotCallable {
not_callable_ty: self,
}),
}
try_call_query(db, self, arguments)
}
/// Return the outcome of calling an class/instance attribute of this type
@@ -2164,13 +2185,13 @@ impl<'db> Type<'db> {
self,
db: &'db dyn Db,
receiver_ty: &Type<'db>,
arguments: &CallArguments<'_, 'db>,
arguments: CallArguments<'db>,
) -> Result<CallOutcome<'db>, CallError<'db>> {
match self {
Type::FunctionLiteral(..) => {
// Functions are always descriptors, so this would effectively call
// the function with the instance as the first argument
self.try_call(db, &arguments.with_self(*receiver_ty))
self.try_call(db, arguments.with_self(db, *receiver_ty))
}
Type::Instance(_) | Type::ClassLiteral(_) => self.try_call(db, arguments),
@@ -2197,7 +2218,7 @@ impl<'db> Type<'db> {
self,
db: &'db dyn Db,
name: &str,
arguments: &CallArguments<'_, 'db>,
arguments: CallArguments<'db>,
) -> Result<CallOutcome<'db>, CallDunderError<'db>> {
match self.to_meta_type(db).member(db, name) {
Symbol::Type(callable_ty, Boundness::Bound) => {
@@ -2226,12 +2247,12 @@ impl<'db> Type<'db> {
};
}
let dunder_iter_result = self.try_call_dunder(db, "__iter__", &CallArguments::none());
let dunder_iter_result = self.try_call_dunder(db, "__iter__", CallArguments::none(db));
match &dunder_iter_result {
Ok(outcome) | Err(CallDunderError::PossiblyUnbound(outcome)) => {
let iterator_ty = outcome.return_type(db);
return match iterator_ty.try_call_dunder(db, "__next__", &CallArguments::none()) {
return match iterator_ty.try_call_dunder(db, "__next__", CallArguments::none(db)) {
Ok(outcome) => {
if matches!(
dunder_iter_result,
@@ -2279,7 +2300,7 @@ impl<'db> Type<'db> {
match self.try_call_dunder(
db,
"__getitem__",
&CallArguments::positional([KnownClass::Int.to_instance(db)]),
CallArguments::positional(db, [KnownClass::Int.to_instance(db)]),
) {
Ok(outcome) => IterationOutcome::Iterable {
element_ty: outcome.return_type(db),
@@ -4148,9 +4169,9 @@ impl<'db> Class<'db> {
let namespace = KnownClass::Dict.to_instance(db);
// TODO: Other keyword arguments?
let arguments = CallArguments::positional([name, bases, namespace]);
let arguments = CallArguments::positional(db, [name, bases, namespace]);
let return_ty_result = match metaclass.try_call(db, &arguments) {
let return_ty_result = match metaclass.try_call(db, arguments) {
Ok(outcome) => Ok(outcome.return_type(db)),
Err(CallError::NotCallable { not_callable_ty }) => Err(MetaclassError {
@@ -4337,16 +4358,6 @@ impl<'db> Class<'db> {
name: &str,
inferred_type_from_class_body: Option<Type<'db>>,
) -> Symbol<'db> {
// We use a separate salsa query here to prevent unrelated changes in the AST of an external
// file from triggering re-evaluations of downstream queries.
// See the `dependency_implicit_instance_attribute` test for more information.
#[salsa::tracked]
fn infer_expression_type<'db>(db: &'db dyn Db, expression: Expression<'db>) -> Type<'db> {
let inference = infer_expression_types(db, expression);
let expr_scope = expression.scope(db);
inference.expression_type(expression.node_ref(db).scoped_expression_id(db, expr_scope))
}
// If we do not see any declarations of an attribute, neither in the class body nor in
// any method, we build a union of `Unknown` with the inferred types of all bindings of
// that attribute. We include `Unknown` in that union to account for the fact that the

View File

@@ -11,7 +11,7 @@ pub(super) use bind::{bind_call, CallBinding};
/// A successfully bound call where all arguments are valid.
///
/// It's guaranteed that the wrapped bindings have no errors.
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, salsa::Update)]
pub(super) enum CallOutcome<'db> {
/// The call resolves to exactly one binding.
Single(CallBinding<'db>),
@@ -84,7 +84,7 @@ impl<'db> CallOutcome<'db> {
}
/// The reason why calling a type failed.
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, salsa::Update)]
pub(super) enum CallError<'db> {
/// The type is not callable.
NotCallable {

View File

@@ -1,63 +1,60 @@
use ruff_python_ast::name::Name;
use crate::Db;
use super::Type;
/// Typed arguments for a single call, in source order.
#[derive(Clone, Debug, Default)]
pub(crate) struct CallArguments<'a, 'db>(Vec<Argument<'a, 'db>>);
#[salsa::tracked]
pub(crate) struct CallArguments<'db> {
args: Vec<Argument<'db>>,
}
impl<'a, 'db> CallArguments<'a, 'db> {
impl<'a, 'db> CallArguments<'db> {
/// Create a [`CallArguments`] with no arguments.
pub(crate) fn none() -> Self {
Self(Vec::new())
pub(crate) fn none(db: &'db dyn Db) -> Self {
CallArguments::new(db, Vec::new())
}
/// Create a [`CallArguments`] from an iterator over non-variadic positional argument types.
pub(crate) fn positional(positional_tys: impl IntoIterator<Item = Type<'db>>) -> Self {
positional_tys
.into_iter()
.map(Argument::Positional)
.collect()
pub(crate) fn positional(
db: &'db dyn Db,
positional_tys: impl IntoIterator<Item = Type<'db>>,
) -> Self {
CallArguments::new(
db,
positional_tys
.into_iter()
.map(Argument::Positional)
.collect(),
)
}
/// Prepend an extra positional argument.
pub(crate) fn with_self(&self, self_ty: Type<'db>) -> Self {
let mut arguments = Vec::with_capacity(self.0.len() + 1);
pub(crate) fn with_self(&self, db: &'db dyn Db, self_ty: Type<'db>) -> Self {
let mut arguments = Vec::with_capacity(self.args(db).len() + 1);
arguments.push(Argument::Synthetic(self_ty));
arguments.extend_from_slice(&self.0);
Self(arguments)
arguments.extend_from_slice(&self.args(db));
CallArguments::new(db, arguments)
}
pub(crate) fn iter(&self) -> impl Iterator<Item = &Argument<'a, 'db>> {
self.0.iter()
pub(crate) fn args_iter(&self, db: &'db dyn Db) -> impl IntoIterator<Item = Argument<'db>> {
self.args(db).into_iter()
}
// TODO this should be eliminated in favor of [`bind_call`]
pub(crate) fn first_argument(&self) -> Option<Type<'db>> {
self.0.first().map(Argument::ty)
pub(crate) fn first_argument(&self, db: &'db dyn Db) -> Option<Type<'db>> {
self.args(db).first().map(Argument::ty)
}
// TODO this should be eliminated in favor of [`bind_call`]
pub(crate) fn second_argument(&self) -> Option<Type<'db>> {
self.0.get(1).map(Argument::ty)
pub(crate) fn second_argument(&self, db: &'db dyn Db) -> Option<Type<'db>> {
self.args(db).get(1).map(Argument::ty)
}
}
impl<'db, 'a, 'b> IntoIterator for &'b CallArguments<'a, 'db> {
type Item = &'b Argument<'a, 'db>;
type IntoIter = std::slice::Iter<'b, Argument<'a, 'db>>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
impl<'a, 'db> FromIterator<Argument<'a, 'db>> for CallArguments<'a, 'db> {
fn from_iter<T: IntoIterator<Item = Argument<'a, 'db>>>(iter: T) -> Self {
Self(iter.into_iter().collect())
}
}
#[derive(Clone, Debug)]
pub(crate) enum Argument<'a, 'db> {
#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)]
pub(crate) enum Argument<'db> {
/// The synthetic `self` or `cls` argument, which doesn't appear explicitly at the call site.
Synthetic(Type<'db>),
/// A positional argument.
@@ -65,12 +62,12 @@ pub(crate) enum Argument<'a, 'db> {
/// A starred positional argument (e.g. `*args`).
Variadic(Type<'db>),
/// A keyword argument (e.g. `a=1`).
Keyword { name: &'a str, ty: Type<'db> },
Keyword { name: Name, ty: Type<'db> },
/// The double-starred keywords argument (e.g. `**kwargs`).
Keywords(Type<'db>),
}
impl<'db> Argument<'_, 'db> {
impl<'db> Argument<'db> {
fn ty(&self) -> Type<'db> {
match self {
Self::Synthetic(ty) => *ty,

View File

@@ -16,7 +16,7 @@ use ruff_text_size::Ranged;
/// parameters, and any errors resulting from binding the call.
pub(crate) fn bind_call<'db>(
db: &'db dyn Db,
arguments: &CallArguments<'_, 'db>,
arguments: CallArguments<'db>,
signature: &Signature<'db>,
callable_ty: Type<'db>,
) -> CallBinding<'db> {
@@ -38,7 +38,7 @@ pub(crate) fn bind_call<'db>(
None
}
};
for (argument_index, argument) in arguments.iter().enumerate() {
for (argument_index, argument) in arguments.args_iter(db).into_iter().enumerate() {
let (index, parameter, argument_ty, positional) = match argument {
Argument::Positional(ty) | Argument::Synthetic(ty) => {
if matches!(argument, Argument::Synthetic(_)) {
@@ -58,7 +58,7 @@ pub(crate) fn bind_call<'db>(
}
Argument::Keyword { name, ty } => {
let Some((index, parameter)) = parameters
.keyword_by_name(name)
.keyword_by_name(&name)
.or_else(|| parameters.keyword_variadic())
else {
errors.push(CallBindingError::UnknownArgument {
@@ -81,13 +81,13 @@ pub(crate) fn bind_call<'db>(
parameter: ParameterContext::new(parameter, index, positional),
argument_index: get_argument_index(argument_index, num_synthetic_args),
expected_ty,
provided_ty: *argument_ty,
provided_ty: argument_ty,
});
}
}
if let Some(existing) = parameter_tys[index].replace(*argument_ty) {
if let Some(existing) = parameter_tys[index].replace(argument_ty) {
if parameter.is_variadic() || parameter.is_keyword_variadic() {
let union = UnionType::from_elements(db, [existing, *argument_ty]);
let union = UnionType::from_elements(db, [existing, argument_ty]);
parameter_tys[index].replace(union);
} else {
errors.push(CallBindingError::ParameterAlreadyAssigned {
@@ -137,7 +137,7 @@ pub(crate) fn bind_call<'db>(
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, salsa::Update)]
pub(crate) struct CallBinding<'db> {
/// Type of the callable object (function, class...)
callable_ty: Type<'db>,
@@ -273,7 +273,7 @@ impl std::fmt::Display for ParameterContexts {
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[derive(Clone, Debug, PartialEq, Eq, salsa::Update)]
pub(crate) enum CallBindingError<'db> {
/// The type of an argument is not assignable to the annotated type of its corresponding
/// parameter.

View File

@@ -118,7 +118,7 @@ fn infer_definition_types_cycle_recovery<'db>(
) -> TypeInference<'db> {
tracing::trace!("infer_definition_types_cycle_recovery");
let mut inference = TypeInference::empty(input.scope(db));
let category = input.category(db);
let category = input.kind(db).category();
if category.is_declaration() {
inference
.declarations
@@ -198,6 +198,36 @@ pub(crate) fn infer_expression_types<'db>(
TypeInferenceBuilder::new(db, InferenceRegion::Expression(expression), index).finish()
}
/// Infers the type of an `expression` that is guaranteed to be in the same file as the calling query.
///
/// This is a small helper around [`infer_expression_types()`] to reduce the boilerplate.
/// Use [`infer_expression_type()`] if it isn't guaranteed that `expression` is in the same file to
/// avoid cross-file query dependencies.
pub(super) fn infer_same_file_expression_type<'db>(
db: &'db dyn Db,
expression: Expression<'db>,
) -> Type<'db> {
let inference = infer_expression_types(db, expression);
let scope = expression.scope(db);
inference.expression_type(expression.node_ref(db).scoped_expression_id(db, scope))
}
/// Infers the type of an expression where the expression might come from another file.
///
/// Use this over [`infer_expression_types`] if the expression might come from another file than the
/// enclosing query to avoid cross-file query dependencies.
///
/// Use [`infer_same_file_expression_type`] if it is guaranteed that `expression` is in the same
/// to avoid unnecessary salsa ingredients. This is normally the case inside the `TypeInferenceBuilder`.
#[salsa::tracked]
pub(crate) fn infer_expression_type<'db>(
db: &'db dyn Db,
expression: Expression<'db>,
) -> Type<'db> {
// It's okay to call the "same file" version here because we're inside a salsa query.
infer_same_file_expression_type(db, expression)
}
/// Infer the types for an [`Unpack`] operation.
///
/// This infers the expression type and performs structural match against the target expression
@@ -870,7 +900,7 @@ impl<'db> TypeInferenceBuilder<'db> {
}
fn add_binding(&mut self, node: AnyNodeRef, binding: Definition<'db>, ty: Type<'db>) {
debug_assert!(binding.is_binding(self.db()));
debug_assert!(binding.kind(self.db()).category().is_binding());
let use_def = self.index.use_def_map(binding.file_scope(self.db()));
let declarations = use_def.declarations_at_binding(binding);
let mut bound_ty = ty;
@@ -905,7 +935,7 @@ impl<'db> TypeInferenceBuilder<'db> {
declaration: Definition<'db>,
ty: TypeAndQualifiers<'db>,
) {
debug_assert!(declaration.is_declaration(self.db()));
debug_assert!(declaration.kind(self.db()).category().is_declaration());
let use_def = self.index.use_def_map(declaration.file_scope(self.db()));
let prior_bindings = use_def.bindings_at_declaration(declaration);
// unbound_ty is Never because for this check we don't care about unbound
@@ -935,8 +965,8 @@ impl<'db> TypeInferenceBuilder<'db> {
definition: Definition<'db>,
declared_and_inferred_ty: &DeclaredAndInferredType<'db>,
) {
debug_assert!(definition.is_binding(self.db()));
debug_assert!(definition.is_declaration(self.db()));
debug_assert!(definition.kind(self.db()).category().is_binding());
debug_assert!(definition.kind(self.db()).category().is_declaration());
let (declared_ty, inferred_ty) = match *declared_and_inferred_ty {
DeclaredAndInferredType::AreTheSame(ty) => (ty.into(), ty),
@@ -1616,7 +1646,7 @@ impl<'db> TypeInferenceBuilder<'db> {
}
let target_ty = enter_ty
.try_call(self.db(), &CallArguments::positional([context_expression_ty]))
.try_call(self.db(), CallArguments::positional(self.db(), [context_expression_ty]))
.map(|outcome| outcome.return_type(self.db()))
.unwrap_or_else(|err| {
// TODO: Use more specific error messages for the different error cases.
@@ -1661,12 +1691,15 @@ impl<'db> TypeInferenceBuilder<'db> {
if exit_ty
.try_call(
self.db(),
&CallArguments::positional([
context_manager_ty,
Type::none(self.db()),
Type::none(self.db()),
Type::none(self.db()),
]),
CallArguments::positional(
self.db(),
[
context_manager_ty,
Type::none(self.db()),
Type::none(self.db()),
Type::none(self.db()),
],
),
)
.is_err()
{
@@ -2211,7 +2244,7 @@ impl<'db> TypeInferenceBuilder<'db> {
{
let call = class_member.try_call(
self.db(),
&CallArguments::positional([target_type, value_type]),
CallArguments::positional(self.db(), [target_type, value_type]),
);
let augmented_return_ty = match call {
Ok(t) => t.return_type(self.db()),
@@ -2692,46 +2725,53 @@ impl<'db> TypeInferenceBuilder<'db> {
&mut self,
arguments: &'a ast::Arguments,
parameter_expectations: ParameterExpectations,
) -> CallArguments<'a, 'db> {
arguments
.arguments_source_order()
.enumerate()
.map(|(index, arg_or_keyword)| {
let infer_argument_type = match parameter_expectations.expectation_at_index(index) {
ParameterExpectation::TypeExpression => Self::infer_type_expression,
ParameterExpectation::ValueExpression => Self::infer_expression,
};
) -> CallArguments<'db> {
CallArguments::new(
self.db(),
arguments
.arguments_source_order()
.enumerate()
.map(|(index, arg_or_keyword)| {
let infer_argument_type =
match parameter_expectations.expectation_at_index(index) {
ParameterExpectation::TypeExpression => Self::infer_type_expression,
ParameterExpectation::ValueExpression => Self::infer_expression,
};
match arg_or_keyword {
ast::ArgOrKeyword::Arg(arg) => match arg {
ast::Expr::Starred(ast::ExprStarred {
match arg_or_keyword {
ast::ArgOrKeyword::Arg(arg) => match arg {
ast::Expr::Starred(ast::ExprStarred {
value,
range: _,
ctx: _,
}) => {
let ty = infer_argument_type(self, value);
self.store_expression_type(arg, ty);
Argument::Variadic(ty)
}
// TODO diagnostic if after a keyword argument
_ => Argument::Positional(infer_argument_type(self, arg)),
},
ast::ArgOrKeyword::Keyword(ast::Keyword {
arg,
value,
range: _,
ctx: _,
}) => {
let ty = infer_argument_type(self, value);
self.store_expression_type(arg, ty);
Argument::Variadic(ty)
}
// TODO diagnostic if after a keyword argument
_ => Argument::Positional(infer_argument_type(self, arg)),
},
ast::ArgOrKeyword::Keyword(ast::Keyword {
arg,
value,
range: _,
}) => {
let ty = infer_argument_type(self, value);
if let Some(arg) = arg {
Argument::Keyword { name: &arg.id, ty }
} else {
// TODO diagnostic if not last
Argument::Keywords(ty)
if let Some(arg) = arg {
Argument::Keyword {
name: arg.id.clone(),
ty,
}
} else {
// TODO diagnostic if not last
Argument::Keywords(ty)
}
}
}
}
})
.collect()
})
.collect(),
)
}
fn infer_optional_expression(&mut self, expression: Option<&ast::Expr>) -> Option<Type<'db>> {
@@ -3247,7 +3287,7 @@ impl<'db> TypeInferenceBuilder<'db> {
.unwrap_or_default();
let call_arguments = self.infer_arguments(arguments, parameter_expectations);
let call = function_type.try_call(self.db(), &call_arguments);
let call = function_type.try_call(self.db(), call_arguments);
match call {
Ok(outcome) => {
@@ -3751,7 +3791,7 @@ impl<'db> TypeInferenceBuilder<'db> {
match operand_type.try_call_dunder(
self.db(),
unary_dunder_method,
&CallArguments::none(),
CallArguments::none(self.db()),
) {
Ok(outcome) => outcome.return_type(self.db()),
Err(e) => {
@@ -4002,7 +4042,7 @@ impl<'db> TypeInferenceBuilder<'db> {
.try_call_dunder(
self.db(),
reflected_dunder,
&CallArguments::positional([left_ty]),
CallArguments::positional(self.db(), [left_ty]),
)
.map(|outcome| outcome.return_type(self.db()))
.or_else(|_| {
@@ -4010,7 +4050,7 @@ impl<'db> TypeInferenceBuilder<'db> {
.try_call_dunder(
self.db(),
op.dunder(),
&CallArguments::positional([right_ty]),
CallArguments::positional(self.db(), [right_ty]),
)
.map(|outcome| outcome.return_type(self.db()))
})
@@ -4023,7 +4063,10 @@ impl<'db> TypeInferenceBuilder<'db> {
left_class.member(self.db(), op.dunder())
{
class_member
.try_call(self.db(), &CallArguments::positional([left_ty, right_ty]))
.try_call(
self.db(),
CallArguments::positional(self.db(), [left_ty, right_ty]),
)
.map(|outcome| outcome.return_type(self.db()))
.ok()
} else {
@@ -4041,7 +4084,7 @@ impl<'db> TypeInferenceBuilder<'db> {
class_member
.try_call(
self.db(),
&CallArguments::positional([right_ty, left_ty]),
CallArguments::positional(self.db(), [right_ty, left_ty]),
)
.map(|outcome| outcome.return_type(self.db()))
.ok()
@@ -4610,21 +4653,23 @@ impl<'db> TypeInferenceBuilder<'db> {
let db = self.db();
// The following resource has details about the rich comparison algorithm:
// https://snarky.ca/unravelling-rich-comparison-operators/
let call_dunder = |op: RichCompareOperator,
left: InstanceType<'db>,
right: InstanceType<'db>| {
// TODO: How do we want to handle possibly unbound dunder methods?
match left.class.class_member(db, op.dunder()) {
Symbol::Type(class_member_dunder, Boundness::Bound) => class_member_dunder
.try_call(
db,
&CallArguments::positional([Type::Instance(left), Type::Instance(right)]),
)
.map(|outcome| outcome.return_type(db))
.ok(),
_ => None,
}
};
let call_dunder =
|op: RichCompareOperator, left: InstanceType<'db>, right: InstanceType<'db>| {
// TODO: How do we want to handle possibly unbound dunder methods?
match left.class.class_member(db, op.dunder()) {
Symbol::Type(class_member_dunder, Boundness::Bound) => class_member_dunder
.try_call(
db,
CallArguments::positional(
db,
[Type::Instance(left), Type::Instance(right)],
),
)
.map(|outcome| outcome.return_type(db))
.ok(),
_ => None,
}
};
// The reflected dunder has priority if the right-hand side is a strict subclass of the left-hand side.
if left != right && right.is_subtype_of(db, left) {
@@ -4668,7 +4713,10 @@ impl<'db> TypeInferenceBuilder<'db> {
contains_dunder
.try_call(
db,
&CallArguments::positional([Type::Instance(right), Type::Instance(left)]),
CallArguments::positional(
db,
[Type::Instance(right), Type::Instance(left)],
),
)
.map(|outcome| outcome.return_type(db))
.ok()
@@ -4926,7 +4974,7 @@ impl<'db> TypeInferenceBuilder<'db> {
match value_ty.try_call_dunder(
self.db(),
"__getitem__",
&CallArguments::positional([slice_ty]),
CallArguments::positional(self.db(), [slice_ty]),
) {
Ok(outcome) => return outcome.return_type(self.db()),
Err(err @ CallDunderError::PossiblyUnbound { .. }) => {
@@ -4987,7 +5035,7 @@ impl<'db> TypeInferenceBuilder<'db> {
}
return ty
.try_call(self.db(), &CallArguments::positional([value_ty, slice_ty]))
.try_call(self.db(), CallArguments::positional(self.db(),[value_ty, slice_ty]))
.map(|outcome| outcome.return_type(self.db()))
.unwrap_or_else(|err| {
self.context.report_lint(
@@ -6629,4 +6677,93 @@ mod tests {
Ok(())
}
/// This test verifies that changing a class's declaration in a non-meaningful way (e.g. by adding a comment)
/// doesn't trigger type inference for expressions that depend on the class's members.
#[test]
fn dependency_own_instance_member() -> anyhow::Result<()> {
fn x_rhs_expression(db: &TestDb) -> Expression<'_> {
let file_main = system_path_to_file(db, "/src/main.py").unwrap();
let ast = parsed_module(db, file_main);
// Get the second statement in `main.py` (x = …) and extract the expression
// node on the right-hand side:
let x_rhs_node = &ast.syntax().body[1].as_assign_stmt().unwrap().value;
let index = semantic_index(db, file_main);
index.expression(x_rhs_node.as_ref())
}
let mut db = setup_db();
db.write_dedented(
"/src/mod.py",
r#"
class C:
if random.choice([True, False]):
attr: int = 42
else:
attr: None = None
"#,
)?;
db.write_dedented(
"/src/main.py",
r#"
from mod import C
x = C().attr
"#,
)?;
let file_main = system_path_to_file(&db, "/src/main.py").unwrap();
let attr_ty = global_symbol(&db, file_main, "x").expect_type();
assert_eq!(attr_ty.display(&db).to_string(), "Unknown | int | None");
// Change the type of `attr` to `str | None`; this should trigger the type of `x` to be re-inferred
db.write_dedented(
"/src/mod.py",
r#"
class C:
if random.choice([True, False]):
attr: str = "42"
else:
attr: None = None
"#,
)?;
let events = {
db.clear_salsa_events();
let attr_ty = global_symbol(&db, file_main, "x").expect_type();
assert_eq!(attr_ty.display(&db).to_string(), "Unknown | str | None");
db.take_salsa_events()
};
assert_function_query_was_run(&db, infer_expression_types, x_rhs_expression(&db), &events);
// Add a comment; this should not trigger the type of `x` to be re-inferred
db.write_dedented(
"/src/mod.py",
r#"
class C:
# comment
if random.choice([True, False]):
attr: str = "42"
else:
attr: None = None
"#,
)?;
let events = {
db.clear_salsa_events();
let attr_ty = global_symbol(&db, file_main, "x").expect_type();
assert_eq!(attr_ty.display(&db).to_string(), "Unknown | str | None");
db.take_salsa_events()
};
assert_function_query_was_not_run(
&db,
infer_expression_types,
x_rhs_expression(&db),
&events,
);
Ok(())
}
}

View File

@@ -6,6 +6,7 @@ use crate::semantic_index::definition::Definition;
use crate::semantic_index::expression::Expression;
use crate::semantic_index::symbol::{ScopeId, ScopedSymbolId, SymbolTable};
use crate::semantic_index::symbol_table;
use crate::types::infer::infer_same_file_expression_type;
use crate::types::{
infer_expression_types, ClassLiteralType, IntersectionBuilder, KnownClass, KnownFunction,
SubclassOfType, Truthiness, Type, UnionBuilder,
@@ -497,11 +498,8 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
if let Some(ast::ExprName { id, .. }) = subject.node_ref(self.db).as_name_expr() {
// SAFETY: we should always have a symbol for every Name node.
let symbol = self.symbols().symbol_id_by_name(id).unwrap();
let scope = self.scope();
let inference = infer_expression_types(self.db, cls);
let ty = inference
.expression_type(cls.node_ref(self.db).scoped_expression_id(self.db, scope))
.to_instance(self.db);
let ty = infer_same_file_expression_type(self.db, cls).to_instance(self.db);
let mut constraints = NarrowingConstraints::default();
constraints.insert(symbol, ty);
Some(constraints)

View File

@@ -178,11 +178,8 @@ use std::cmp::Ordering;
use ruff_index::{Idx, IndexVec};
use rustc_hash::FxHashMap;
use crate::semantic_index::{
ast_ids::HasScopedExpressionId,
constraint::{Constraint, ConstraintNode, PatternConstraintKind},
};
use crate::types::{infer_expression_types, Truthiness};
use crate::semantic_index::constraint::{Constraint, ConstraintNode, PatternConstraintKind};
use crate::types::{infer_expression_type, Truthiness};
use crate::Db;
/// A ternary formula that defines under what conditions a binding is visible. (A ternary formula
@@ -617,28 +614,14 @@ impl<'db> VisibilityConstraints<'db> {
fn analyze_single(db: &dyn Db, constraint: &Constraint) -> Truthiness {
match constraint.node {
ConstraintNode::Expression(test_expr) => {
let inference = infer_expression_types(db, test_expr);
let scope = test_expr.scope(db);
let ty = inference
.expression_type(test_expr.node_ref(db).scoped_expression_id(db, scope));
let ty = infer_expression_type(db, test_expr);
ty.bool(db).negate_if(!constraint.is_positive)
}
ConstraintNode::Pattern(inner) => match inner.kind(db) {
PatternConstraintKind::Value(value, guard) => {
let subject_expression = inner.subject(db);
let inference = infer_expression_types(db, subject_expression);
let scope = subject_expression.scope(db);
let subject_ty = inference.expression_type(
subject_expression
.node_ref(db)
.scoped_expression_id(db, scope),
);
let inference = infer_expression_types(db, *value);
let scope = value.scope(db);
let value_ty = inference
.expression_type(value.node_ref(db).scoped_expression_id(db, scope));
let subject_ty = infer_expression_type(db, subject_expression);
let value_ty = infer_expression_type(db, *value);
if subject_ty.is_single_valued(db) {
let truthiness =