From 97dd83daabd8ebffa32d7870c9b9a7cf1e0c2730 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Fri, 14 Nov 2025 13:52:20 +0530 Subject: [PATCH 1/2] Add support for `P.args` and `P.kwargs` --- crates/ty_python_semantic/src/types.rs | 73 +++++++-- .../ty_python_semantic/src/types/display.rs | 95 +++++++----- .../ty_python_semantic/src/types/generics.rs | 6 +- .../src/types/infer/builder.rs | 139 +++++++++++++++--- .../infer/builder/annotation_expression.rs | 15 +- .../types/infer/builder/type_expression.rs | 37 ++--- .../src/types/signatures.rs | 123 +++++++++++++--- 7 files changed, 371 insertions(+), 117 deletions(-) diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index b7f4327ad6..21e97c64d5 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -4466,11 +4466,16 @@ impl<'db> Type<'db> { .into() } - Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) + Type::TypeVar(typevar) if typevar.kind(db).is_paramspec() && matches!(name.as_str(), "args" | "kwargs") => { - Place::bound(todo_type!("ParamSpecArgs / ParamSpecKwargs")).into() + Place::bound(Type::TypeVar(match name.as_str() { + "args" => typevar.with_paramspec_attr(db, ParamSpecAttrKind::Args), + "kwargs" => typevar.with_paramspec_attr(db, ParamSpecAttrKind::Kwargs), + _ => unreachable!(), + })) + .into() } Type::NominalInstance(instance) @@ -6556,6 +6561,15 @@ impl<'db> Type<'db> { KnownInstanceType::TypeAliasType(alias) => Ok(Type::TypeAlias(*alias)), KnownInstanceType::NewType(newtype) => Ok(Type::NewTypeInstance(*newtype)), KnownInstanceType::TypeVar(typevar) => { + // A `ParamSpec` type variable cannot be used in type expressions. + if typevar.kind(db).is_paramspec() { + return Err(InvalidTypeExpressionError { + invalid_expressions: smallvec::smallvec_inline![ + InvalidTypeExpression::InvalidType(*self, scope_id) + ], + fallback_type: Type::unknown(), + }); + } let index = semantic_index(db, scope_id.file(db)); Ok(bind_typevar( db, @@ -6775,9 +6789,12 @@ impl<'db> Type<'db> { Some(KnownClass::TypeVar) => Ok(todo_type!( "Support for `typing.TypeVar` instances in type expressions" )), - Some( - KnownClass::ParamSpec | KnownClass::ParamSpecArgs | KnownClass::ParamSpecKwargs, - ) => Ok(todo_type!("Support for `typing.ParamSpec`")), + Some(KnownClass::ParamSpecArgs) => { + Ok(todo_type!("Support for `typing.ParamSpecArgs`")) + } + Some(KnownClass::ParamSpecKwargs) => { + Ok(todo_type!("Support for `typing.ParamSpecKwargs`")) + } Some(KnownClass::TypeVarTuple) => Ok(todo_type!( "Support for `typing.TypeVarTuple` instances in type expressions" )), @@ -6996,7 +7013,7 @@ impl<'db> Type<'db> { Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) => match type_mapping { TypeMapping::BindLegacyTypevars(binding_context) => { - Type::TypeVar(BoundTypeVarInstance::new(db, typevar, *binding_context)) + Type::TypeVar(BoundTypeVarInstance::new(db, typevar, *binding_context, None)) } TypeMapping::Specialization(_) | TypeMapping::PartialSpecialization(_) | @@ -7801,6 +7818,8 @@ pub struct TrackedConstraintSet<'db> { // The Salsa heap is tracked separately. impl get_size2::GetSize for TrackedConstraintSet<'_> {} +// TODO: The origin is either `TypeVarInstance` or `BoundTypeVarInstance` + /// Singleton types that are heavily special-cased by ty. Despite its name, /// quite a different type to [`NominalInstanceType`]. /// @@ -8593,12 +8612,12 @@ fn walk_type_var_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( #[salsa::tracked] impl<'db> TypeVarInstance<'db> { - pub(crate) fn with_binding_context( + pub(crate) fn as_bound_type_var_instance( self, db: &'db dyn Db, binding_context: Definition<'db>, ) -> BoundTypeVarInstance<'db> { - BoundTypeVarInstance::new(db, self, BindingContext::Definition(binding_context)) + BoundTypeVarInstance::new(db, self, BindingContext::Definition(binding_context), None) } pub(crate) fn name(self, db: &'db dyn Db) -> &'db ast::name::Name { @@ -8904,6 +8923,21 @@ impl<'db> BindingContext<'db> { } } +#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, get_size2::GetSize)] +pub enum ParamSpecAttrKind { + Args, + Kwargs, +} + +impl std::fmt::Display for ParamSpecAttrKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ParamSpecAttrKind::Args => f.write_str("args"), + ParamSpecAttrKind::Kwargs => f.write_str("kwargs"), + } + } +} + /// The identity of a bound type variable. /// /// This identifies a specific binding of a typevar to a context (e.g., `T@ClassC` vs `T@FunctionF`), @@ -8916,14 +8950,17 @@ impl<'db> BindingContext<'db> { pub struct BoundTypeVarIdentity<'db> { pub(crate) identity: TypeVarIdentity<'db>, pub(crate) binding_context: BindingContext<'db>, + paramspec_attr: Option, } /// A type variable that has been bound to a generic context, and which can be specialized to a /// concrete type. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] +#[derive(PartialOrd, Ord)] pub struct BoundTypeVarInstance<'db> { pub typevar: TypeVarInstance<'db>, binding_context: BindingContext<'db>, + paramspec_attr: Option, } // The Salsa heap is tracked separately. @@ -8938,9 +8975,22 @@ impl<'db> BoundTypeVarInstance<'db> { BoundTypeVarIdentity { identity: self.typevar(db).identity(db), binding_context: self.binding_context(db), + paramspec_attr: self.paramspec_attr(db), } } + pub(crate) fn name(self, db: &'db dyn Db) -> &'db ast::name::Name { + self.typevar(db).name(db) + } + + pub(crate) fn kind(self, db: &'db dyn Db) -> TypeVarKind { + self.typevar(db).kind(db) + } + + pub(crate) fn with_paramspec_attr(self, db: &'db dyn Db, kind: ParamSpecAttrKind) -> Self { + Self::new(db, self.typevar(db), self.binding_context(db), Some(kind)) + } + /// Returns whether two bound typevars represent the same logical typevar, regardless of e.g. /// differences in their bounds or constraints due to materialization. pub(crate) fn is_same_typevar_as(self, db: &'db dyn Db, other: Self) -> bool { @@ -8967,7 +9017,7 @@ impl<'db> BoundTypeVarInstance<'db> { Some(variance), None, // _default ); - Self::new(db, typevar, BindingContext::Synthetic) + Self::new(db, typevar, BindingContext::Synthetic, None) } /// Create a new synthetic `Self` type variable with the given upper bound. @@ -8989,7 +9039,7 @@ impl<'db> BoundTypeVarInstance<'db> { Some(TypeVarVariance::Invariant), None, // _default ); - Self::new(db, typevar, binding_context) + Self::new(db, typevar, binding_context, None) } pub(crate) fn variance_with_polarity( @@ -9065,6 +9115,7 @@ impl<'db> BoundTypeVarInstance<'db> { db, self.typevar(db).normalized_impl(db, visitor), self.binding_context(db), + self.paramspec_attr(db), ) } @@ -9079,6 +9130,7 @@ impl<'db> BoundTypeVarInstance<'db> { self.typevar(db) .materialize_impl(db, materialization_kind, visitor), self.binding_context(db), + self.paramspec_attr(db), ) } @@ -9087,6 +9139,7 @@ impl<'db> BoundTypeVarInstance<'db> { db, self.typevar(db).to_instance(db)?, self.binding_context(db), + self.paramspec_attr(db), )) } } diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index b8a8a05ac4..9bd0e2fdf5 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -20,7 +20,9 @@ use crate::semantic_index::{scope::ScopeKind, semantic_index}; use crate::types::class::{ClassLiteral, ClassType, GenericAlias}; use crate::types::function::{FunctionType, OverloadLiteral}; use crate::types::generics::{GenericContext, Specialization}; -use crate::types::signatures::{CallableSignature, Parameter, Parameters, Signature}; +use crate::types::signatures::{ + CallableSignature, Parameter, Parameters, ParametersKind, Signature, +}; use crate::types::tuple::TupleSpec; use crate::types::visitor::TypeVisitor; use crate::types::{ @@ -643,6 +645,9 @@ impl Display for DisplayBoundTypeVarIdentity<'_> { if let Some(binding_context) = self.bound_typevar_identity.binding_context.name(self.db) { write!(f, "@{binding_context}")?; } + if let Some(paramspec_attr) = self.bound_typevar_identity.paramspec_attr { + write!(f, ".{paramspec_attr}")?; + } Ok(()) } } @@ -1116,57 +1121,69 @@ impl DisplaySignature<'_> { if multiline { writer.write_str("\n ")?; } - if self.parameters.is_gradual() { - // We represent gradual form as `...` in the signature, internally the parameters still - // contain `(*args, **kwargs)` parameters. - writer.write_str("...")?; - } else { - let mut star_added = false; - let mut needs_slash = false; - let mut first = true; - let arg_separator = if multiline { ",\n " } else { ", " }; + match self.parameters.kind() { + ParametersKind::Standard => { + let mut star_added = false; + let mut needs_slash = false; + let mut first = true; + let arg_separator = if multiline { ",\n " } else { ", " }; - for parameter in self.parameters.as_slice() { - // Handle special separators - if !star_added && parameter.is_keyword_only() { + for parameter in self.parameters.as_slice() { + // Handle special separators + if !star_added && parameter.is_keyword_only() { + if !first { + writer.write_str(arg_separator)?; + } + writer.write_char('*')?; + star_added = true; + first = false; + } + if parameter.is_positional_only() { + needs_slash = true; + } else if needs_slash { + if !first { + writer.write_str(arg_separator)?; + } + writer.write_char('/')?; + needs_slash = false; + first = false; + } + + // Add comma before parameter if not first if !first { writer.write_str(arg_separator)?; } - writer.write_char('*')?; - star_added = true; + + // Write parameter with range tracking + let param_name = parameter.display_name(); + writer.write_parameter( + ¶meter.display_with(self.db, self.settings.singleline()), + param_name.as_deref(), + )?; + first = false; } - if parameter.is_positional_only() { - needs_slash = true; - } else if needs_slash { + + if needs_slash { if !first { writer.write_str(arg_separator)?; } writer.write_char('/')?; - needs_slash = false; - first = false; } - - // Add comma before parameter if not first - if !first { - writer.write_str(arg_separator)?; - } - - // Write parameter with range tracking - let param_name = parameter.display_name(); - writer.write_parameter( - ¶meter.display_with(self.db, self.settings.singleline()), - param_name.as_deref(), - )?; - - first = false; } - - if needs_slash { - if !first { - writer.write_str(arg_separator)?; + ParametersKind::Gradual => { + // We represent gradual form as `...` in the signature, internally the parameters still + // contain `(*args, **kwargs)` parameters. + writer.write_str("...")?; + } + ParametersKind::ParamSpec(origin) => { + writer.write_str(&format!("**{}", origin.name(self.db)))?; + if let Some(name) = origin + .binding_context(self.db) + .and_then(|binding_context| binding_context.name(self.db)) + { + writer.write_str(&format!("@{name}"))?; } - writer.write_char('/')?; } } diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 8be33c95fc..c4174f0dbe 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -64,7 +64,7 @@ pub(crate) fn bind_typevar<'db>( if outer.kind().is_class() { if let NodeWithScopeKind::Function(function) = inner.node() { let definition = index.expect_single_definition(function); - return Some(typevar.with_binding_context(db, definition)); + return Some(typevar.as_bound_type_var_instance(db, definition)); } } } @@ -73,7 +73,7 @@ pub(crate) fn bind_typevar<'db>( .find_map(|enclosing_context| enclosing_context.binds_typevar(db, typevar)) .or_else(|| { typevar_binding_context.map(|typevar_binding_context| { - typevar.with_binding_context(db, typevar_binding_context) + typevar.as_bound_type_var_instance(db, typevar_binding_context) }) }) } @@ -358,7 +358,7 @@ impl<'db> GenericContext<'db> { else { return None; }; - Some(typevar.with_binding_context(db, binding_context)) + Some(typevar.as_bound_type_var_instance(db, binding_context)) } // TODO: Support these! ast::TypeParam::ParamSpec(_) => None, diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 05c6193060..ea656b2d76 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -55,16 +55,16 @@ use crate::types::class::{CodeGeneratorKind, FieldKind, MetaclassErrorKind, Meth use crate::types::context::{InNoTypeCheck, InferContext}; use crate::types::cyclic::CycleDetector; 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_LEGACY_TYPE_VARIABLE, INVALID_METACLASS, - INVALID_NAMED_TUPLE, INVALID_NEWTYPE, INVALID_OVERLOAD, INVALID_PARAMETER_DEFAULT, - INVALID_PARAMSPEC, INVALID_PROTOCOL, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL, - INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases, NON_SUBSCRIPTABLE, - POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT, SUBCLASS_OF_FINAL_CLASS, - UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, - UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY, + 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_KEY, INVALID_LEGACY_TYPE_VARIABLE, + INVALID_METACLASS, INVALID_NAMED_TUPLE, INVALID_NEWTYPE, INVALID_OVERLOAD, + INVALID_PARAMETER_DEFAULT, INVALID_PARAMSPEC, INVALID_PROTOCOL, INVALID_TYPE_FORM, + INVALID_TYPE_GUARD_CALL, INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases, + NON_SUBSCRIPTABLE, POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT, + SUBCLASS_OF_FINAL_CLASS, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, + UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY, hint_if_stdlib_attribute_exists_on_other_versions, hint_if_stdlib_submodule_exists_on_other_versions, report_attempted_protocol_instantiation, report_bad_dunder_set_call, report_cannot_pop_required_field_on_typed_dict, @@ -103,11 +103,11 @@ use crate::types::{ CallDunderError, CallableBinding, CallableType, ClassLiteral, ClassType, DataclassParams, DynamicType, InferredAs, InternedType, InternedTypes, IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType, LintDiagnosticGuard, MemberLookupPolicy, MetaclassCandidate, - PEP695TypeAliasType, Parameter, ParameterForm, Parameters, SpecialFormType, SubclassOfType, - TrackedConstraintSet, Truthiness, Type, TypeAliasType, TypeAndQualifiers, TypeContext, - TypeQualifiers, TypeVarBoundOrConstraintsEvaluation, TypeVarDefaultEvaluation, TypeVarIdentity, - TypeVarInstance, TypeVarKind, TypeVarVariance, TypedDictType, UnionBuilder, UnionType, - binding_type, todo_type, + PEP695TypeAliasType, ParamSpecAttrKind, Parameter, ParameterForm, Parameters, SpecialFormType, + SubclassOfType, TrackedConstraintSet, Truthiness, Type, TypeAliasType, TypeAndQualifiers, + TypeContext, TypeQualifiers, TypeVarBoundOrConstraintsEvaluation, TypeVarDefaultEvaluation, + TypeVarIdentity, TypeVarInstance, TypeVarKind, TypeVarVariance, TypedDictType, UnionBuilder, + UnionType, binding_type, todo_type, }; use crate::types::{ClassBase, add_inferred_python_version_hint_to_diagnostic}; use crate::unpack::{EvaluationMode, UnpackPosition}; @@ -2539,7 +2539,40 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { todo_type!("PEP 646") } else { let annotated_type = self.file_expression_type(annotation); - Type::homogeneous_tuple(self.db(), annotated_type) + if let Type::TypeVar(typevar) = annotated_type + && typevar.kind(self.db()).is_paramspec() + { + match typevar.paramspec_attr(self.db()) { + // `*args: P.args` + Some(ParamSpecAttrKind::Args) => annotated_type, + + // `*args: P.kwargs` + Some(ParamSpecAttrKind::Kwargs) => { + if let Some(builder) = + self.context.report_lint(&INVALID_TYPE_FORM, annotation) + { + let name = typevar.name(self.db()); + let mut diag = builder.into_diagnostic(format_args!( + "`{name}.kwargs` is valid only in `**kwargs` annotation", + )); + diag.set_primary_message(format_args!( + "Did you mean `{name}.args`?" + )); + diagnostic::add_type_expression_reference_link(diag); + } + // TODO: Should this be `Unknown` instead? + Type::homogeneous_tuple(self.db(), Type::unknown()) + } + + // `*args: P` + None => { + // TODO: Should this be `Unknown` instead? + Type::homogeneous_tuple(self.db(), Type::unknown()) + } + } + } else { + Type::homogeneous_tuple(self.db(), annotated_type) + } }; self.add_declaration_with_binding( @@ -2622,7 +2655,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { typing_self(db, self.scope(), Some(method_definition), class_literal) } - /// Set initial declared/inferred types for a `*args` variadic positional parameter. + /// Set initial declared/inferred types for a `**kwargs` keyword-variadic parameter. /// /// The annotated type is implicitly wrapped in a string-keyed dictionary. /// @@ -2635,11 +2668,47 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { definition: Definition<'db>, ) { if let Some(annotation) = parameter.annotation() { - let annotated_ty = self.file_expression_type(annotation); - let ty = KnownClass::Dict.to_specialized_instance( - self.db(), - [KnownClass::Str.to_instance(self.db()), annotated_ty], - ); + let annotated_type = self.file_expression_type(annotation); + tracing::debug!("annotated_type: {}", annotated_type.display(self.db())); + let ty = if let Type::TypeVar(typevar) = annotated_type + && typevar.kind(self.db()).is_paramspec() + { + match typevar.paramspec_attr(self.db()) { + // `**kwargs: P.args` + Some(ParamSpecAttrKind::Args) => { + if let Some(builder) = + self.context.report_lint(&INVALID_TYPE_FORM, annotation) + { + let name = typevar.name(self.db()); + let mut diag = builder.into_diagnostic(format_args!( + "`{name}.args` is valid only in `*args` annotation", + )); + diag.set_primary_message(format_args!("Did you mean `{name}.kwargs`?")); + diagnostic::add_type_expression_reference_link(diag); + } + // TODO: Should this be `Unknown` instead? + KnownClass::Dict.to_specialized_instance( + self.db(), + [KnownClass::Str.to_instance(self.db()), Type::unknown()], + ) + } + + // `**kwargs: P.kwargs` + Some(ParamSpecAttrKind::Kwargs) => annotated_type, + + // `**kwargs: P` + // TODO: Should this be `Unknown` instead? + None => KnownClass::Dict.to_specialized_instance( + self.db(), + [KnownClass::Str.to_instance(self.db()), Type::unknown()], + ), + } + } else { + KnownClass::Dict.to_specialized_instance( + self.db(), + [KnownClass::Str.to_instance(self.db()), annotated_type], + ) + }; self.add_declaration_with_binding( parameter.into(), definition, @@ -8875,10 +8944,28 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { fn infer_attribute_load(&mut self, attribute: &ast::ExprAttribute) -> Type<'db> { let ast::ExprAttribute { value, attr, .. } = attribute; - let value_type = self.infer_maybe_standalone_expression(value, TypeContext::default()); + let mut value_type = self.infer_maybe_standalone_expression(value, TypeContext::default()); let db = self.db(); let mut constraint_keys = vec![]; + tracing::debug!( + "value_type for attribute access: {}", + value_type.display(db) + ); + if let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) = value_type + && typevar.kind(db).is_paramspec() + && let Some(bound_typevar) = bind_typevar( + db, + self.index, + self.scope().file_scope_id(db), + self.typevar_binding_context, + typevar, + ) + { + value_type = Type::TypeVar(bound_typevar); + tracing::debug!("updated value_type: {}", value_type.display(db)); + } + let mut assigned_type = None; if let Some(place_expr) = PlaceExpr::try_from_expr(attribute) { let (resolved, keys) = self.infer_place_load( @@ -8890,6 +8977,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { assigned_type = Some(ty); } } + tracing::debug!("assigned_type for attribute access: {:?}", assigned_type); let fallback_place = value_type.member(db, &attr.id); // Exclude non-definitely-bound places for purposes of reachability // analysis. We currently do not perform boundness analysis for implicit @@ -8988,6 +9076,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }) .inner_type(); + tracing::debug!( + "resolved_type for attribute access: {}", + resolved_type.display(db) + ); + self.check_deprecated(attr, resolved_type); // Even if we can obtain the attribute type based on the assignments, we still perform default type inference diff --git a/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs index 5e1f852695..f30eab7a84 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs @@ -144,11 +144,16 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } ast::Expr::Attribute(attribute) => match attribute.ctx { - ast::ExprContext::Load => infer_name_or_attribute( - self.infer_attribute_expression(attribute), - annotation, - self, - ), + ast::ExprContext::Load => { + let attribute_type = self.infer_attribute_expression(attribute); + if let Type::TypeVar(typevar) = attribute_type + && typevar.paramspec_attr(self.db()).is_some() + { + TypeAndQualifiers::declared(attribute_type) + } else { + infer_name_or_attribute(attribute_type, annotation, self) + } + } ast::ExprContext::Invalid => TypeAndQualifiers::declared(Type::unknown()), ast::ExprContext::Store | ast::ExprContext::Del => TypeAndQualifiers::declared( todo_type!("Attribute expression annotation in Store/Del context"), diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index 20c5362a0b..c2655548a5 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -2,14 +2,15 @@ use itertools::Either; use ruff_python_ast as ast; use super::{DeferredExpressionState, TypeInferenceBuilder}; +use crate::semantic_index::semantic_index; use crate::types::diagnostic::{ self, INVALID_TYPE_FORM, NON_SUBSCRIPTABLE, report_invalid_argument_number_to_special_form, report_invalid_arguments_to_callable, }; -use crate::types::signatures::Signature; +use crate::types::generics::bind_typevar; +use crate::types::signatures::{ParamSpecOrigin, Signature}; use crate::types::string_annotation::parse_string_annotation; use crate::types::tuple::{TupleSpecBuilder, TupleType}; -use crate::types::visitor::any_over_type; use crate::types::{ CallableType, DynamicType, IntersectionBuilder, KnownClass, KnownInstanceType, LintDiagnosticGuard, Parameter, Parameters, SpecialFormType, SubclassOfType, Type, @@ -1535,21 +1536,23 @@ impl<'db> TypeInferenceBuilder<'db, '_> { // `Callable[]`. return None; } - if any_over_type( - self.db(), - self.infer_name_load(name), - &|ty| match ty { - Type::KnownInstance(known_instance) => { - known_instance.class(self.db()) == KnownClass::ParamSpec - } - Type::NominalInstance(nominal) => { - nominal.has_known_class(self.db(), KnownClass::ParamSpec) - } - _ => false, - }, - true, - ) { - return Some(Parameters::todo()); + let name_ty = self.infer_name_load(name); + if let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) = name_ty + && typevar.kind(self.db()).is_paramspec() + { + let index = semantic_index(self.db(), self.scope().file(self.db())); + let origin = bind_typevar( + self.db(), + index, + self.scope().file_scope_id(self.db()), + self.typevar_binding_context, + typevar, + ) + .map_or( + ParamSpecOrigin::Unbounded(typevar), + ParamSpecOrigin::Bounded, + ); + return Some(Parameters::paramspec(self.db(), origin)); } } _ => {} diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index 7c48b4c289..a064b3fa74 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -29,9 +29,10 @@ use crate::types::generics::{ }; use crate::types::infer::nearest_enclosing_class; use crate::types::{ - ApplyTypeMappingVisitor, BoundTypeVarInstance, ClassLiteral, FindLegacyTypeVarsVisitor, - HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, KnownClass, MaterializationKind, - NormalizedVisitor, TypeContext, TypeMapping, TypeRelation, VarianceInferable, todo_type, + ApplyTypeMappingVisitor, BindingContext, BoundTypeVarInstance, ClassLiteral, + FindLegacyTypeVarsVisitor, HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, + KnownClass, KnownInstanceType, MaterializationKind, NormalizedVisitor, ParamSpecAttrKind, + TypeContext, TypeMapping, TypeRelation, TypeVarInstance, VarianceInferable, todo_type, }; use crate::{Db, FxOrderSet}; use ruff_python_ast::{self as ast, name::Name}; @@ -1169,10 +1170,56 @@ impl<'db> VarianceInferable<'db> for &Signature<'db> { } } -#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] -pub(crate) struct Parameters<'db> { - // TODO: use SmallVec here once invariance bug is fixed - value: Vec>, +#[derive( + Copy, Clone, Debug, Eq, Hash, PartialEq, salsa::Update, Ord, PartialOrd, get_size2::GetSize, +)] +pub(crate) enum ParamSpecOrigin<'db> { + Bounded(BoundTypeVarInstance<'db>), + Unbounded(TypeVarInstance<'db>), +} + +impl<'db> ParamSpecOrigin<'db> { + pub(crate) fn with_paramspec_attr( + self, + db: &'db dyn Db, + paramspec_attr: ParamSpecAttrKind, + ) -> Self { + match self { + ParamSpecOrigin::Bounded(typevar) => { + ParamSpecOrigin::Bounded(typevar.with_paramspec_attr(db, paramspec_attr)) + } + ParamSpecOrigin::Unbounded(_) => self, + } + } + + pub(crate) fn name(&self, db: &'db dyn Db) -> &ast::name::Name { + match self { + ParamSpecOrigin::Bounded(bound) => bound.typevar(db).name(db), + ParamSpecOrigin::Unbounded(unbound) => unbound.name(db), + } + } + + pub(crate) fn binding_context(&self, db: &'db dyn Db) -> Option> { + match self { + ParamSpecOrigin::Bounded(bound) => Some(bound.binding_context(db)), + ParamSpecOrigin::Unbounded(_) => None, + } + } + + pub(crate) fn into_type(self) -> Type<'db> { + match self { + ParamSpecOrigin::Bounded(bound) => Type::TypeVar(bound), + ParamSpecOrigin::Unbounded(unbound) => { + Type::KnownInstance(KnownInstanceType::TypeVar(unbound)) + } + } + } +} + +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] +pub(crate) enum ParametersKind<'db> { + #[default] + Standard, /// Whether this parameter list represents a gradual form using `...` as the only parameter. /// @@ -1193,27 +1240,41 @@ pub(crate) struct Parameters<'db> { /// some adjustments to represent that. /// /// [the typing specification]: https://typing.python.org/en/latest/spec/callables.html#meaning-of-in-callable - is_gradual: bool, + Gradual, + + // TODO: Need to store the name of the paramspec variable for the display implementation. + ParamSpec(ParamSpecOrigin<'db>), +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] +pub(crate) struct Parameters<'db> { + // TODO: use SmallVec here once invariance bug is fixed + value: Vec>, + kind: ParametersKind<'db>, } impl<'db> Parameters<'db> { pub(crate) fn new(parameters: impl IntoIterator>) -> Self { let value: Vec> = parameters.into_iter().collect(); - let is_gradual = value.len() == 2 + let kind = if value.len() == 2 && value .iter() .any(|p| p.is_variadic() && p.annotated_type().is_none_or(|ty| ty.is_dynamic())) && value.iter().any(|p| { p.is_keyword_variadic() && p.annotated_type().is_none_or(|ty| ty.is_dynamic()) - }); - Self { value, is_gradual } + }) { + ParametersKind::Gradual + } else { + ParametersKind::Standard + }; + Self { value, kind } } /// Create an empty parameter list. pub(crate) fn empty() -> Self { Self { value: Vec::new(), - is_gradual: false, + kind: ParametersKind::Standard, } } @@ -1221,8 +1282,12 @@ impl<'db> Parameters<'db> { self.value.as_slice() } + pub(crate) const fn kind(&self) -> ParametersKind<'db> { + self.kind + } + pub(crate) const fn is_gradual(&self) -> bool { - self.is_gradual + matches!(self.kind, ParametersKind::Gradual) } /// Return todo parameters: (*args: Todo, **kwargs: Todo) @@ -1234,7 +1299,7 @@ impl<'db> Parameters<'db> { Parameter::keyword_variadic(Name::new_static("kwargs")) .with_annotated_type(todo_type!("todo signature **kwargs")), ], - is_gradual: true, + kind: ParametersKind::Gradual, } } @@ -1251,7 +1316,25 @@ impl<'db> Parameters<'db> { Parameter::keyword_variadic(Name::new_static("kwargs")) .with_annotated_type(Type::Dynamic(DynamicType::Any)), ], - is_gradual: true, + kind: ParametersKind::Gradual, + } + } + + pub(crate) fn paramspec(db: &'db dyn Db, origin: ParamSpecOrigin<'db>) -> Self { + Self { + value: vec![ + Parameter::variadic(Name::new_static("args")).with_annotated_type( + origin + .with_paramspec_attr(db, ParamSpecAttrKind::Args) + .into_type(), + ), + Parameter::keyword_variadic(Name::new_static("kwargs")).with_annotated_type( + origin + .with_paramspec_attr(db, ParamSpecAttrKind::Kwargs) + .into_type(), + ), + ], + kind: ParametersKind::ParamSpec(origin), } } @@ -1269,7 +1352,7 @@ impl<'db> Parameters<'db> { Parameter::keyword_variadic(Name::new_static("kwargs")) .with_annotated_type(Type::Dynamic(DynamicType::Unknown)), ], - is_gradual: true, + kind: ParametersKind::Gradual, } } @@ -1281,7 +1364,7 @@ impl<'db> Parameters<'db> { Parameter::keyword_variadic(Name::new_static("kwargs")) .with_annotated_type(Type::object()), ], - is_gradual: false, + kind: ParametersKind::Standard, } } @@ -1471,13 +1554,13 @@ impl<'db> Parameters<'db> { // Note that we've already flipped the materialization in Signature.apply_type_mapping_impl(), // so the "top" materialization here is the bottom materialization of the whole Signature. // It might make sense to flip the materialization here instead. - TypeMapping::Materialize(MaterializationKind::Top) if self.is_gradual => { + TypeMapping::Materialize(MaterializationKind::Top) if self.is_gradual() => { Parameters::object() } // TODO: This is wrong, the empty Parameters is not a subtype of all materializations. // The bottom materialization is not currently representable and implementing it // properly requires extending the Parameters struct. - TypeMapping::Materialize(MaterializationKind::Bottom) if self.is_gradual => { + TypeMapping::Materialize(MaterializationKind::Bottom) if self.is_gradual() => { Parameters::empty() } _ => Self { @@ -1486,7 +1569,7 @@ impl<'db> Parameters<'db> { .iter() .map(|param| param.apply_type_mapping_impl(db, type_mapping, tcx, visitor)) .collect(), - is_gradual: self.is_gradual, + kind: self.kind, }, } } From c32615af370f852b6fb83ff6698f74df8d8b7f5c Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Fri, 14 Nov 2025 16:04:34 +0530 Subject: [PATCH 2/2] Avoid raising error when `P` is used in invalid context --- crates/ty_python_semantic/src/types.rs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 21e97c64d5..0d9719427b 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -6561,15 +6561,17 @@ impl<'db> Type<'db> { KnownInstanceType::TypeAliasType(alias) => Ok(Type::TypeAlias(*alias)), KnownInstanceType::NewType(newtype) => Ok(Type::NewTypeInstance(*newtype)), KnownInstanceType::TypeVar(typevar) => { - // A `ParamSpec` type variable cannot be used in type expressions. - if typevar.kind(db).is_paramspec() { - return Err(InvalidTypeExpressionError { - invalid_expressions: smallvec::smallvec_inline![ - InvalidTypeExpression::InvalidType(*self, scope_id) - ], - fallback_type: Type::unknown(), - }); - } + // TODO: A `ParamSpec` type variable cannot be used in type expressions. This + // requires storing additional context as it's allowed in some places + // (`Concatenate`, `Callable`) but not others. + // if typevar.kind(db).is_paramspec() { + // return Err(InvalidTypeExpressionError { + // invalid_expressions: smallvec::smallvec_inline![ + // InvalidTypeExpression::InvalidType(*self, scope_id) + // ], + // fallback_type: Type::unknown(), + // }); + // } let index = semantic_index(db, scope_id.file(db)); Ok(bind_typevar( db,