From bc4930e9a306aef97e3afd596d2fb281f59adfbc Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Sat, 27 Sep 2025 13:05:03 -0700 Subject: [PATCH] WIP: working without standalone --- .../mdtest/generics/legacy/classes.md | 7 + .../mdtest/generics/legacy/variables.md | 111 ++++++- .../mdtest/type_properties/constraints.md | 4 +- .../src/semantic_index/builder.rs | 5 +- crates/ty_python_semantic/src/types.rs | 50 ---- crates/ty_python_semantic/src/types/class.rs | 138 +-------- .../src/types/infer/builder.rs | 282 ++++++++++++++++-- .../src/types/infer/tests.rs | 11 +- .../src/types/signatures.rs | 8 +- 9 files changed, 376 insertions(+), 240 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md index 234de56a8c..57e895d747 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md @@ -1,5 +1,12 @@ # Generic classes: Legacy syntax +We use TypeVar defaults here, which was added in Python 3.13 for legacy typevars. + +```toml +[environment] +python-version = "3.13" +``` + ## Defining a generic class At its simplest, to define a generic class using the legacy syntax, you inherit from the diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md index 122879935c..73594ee0fe 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md @@ -33,13 +33,12 @@ reveal_type(T.__name__) # revealed: Literal["T"] from typing import TypeVar T = TypeVar("T") -# TODO: no error # error: [invalid-legacy-type-variable] U: TypeVar = TypeVar("U") -# error: [invalid-legacy-type-variable] "A legacy `typing.TypeVar` must be immediately assigned to a variable" -# error: [invalid-type-form] "Function calls are not allowed in type expressions" -TestList = list[TypeVar("W")] +# error: [invalid-legacy-type-variable] +tuple_with_typevar = ("foo", TypeVar("W")) +reveal_type(tuple_with_typevar[1]) # revealed: TypeVar ``` ### `TypeVar` parameter must match variable name @@ -49,7 +48,7 @@ TestList = list[TypeVar("W")] ```py from typing import TypeVar -# error: [invalid-legacy-type-variable] "The name of a legacy `typing.TypeVar` (`Q`) must match the name of the variable it is assigned to (`T`)" +# error: [invalid-legacy-type-variable] T = TypeVar("Q") ``` @@ -66,6 +65,22 @@ T = TypeVar("T") T = TypeVar("T") ``` +### No variadic arguments + +```py +from typing import TypeVar + +types = (int, str) + +# error: [invalid-legacy-type-variable] +T = TypeVar("T", *types) +reveal_type(T) # revealed: TypeVar + +# error: [invalid-legacy-type-variable] +S = TypeVar("S", **{"bound": int}) +reveal_type(S) # revealed: TypeVar +``` + ### Type variables with a default Note that the `__default__` property is only available in Python ≥3.13. @@ -91,6 +106,11 @@ reveal_type(S.__default__) # revealed: NoDefault ### Using other typevars as a default +```toml +[environment] +python-version = "3.13" +``` + ```py from typing import Generic, TypeVar, Union @@ -122,6 +142,20 @@ reveal_type(T.__constraints__) # revealed: tuple[()] S = TypeVar("S") reveal_type(S.__bound__) # revealed: None + +from typing import TypedDict + +# error: [invalid-type-form] +T = TypeVar("T", bound=TypedDict) +``` + +The upper bound must be a valid type expression: + +```py +from typing import TypedDict + +# error: [invalid-type-form] +T = TypeVar("T", bound=TypedDict) ``` ### Type variables with constraints @@ -144,8 +178,8 @@ Constraints are not simplified relative to each other, even if one is a subtype T = TypeVar("T", int, bool) reveal_type(T.__constraints__) # revealed: tuple[int, bool] -S = TypeVar("S", float) -reveal_type(S.__constraints__) # revealed: tuple[int | float] +S = TypeVar("S", float, str) +reveal_type(S.__constraints__) # revealed: tuple[int | float, str] ``` ### Cannot have only one constraint @@ -156,10 +190,19 @@ reveal_type(S.__constraints__) # revealed: tuple[int | float] ```py from typing import TypeVar -# TODO: error: [invalid-type-variable-constraints] +# error: [invalid-legacy-type-variable] T = TypeVar("T", int) ``` +### Cannot have both bound and constraint + +```py +from typing import TypeVar + +# error: [invalid-legacy-type-variable] +T = TypeVar("T", int, str, bound=bytes) +``` + ### Cannot be both covariant and contravariant > To facilitate the declaration of container types where covariant or contravariant type checking is @@ -188,6 +231,46 @@ T = TypeVar("T", covariant=cond()) U = TypeVar("U", contravariant=cond()) ``` +### Invalid keyword arguments + +```py +from typing import TypeVar + +# error: [invalid-legacy-type-variable] +T = TypeVar("T", invalid_keyword=True) +``` + +```pyi +from typing import TypeVar + +# error: [invalid-legacy-type-variable] +T = TypeVar("T", invalid_keyword=True) + +``` + +### Constructor signature versioning + +```toml +[environment] +python-version = "3.10" +``` + +In a stub file, features from the latest supported Python version can be used on any version: + +```pyi +from typing import TypeVar +T = TypeVar("T", default=int) +``` + +But this raises an error in a non-stub file: + +```py +from typing import TypeVar + +# error: [invalid-legacy-type-variable] +T = TypeVar("T", default=int) +``` + ## Callability A typevar bound to a Callable type is callable: @@ -256,13 +339,10 @@ S = TypeVar("S") T = TypeVar("T", bound=list[S]) # TODO: error -U = TypeVar("U", list["T"]) +U = TypeVar("U", list["T"], str) # TODO: error -V = TypeVar("V", list[S], str) - -# TODO: error -W = TypeVar("W", list["W"], str) +V = TypeVar("V", list["V"], str) ``` However, they are lazily evaluated and can cyclically refer to their own type: @@ -280,6 +360,11 @@ reveal_type(G[list[G]]().x) # revealed: list[G[Unknown]] ### Defaults +```toml +[environment] +python-version = "3.13" +``` + Defaults can be generic, but can only refer to earlier typevars: ```py diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/constraints.md b/crates/ty_python_semantic/resources/mdtest/type_properties/constraints.md index fb3254f87a..4942f976dd 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/constraints.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/constraints.md @@ -343,7 +343,7 @@ def _[T]() -> None: reveal_type(negated_range_constraint(Sub, T, Base) & negated_range_constraint(Base, T, Super)) # revealed: ty_extensions.ConstraintSet[(¬(Base ≤ T@_ ≤ Super) ∧ ¬(SubSub ≤ T@_ ≤ Sub))] reveal_type(negated_range_constraint(SubSub, T, Sub) & negated_range_constraint(Base, T, Super)) - # revealed: ty_extensions.ConstraintSet[(¬(SubSub ≤ T@_ ≤ Sub) ∧ ¬(Unrelated ≤ T@_))] + # revealed: ty_extensions.ConstraintSet[(¬(Unrelated ≤ T@_) ∧ ¬(SubSub ≤ T@_ ≤ Sub))] reveal_type(negated_range_constraint(SubSub, T, Sub) & negated_range_constraint(Unrelated, T, object)) ``` @@ -421,7 +421,7 @@ def _[T]() -> None: reveal_type(range_constraint(Sub, T, Base) | range_constraint(Base, T, Super)) # revealed: ty_extensions.ConstraintSet[(Base ≤ T@_ ≤ Super) ∨ (SubSub ≤ T@_ ≤ Sub)] reveal_type(range_constraint(SubSub, T, Sub) | range_constraint(Base, T, Super)) - # revealed: ty_extensions.ConstraintSet[(SubSub ≤ T@_ ≤ Sub) ∨ (Unrelated ≤ T@_)] + # revealed: ty_extensions.ConstraintSet[(Unrelated ≤ T@_) ∨ (SubSub ≤ T@_ ≤ Sub)] reveal_type(range_constraint(SubSub, T, Sub) | range_constraint(Unrelated, T, object)) ``` diff --git a/crates/ty_python_semantic/src/semantic_index/builder.rs b/crates/ty_python_semantic/src/semantic_index/builder.rs index 75f88f8ade..440f128367 100644 --- a/crates/ty_python_semantic/src/semantic_index/builder.rs +++ b/crates/ty_python_semantic/src/semantic_index/builder.rs @@ -1626,9 +1626,10 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> { self.visit_expr(&node.value); // Optimization for the common case: if there's just one target, and it's not an - // unpacking, we don't need the RHS to be a standalone expression at all. + // unpacking, and the target is a simple name, we don't need the RHS to be a + // standalone expression at all. if let [target] = &node.targets[..] - && !matches!(target, ast::Expr::List(_) | ast::Expr::Tuple(_)) + && target.is_name_expr() { self.push_assignment(CurrentAssignment::Assign { node, unpack: None }); self.visit_expr(target); diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index e2426017af..9d75ae94ae 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -4493,56 +4493,6 @@ impl<'db> Type<'db> { .into() } - Some(KnownClass::TypeVar) => { - // ```py - // class TypeVar: - // def __new__( - // cls, - // name: str, - // *constraints: Any, - // bound: Any | None = None, - // contravariant: bool = False, - // covariant: bool = False, - // infer_variance: bool = False, - // default: Any = ..., - // ) -> Self: ... - // ``` - Binding::single( - self, - Signature::new( - Parameters::new([ - Parameter::positional_or_keyword(Name::new_static("name")) - .with_annotated_type(Type::LiteralString), - Parameter::variadic(Name::new_static("constraints")) - .deferred_type_form() - .with_annotated_type(Type::any()), - Parameter::keyword_only(Name::new_static("bound")) - .deferred_type_form() - .with_annotated_type(UnionType::from_elements( - db, - [Type::any(), Type::none(db)], - )) - .with_default_type(Type::none(db)), - Parameter::keyword_only(Name::new_static("default")) - .deferred_type_form() - .with_annotated_type(Type::any()) - .with_default_type(KnownClass::NoneType.to_instance(db)), - Parameter::keyword_only(Name::new_static("contravariant")) - .with_annotated_type(KnownClass::Bool.to_instance(db)) - .with_default_type(Type::BooleanLiteral(false)), - Parameter::keyword_only(Name::new_static("covariant")) - .with_annotated_type(KnownClass::Bool.to_instance(db)) - .with_default_type(Type::BooleanLiteral(false)), - Parameter::keyword_only(Name::new_static("infer_variance")) - .with_annotated_type(KnownClass::Bool.to_instance(db)) - .with_default_type(Type::BooleanLiteral(false)), - ]), - Some(KnownClass::TypeVar.to_instance(db)), - ), - ) - .into() - } - Some(KnownClass::Deprecated) => { // ```py // class deprecated: diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index eec73a25dd..a932ed04ae 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -16,7 +16,7 @@ use crate::semantic_index::{ }; use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension}; use crate::types::context::InferContext; -use crate::types::diagnostic::{INVALID_LEGACY_TYPE_VARIABLE, INVALID_TYPE_ALIAS_TYPE}; +use crate::types::diagnostic::INVALID_TYPE_ALIAS_TYPE; use crate::types::enums::enum_metadata; use crate::types::function::{DataclassTransformerParams, KnownFunction}; use crate::types::generics::{GenericContext, Specialization, walk_specialization}; @@ -29,9 +29,8 @@ use crate::types::{ DataclassParams, DeprecatedInstance, FindLegacyTypeVarsVisitor, HasRelationToVisitor, IsEquivalentVisitor, KnownInstanceType, ManualPEP695TypeAliasType, MaterializationKind, NormalizedVisitor, PropertyInstanceType, StringLiteralType, TypeAliasType, TypeContext, - TypeMapping, TypeRelation, TypeVarBoundOrConstraintsEvaluation, TypeVarDefaultEvaluation, - TypeVarInstance, TypeVarKind, TypedDictParams, UnionBuilder, VarianceInferable, - declaration_type, determine_upper_bound, infer_definition_types, + TypeMapping, TypeRelation, TypedDictParams, UnionBuilder, VarianceInferable, declaration_type, + determine_upper_bound, infer_definition_types, }; use crate::{ Db, FxIndexMap, FxOrderSet, Program, @@ -4980,6 +4979,7 @@ impl KnownClass { _ => {} } } + KnownClass::Deprecated => { // Parsing something of the form: // @@ -5006,136 +5006,6 @@ impl KnownClass { DeprecatedInstance::new(db, message.into_string_literal()), ))); } - 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.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, _) => { - if let Some(builder) = - context.report_lint(&INVALID_LEGACY_TYPE_VARIABLE, call_expression) - { - builder.into_diagnostic( - "The `contravariant` parameter of a legacy `typing.TypeVar` \ - cannot have an ambiguous value", - ); - } - return; - } - (_, Truthiness::Ambiguous) => { - if let Some(builder) = - context.report_lint(&INVALID_LEGACY_TYPE_VARIABLE, call_expression) - { - builder.into_diagnostic( - "The `covariant` parameter of a legacy `typing.TypeVar` \ - cannot have an ambiguous value", - ); - } - return; - } - (Truthiness::AlwaysTrue, Truthiness::AlwaysTrue) => { - if let Some(builder) = - context.report_lint(&INVALID_LEGACY_TYPE_VARIABLE, call_expression) - { - 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) { - if let Some(builder) = - 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, - )); - } - return; - } - - let bound_or_constraint = match (bound, constraints) { - (Some(_), None) => Some(TypeVarBoundOrConstraintsEvaluation::LazyUpperBound), - - (None, Some(_)) => Some(TypeVarBoundOrConstraintsEvaluation::LazyConstraints), - - // 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.set_return_type(Type::KnownInstance(KnownInstanceType::TypeVar( - TypeVarInstance::new( - db, - &target.id, - Some(containing_assignment), - bound_or_constraint, - Some(variance), - default.map(|_| TypeVarDefaultEvaluation::Lazy), - TypeVarKind::Legacy, - ), - ))); - } KnownClass::TypeAliasType => { let assigned_to = index diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index ed80e9fa05..8fbe340c20 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -53,11 +53,12 @@ use crate::types::diagnostic::{ 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_KEY, INVALID_NAMED_TUPLE, INVALID_PARAMETER_DEFAULT, - INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL, INVALID_TYPE_VARIABLE_CONSTRAINTS, - IncompatibleBases, NON_SUBSCRIPTABLE, POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT, - UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, - UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, report_bad_dunder_set_call, + INVALID_GENERIC_CLASS, INVALID_KEY, INVALID_LEGACY_TYPE_VARIABLE, INVALID_NAMED_TUPLE, + INVALID_PARAMETER_DEFAULT, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL, + INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases, NON_SUBSCRIPTABLE, + POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT, UNDEFINED_REVEAL, + UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, + UNSUPPORTED_OPERATOR, report_bad_dunder_set_call, report_cannot_pop_required_field_on_typed_dict, report_implicit_return_type, report_instance_layout_conflict, report_invalid_assignment, report_invalid_attribute_assignment, report_invalid_generator_function_return_type, @@ -95,7 +96,7 @@ use crate::types::{ Parameters, SpecialFormType, SubclassOfType, TrackedConstraintSet, Truthiness, Type, TypeAliasType, TypeAndQualifiers, TypeContext, TypeMapping, TypeQualifiers, TypeVarBoundOrConstraintsEvaluation, TypeVarDefaultEvaluation, TypeVarInstance, TypeVarKind, - UnionBuilder, UnionType, binding_type, todo_type, + TypeVarVariance, UnionBuilder, UnionType, binding_type, todo_type, }; use crate::types::{ClassBase, add_inferred_python_version_hint_to_diagnostic}; use crate::unpack::{EvaluationMode, UnpackPosition}; @@ -375,15 +376,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.scope } - fn definition(&self) -> Option> { - match self.region { - InferenceRegion::Definition(definition) | InferenceRegion::Deferred(definition) => { - Some(definition) - } - _ => None, - } - } - /// Are we currently inferring types in file with deferred types? /// This is true for stub files and files with `__future__.annotations` fn defer_annotations(&self) -> bool { @@ -3967,8 +3959,33 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { unpacked.expression_type(target) } TargetKind::Single => { - let value_ty = - self.infer_maybe_standalone_expression(value, TypeContext::default()); + let tcx = TypeContext::default(); + let value_ty = if let Some(standalone_expression) = self.index.try_expression(value) + { + self.infer_standalone_expression_impl(value, standalone_expression, tcx) + } else { + // If the RHS is not a standalone expression, this is a simple assignment + // (single target, no unpackings). That means it's a valid syntactic form + // for a legacy TypeVar creation; check for that. + if let Some(call_expr) = value.as_call_expr() { + let callable_type = self.infer_maybe_standalone_expression( + call_expr.func.as_ref(), + TypeContext::default(), + ); + let ty = if callable_type + .into_class_literal() + .is_some_and(|cls| cls.is_known(self.db(), KnownClass::TypeVar)) + { + self.infer_legacy_typevar(target, call_expr, definition) + } else { + self.infer_call_expression_impl(call_expr, callable_type, tcx) + }; + self.store_expression_type(value, ty); + ty + } else { + self.infer_expression(value, tcx) + } + }; // `TYPE_CHECKING` is a special variable that should only be assigned `False` // at runtime, but is always considered `True` in type checking. @@ -3999,8 +4016,199 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.add_binding(target.into(), definition, target_ty); } + fn infer_legacy_typevar( + &mut self, + target: &ast::Expr, + call_expr: &ast::ExprCall, + definition: Definition<'db>, + ) -> Type<'db> { + fn error<'db>( + context: &InferContext<'db, '_>, + message: impl std::fmt::Display, + node: impl Ranged, + ) -> Type<'db> { + if let Some(builder) = context.report_lint(&INVALID_LEGACY_TYPE_VARIABLE, node) { + builder.into_diagnostic(message); + } + // If the call doesn't create a valid typevar, we'll emit diagnostics and fall back to + // just creating a regular instance of `typing.TypeVar`. + KnownClass::TypeVar.to_instance(context.db()) + } + + let db = self.db(); + let arguments = &call_expr.arguments; + + let mut has_bound = false; + let mut default = None; + let mut covariant = false; + let mut contravariant = false; + + for kwarg in &arguments.keywords { + let Some(identifier) = kwarg.arg.as_ref() else { + return error( + &self.context, + "Starred arguments are not supported in legacy `typing.TypeVar` creation", + kwarg, + ); + }; + match identifier.id().as_str() { + "bound" => has_bound = true, + "covariant" => { + match self + .infer_expression(&kwarg.value, TypeContext::default()) + .bool(db) + { + Truthiness::AlwaysTrue => covariant = true, + Truthiness::AlwaysFalse => {} + Truthiness::Ambiguous => { + return error( + &self.context, + "The `covariant` parameter of a legacy `typing.TypeVar` \ + cannot have an ambiguous value", + kwarg, + ); + } + } + } + "contravariant" => { + match self + .infer_expression(&kwarg.value, TypeContext::default()) + .bool(db) + { + Truthiness::AlwaysTrue => contravariant = true, + Truthiness::AlwaysFalse => {} + Truthiness::Ambiguous => { + return error( + &self.context, + "The `contravariant` parameter of a legacy `typing.TypeVar` \ + cannot have an ambiguous value", + kwarg, + ); + } + } + } + "default" => { + if !self.in_stub() && Program::get(db).python_version(db) < PythonVersion::PY313 + { + return error( + &self.context, + "TypeVar `default` keyword was added in Python 3.13", + kwarg, + ); + } + + default = Some(TypeVarDefaultEvaluation::Lazy); + } + "infer_variance" => { + if !self.in_stub() && Program::get(db).python_version(db) < PythonVersion::PY312 + { + return error( + &self.context, + "TypeVar `infer_variance` keyword was added in Python 3.12", + kwarg, + ); + } + // TODO support `infer_variance` in legacy TypeVars + } + name => { + return error( + &self.context, + format_args!( + "Unknown keyword argument `{name}` in legacy `typing.TypeVar` creation", + ), + kwarg, + ); + } + } + } + + let variance = match (covariant, contravariant) { + (true, true) => { + return error( + &self.context, + "A legacy `typing.TypeVar` cannot be both covariant and contravariant", + call_expr, + ); + } + (true, false) => TypeVarVariance::Covariant, + (false, true) => TypeVarVariance::Contravariant, + (false, false) => TypeVarVariance::Invariant, + }; + + let Some(name_param) = arguments + .find_positional(0) + .map(|arg| self.infer_expression(arg, TypeContext::default())) + .and_then(Type::into_string_literal) + .map(|name| name.value(db)) + else { + return error( + &self.context, + "The first argument to a legacy `typing.TypeVar` must be a string literal.", + call_expr, + ); + }; + + let Some(target_name) = target.as_name_expr().map(|n| &n.id) else { + return error( + &self.context, + "A legacy `typing.TypeVar` must be assigned to a variable", + target, + ); + }; + + if name_param != target_name { + return error( + &self.context, + format_args!( + "The name of a legacy `typing.TypeVar` (`{name_param}`) must match \ + the name of the variable it is assigned to (`{target_name}`)" + ), + target, + ); + } + + // Inference of bounds, constraints, and defaults must be deferred, to avoid cycles. So we + // only check presence/absence/number here. + + let num_constraints = arguments.args.len() - 1; + + let bound_or_constraints = match (has_bound, num_constraints) { + (false, 0) => None, + (true, 0) => Some(TypeVarBoundOrConstraintsEvaluation::LazyUpperBound), + (true, _) => { + return error( + &self.context, + "A legacy `typing.TypeVar` cannot have both a bound and constraints", + call_expr, + ); + } + (_, 1) => { + return error( + &self.context, + "A legacy `typing.TypeVar` cannot have exactly one constraint", + call_expr, + ); + } + (false, _) => Some(TypeVarBoundOrConstraintsEvaluation::LazyConstraints), + }; + + if bound_or_constraints.is_some() || default.is_some() { + self.deferred.insert(definition); + } + + Type::KnownInstance(KnownInstanceType::TypeVar(TypeVarInstance::new( + db, + target_name, + Some(definition), + bound_or_constraints, + Some(variance), + default, + TypeVarKind::Legacy, + ))) + } + fn infer_assignment_deferred(&mut self, value: &ast::Expr) { - // infer deferred bounds/constraints/defaults of a legacy TypeVar + // Infer deferred bounds/constraints/defaults of a legacy TypeVar. let Some(call_expr) = value.as_call_expr() else { return; }; @@ -4987,12 +5195,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { match form { None | Some(ParameterForm::Value) => self.infer_expression(ast_argument, tcx), Some(ParameterForm::Type) => self.infer_type_expression(ast_argument), - Some(ParameterForm::TypeDeferred) => { - if let Some(definition) = self.definition() { - self.deferred.insert(definition); - } - Type::unknown() - } } } @@ -5861,6 +6063,19 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { &mut self, call_expression: &ast::ExprCall, tcx: TypeContext<'db>, + ) -> Type<'db> { + // TODO: Use the type context for more precise inference. + let callable_type = + self.infer_maybe_standalone_expression(&call_expression.func, TypeContext::default()); + + self.infer_call_expression_impl(call_expression, callable_type, tcx) + } + + fn infer_call_expression_impl( + &mut self, + call_expression: &ast::ExprCall, + callable_type: Type<'db>, + tcx: TypeContext<'db>, ) -> Type<'db> { let ast::ExprCall { range: _, @@ -5881,9 +6096,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ty }); - // TODO: Use the type context for more precise inference. - let callable_type = self.infer_maybe_standalone_expression(func, TypeContext::default()); - // Special handling for `TypedDict` method calls if let ast::Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = func.as_ref() { let value_type = self.expression_type(value); @@ -5987,7 +6199,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { | KnownClass::Object | KnownClass::Property | KnownClass::Super - | KnownClass::TypeVar | KnownClass::TypeAliasType | KnownClass::Deprecated ) @@ -6010,6 +6221,21 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let argument_forms = vec![Some(ParameterForm::Value); call_arguments.len()]; self.infer_argument_types(arguments, &mut call_arguments, &argument_forms); + if class.is_known(self.db(), KnownClass::TypeVar) { + // Inference of correctly-placed `TypeVar` definitions is done in + // `TypeInferenceBuilder::infer_legacy_typevar`, and doesn't use the full + // call-binding machinery. If we reach here, it means that someone is trying to + // instantiate a `typing.TypeVar` in an invalid context. + if let Some(builder) = self + .context + .report_lint(&INVALID_LEGACY_TYPE_VARIABLE, call_expression) + { + builder.into_diagnostic( + "A legacy `typing.TypeVar` must be immediately assigned to a variable", + ); + } + } + return callable_type .try_call_constructor(self.db(), call_arguments) .unwrap_or_else(|err| { diff --git a/crates/ty_python_semantic/src/types/infer/tests.rs b/crates/ty_python_semantic/src/types/infer/tests.rs index ec4a0d46c8..1c7c71c8bd 100644 --- a/crates/ty_python_semantic/src/types/infer/tests.rs +++ b/crates/ty_python_semantic/src/types/infer/tests.rs @@ -418,7 +418,8 @@ fn dependency_implicit_instance_attribute() -> anyhow::Result<()> { "/src/main.py", r#" from mod import C - x = C().attr + # multiple targets ensures RHS is a standalone expression, relied on by this test + x = y = C().attr "#, )?; @@ -508,7 +509,8 @@ fn dependency_own_instance_member() -> anyhow::Result<()> { "/src/main.py", r#" from mod import C - x = C().attr + # multiple targets ensures RHS is a standalone expression, relied on by this test + x = y = C().attr "#, )?; @@ -603,7 +605,8 @@ fn dependency_implicit_class_member() -> anyhow::Result<()> { r#" from mod import C C.method() - x = C().class_attr + # multiple targets ensures RHS is a standalone expression, relied on by this test + x = y = C().class_attr "#, )?; @@ -688,7 +691,7 @@ fn call_type_doesnt_rerun_when_only_callee_changed() -> anyhow::Result<()> { r#" from foo import foo - a = foo() + a = b = foo() "#, )?; diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index c48c6b11ac..d5ae64fdaf 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -1032,7 +1032,7 @@ impl<'db> VarianceInferable<'db> for &Signature<'db> { self.parameters .iter() .filter_map(|parameter| match parameter.form { - ParameterForm::Type | ParameterForm::TypeDeferred => None, + ParameterForm::Type => None, ParameterForm::Value => parameter.annotated_type().map(|ty| { ty.with_polarity(TypeVarVariance::Contravariant) .variance_of(db, typevar) @@ -1472,11 +1472,6 @@ impl<'db> Parameter<'db> { self } - pub(crate) fn deferred_type_form(mut self) -> Self { - self.form = ParameterForm::TypeDeferred; - self - } - fn apply_type_mapping_impl<'a>( &self, db: &'db dyn Db, @@ -1728,7 +1723,6 @@ impl<'db> ParameterKind<'db> { pub(crate) enum ParameterForm { Value, Type, - TypeDeferred, } #[cfg(test)]