Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1443,7 +1443,7 @@ from typing import List, Dict
# error: [invalid-type-form] "Int literals are not allowed in this context in a type expression"
InvalidList = List[1]

# error: [invalid-type-form] "`typing.List` requires exactly one argument"
# error: [invalid-type-form] "`typing.List` requires exactly 1 argument, got 2"
ListTooManyArgs = List[int, str]

# error: [invalid-type-form] "Int literals are not allowed in this context in a type expression"
Expand All @@ -1452,10 +1452,10 @@ InvalidDict1 = Dict[1, str]
# error: [invalid-type-form] "Int literals are not allowed in this context in a type expression"
InvalidDict2 = Dict[str, 2]

# error: [invalid-type-form] "`typing.Dict` requires exactly two arguments, got 1"
# error: [invalid-type-form] "`typing.Dict` requires exactly 2 arguments, got 1"
DictTooFewArgs = Dict[str]

# error: [invalid-type-form] "`typing.Dict` requires exactly two arguments, got 3"
# error: [invalid-type-form] "`typing.Dict` requires exactly 2 arguments, got 3"
DictTooManyArgs = Dict[str, int, float]

def _(
Expand All @@ -1470,7 +1470,7 @@ def _(
reveal_type(list_too_many_args) # revealed: list[Unknown]
reveal_type(invalid_dict1) # revealed: dict[Unknown, str]
reveal_type(invalid_dict2) # revealed: dict[str, Unknown]
reveal_type(dict_too_few_args) # revealed: dict[str, Unknown]
reveal_type(dict_too_few_args) # revealed: dict[Unknown, Unknown]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copying my comment from #23513 (comment):

I think this is the only change in behaviour from this PR, which comes as a result of unifying the parsing of stdlib aliases between infer/builder.rs and infer/builder/type_expression.rs so that they now share a common implementation. Either the answer we give on main or the new answer seem fine to me, since this is an invalid type expression, but I can look into it if anybody strongly prefers the answer we have on main

reveal_type(dict_too_many_args) # revealed: dict[Unknown, Unknown]
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -478,9 +478,14 @@ bad4: TypeAlias = Final # error: [invalid-type-form]
bad5: TypeAlias = Required[int] # error: [invalid-type-form]
bad6: TypeAlias = NotRequired[int] # error: [invalid-type-form]
bad7: TypeAlias = ReadOnly[int] # error: [invalid-type-form]
bad8: TypeAlias = Unpack[tuple[int, ...]] # error: [invalid-type-form]
bad9: TypeAlias = InitVar[int] # error: [invalid-type-form]
bad10: TypeAlias = InitVar # error: [invalid-type-form]

# TODO: this should cause us to emit an error (`Unpack` is not valid at the
# top level in this context), but for different reasons to the above cases:
# `Unpack` is not a type qualifier, and so the error message in our diagnostic
# shouldn't say that it is.
differently_bad: TypeAlias = Unpack[tuple[int, ...]]
```

[type expression]: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions
145 changes: 10 additions & 135 deletions crates/ty_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use compact_str::ToCompactString;
use infer::nearest_enclosing_class;
use itertools::{Either, Itertools};
use ruff_diagnostics::{Edit, Fix};
use rustc_hash::FxHashMap;
Expand Down Expand Up @@ -63,8 +62,7 @@ use crate::types::function::{
FunctionType, KnownFunction,
};
use crate::types::generics::{
ApplySpecialization, InferableTypeVars, Specialization, bind_typevar, typing_self,
walk_generic_context,
ApplySpecialization, InferableTypeVars, Specialization, bind_typevar, walk_generic_context,
};
pub(crate) use crate::types::generics::{GenericContext, SpecializationBuilder};
use crate::types::mro::{Mro, MroIterator, StaticMroError};
Expand All @@ -75,6 +73,7 @@ pub(crate) use crate::types::narrow::{
use crate::types::newtype::NewType;
pub(crate) use crate::types::signatures::{Parameter, Parameters};
use crate::types::signatures::{ParameterForm, walk_signature};
use crate::types::special_form::TypeQualifier;
use crate::types::tuple::{Tuple, TupleSpec, TupleSpecBuilder};
use crate::types::typed_dict::TypedDictField;
pub(crate) use crate::types::typed_dict::{TypedDictParams, TypedDictType, walk_typed_dict_type};
Expand Down Expand Up @@ -5991,134 +5990,9 @@ impl<'db> Type<'db> {
KnownInstanceType::LiteralStringAlias(ty) => Ok(ty.inner(db)),
},

Type::SpecialForm(special_form) => match special_form {
SpecialFormType::Never | SpecialFormType::NoReturn => Ok(Type::Never),
SpecialFormType::LiteralString => Ok(Type::literal_string()),
SpecialFormType::Any => Ok(Type::any()),
SpecialFormType::Unknown => Ok(Type::unknown()),
SpecialFormType::AlwaysTruthy => Ok(Type::AlwaysTruthy),
SpecialFormType::AlwaysFalsy => Ok(Type::AlwaysFalsy),

// We treat `typing.Type` exactly the same as `builtins.type`:
SpecialFormType::Type => Ok(KnownClass::Type.to_instance(db)),
SpecialFormType::Tuple => Ok(Type::homogeneous_tuple(db, Type::unknown())),

// Legacy `typing` aliases
SpecialFormType::List => Ok(KnownClass::List.to_instance(db)),
SpecialFormType::Dict => Ok(KnownClass::Dict.to_instance(db)),
SpecialFormType::Set => Ok(KnownClass::Set.to_instance(db)),
SpecialFormType::FrozenSet => Ok(KnownClass::FrozenSet.to_instance(db)),
SpecialFormType::ChainMap => Ok(KnownClass::ChainMap.to_instance(db)),
SpecialFormType::Counter => Ok(KnownClass::Counter.to_instance(db)),
SpecialFormType::DefaultDict => Ok(KnownClass::DefaultDict.to_instance(db)),
SpecialFormType::Deque => Ok(KnownClass::Deque.to_instance(db)),
SpecialFormType::OrderedDict => Ok(KnownClass::OrderedDict.to_instance(db)),

// TODO: Use an opt-in rule for a bare `Callable`
SpecialFormType::Callable => Ok(Type::Callable(CallableType::unknown(db))),

// Special case: `NamedTuple` in a type expression is understood to describe the type
// `tuple[object, ...] & <a protocol that any `NamedTuple` class would satisfy>`.
// This isn't very principled (since at runtime, `NamedTuple` is just a function),
// but it appears to be what users often expect, and it improves compatibility with
// other type checkers such as mypy.
// See conversation in https://github.com/astral-sh/ruff/pull/19915.
SpecialFormType::NamedTuple => Ok(IntersectionType::from_elements(
db,
[
Type::homogeneous_tuple(db, Type::object()),
KnownClass::NamedTupleLike.to_instance(db),
],
)),
SpecialFormType::TypingSelf => {
let index = semantic_index(db, scope_id.file(db));
let Some(class) = nearest_enclosing_class(db, index, scope_id) else {
return Err(InvalidTypeExpressionError {
fallback_type: Type::unknown(),
invalid_expressions: smallvec_inline![
InvalidTypeExpression::InvalidType(*self, scope_id)
],
});
};

Ok(
typing_self(db, scope_id, typevar_binding_context, class.into())
.map(Type::TypeVar)
.unwrap_or(*self),
)
}
// We ensure that `typing.TypeAlias` used in the expected position (annotating an
// annotated assignment statement) doesn't reach here. Using it in any other type
// expression is an error.
SpecialFormType::TypeAlias => Err(InvalidTypeExpressionError {
invalid_expressions: smallvec_inline![InvalidTypeExpression::TypeAlias],
fallback_type: Type::unknown(),
}),
SpecialFormType::TypedDict => Err(InvalidTypeExpressionError {
invalid_expressions: smallvec_inline![InvalidTypeExpression::TypedDict],
fallback_type: Type::unknown(),
}),

SpecialFormType::Literal
| SpecialFormType::Union
| SpecialFormType::Intersection => Err(InvalidTypeExpressionError {
invalid_expressions: smallvec_inline![
InvalidTypeExpression::RequiresArguments(*special_form)
],
fallback_type: Type::unknown(),
}),

SpecialFormType::Protocol => Err(InvalidTypeExpressionError {
invalid_expressions: smallvec_inline![InvalidTypeExpression::Protocol],
fallback_type: Type::unknown(),
}),
SpecialFormType::Generic => Err(InvalidTypeExpressionError {
invalid_expressions: smallvec_inline![InvalidTypeExpression::Generic],
fallback_type: Type::unknown(),
}),

SpecialFormType::Optional
| SpecialFormType::Not
| SpecialFormType::Top
| SpecialFormType::Bottom
| SpecialFormType::TypeOf
| SpecialFormType::TypeIs
| SpecialFormType::TypeGuard
| SpecialFormType::Unpack
| SpecialFormType::CallableTypeOf => Err(InvalidTypeExpressionError {
invalid_expressions: smallvec_inline![
InvalidTypeExpression::RequiresOneArgument(*special_form)
],
fallback_type: Type::unknown(),
}),

SpecialFormType::Annotated | SpecialFormType::Concatenate => {
Err(InvalidTypeExpressionError {
invalid_expressions: smallvec_inline![
InvalidTypeExpression::RequiresTwoArguments(*special_form)
],
fallback_type: Type::unknown(),
})
}

SpecialFormType::ClassVar | SpecialFormType::Final => {
Err(InvalidTypeExpressionError {
invalid_expressions: smallvec_inline![
InvalidTypeExpression::TypeQualifier(*special_form)
],
fallback_type: Type::unknown(),
})
}

SpecialFormType::ReadOnly
| SpecialFormType::NotRequired
| SpecialFormType::Required => Err(InvalidTypeExpressionError {
invalid_expressions: smallvec_inline![
InvalidTypeExpression::TypeQualifierRequiresOneArgument(*special_form)
],
fallback_type: Type::unknown(),
}),
},
Type::SpecialForm(special_form) => {
special_form.in_type_expression(db, scope_id, typevar_binding_context)
}

Type::Union(union) => {
let mut builder = UnionBuilder::new(db);
Expand Down Expand Up @@ -7956,9 +7830,10 @@ impl<'db> TypeAndQualifiers<'db> {
self.origin
}

/// Insert/add an additional type qualifier.
pub(crate) fn add_qualifier(&mut self, qualifier: TypeQualifiers) {
/// Return `self` with an additional qualifier added to the set of qualifiers.
pub(crate) fn with_qualifier(mut self, qualifier: TypeQualifiers) -> Self {
self.qualifiers |= qualifier;
self
}

/// Return the set of type qualifiers.
Expand Down Expand Up @@ -8043,10 +7918,10 @@ enum InvalidTypeExpression<'db> {
TypeAlias,
/// Type qualifiers are always invalid in *type expressions*,
/// but these ones are okay with 0 arguments in *annotation expressions*
TypeQualifier(SpecialFormType),
TypeQualifier(TypeQualifier),
/// Type qualifiers that are invalid in type expressions,
/// and which would require exactly one argument even if they appeared in an annotation expression
TypeQualifierRequiresOneArgument(SpecialFormType),
TypeQualifierRequiresOneArgument(TypeQualifier),
/// Some types are always invalid in type expressions
InvalidType(Type<'db>, ScopeId<'db>),
}
Expand Down
46 changes: 10 additions & 36 deletions crates/ty_python_semantic/src/types/class_base.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
use crate::types::class::CodeGeneratorKind;
use crate::types::generics::{ApplySpecialization, Specialization};
use crate::types::mro::MroIterator;
use crate::{Db, DisplaySettings};

use crate::types::tuple::TupleType;
use crate::types::{
ApplyTypeMappingVisitor, ClassLiteral, ClassType, DynamicType, KnownClass, KnownInstanceType,
MaterializationKind, NormalizedVisitor, SpecialFormType, StaticMroError, Type, TypeContext,
TypeMapping, todo_type,
};
use crate::{Db, DisplaySettings};

/// Enumeration of the possible kinds of types we allow in class bases.
///
Expand Down Expand Up @@ -213,23 +212,20 @@ impl<'db> ClassBase<'db> {
},

Type::SpecialForm(special_form) => match special_form {
SpecialFormType::TypeQualifier(_) => None,

SpecialFormType::Annotated
| SpecialFormType::Literal
| SpecialFormType::LiteralString
| SpecialFormType::Union
| SpecialFormType::NoReturn
| SpecialFormType::Never
| SpecialFormType::Final
| SpecialFormType::NotRequired
| SpecialFormType::TypeGuard
| SpecialFormType::TypeIs
| SpecialFormType::TypingSelf
| SpecialFormType::Unpack
| SpecialFormType::ClassVar
| SpecialFormType::Concatenate
| SpecialFormType::Required
| SpecialFormType::TypeAlias
| SpecialFormType::ReadOnly
| SpecialFormType::Optional
| SpecialFormType::Not
| SpecialFormType::Top
Expand All @@ -242,9 +238,9 @@ impl<'db> ClassBase<'db> {

SpecialFormType::Any => Some(Self::Dynamic(DynamicType::Any)),
SpecialFormType::Unknown => Some(Self::unknown()),

SpecialFormType::Protocol => Some(Self::Protocol),
SpecialFormType::Generic => Some(Self::Generic),
SpecialFormType::TypedDict => Some(Self::TypedDict),

SpecialFormType::NamedTuple => {
let class = subclass?.as_static()?;
Expand All @@ -261,41 +257,19 @@ impl<'db> ClassBase<'db> {
)
}

// TODO: Classes inheriting from `typing.Type` et al. also have `Generic` in their MRO
SpecialFormType::Dict => {
Self::try_from_type(db, KnownClass::Dict.to_class_literal(db), subclass)
}
SpecialFormType::List => {
Self::try_from_type(db, KnownClass::List.to_class_literal(db), subclass)
}
// TODO: Classes inheriting from `typing.Type` also have `Generic` in their MRO
SpecialFormType::Type => {
Self::try_from_type(db, KnownClass::Type.to_class_literal(db), subclass)
}

SpecialFormType::Tuple => {
Self::try_from_type(db, KnownClass::Tuple.to_class_literal(db), subclass)
}
SpecialFormType::Set => {
Self::try_from_type(db, KnownClass::Set.to_class_literal(db), subclass)
}
SpecialFormType::FrozenSet => {
Self::try_from_type(db, KnownClass::FrozenSet.to_class_literal(db), subclass)
}
SpecialFormType::ChainMap => {
Self::try_from_type(db, KnownClass::ChainMap.to_class_literal(db), subclass)
}
SpecialFormType::Counter => {
Self::try_from_type(db, KnownClass::Counter.to_class_literal(db), subclass)
}
SpecialFormType::DefaultDict => {
Self::try_from_type(db, KnownClass::DefaultDict.to_class_literal(db), subclass)
}
SpecialFormType::Deque => {
Self::try_from_type(db, KnownClass::Deque.to_class_literal(db), subclass)
}
SpecialFormType::OrderedDict => {
Self::try_from_type(db, KnownClass::OrderedDict.to_class_literal(db), subclass)

SpecialFormType::LegacyStdlibAlias(alias) => {
Self::try_from_type(db, alias.aliased_class().to_class_literal(db), subclass)
}
SpecialFormType::TypedDict => Some(Self::TypedDict),

SpecialFormType::Callable => Self::try_from_type(
db,
todo_type!("Support for Callable as a base class"),
Expand Down
3 changes: 2 additions & 1 deletion crates/ty_python_semantic/src/types/diagnostic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3926,7 +3926,7 @@ pub(crate) fn report_invalid_arguments_to_annotated(
pub(crate) fn report_invalid_argument_number_to_special_form(
context: &InferContext,
subscript: &ast::ExprSubscript,
special_form: SpecialFormType,
special_form: impl Into<SpecialFormType>,
received_arguments: usize,
expected_arguments: u8,
) {
Expand All @@ -3939,6 +3939,7 @@ pub(crate) fn report_invalid_argument_number_to_special_form(
builder.into_diagnostic(format_args!(
"Special form `{special_form}` expected exactly {expected_arguments} {noun}, \
got {received_arguments}",
special_form = special_form.into(),
));
}
}
Expand Down
Loading