This commit is contained in:
Dhruv Manilawala 2025-11-16 18:29:33 +00:00 committed by GitHub
commit 5992af4894
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 373 additions and 117 deletions

View file

@ -4581,11 +4581,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)
@ -6692,6 +6697,17 @@ impl<'db> Type<'db> {
KnownInstanceType::TypeAliasType(alias) => Ok(Type::TypeAlias(*alias)),
KnownInstanceType::NewType(newtype) => Ok(Type::NewTypeInstance(*newtype)),
KnownInstanceType::TypeVar(typevar) => {
// 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,
@ -6907,9 +6923,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"
)),
@ -7128,7 +7147,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(_) |
@ -7933,6 +7952,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`].
///
@ -8725,12 +8746,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 {
@ -9036,6 +9057,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`),
@ -9048,14 +9084,17 @@ impl<'db> BindingContext<'db> {
pub struct BoundTypeVarIdentity<'db> {
pub(crate) identity: TypeVarIdentity<'db>,
pub(crate) binding_context: BindingContext<'db>,
paramspec_attr: Option<ParamSpecAttrKind>,
}
/// 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<ParamSpecAttrKind>,
}
// The Salsa heap is tracked separately.
@ -9070,9 +9109,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 {
@ -9099,7 +9151,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.
@ -9121,7 +9173,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(
@ -9197,6 +9249,7 @@ impl<'db> BoundTypeVarInstance<'db> {
db,
self.typevar(db).normalized_impl(db, visitor),
self.binding_context(db),
self.paramspec_attr(db),
)
}
@ -9211,6 +9264,7 @@ impl<'db> BoundTypeVarInstance<'db> {
self.typevar(db)
.materialize_impl(db, materialization_kind, visitor),
self.binding_context(db),
self.paramspec_attr(db),
)
}
@ -9219,6 +9273,7 @@ impl<'db> BoundTypeVarInstance<'db> {
db,
self.typevar(db).to_instance(db)?,
self.binding_context(db),
self.paramspec_attr(db),
))
}
}

View file

@ -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,11 +1121,8 @@ 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 {
match self.parameters.kind() {
ParametersKind::Standard => {
let mut star_added = false;
let mut needs_slash = false;
let mut first = true;
@ -1169,6 +1171,21 @@ impl DisplaySignature<'_> {
writer.write_char('/')?;
}
}
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}"))?;
}
}
}
if multiline {
writer.write_char('\n')?;

View file

@ -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)
})
})
}
@ -318,7 +318,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,

View file

@ -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,
@ -104,11 +104,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};
@ -2540,7 +2540,40 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
todo_type!("PEP 646")
} else {
let annotated_type = self.file_expression_type(annotation);
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(
@ -2623,7 +2656,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.
///
@ -2636,11 +2669,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(
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()), annotated_ty],
);
[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,
@ -8925,10 +8994,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(
@ -8940,6 +9027,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
@ -9038,6 +9126,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

View file

@ -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"),

View file

@ -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,
@ -1538,21 +1539,23 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
// `Callable[]`.
return None;
}
if any_over_type(
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(),
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());
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));
}
}
_ => {}

View file

@ -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<Parameter<'db>>,
#[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<BindingContext<'db>> {
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<Parameter<'db>>,
kind: ParametersKind<'db>,
}
impl<'db> Parameters<'db> {
pub(crate) fn new(parameters: impl IntoIterator<Item = Parameter<'db>>) -> Self {
let value: Vec<Parameter<'db>> = 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,
},
}
}