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() .into()
} }
Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) Type::TypeVar(typevar)
if typevar.kind(db).is_paramspec() if typevar.kind(db).is_paramspec()
&& matches!(name.as_str(), "args" | "kwargs") => && 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) Type::NominalInstance(instance)
@ -6692,6 +6697,17 @@ impl<'db> Type<'db> {
KnownInstanceType::TypeAliasType(alias) => Ok(Type::TypeAlias(*alias)), KnownInstanceType::TypeAliasType(alias) => Ok(Type::TypeAlias(*alias)),
KnownInstanceType::NewType(newtype) => Ok(Type::NewTypeInstance(*newtype)), KnownInstanceType::NewType(newtype) => Ok(Type::NewTypeInstance(*newtype)),
KnownInstanceType::TypeVar(typevar) => { 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)); let index = semantic_index(db, scope_id.file(db));
Ok(bind_typevar( Ok(bind_typevar(
db, db,
@ -6907,9 +6923,12 @@ impl<'db> Type<'db> {
Some(KnownClass::TypeVar) => Ok(todo_type!( Some(KnownClass::TypeVar) => Ok(todo_type!(
"Support for `typing.TypeVar` instances in type expressions" "Support for `typing.TypeVar` instances in type expressions"
)), )),
Some( Some(KnownClass::ParamSpecArgs) => {
KnownClass::ParamSpec | KnownClass::ParamSpecArgs | KnownClass::ParamSpecKwargs, Ok(todo_type!("Support for `typing.ParamSpecArgs`"))
) => Ok(todo_type!("Support for `typing.ParamSpec`")), }
Some(KnownClass::ParamSpecKwargs) => {
Ok(todo_type!("Support for `typing.ParamSpecKwargs`"))
}
Some(KnownClass::TypeVarTuple) => Ok(todo_type!( Some(KnownClass::TypeVarTuple) => Ok(todo_type!(
"Support for `typing.TypeVarTuple` instances in type expressions" "Support for `typing.TypeVarTuple` instances in type expressions"
)), )),
@ -7128,7 +7147,7 @@ impl<'db> Type<'db> {
Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) => match type_mapping { Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) => match type_mapping {
TypeMapping::BindLegacyTypevars(binding_context) => { TypeMapping::BindLegacyTypevars(binding_context) => {
Type::TypeVar(BoundTypeVarInstance::new(db, typevar, *binding_context)) Type::TypeVar(BoundTypeVarInstance::new(db, typevar, *binding_context, None))
} }
TypeMapping::Specialization(_) | TypeMapping::Specialization(_) |
TypeMapping::PartialSpecialization(_) | TypeMapping::PartialSpecialization(_) |
@ -7933,6 +7952,8 @@ pub struct TrackedConstraintSet<'db> {
// The Salsa heap is tracked separately. // The Salsa heap is tracked separately.
impl get_size2::GetSize for TrackedConstraintSet<'_> {} 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, /// Singleton types that are heavily special-cased by ty. Despite its name,
/// quite a different type to [`NominalInstanceType`]. /// quite a different type to [`NominalInstanceType`].
/// ///
@ -8725,12 +8746,12 @@ fn walk_type_var_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>(
#[salsa::tracked] #[salsa::tracked]
impl<'db> TypeVarInstance<'db> { impl<'db> TypeVarInstance<'db> {
pub(crate) fn with_binding_context( pub(crate) fn as_bound_type_var_instance(
self, self,
db: &'db dyn Db, db: &'db dyn Db,
binding_context: Definition<'db>, binding_context: Definition<'db>,
) -> BoundTypeVarInstance<'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 { 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. /// 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`), /// 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 struct BoundTypeVarIdentity<'db> {
pub(crate) identity: TypeVarIdentity<'db>, pub(crate) identity: TypeVarIdentity<'db>,
pub(crate) binding_context: BindingContext<'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 /// A type variable that has been bound to a generic context, and which can be specialized to a
/// concrete type. /// concrete type.
#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)]
#[derive(PartialOrd, Ord)]
pub struct BoundTypeVarInstance<'db> { pub struct BoundTypeVarInstance<'db> {
pub typevar: TypeVarInstance<'db>, pub typevar: TypeVarInstance<'db>,
binding_context: BindingContext<'db>, binding_context: BindingContext<'db>,
paramspec_attr: Option<ParamSpecAttrKind>,
} }
// The Salsa heap is tracked separately. // The Salsa heap is tracked separately.
@ -9070,9 +9109,22 @@ impl<'db> BoundTypeVarInstance<'db> {
BoundTypeVarIdentity { BoundTypeVarIdentity {
identity: self.typevar(db).identity(db), identity: self.typevar(db).identity(db),
binding_context: self.binding_context(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. /// Returns whether two bound typevars represent the same logical typevar, regardless of e.g.
/// differences in their bounds or constraints due to materialization. /// differences in their bounds or constraints due to materialization.
pub(crate) fn is_same_typevar_as(self, db: &'db dyn Db, other: Self) -> bool { 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), Some(variance),
None, // _default 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. /// Create a new synthetic `Self` type variable with the given upper bound.
@ -9121,7 +9173,7 @@ impl<'db> BoundTypeVarInstance<'db> {
Some(TypeVarVariance::Invariant), Some(TypeVarVariance::Invariant),
None, // _default None, // _default
); );
Self::new(db, typevar, binding_context) Self::new(db, typevar, binding_context, None)
} }
pub(crate) fn variance_with_polarity( pub(crate) fn variance_with_polarity(
@ -9197,6 +9249,7 @@ impl<'db> BoundTypeVarInstance<'db> {
db, db,
self.typevar(db).normalized_impl(db, visitor), self.typevar(db).normalized_impl(db, visitor),
self.binding_context(db), self.binding_context(db),
self.paramspec_attr(db),
) )
} }
@ -9211,6 +9264,7 @@ impl<'db> BoundTypeVarInstance<'db> {
self.typevar(db) self.typevar(db)
.materialize_impl(db, materialization_kind, visitor), .materialize_impl(db, materialization_kind, visitor),
self.binding_context(db), self.binding_context(db),
self.paramspec_attr(db),
) )
} }
@ -9219,6 +9273,7 @@ impl<'db> BoundTypeVarInstance<'db> {
db, db,
self.typevar(db).to_instance(db)?, self.typevar(db).to_instance(db)?,
self.binding_context(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::class::{ClassLiteral, ClassType, GenericAlias};
use crate::types::function::{FunctionType, OverloadLiteral}; use crate::types::function::{FunctionType, OverloadLiteral};
use crate::types::generics::{GenericContext, Specialization}; 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::tuple::TupleSpec;
use crate::types::visitor::TypeVisitor; use crate::types::visitor::TypeVisitor;
use crate::types::{ 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) { if let Some(binding_context) = self.bound_typevar_identity.binding_context.name(self.db) {
write!(f, "@{binding_context}")?; write!(f, "@{binding_context}")?;
} }
if let Some(paramspec_attr) = self.bound_typevar_identity.paramspec_attr {
write!(f, ".{paramspec_attr}")?;
}
Ok(()) Ok(())
} }
} }
@ -1116,57 +1121,69 @@ impl DisplaySignature<'_> {
if multiline { if multiline {
writer.write_str("\n ")?; writer.write_str("\n ")?;
} }
if self.parameters.is_gradual() { match self.parameters.kind() {
// We represent gradual form as `...` in the signature, internally the parameters still ParametersKind::Standard => {
// contain `(*args, **kwargs)` parameters. let mut star_added = false;
writer.write_str("...")?; let mut needs_slash = false;
} else { let mut first = true;
let mut star_added = false; let arg_separator = if multiline { ",\n " } else { ", " };
let mut needs_slash = false;
let mut first = true;
let arg_separator = if multiline { ",\n " } else { ", " };
for parameter in self.parameters.as_slice() { for parameter in self.parameters.as_slice() {
// Handle special separators // Handle special separators
if !star_added && parameter.is_keyword_only() { 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 { if !first {
writer.write_str(arg_separator)?; 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(
&parameter.display_with(self.db, self.settings.singleline()),
param_name.as_deref(),
)?;
first = false; first = false;
} }
if parameter.is_positional_only() {
needs_slash = true; if needs_slash {
} else if needs_slash {
if !first { if !first {
writer.write_str(arg_separator)?; writer.write_str(arg_separator)?;
} }
writer.write_char('/')?; 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(
&parameter.display_with(self.db, self.settings.singleline()),
param_name.as_deref(),
)?;
first = false;
} }
ParametersKind::Gradual => {
if needs_slash { // We represent gradual form as `...` in the signature, internally the parameters still
if !first { // contain `(*args, **kwargs)` parameters.
writer.write_str(arg_separator)?; 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('/')?;
} }
} }

View file

@ -64,7 +64,7 @@ pub(crate) fn bind_typevar<'db>(
if outer.kind().is_class() { if outer.kind().is_class() {
if let NodeWithScopeKind::Function(function) = inner.node() { if let NodeWithScopeKind::Function(function) = inner.node() {
let definition = index.expect_single_definition(function); 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)) .find_map(|enclosing_context| enclosing_context.binds_typevar(db, typevar))
.or_else(|| { .or_else(|| {
typevar_binding_context.map(|typevar_binding_context| { 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 { else {
return None; return None;
}; };
Some(typevar.with_binding_context(db, binding_context)) Some(typevar.as_bound_type_var_instance(db, binding_context))
} }
// TODO: Support these! // TODO: Support these!
ast::TypeParam::ParamSpec(_) => None, 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::context::{InNoTypeCheck, InferContext};
use crate::types::cyclic::CycleDetector; use crate::types::cyclic::CycleDetector;
use crate::types::diagnostic::{ use crate::types::diagnostic::{
CALL_NON_CALLABLE, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS, CYCLIC_CLASS_DEFINITION, self, CALL_NON_CALLABLE, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS,
DIVISION_BY_ZERO, DUPLICATE_KW_ONLY, INCONSISTENT_MRO, INVALID_ARGUMENT_TYPE, CYCLIC_CLASS_DEFINITION, DIVISION_BY_ZERO, DUPLICATE_KW_ONLY, INCONSISTENT_MRO,
INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE, INVALID_DECLARATION, INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE,
INVALID_GENERIC_CLASS, INVALID_KEY, INVALID_LEGACY_TYPE_VARIABLE, INVALID_METACLASS, INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_KEY, INVALID_LEGACY_TYPE_VARIABLE,
INVALID_NAMED_TUPLE, INVALID_NEWTYPE, INVALID_OVERLOAD, INVALID_PARAMETER_DEFAULT, INVALID_METACLASS, INVALID_NAMED_TUPLE, INVALID_NEWTYPE, INVALID_OVERLOAD,
INVALID_PARAMSPEC, INVALID_PROTOCOL, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL, INVALID_PARAMETER_DEFAULT, INVALID_PARAMSPEC, INVALID_PROTOCOL, INVALID_TYPE_FORM,
INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases, NON_SUBSCRIPTABLE, INVALID_TYPE_GUARD_CALL, INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases,
POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT, SUBCLASS_OF_FINAL_CLASS, NON_SUBSCRIPTABLE, POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT,
UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, SUBCLASS_OF_FINAL_CLASS, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL,
UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY, UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY,
hint_if_stdlib_attribute_exists_on_other_versions, hint_if_stdlib_attribute_exists_on_other_versions,
hint_if_stdlib_submodule_exists_on_other_versions, report_attempted_protocol_instantiation, 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, 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, CallDunderError, CallableBinding, CallableType, ClassLiteral, ClassType, DataclassParams,
DynamicType, InferredAs, InternedType, InternedTypes, IntersectionBuilder, IntersectionType, DynamicType, InferredAs, InternedType, InternedTypes, IntersectionBuilder, IntersectionType,
KnownClass, KnownInstanceType, LintDiagnosticGuard, MemberLookupPolicy, MetaclassCandidate, KnownClass, KnownInstanceType, LintDiagnosticGuard, MemberLookupPolicy, MetaclassCandidate,
PEP695TypeAliasType, Parameter, ParameterForm, Parameters, SpecialFormType, SubclassOfType, PEP695TypeAliasType, ParamSpecAttrKind, Parameter, ParameterForm, Parameters, SpecialFormType,
TrackedConstraintSet, Truthiness, Type, TypeAliasType, TypeAndQualifiers, TypeContext, SubclassOfType, TrackedConstraintSet, Truthiness, Type, TypeAliasType, TypeAndQualifiers,
TypeQualifiers, TypeVarBoundOrConstraintsEvaluation, TypeVarDefaultEvaluation, TypeVarIdentity, TypeContext, TypeQualifiers, TypeVarBoundOrConstraintsEvaluation, TypeVarDefaultEvaluation,
TypeVarInstance, TypeVarKind, TypeVarVariance, TypedDictType, UnionBuilder, UnionType, TypeVarIdentity, TypeVarInstance, TypeVarKind, TypeVarVariance, TypedDictType, UnionBuilder,
binding_type, todo_type, UnionType, binding_type, todo_type,
}; };
use crate::types::{ClassBase, add_inferred_python_version_hint_to_diagnostic}; use crate::types::{ClassBase, add_inferred_python_version_hint_to_diagnostic};
use crate::unpack::{EvaluationMode, UnpackPosition}; use crate::unpack::{EvaluationMode, UnpackPosition};
@ -2540,7 +2540,40 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
todo_type!("PEP 646") todo_type!("PEP 646")
} else { } else {
let annotated_type = self.file_expression_type(annotation); 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( 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) 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. /// The annotated type is implicitly wrapped in a string-keyed dictionary.
/// ///
@ -2636,11 +2669,47 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
definition: Definition<'db>, definition: Definition<'db>,
) { ) {
if let Some(annotation) = parameter.annotation() { if let Some(annotation) = parameter.annotation() {
let annotated_ty = self.file_expression_type(annotation); let annotated_type = self.file_expression_type(annotation);
let ty = KnownClass::Dict.to_specialized_instance( tracing::debug!("annotated_type: {}", annotated_type.display(self.db()));
self.db(), let ty = if let Type::TypeVar(typevar) = annotated_type
[KnownClass::Str.to_instance(self.db()), annotated_ty], && 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( self.add_declaration_with_binding(
parameter.into(), parameter.into(),
definition, definition,
@ -8925,10 +8994,28 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
fn infer_attribute_load(&mut self, attribute: &ast::ExprAttribute) -> Type<'db> { fn infer_attribute_load(&mut self, attribute: &ast::ExprAttribute) -> Type<'db> {
let ast::ExprAttribute { value, attr, .. } = attribute; 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 db = self.db();
let mut constraint_keys = vec![]; 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; let mut assigned_type = None;
if let Some(place_expr) = PlaceExpr::try_from_expr(attribute) { if let Some(place_expr) = PlaceExpr::try_from_expr(attribute) {
let (resolved, keys) = self.infer_place_load( let (resolved, keys) = self.infer_place_load(
@ -8940,6 +9027,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
assigned_type = Some(ty); assigned_type = Some(ty);
} }
} }
tracing::debug!("assigned_type for attribute access: {:?}", assigned_type);
let fallback_place = value_type.member(db, &attr.id); let fallback_place = value_type.member(db, &attr.id);
// Exclude non-definitely-bound places for purposes of reachability // Exclude non-definitely-bound places for purposes of reachability
// analysis. We currently do not perform boundness analysis for implicit // analysis. We currently do not perform boundness analysis for implicit
@ -9038,6 +9126,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}) })
.inner_type(); .inner_type();
tracing::debug!(
"resolved_type for attribute access: {}",
resolved_type.display(db)
);
self.check_deprecated(attr, resolved_type); self.check_deprecated(attr, resolved_type);
// Even if we can obtain the attribute type based on the assignments, we still perform default type inference // 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::Expr::Attribute(attribute) => match attribute.ctx {
ast::ExprContext::Load => infer_name_or_attribute( ast::ExprContext::Load => {
self.infer_attribute_expression(attribute), let attribute_type = self.infer_attribute_expression(attribute);
annotation, if let Type::TypeVar(typevar) = attribute_type
self, && 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::Invalid => TypeAndQualifiers::declared(Type::unknown()),
ast::ExprContext::Store | ast::ExprContext::Del => TypeAndQualifiers::declared( ast::ExprContext::Store | ast::ExprContext::Del => TypeAndQualifiers::declared(
todo_type!("Attribute expression annotation in Store/Del context"), 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 ruff_python_ast as ast;
use super::{DeferredExpressionState, TypeInferenceBuilder}; use super::{DeferredExpressionState, TypeInferenceBuilder};
use crate::semantic_index::semantic_index;
use crate::types::diagnostic::{ use crate::types::diagnostic::{
self, INVALID_TYPE_FORM, NON_SUBSCRIPTABLE, report_invalid_argument_number_to_special_form, self, INVALID_TYPE_FORM, NON_SUBSCRIPTABLE, report_invalid_argument_number_to_special_form,
report_invalid_arguments_to_callable, 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::string_annotation::parse_string_annotation;
use crate::types::tuple::{TupleSpecBuilder, TupleType}; use crate::types::tuple::{TupleSpecBuilder, TupleType};
use crate::types::visitor::any_over_type;
use crate::types::{ use crate::types::{
CallableType, DynamicType, IntersectionBuilder, KnownClass, KnownInstanceType, CallableType, DynamicType, IntersectionBuilder, KnownClass, KnownInstanceType,
LintDiagnosticGuard, Parameter, Parameters, SpecialFormType, SubclassOfType, Type, LintDiagnosticGuard, Parameter, Parameters, SpecialFormType, SubclassOfType, Type,
@ -1538,21 +1539,23 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
// `Callable[]`. // `Callable[]`.
return None; return None;
} }
if any_over_type( let name_ty = self.infer_name_load(name);
self.db(), if let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) = name_ty
self.infer_name_load(name), && typevar.kind(self.db()).is_paramspec()
&|ty| match ty { {
Type::KnownInstance(known_instance) => { let index = semantic_index(self.db(), self.scope().file(self.db()));
known_instance.class(self.db()) == KnownClass::ParamSpec let origin = bind_typevar(
} self.db(),
Type::NominalInstance(nominal) => { index,
nominal.has_known_class(self.db(), KnownClass::ParamSpec) self.scope().file_scope_id(self.db()),
} self.typevar_binding_context,
_ => false, typevar,
}, )
true, .map_or(
) { ParamSpecOrigin::Unbounded(typevar),
return Some(Parameters::todo()); 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::infer::nearest_enclosing_class;
use crate::types::{ use crate::types::{
ApplyTypeMappingVisitor, BoundTypeVarInstance, ClassLiteral, FindLegacyTypeVarsVisitor, ApplyTypeMappingVisitor, BindingContext, BoundTypeVarInstance, ClassLiteral,
HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, KnownClass, MaterializationKind, FindLegacyTypeVarsVisitor, HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor,
NormalizedVisitor, TypeContext, TypeMapping, TypeRelation, VarianceInferable, todo_type, KnownClass, KnownInstanceType, MaterializationKind, NormalizedVisitor, ParamSpecAttrKind,
TypeContext, TypeMapping, TypeRelation, TypeVarInstance, VarianceInferable, todo_type,
}; };
use crate::{Db, FxOrderSet}; use crate::{Db, FxOrderSet};
use ruff_python_ast::{self as ast, name::Name}; 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)] #[derive(
pub(crate) struct Parameters<'db> { Copy, Clone, Debug, Eq, Hash, PartialEq, salsa::Update, Ord, PartialOrd, get_size2::GetSize,
// TODO: use SmallVec here once invariance bug is fixed )]
value: Vec<Parameter<'db>>, 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. /// 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. /// some adjustments to represent that.
/// ///
/// [the typing specification]: https://typing.python.org/en/latest/spec/callables.html#meaning-of-in-callable /// [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> { impl<'db> Parameters<'db> {
pub(crate) fn new(parameters: impl IntoIterator<Item = Parameter<'db>>) -> Self { pub(crate) fn new(parameters: impl IntoIterator<Item = Parameter<'db>>) -> Self {
let value: Vec<Parameter<'db>> = parameters.into_iter().collect(); let value: Vec<Parameter<'db>> = parameters.into_iter().collect();
let is_gradual = value.len() == 2 let kind = if value.len() == 2
&& value && value
.iter() .iter()
.any(|p| p.is_variadic() && p.annotated_type().is_none_or(|ty| ty.is_dynamic())) .any(|p| p.is_variadic() && p.annotated_type().is_none_or(|ty| ty.is_dynamic()))
&& value.iter().any(|p| { && value.iter().any(|p| {
p.is_keyword_variadic() && p.annotated_type().is_none_or(|ty| ty.is_dynamic()) 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. /// Create an empty parameter list.
pub(crate) fn empty() -> Self { pub(crate) fn empty() -> Self {
Self { Self {
value: Vec::new(), value: Vec::new(),
is_gradual: false, kind: ParametersKind::Standard,
} }
} }
@ -1221,8 +1282,12 @@ impl<'db> Parameters<'db> {
self.value.as_slice() self.value.as_slice()
} }
pub(crate) const fn kind(&self) -> ParametersKind<'db> {
self.kind
}
pub(crate) const fn is_gradual(&self) -> bool { pub(crate) const fn is_gradual(&self) -> bool {
self.is_gradual matches!(self.kind, ParametersKind::Gradual)
} }
/// Return todo parameters: (*args: Todo, **kwargs: Todo) /// Return todo parameters: (*args: Todo, **kwargs: Todo)
@ -1234,7 +1299,7 @@ impl<'db> Parameters<'db> {
Parameter::keyword_variadic(Name::new_static("kwargs")) Parameter::keyword_variadic(Name::new_static("kwargs"))
.with_annotated_type(todo_type!("todo signature **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")) Parameter::keyword_variadic(Name::new_static("kwargs"))
.with_annotated_type(Type::Dynamic(DynamicType::Any)), .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")) Parameter::keyword_variadic(Name::new_static("kwargs"))
.with_annotated_type(Type::Dynamic(DynamicType::Unknown)), .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")) Parameter::keyword_variadic(Name::new_static("kwargs"))
.with_annotated_type(Type::object()), .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(), // 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. // so the "top" materialization here is the bottom materialization of the whole Signature.
// It might make sense to flip the materialization here instead. // 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() Parameters::object()
} }
// TODO: This is wrong, the empty Parameters is not a subtype of all materializations. // TODO: This is wrong, the empty Parameters is not a subtype of all materializations.
// The bottom materialization is not currently representable and implementing it // The bottom materialization is not currently representable and implementing it
// properly requires extending the Parameters struct. // properly requires extending the Parameters struct.
TypeMapping::Materialize(MaterializationKind::Bottom) if self.is_gradual => { TypeMapping::Materialize(MaterializationKind::Bottom) if self.is_gradual() => {
Parameters::empty() Parameters::empty()
} }
_ => Self { _ => Self {
@ -1486,7 +1569,7 @@ impl<'db> Parameters<'db> {
.iter() .iter()
.map(|param| param.apply_type_mapping_impl(db, type_mapping, tcx, visitor)) .map(|param| param.apply_type_mapping_impl(db, type_mapping, tcx, visitor))
.collect(), .collect(),
is_gradual: self.is_gradual, kind: self.kind,
}, },
} }
} }