[ty] Reduce the overwhelming complexity of TypeInferenceBuilder::infer_call_expression (#18943)
Some checks are pending
CI / cargo clippy (push) Blocked by required conditions
CI / cargo build (msrv) (push) Blocked by required conditions
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks-instrumented (push) Blocked by required conditions
CI / benchmarks-walltime (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run

## Summary

This function is huge, and hugely indented. This PR breaks most of it
out into two helper functions: `KnownFunction::check_call()` and
`KnownClass::check_call`.

My immediate motivation is that we need to add yet more special cases to
this function in order to properly handle `tuple` instantiations and
instantiations of tuple subclasses. But I really don't relish the
thought of doing that with the function's current structure 😆

## Test Plan

Existing tests all pass. No new ones are added; this is a pure refactor
that should have no functional change.
This commit is contained in:
Alex Waygood 2025-06-25 21:10:55 +01:00 committed by GitHub
parent c77e72ea1a
commit 4a5715b97a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 592 additions and 646 deletions

View file

@ -9,14 +9,20 @@ use super::{
function::{FunctionDecorators, FunctionType},
infer_expression_type, infer_unpack_types,
};
use crate::semantic_index::DeclarationWithConstraint;
use crate::semantic_index::definition::{Definition, DefinitionState};
use crate::semantic_index::place::NodeWithScopeKind;
use crate::semantic_index::{DeclarationWithConstraint, SemanticIndex};
use crate::types::context::InferContext;
use crate::types::diagnostic::{INVALID_LEGACY_TYPE_VARIABLE, INVALID_TYPE_ALIAS_TYPE};
use crate::types::function::{DataclassTransformerParams, KnownFunction};
use crate::types::generics::{GenericContext, Specialization};
use crate::types::infer::nearest_enclosing_class;
use crate::types::signatures::{CallableSignature, Parameter, Parameters, Signature};
use crate::types::tuple::TupleType;
use crate::types::{
CallableType, DataclassParams, KnownInstanceType, TypeMapping, TypeRelation, TypeVarInstance,
BareTypeAliasType, Binding, BoundSuperError, BoundSuperType, CallableType, DataclassParams,
KnownInstanceType, TypeAliasType, TypeMapping, TypeRelation, TypeVarBoundOrConstraints,
TypeVarInstance, TypeVarKind, infer_definition_types,
};
use crate::{
Db, FxOrderSet, KnownModule, Program,
@ -2330,7 +2336,7 @@ pub enum KnownClass {
NamedTupleFallback,
}
impl<'db> KnownClass {
impl KnownClass {
pub(crate) const fn is_bool(self) -> bool {
matches!(self, Self::Bool)
}
@ -2571,7 +2577,7 @@ impl<'db> KnownClass {
}
}
pub(crate) fn name(self, db: &'db dyn Db) -> &'static str {
pub(crate) fn name(self, db: &dyn Db) -> &'static str {
match self {
Self::Any => "Any",
Self::Bool => "bool",
@ -2649,7 +2655,7 @@ impl<'db> KnownClass {
}
}
pub(super) fn display(self, db: &'db dyn Db) -> impl std::fmt::Display + 'db {
pub(super) fn display(self, db: &dyn Db) -> impl std::fmt::Display + '_ {
struct KnownClassDisplay<'db> {
db: &'db dyn Db,
class: KnownClass,
@ -2677,7 +2683,7 @@ impl<'db> KnownClass {
/// representing all possible instances of the class.
///
/// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this.
pub(crate) fn to_instance(self, db: &'db dyn Db) -> Type<'db> {
pub(crate) fn to_instance(self, db: &dyn Db) -> Type {
self.to_class_literal(db)
.to_class_type(db)
.map(|class| Type::instance(db, class))
@ -2689,7 +2695,7 @@ impl<'db> KnownClass {
///
/// If the class cannot be found in typeshed, or if you provide a specialization with the wrong
/// number of types, a debug-level log message will be emitted stating this.
pub(crate) fn to_specialized_class_type(
pub(crate) fn to_specialized_class_type<'db>(
self,
db: &'db dyn Db,
specialization: impl IntoIterator<Item = Type<'db>>,
@ -2722,7 +2728,7 @@ impl<'db> KnownClass {
///
/// If the class cannot be found in typeshed, or if you provide a specialization with the wrong
/// number of types, a debug-level log message will be emitted stating this.
pub(crate) fn to_specialized_instance(
pub(crate) fn to_specialized_instance<'db>(
self,
db: &'db dyn Db,
specialization: impl IntoIterator<Item = Type<'db>>,
@ -2738,8 +2744,8 @@ impl<'db> KnownClass {
/// or if the symbol is not a class definition, or if the symbol is possibly unbound.
fn try_to_class_literal_without_logging(
self,
db: &'db dyn Db,
) -> Result<ClassLiteral<'db>, KnownClassLookupError<'db>> {
db: &dyn Db,
) -> Result<ClassLiteral, KnownClassLookupError> {
let symbol = known_module_symbol(db, self.canonical_module(db), self.name(db)).place;
match symbol {
Place::Type(Type::ClassLiteral(class_literal), Boundness::Bound) => Ok(class_literal),
@ -2756,7 +2762,7 @@ impl<'db> KnownClass {
/// Lookup a [`KnownClass`] in typeshed and return a [`Type`] representing that class-literal.
///
/// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this.
pub(crate) fn try_to_class_literal(self, db: &'db dyn Db) -> Option<ClassLiteral<'db>> {
pub(crate) fn try_to_class_literal(self, db: &dyn Db) -> Option<ClassLiteral> {
// a cache of the `KnownClass`es that we have already failed to lookup in typeshed
// (and therefore that we've already logged a warning for)
static MESSAGES: LazyLock<Mutex<FxHashSet<KnownClass>>> = LazyLock::new(Mutex::default);
@ -2791,7 +2797,7 @@ impl<'db> KnownClass {
/// Lookup a [`KnownClass`] in typeshed and return a [`Type`] representing that class-literal.
///
/// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this.
pub(crate) fn to_class_literal(self, db: &'db dyn Db) -> Type<'db> {
pub(crate) fn to_class_literal(self, db: &dyn Db) -> Type {
self.try_to_class_literal(db)
.map(Type::ClassLiteral)
.unwrap_or_else(Type::unknown)
@ -2801,7 +2807,7 @@ impl<'db> KnownClass {
/// representing that class and all possible subclasses of the class.
///
/// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this.
pub(crate) fn to_subclass_of(self, db: &'db dyn Db) -> Type<'db> {
pub(crate) fn to_subclass_of(self, db: &dyn Db) -> Type {
self.to_class_literal(db)
.to_class_type(db)
.map(|class| SubclassOfType::from(db, class))
@ -2810,13 +2816,13 @@ impl<'db> KnownClass {
/// Return `true` if this symbol can be resolved to a class definition `class` in typeshed,
/// *and* `class` is a subclass of `other`.
pub(super) fn is_subclass_of(self, db: &'db dyn Db, other: ClassType<'db>) -> bool {
pub(super) fn is_subclass_of<'db>(self, db: &'db dyn Db, other: ClassType<'db>) -> bool {
self.try_to_class_literal_without_logging(db)
.is_ok_and(|class| class.is_subclass_of(db, None, other))
}
/// Return the module in which we should look up the definition for this class
fn canonical_module(self, db: &'db dyn Db) -> KnownModule {
fn canonical_module(self, db: &dyn Db) -> KnownModule {
match self {
Self::Bool
| Self::Object
@ -3114,7 +3120,7 @@ impl<'db> KnownClass {
}
/// Return `true` if the module of `self` matches `module`
fn check_module(self, db: &'db dyn Db, module: KnownModule) -> bool {
fn check_module(self, db: &dyn Db, module: KnownModule) -> bool {
match self {
Self::Any
| Self::Bool
@ -3177,6 +3183,278 @@ impl<'db> KnownClass {
| Self::NewType => matches!(module, KnownModule::Typing | KnownModule::TypingExtensions),
}
}
/// Evaluate a call to this known class, and emit any diagnostics that are necessary
/// as a result of the call.
pub(super) fn check_call<'db>(
self,
context: &InferContext<'db, '_>,
index: &SemanticIndex<'db>,
overload_binding: &mut Binding<'db>,
call_argument_types: &CallArgumentTypes<'_, 'db>,
call_expression: &ast::ExprCall,
) {
let db = context.db();
let scope = context.scope();
let module = context.module();
match self {
KnownClass::Super => {
// Handle the case where `super()` is called with no arguments.
// In this case, we need to infer the two arguments:
// 1. The nearest enclosing class
// 2. The first parameter of the current function (typically `self` or `cls`)
match overload_binding.parameter_types() {
[] => {
let Some(enclosing_class) =
nearest_enclosing_class(db, index, scope, module)
else {
overload_binding.set_return_type(Type::unknown());
BoundSuperError::UnavailableImplicitArguments
.report_diagnostic(context, call_expression.into());
return;
};
// The type of the first parameter if the given scope is function-like (i.e. function or lambda).
// `None` if the scope is not function-like, or has no parameters.
let first_param = match scope.node(db) {
NodeWithScopeKind::Function(f) => {
f.node(module).parameters.iter().next()
}
NodeWithScopeKind::Lambda(l) => l
.node(module)
.parameters
.as_ref()
.into_iter()
.flatten()
.next(),
_ => None,
};
let Some(first_param) = first_param else {
overload_binding.set_return_type(Type::unknown());
BoundSuperError::UnavailableImplicitArguments
.report_diagnostic(context, call_expression.into());
return;
};
let definition = index.expect_single_definition(first_param);
let first_param =
infer_definition_types(db, definition).binding_type(definition);
let bound_super = BoundSuperType::build(
db,
Type::ClassLiteral(enclosing_class),
first_param,
)
.unwrap_or_else(|err| {
err.report_diagnostic(context, call_expression.into());
Type::unknown()
});
overload_binding.set_return_type(bound_super);
}
[Some(pivot_class_type), Some(owner_type)] => {
let bound_super = BoundSuperType::build(db, *pivot_class_type, *owner_type)
.unwrap_or_else(|err| {
err.report_diagnostic(context, call_expression.into());
Type::unknown()
});
overload_binding.set_return_type(bound_super);
}
_ => {}
}
}
KnownClass::TypeVar => {
let assigned_to = index
.try_expression(ast::ExprRef::from(call_expression))
.and_then(|expr| expr.assigned_to(db));
let Some(target) = assigned_to.as_ref().and_then(|assigned_to| {
match assigned_to.node(module).targets.as_slice() {
[ast::Expr::Name(target)] => Some(target),
_ => None,
}
}) else {
if let Some(builder) =
context.report_lint(&INVALID_LEGACY_TYPE_VARIABLE, call_expression)
{
builder.into_diagnostic(
"A legacy `typing.TypeVar` must be immediately assigned to a variable",
);
}
return;
};
let [
Some(name_param),
constraints,
bound,
default,
contravariant,
covariant,
_infer_variance,
] = overload_binding.parameter_types()
else {
return;
};
let covariant = covariant
.map(|ty| ty.bool(db))
.unwrap_or(Truthiness::AlwaysFalse);
let contravariant = contravariant
.map(|ty| ty.bool(db))
.unwrap_or(Truthiness::AlwaysFalse);
let variance = match (contravariant, covariant) {
(Truthiness::Ambiguous, _) => {
let Some(builder) =
context.report_lint(&INVALID_LEGACY_TYPE_VARIABLE, call_expression)
else {
return;
};
builder.into_diagnostic(
"The `contravariant` parameter of a legacy `typing.TypeVar` \
cannot have an ambiguous value",
);
return;
}
(_, Truthiness::Ambiguous) => {
let Some(builder) =
context.report_lint(&INVALID_LEGACY_TYPE_VARIABLE, call_expression)
else {
return;
};
builder.into_diagnostic(
"The `covariant` parameter of a legacy `typing.TypeVar` \
cannot have an ambiguous value",
);
return;
}
(Truthiness::AlwaysTrue, Truthiness::AlwaysTrue) => {
let Some(builder) =
context.report_lint(&INVALID_LEGACY_TYPE_VARIABLE, call_expression)
else {
return;
};
builder.into_diagnostic(
"A legacy `typing.TypeVar` cannot be both covariant and contravariant",
);
return;
}
(Truthiness::AlwaysTrue, Truthiness::AlwaysFalse) => {
TypeVarVariance::Contravariant
}
(Truthiness::AlwaysFalse, Truthiness::AlwaysTrue) => TypeVarVariance::Covariant,
(Truthiness::AlwaysFalse, Truthiness::AlwaysFalse) => {
TypeVarVariance::Invariant
}
};
let name_param = name_param.into_string_literal().map(|name| name.value(db));
if name_param.is_none_or(|name_param| name_param != target.id) {
let Some(builder) =
context.report_lint(&INVALID_LEGACY_TYPE_VARIABLE, call_expression)
else {
return;
};
builder.into_diagnostic(format_args!(
"The name of a legacy `typing.TypeVar`{} must match \
the name of the variable it is assigned to (`{}`)",
if let Some(name_param) = name_param {
format!(" (`{name_param}`)")
} else {
String::new()
},
target.id,
));
return;
}
let bound_or_constraint = match (bound, constraints) {
(Some(bound), None) => Some(TypeVarBoundOrConstraints::UpperBound(*bound)),
(None, Some(_constraints)) => {
// We don't use UnionType::from_elements or UnionBuilder here,
// because we don't want to simplify the list of constraints like
// we do with the elements of an actual union type.
// TODO: Consider using a new `OneOfType` connective here instead,
// since that more accurately represents the actual semantics of
// typevar constraints.
let elements = UnionType::new(
db,
overload_binding
.arguments_for_parameter(call_argument_types, 1)
.map(|(_, ty)| ty)
.collect::<Box<_>>(),
);
Some(TypeVarBoundOrConstraints::Constraints(elements))
}
// TODO: Emit a diagnostic that TypeVar cannot be both bounded and
// constrained
(Some(_), Some(_)) => return,
(None, None) => None,
};
let containing_assignment = index.expect_single_definition(target);
overload_binding.set_return_type(Type::KnownInstance(KnownInstanceType::TypeVar(
TypeVarInstance::new(
db,
target.id.clone(),
Some(containing_assignment),
bound_or_constraint,
variance,
*default,
TypeVarKind::Legacy,
),
)));
}
KnownClass::TypeAliasType => {
let assigned_to = index
.try_expression(ast::ExprRef::from(call_expression))
.and_then(|expr| expr.assigned_to(db));
let containing_assignment = assigned_to.as_ref().and_then(|assigned_to| {
match assigned_to.node(module).targets.as_slice() {
[ast::Expr::Name(target)] => Some(index.expect_single_definition(target)),
_ => None,
}
});
let [Some(name), Some(value), ..] = overload_binding.parameter_types() else {
return;
};
if let Some(name) = name.into_string_literal() {
overload_binding.set_return_type(Type::KnownInstance(
KnownInstanceType::TypeAliasType(TypeAliasType::Bare(
BareTypeAliasType::new(
db,
ast::name::Name::new(name.value(db)),
containing_assignment,
value,
),
)),
));
} else if let Some(builder) =
context.report_lint(&INVALID_TYPE_ALIAS_TYPE, call_expression)
{
builder.into_diagnostic(
"The name of a `typing.TypeAlias` must be a string literal",
);
}
}
_ => {}
}
}
}
/// Enumeration of ways in which looking up a [`KnownClass`] in typeshed could fail.

View file

@ -68,6 +68,10 @@ impl<'db, 'ast> InferContext<'db, 'ast> {
self.module
}
pub(crate) fn scope(&self) -> ScopeId<'db> {
self.scope
}
/// Create a span with the range of the given expression
/// in the file being currently type checked.
///

View file

@ -52,7 +52,7 @@
use std::str::FromStr;
use bitflags::bitflags;
use ruff_db::diagnostic::Span;
use ruff_db::diagnostic::{Annotation, DiagnosticId, Severity, Span};
use ruff_db::files::{File, FileRange};
use ruff_db::parsed::{ParsedModuleRef, parsed_module};
use ruff_python_ast as ast;
@ -64,11 +64,18 @@ use crate::semantic_index::ast_ids::HasScopedUseId;
use crate::semantic_index::definition::Definition;
use crate::semantic_index::place::ScopeId;
use crate::semantic_index::semantic_index;
use crate::types::context::InferContext;
use crate::types::diagnostic::{
REDUNDANT_CAST, STATIC_ASSERT_ERROR, TYPE_ASSERTION_FAILURE,
report_bad_argument_to_get_protocol_members,
report_runtime_check_against_non_runtime_checkable_protocol,
};
use crate::types::generics::GenericContext;
use crate::types::narrow::ClassInfoConstraintFunction;
use crate::types::signatures::{CallableSignature, Signature};
use crate::types::{
BoundMethodType, CallableType, Type, TypeMapping, TypeRelation, TypeVarInstance,
Binding, BoundMethodType, CallableType, DynamicType, Type, TypeMapping, TypeRelation,
TypeVarInstance,
};
use crate::{Db, FxOrderSet};
@ -942,6 +949,199 @@ impl KnownFunction {
| Self::AllMembers => module.is_ty_extensions(),
}
}
/// Evaluate a call to this known function, and emit any diagnostics that are necessary
/// as a result of the call.
pub(super) fn check_call(
self,
context: &InferContext,
overload_binding: &mut Binding,
call_expression: &ast::ExprCall,
) {
let db = context.db();
match self {
KnownFunction::RevealType => {
let [Some(revealed_type)] = overload_binding.parameter_types() else {
return;
};
let Some(builder) =
context.report_diagnostic(DiagnosticId::RevealedType, Severity::Info)
else {
return;
};
let mut diag = builder.into_diagnostic("Revealed type");
let span = context.span(&call_expression.arguments.args[0]);
diag.annotate(
Annotation::primary(span)
.message(format_args!("`{}`", revealed_type.display(db))),
);
}
KnownFunction::AssertType => {
let [Some(actual_ty), Some(asserted_ty)] = overload_binding.parameter_types()
else {
return;
};
if actual_ty.is_equivalent_to(db, *asserted_ty) {
return;
}
let Some(builder) = context.report_lint(&TYPE_ASSERTION_FAILURE, call_expression)
else {
return;
};
let mut diagnostic = builder.into_diagnostic(format_args!(
"Argument does not have asserted type `{}`",
asserted_ty.display(db),
));
diagnostic.annotate(
Annotation::secondary(context.span(&call_expression.arguments.args[0]))
.message(format_args!(
"Inferred type of argument is `{}`",
actual_ty.display(db),
)),
);
diagnostic.info(format_args!(
"`{asserted_type}` and `{inferred_type}` are not equivalent types",
asserted_type = asserted_ty.display(db),
inferred_type = actual_ty.display(db),
));
}
KnownFunction::AssertNever => {
let [Some(actual_ty)] = overload_binding.parameter_types() else {
return;
};
if actual_ty.is_equivalent_to(db, Type::Never) {
return;
}
let Some(builder) = context.report_lint(&TYPE_ASSERTION_FAILURE, call_expression)
else {
return;
};
let mut diagnostic =
builder.into_diagnostic("Argument does not have asserted type `Never`");
diagnostic.annotate(
Annotation::secondary(context.span(&call_expression.arguments.args[0]))
.message(format_args!(
"Inferred type of argument is `{}`",
actual_ty.display(db)
)),
);
diagnostic.info(format_args!(
"`Never` and `{inferred_type}` are not equivalent types",
inferred_type = actual_ty.display(db),
));
}
KnownFunction::StaticAssert => {
let [Some(parameter_ty), message] = overload_binding.parameter_types() else {
return;
};
let truthiness = match parameter_ty.try_bool(db) {
Ok(truthiness) => truthiness,
Err(err) => {
let condition = call_expression
.arguments
.find_argument("condition", 0)
.map(|argument| match argument {
ruff_python_ast::ArgOrKeyword::Arg(expr) => {
ast::AnyNodeRef::from(expr)
}
ruff_python_ast::ArgOrKeyword::Keyword(keyword) => {
ast::AnyNodeRef::from(keyword)
}
})
.unwrap_or(ast::AnyNodeRef::from(call_expression));
err.report_diagnostic(context, condition);
return;
}
};
let Some(builder) = context.report_lint(&STATIC_ASSERT_ERROR, call_expression)
else {
return;
};
if truthiness.is_always_true() {
return;
}
if let Some(message) = message
.and_then(Type::into_string_literal)
.map(|s| s.value(db))
{
builder.into_diagnostic(format_args!("Static assertion error: {message}"));
} else if *parameter_ty == Type::BooleanLiteral(false) {
builder
.into_diagnostic("Static assertion error: argument evaluates to `False`");
} else if truthiness.is_always_false() {
builder.into_diagnostic(format_args!(
"Static assertion error: argument of type `{parameter_ty}` \
is statically known to be falsy",
parameter_ty = parameter_ty.display(db)
));
} else {
builder.into_diagnostic(format_args!(
"Static assertion error: argument of type `{parameter_ty}` \
has an ambiguous static truthiness",
parameter_ty = parameter_ty.display(db)
));
}
}
KnownFunction::Cast => {
let [Some(casted_type), Some(source_type)] = overload_binding.parameter_types()
else {
return;
};
let contains_unknown_or_todo =
|ty| matches!(ty, Type::Dynamic(dynamic) if dynamic != DynamicType::Any);
if source_type.is_equivalent_to(db, *casted_type)
&& !casted_type.any_over_type(db, &|ty| contains_unknown_or_todo(ty))
&& !source_type.any_over_type(db, &|ty| contains_unknown_or_todo(ty))
{
let Some(builder) = context.report_lint(&REDUNDANT_CAST, call_expression)
else {
return;
};
builder.into_diagnostic(format_args!(
"Value is already of type `{}`",
casted_type.display(db),
));
}
}
KnownFunction::GetProtocolMembers => {
let [Some(Type::ClassLiteral(class))] = overload_binding.parameter_types() else {
return;
};
if class.is_protocol(db) {
return;
}
report_bad_argument_to_get_protocol_members(context, call_expression, *class);
}
KnownFunction::IsInstance | KnownFunction::IsSubclass => {
let [_, Some(Type::ClassLiteral(class))] = overload_binding.parameter_types()
else {
return;
};
let Some(protocol_class) = class.into_protocol_class(db) else {
return;
};
if protocol_class.is_runtime_checkable(db) {
return;
}
report_runtime_check_against_non_runtime_checkable_protocol(
context,
call_expression,
protocol_class,
self,
);
}
_ => {}
}
}
}
#[cfg(test)]

View file

@ -35,7 +35,6 @@
//! be considered a bug.)
use itertools::{Either, Itertools};
use ruff_db::diagnostic::{Annotation, DiagnosticId, Severity};
use ruff_db::files::File;
use ruff_db::parsed::{ParsedModuleRef, parsed_module};
use ruff_python_ast::visitor::{Visitor, walk_expr};
@ -79,16 +78,15 @@ use crate::types::diagnostic::{
self, CALL_NON_CALLABLE, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS,
CYCLIC_CLASS_DEFINITION, DIVISION_BY_ZERO, DUPLICATE_KW_ONLY, INCONSISTENT_MRO,
INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE,
INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_LEGACY_TYPE_VARIABLE,
INVALID_PARAMETER_DEFAULT, INVALID_TYPE_ALIAS_TYPE, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL,
INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases, POSSIBLY_UNBOUND_IMPLICIT_CALL,
POSSIBLY_UNBOUND_IMPORT, TypeCheckDiagnostics, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE,
UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, report_implicit_return_type,
report_instance_layout_conflict, report_invalid_argument_number_to_special_form,
report_invalid_arguments_to_annotated, report_invalid_arguments_to_callable,
report_invalid_assignment, report_invalid_attribute_assignment,
report_invalid_generator_function_return_type, report_invalid_return_type,
report_possibly_unbound_attribute,
INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_PARAMETER_DEFAULT, INVALID_TYPE_FORM,
INVALID_TYPE_GUARD_CALL, INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases,
POSSIBLY_UNBOUND_IMPLICIT_CALL, POSSIBLY_UNBOUND_IMPORT, TypeCheckDiagnostics,
UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE,
UNSUPPORTED_OPERATOR, report_implicit_return_type, report_instance_layout_conflict,
report_invalid_argument_number_to_special_form, report_invalid_arguments_to_annotated,
report_invalid_arguments_to_callable, report_invalid_assignment,
report_invalid_attribute_assignment, report_invalid_generator_function_return_type,
report_invalid_return_type, report_possibly_unbound_attribute,
};
use crate::types::function::{
FunctionDecorators, FunctionLiteral, FunctionType, KnownFunction, OverloadLiteral,
@ -99,11 +97,11 @@ use crate::types::signatures::{CallableSignature, Signature};
use crate::types::tuple::{TupleSpec, TupleType};
use crate::types::unpacker::{UnpackResult, Unpacker};
use crate::types::{
BareTypeAliasType, CallDunderError, CallableType, ClassLiteral, ClassType, DataclassParams,
DynamicType, IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType,
LintDiagnosticGuard, MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType, Parameter,
ParameterForm, Parameters, SpecialFormType, StringLiteralType, SubclassOfType, Truthiness,
Type, TypeAliasType, TypeAndQualifiers, TypeArrayDisplay, TypeIsType, TypeQualifiers,
CallDunderError, CallableType, ClassLiteral, ClassType, DataclassParams, DynamicType,
IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType, LintDiagnosticGuard,
MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType, Parameter, ParameterForm,
Parameters, SpecialFormType, StringLiteralType, SubclassOfType, Truthiness, Type,
TypeAliasType, TypeAndQualifiers, TypeArrayDisplay, TypeIsType, TypeQualifiers,
TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind, TypeVarVariance, UnionBuilder,
UnionType, binding_type, todo_type,
};
@ -113,25 +111,19 @@ use crate::{Db, FxOrderSet, Program};
use super::context::{InNoTypeCheck, InferContext};
use super::diagnostic::{
INVALID_METACLASS, INVALID_OVERLOAD, INVALID_PROTOCOL, REDUNDANT_CAST, STATIC_ASSERT_ERROR,
SUBCLASS_OF_FINAL_CLASS, TYPE_ASSERTION_FAILURE,
INVALID_METACLASS, INVALID_OVERLOAD, INVALID_PROTOCOL, SUBCLASS_OF_FINAL_CLASS,
hint_if_stdlib_submodule_exists_on_other_versions, report_attempted_protocol_instantiation,
report_bad_argument_to_get_protocol_members, report_duplicate_bases,
report_index_out_of_bounds, report_invalid_exception_caught, report_invalid_exception_cause,
report_invalid_exception_raised, report_invalid_or_unsupported_base,
report_invalid_type_checking_constant, report_non_subscriptable,
report_possibly_unresolved_reference,
report_runtime_check_against_non_runtime_checkable_protocol, report_slice_step_size_zero,
report_duplicate_bases, report_index_out_of_bounds, report_invalid_exception_caught,
report_invalid_exception_cause, report_invalid_exception_raised,
report_invalid_or_unsupported_base, report_invalid_type_checking_constant,
report_non_subscriptable, report_possibly_unresolved_reference, report_slice_step_size_zero,
};
use super::generics::LegacyGenericBase;
use super::string_annotation::{
BYTE_STRING_TYPE_ANNOTATION, FSTRING_TYPE_ANNOTATION, parse_string_annotation,
};
use super::subclass_of::SubclassOfInner;
use super::{
BoundSuperError, BoundSuperType, ClassBase, NominalInstanceType,
add_inferred_python_version_hint_to_diagnostic,
};
use super::{ClassBase, NominalInstanceType, add_inferred_python_version_hint_to_diagnostic};
/// Infer all types for a [`ScopeId`], including all definitions and expressions in that scope.
/// Use when checking a scope, or needing to provide a type for an arbitrary expression in the
@ -4682,9 +4674,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
ast::Expr::Named(named) => self.infer_named_expression(named),
ast::Expr::If(if_expression) => self.infer_if_expression(if_expression),
ast::Expr::Lambda(lambda_expression) => self.infer_lambda_expression(lambda_expression),
ast::Expr::Call(call_expression) => {
self.infer_call_expression(expression, call_expression)
}
ast::Expr::Call(call_expression) => self.infer_call_expression(call_expression),
ast::Expr::Starred(starred) => self.infer_starred_expression(starred),
ast::Expr::Yield(yield_expression) => self.infer_yield_expression(yield_expression),
ast::Expr::YieldFrom(yield_from) => self.infer_yield_from_expression(yield_from),
@ -5292,27 +5282,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
CallableType::function_like(self.db(), Signature::new(parameters, Some(Type::unknown())))
}
/// Returns the type of the first parameter if the given scope is function-like (i.e. function or lambda).
/// Returns `None` if the scope is not function-like, or has no parameters.
fn first_param_type_in_scope(&self, scope: ScopeId) -> Option<Type<'db>> {
let first_param = match scope.node(self.db()) {
NodeWithScopeKind::Function(f) => f.node(self.module()).parameters.iter().next(),
NodeWithScopeKind::Lambda(l) => {
l.node(self.module()).parameters.as_ref()?.iter().next()
}
_ => None,
}?;
let definition = self.index.expect_single_definition(first_param);
Some(infer_definition_types(self.db(), definition).binding_type(definition))
}
fn infer_call_expression(
&mut self,
call_expression_node: &ast::Expr,
call_expression: &ast::ExprCall,
) -> Type<'db> {
fn infer_call_expression(&mut self, call_expression: &ast::ExprCall) -> Type<'db> {
let ast::ExprCall {
range: _,
node_index: _,
@ -5412,584 +5382,78 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let call_argument_types =
self.infer_argument_types(arguments, call_arguments, &bindings.argument_forms);
match bindings.check_types(self.db(), &call_argument_types) {
Ok(mut bindings) => {
for binding in &mut bindings {
let binding_type = binding.callable_type;
for (_, overload) in binding.matching_overloads_mut() {
match binding_type {
Type::FunctionLiteral(function_literal) => {
let Some(known_function) = function_literal.known(self.db()) else {
continue;
};
match known_function {
KnownFunction::RevealType => {
if let [Some(revealed_type)] = overload.parameter_types() {
if let Some(builder) = self.context.report_diagnostic(
DiagnosticId::RevealedType,
Severity::Info,
) {
let mut diag =
builder.into_diagnostic("Revealed type");
let span = self
.context
.span(&call_expression.arguments.args[0]);
diag.annotate(Annotation::primary(span).message(
format_args!(
"`{}`",
revealed_type.display(self.db())
),
));
}
}
}
KnownFunction::AssertType => {
if let [Some(actual_ty), Some(asserted_ty)] =
overload.parameter_types()
{
if !actual_ty.is_equivalent_to(self.db(), *asserted_ty)
{
if let Some(builder) = self.context.report_lint(
&TYPE_ASSERTION_FAILURE,
call_expression,
) {
let mut diagnostic =
builder.into_diagnostic(format_args!(
"Argument does not have asserted type `{}`",
asserted_ty.display(self.db()),
));
diagnostic.annotate(
Annotation::secondary(self.context.span(
&call_expression.arguments.args[0],
))
.message(format_args!(
"Inferred type of argument is `{}`",
actual_ty.display(self.db()),
)),
);
diagnostic.info(
format_args!(
"`{asserted_type}` and `{inferred_type}` are not equivalent types",
asserted_type = asserted_ty.display(self.db()),
inferred_type = actual_ty.display(self.db()),
)
);
}
}
}
}
KnownFunction::AssertNever => {
if let [Some(actual_ty)] = overload.parameter_types() {
if !actual_ty.is_equivalent_to(self.db(), Type::Never) {
if let Some(builder) = self.context.report_lint(
&TYPE_ASSERTION_FAILURE,
call_expression,
) {
let mut diagnostic = builder.into_diagnostic(
"Argument does not have asserted type `Never`",
);
diagnostic.annotate(
Annotation::secondary(self.context.span(
&call_expression.arguments.args[0],
))
.message(format_args!(
"Inferred type of argument is `{}`",
actual_ty.display(self.db())
)),
);
diagnostic.info(
format_args!(
"`Never` and `{inferred_type}` are not equivalent types",
inferred_type = actual_ty.display(self.db()),
)
);
}
}
}
}
KnownFunction::StaticAssert => {
if let [Some(parameter_ty), message] =
overload.parameter_types()
{
let truthiness = match parameter_ty.try_bool(self.db())
{
Ok(truthiness) => truthiness,
Err(err) => {
let condition = arguments
.find_argument("condition", 0)
.map(|argument| {
match argument {
ruff_python_ast::ArgOrKeyword::Arg(
expr,
) => ast::AnyNodeRef::from(expr),
ruff_python_ast::ArgOrKeyword::Keyword(
keyword,
) => ast::AnyNodeRef::from(keyword),
}
})
.unwrap_or(ast::AnyNodeRef::from(
call_expression,
));
err.report_diagnostic(&self.context, condition);
continue;
}
};
if let Some(builder) = self
.context
.report_lint(&STATIC_ASSERT_ERROR, call_expression)
{
if !truthiness.is_always_true() {
if let Some(message) = message
.and_then(Type::into_string_literal)
.map(|s| s.value(self.db()))
{
builder.into_diagnostic(format_args!(
"Static assertion error: {message}"
));
} else if *parameter_ty
== Type::BooleanLiteral(false)
{
builder.into_diagnostic(
"Static assertion error: \
argument evaluates to `False`",
);
} else if truthiness.is_always_false() {
builder.into_diagnostic(format_args!(
"Static assertion error: \
argument of type `{parameter_ty}` \
is statically known to be falsy",
parameter_ty =
parameter_ty.display(self.db())
));
} else {
builder.into_diagnostic(format_args!(
"Static assertion error: \
argument of type `{parameter_ty}` \
has an ambiguous static truthiness",
parameter_ty =
parameter_ty.display(self.db())
));
}
}
}
}
}
KnownFunction::Cast => {
if let [Some(casted_type), Some(source_type)] =
overload.parameter_types()
{
let db = self.db();
let contains_unknown_or_todo = |ty| matches!(ty, Type::Dynamic(dynamic) if dynamic != DynamicType::Any);
if source_type.is_equivalent_to(db, *casted_type)
&& !casted_type.any_over_type(db, &|ty| {
contains_unknown_or_todo(ty)
})
&& !source_type.any_over_type(db, &|ty| {
contains_unknown_or_todo(ty)
})
{
if let Some(builder) = self
.context
.report_lint(&REDUNDANT_CAST, call_expression)
{
builder.into_diagnostic(format_args!(
"Value is already of type `{}`",
casted_type.display(db),
));
}
}
}
}
KnownFunction::GetProtocolMembers => {
if let [Some(Type::ClassLiteral(class))] =
overload.parameter_types()
{
if !class.is_protocol(self.db()) {
report_bad_argument_to_get_protocol_members(
&self.context,
call_expression,
*class,
);
}
}
}
KnownFunction::IsInstance | KnownFunction::IsSubclass => {
if let [_, Some(Type::ClassLiteral(class))] =
overload.parameter_types()
{
if let Some(protocol_class) =
class.into_protocol_class(self.db())
{
if !protocol_class.is_runtime_checkable(self.db()) {
report_runtime_check_against_non_runtime_checkable_protocol(
&self.context,
call_expression,
protocol_class,
known_function
);
}
}
}
}
_ => {}
}
}
Type::ClassLiteral(class) => {
let Some(known_class) = class.known(self.db()) else {
continue;
};
match known_class {
KnownClass::Super => {
// Handle the case where `super()` is called with no arguments.
// In this case, we need to infer the two arguments:
// 1. The nearest enclosing class
// 2. The first parameter of the current function (typically `self` or `cls`)
match overload.parameter_types() {
[] => {
let scope = self.scope();
let Some(enclosing_class) = nearest_enclosing_class(
self.db(),
self.index,
scope,
self.module(),
) else {
overload.set_return_type(Type::unknown());
BoundSuperError::UnavailableImplicitArguments
.report_diagnostic(
&self.context,
call_expression.into(),
);
continue;
};
let Some(first_param) =
self.first_param_type_in_scope(scope)
else {
overload.set_return_type(Type::unknown());
BoundSuperError::UnavailableImplicitArguments
.report_diagnostic(
&self.context,
call_expression.into(),
);
continue;
};
let bound_super = BoundSuperType::build(
self.db(),
Type::ClassLiteral(enclosing_class),
first_param,
)
.unwrap_or_else(|err| {
err.report_diagnostic(
&self.context,
call_expression.into(),
);
Type::unknown()
});
overload.set_return_type(bound_super);
}
[Some(pivot_class_type), Some(owner_type)] => {
let bound_super = BoundSuperType::build(
self.db(),
*pivot_class_type,
*owner_type,
)
.unwrap_or_else(|err| {
err.report_diagnostic(
&self.context,
call_expression.into(),
);
Type::unknown()
});
overload.set_return_type(bound_super);
}
_ => (),
}
}
KnownClass::TypeVar => {
let assigned_to = (self.index)
.try_expression(call_expression_node)
.and_then(|expr| expr.assigned_to(self.db()));
let Some(target) =
assigned_to.as_ref().and_then(|assigned_to| {
match assigned_to
.node(self.module())
.targets
.as_slice()
{
[ast::Expr::Name(target)] => Some(target),
_ => None,
}
})
else {
if let Some(builder) = self.context.report_lint(
&INVALID_LEGACY_TYPE_VARIABLE,
call_expression,
) {
builder.into_diagnostic(format_args!(
"A legacy `typing.TypeVar` must be immediately assigned to a variable",
));
}
continue;
};
let [
Some(name_param),
constraints,
bound,
default,
contravariant,
covariant,
_infer_variance,
] = overload.parameter_types()
else {
continue;
};
let covariant = match covariant {
Some(ty) => ty.bool(self.db()),
None => Truthiness::AlwaysFalse,
};
let contravariant = match contravariant {
Some(ty) => ty.bool(self.db()),
None => Truthiness::AlwaysFalse,
};
let variance = match (contravariant, covariant) {
(Truthiness::Ambiguous, _) => {
if let Some(builder) = self.context.report_lint(
&INVALID_LEGACY_TYPE_VARIABLE,
call_expression,
) {
builder.into_diagnostic(format_args!(
"The `contravariant` parameter of \
a legacy `typing.TypeVar` cannot have \
an ambiguous value",
));
}
continue;
}
(_, Truthiness::Ambiguous) => {
if let Some(builder) = self.context.report_lint(
&INVALID_LEGACY_TYPE_VARIABLE,
call_expression,
) {
builder.into_diagnostic(format_args!(
"The `covariant` parameter of \
a legacy `typing.TypeVar` cannot have \
an ambiguous value",
));
}
continue;
}
(Truthiness::AlwaysTrue, Truthiness::AlwaysTrue) => {
if let Some(builder) = self.context.report_lint(
&INVALID_LEGACY_TYPE_VARIABLE,
call_expression,
) {
builder.into_diagnostic(format_args!(
"A legacy `typing.TypeVar` cannot be \
both covariant and contravariant",
));
}
continue;
}
(Truthiness::AlwaysTrue, Truthiness::AlwaysFalse) => {
TypeVarVariance::Contravariant
}
(Truthiness::AlwaysFalse, Truthiness::AlwaysTrue) => {
TypeVarVariance::Covariant
}
(Truthiness::AlwaysFalse, Truthiness::AlwaysFalse) => {
TypeVarVariance::Invariant
}
};
let name_param = name_param
.into_string_literal()
.map(|name| name.value(self.db()));
if name_param
.is_none_or(|name_param| name_param != target.id)
{
if let Some(builder) = self.context.report_lint(
&INVALID_LEGACY_TYPE_VARIABLE,
call_expression,
) {
builder.into_diagnostic(format_args!(
"The name of a legacy `typing.TypeVar`{} must match \
the name of the variable it is assigned to (`{}`)",
if let Some(name_param) = name_param {
format!(" (`{name_param}`)")
} else {
String::new()
},
target.id,
));
}
continue;
}
let bound_or_constraint = match (bound, constraints) {
(Some(bound), None) => {
Some(TypeVarBoundOrConstraints::UpperBound(*bound))
}
(None, Some(_constraints)) => {
// We don't use UnionType::from_elements or UnionBuilder here,
// because we don't want to simplify the list of constraints like
// we do with the elements of an actual union type.
// TODO: Consider using a new `OneOfType` connective here instead,
// since that more accurately represents the actual semantics of
// typevar constraints.
let elements = UnionType::new(
self.db(),
overload
.arguments_for_parameter(
&call_argument_types,
1,
)
.map(|(_, ty)| ty)
.collect::<Box<_>>(),
);
Some(TypeVarBoundOrConstraints::Constraints(
elements,
))
}
// TODO: Emit a diagnostic that TypeVar cannot be both bounded and
// constrained
(Some(_), Some(_)) => continue,
(None, None) => None,
};
let containing_assignment =
self.index.expect_single_definition(target);
overload.set_return_type(Type::KnownInstance(
KnownInstanceType::TypeVar(TypeVarInstance::new(
self.db(),
target.id.clone(),
Some(containing_assignment),
bound_or_constraint,
variance,
*default,
TypeVarKind::Legacy,
)),
));
}
KnownClass::TypeAliasType => {
let assigned_to = (self.index)
.try_expression(call_expression_node)
.and_then(|expr| expr.assigned_to(self.db()));
let containing_assignment =
assigned_to.as_ref().and_then(|assigned_to| {
match assigned_to
.node(self.module())
.targets
.as_slice()
{
[ast::Expr::Name(target)] => Some(
self.index.expect_single_definition(target),
),
_ => None,
}
});
let [Some(name), Some(value), ..] =
overload.parameter_types()
else {
continue;
};
if let Some(name) = name.into_string_literal() {
overload.set_return_type(Type::KnownInstance(
KnownInstanceType::TypeAliasType(
TypeAliasType::Bare(BareTypeAliasType::new(
self.db(),
ast::name::Name::new(name.value(self.db())),
containing_assignment,
value,
)),
),
));
} else {
if let Some(builder) = self.context.report_lint(
&INVALID_TYPE_ALIAS_TYPE,
call_expression,
) {
builder.into_diagnostic(format_args!(
"The name of a `typing.TypeAlias` must be a string literal",
));
}
}
}
_ => (),
}
}
_ => (),
}
}
}
let db = self.db();
let scope = self.scope();
let return_ty = bindings.return_type(db);
let find_narrowed_place = || match arguments.args.first() {
None => {
// This branch looks extraneous, especially in the face of `missing-arguments`.
// However, that lint won't be able to catch this:
//
// ```python
// def f(v: object = object()) -> TypeIs[int]: ...
//
// if f(): ...
// ```
//
// TODO: Will this report things that is actually fine?
if let Some(builder) = self
.context
.report_lint(&INVALID_TYPE_GUARD_CALL, arguments)
{
builder.into_diagnostic("Type guard call does not have a target");
}
None
}
Some(expr) => match PlaceExpr::try_from(expr) {
Ok(place_expr) => place_table(db, scope).place_id_by_expr(&place_expr),
Err(()) => None,
},
};
match return_ty {
// TODO: TypeGuard
Type::TypeIs(type_is) => match find_narrowed_place() {
Some(place) => type_is.bind(db, scope, place),
None => return_ty,
},
_ => return_ty,
}
}
let mut bindings = match bindings.check_types(self.db(), &call_argument_types) {
Ok(bindings) => bindings,
Err(CallError(_, bindings)) => {
bindings.report_diagnostics(&self.context, call_expression.into());
bindings.return_type(self.db())
return bindings.return_type(self.db());
}
};
for binding in &mut bindings {
let binding_type = binding.callable_type;
for (_, overload) in binding.matching_overloads_mut() {
match binding_type {
Type::FunctionLiteral(function_literal) => {
if let Some(known_function) = function_literal.known(self.db()) {
known_function.check_call(&self.context, overload, call_expression);
}
}
Type::ClassLiteral(class) => {
let Some(known_class) = class.known(self.db()) else {
continue;
};
known_class.check_call(
&self.context,
self.index,
overload,
&call_argument_types,
call_expression,
);
}
_ => {}
}
}
}
let db = self.db();
let scope = self.scope();
let return_ty = bindings.return_type(db);
let find_narrowed_place = || match arguments.args.first() {
None => {
// This branch looks extraneous, especially in the face of `missing-arguments`.
// However, that lint won't be able to catch this:
//
// ```python
// def f(v: object = object()) -> TypeIs[int]: ...
//
// if f(): ...
// ```
//
// TODO: Will this report things that is actually fine?
if let Some(builder) = self
.context
.report_lint(&INVALID_TYPE_GUARD_CALL, arguments)
{
builder.into_diagnostic("Type guard call does not have a target");
}
None
}
Some(expr) => match PlaceExpr::try_from(expr) {
Ok(place_expr) => place_table(db, scope).place_id_by_expr(&place_expr),
Err(()) => None,
},
};
match return_ty {
// TODO: TypeGuard
Type::TypeIs(type_is) => match find_narrowed_place() {
Some(place) => type_is.bind(db, scope, place),
None => return_ty,
},
_ => return_ty,
}
}
@ -9177,7 +8641,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
}
ast::Expr::Call(call_expr) => {
self.infer_call_expression(expression, call_expr);
self.infer_call_expression(call_expr);
self.report_invalid_type_expression(
expression,
format_args!("Function calls are not allowed in type expressions"),