From 4a5715b97aed71a8682d62143dd18d46895ab861 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 25 Jun 2025 21:10:55 +0100 Subject: [PATCH] [ty] Reduce the overwhelming complexity of `TypeInferenceBuilder::infer_call_expression` (#18943) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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. --- crates/ty_python_semantic/src/types/class.rs | 310 +++++++- .../ty_python_semantic/src/types/context.rs | 4 + .../ty_python_semantic/src/types/function.rs | 204 ++++- crates/ty_python_semantic/src/types/infer.rs | 720 +++--------------- 4 files changed, 592 insertions(+), 646 deletions(-) diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 3cd7bffa5b..4eeb050046 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -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>, @@ -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>, @@ -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, KnownClassLookupError<'db>> { + db: &dyn Db, + ) -> Result { 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> { + pub(crate) fn try_to_class_literal(self, db: &dyn Db) -> Option { // 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>> = 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::>(), + ); + 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. diff --git a/crates/ty_python_semantic/src/types/context.rs b/crates/ty_python_semantic/src/types/context.rs index f2cf03fac9..e48befbd3a 100644 --- a/crates/ty_python_semantic/src/types/context.rs +++ b/crates/ty_python_semantic/src/types/context.rs @@ -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. /// diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index 31daf84fcd..504f84dbd9 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -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)] diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index d3aabea5c6..022804b1b7 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -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> { - 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::>(), - ); - 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"),