[ty] Support typing.TypeAliasType (#18156)

## Summary

Support direct uses of `typing.TypeAliasType`, as in:

```py
from typing import TypeAliasType

IntOrStr = TypeAliasType("IntOrStr", int | str)

def f(x: IntOrStr) -> None:
    reveal_type(x)  # revealed: int | str
```

closes https://github.com/astral-sh/ty/issues/392

## Ecosystem

The new false positive here:
```diff
+ error[invalid-type-form] altair/utils/core.py:49:53: The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`
```
comes from the fact that we infer the second argument as a type
expression now. We silence false positives for PEP695 `ParamSpec`s, but
not for `P = ParamSpec("P")` inside `Callable[P, ...]`.

## Test Plan

New Markdown tests
This commit is contained in:
David Peter 2025-05-19 16:36:49 +02:00 committed by GitHub
parent 220137ca7b
commit 4c889d5251
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 347 additions and 74 deletions

View file

@ -87,3 +87,54 @@ type TypeAliasType2 = TypeOf[Alias2]
static_assert(not is_equivalent_to(TypeAliasType1, TypeAliasType2))
static_assert(is_disjoint_from(TypeAliasType1, TypeAliasType2))
```
## Direct use of `TypeAliasType`
`TypeAliasType` can also be used directly. This is useful for versions of Python prior to 3.12.
```toml
[environment]
python-version = "3.9"
```
### Basic example
```py
from typing_extensions import TypeAliasType, Union
IntOrStr = TypeAliasType("IntOrStr", Union[int, str])
reveal_type(IntOrStr) # revealed: typing.TypeAliasType
reveal_type(IntOrStr.__name__) # revealed: Literal["IntOrStr"]
def f(x: IntOrStr) -> None:
reveal_type(x) # revealed: int | str
```
### Generic example
```py
from typing_extensions import TypeAliasType, TypeVar
T = TypeVar("T")
IntAnd = TypeAliasType("IntAndT", tuple[int, T], type_params=(T,))
def f(x: IntAnd[str]) -> None:
reveal_type(x) # revealed: @Todo(Generic PEP-695 type alias)
```
### Error cases
#### Name is not a string literal
```py
from typing_extensions import TypeAliasType
def get_name() -> str:
return "IntOrStr"
# error: [invalid-type-alias-type] "The name of a `typing.TypeAlias` must be a string literal"
IntOrStr = TypeAliasType(get_name(), int | str)
```

View file

@ -4012,6 +4012,45 @@ impl<'db> Type<'db> {
Signatures::single(signature)
}
Some(KnownClass::TypeAliasType) => {
// ```py
// def __new__(
// cls,
// name: str,
// value: Any,
// *,
// type_params: tuple[TypeVar | ParamSpec | TypeVarTuple, ...] = ()
// ) -> Self: ...
// ```
let signature = CallableSignature::single(
self,
Signature::new(
Parameters::new([
Parameter::positional_or_keyword(Name::new_static("name"))
.with_annotated_type(KnownClass::Str.to_instance(db)),
Parameter::positional_or_keyword(Name::new_static("value"))
.with_annotated_type(Type::any())
.type_form(),
Parameter::keyword_only(Name::new_static("type_params"))
.with_annotated_type(KnownClass::Tuple.to_specialized_instance(
db,
[UnionType::from_elements(
db,
[
KnownClass::TypeVar.to_instance(db),
KnownClass::ParamSpec.to_instance(db),
KnownClass::TypeVarTuple.to_instance(db),
],
)],
))
.with_default_type(TupleType::empty(db)),
]),
None,
),
);
Signatures::single(signature)
}
Some(KnownClass::Property) => {
let getter_signature = Signature::new(
Parameters::new([
@ -5318,7 +5357,7 @@ impl<'db> Type<'db> {
Some(TypeDefinition::TypeVar(var.definition(db)))
}
KnownInstanceType::TypeAliasType(type_alias) => {
Some(TypeDefinition::TypeAlias(type_alias.definition(db)))
type_alias.definition(db).map(TypeDefinition::TypeAlias)
}
_ => None,
},
@ -7467,7 +7506,7 @@ impl<'db> ModuleLiteralType<'db> {
}
#[salsa::interned(debug)]
pub struct TypeAliasType<'db> {
pub struct PEP695TypeAliasType<'db> {
#[returns(ref)]
pub name: ast::name::Name,
@ -7475,7 +7514,7 @@ pub struct TypeAliasType<'db> {
}
#[salsa::tracked]
impl<'db> TypeAliasType<'db> {
impl<'db> PEP695TypeAliasType<'db> {
pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> {
let scope = self.rhs_scope(db);
let type_alias_stmt_node = scope.node(db).expect_type_alias();
@ -7492,6 +7531,43 @@ impl<'db> TypeAliasType<'db> {
}
}
#[salsa::interned(debug)]
pub struct BareTypeAliasType<'db> {
#[returns(ref)]
pub name: ast::name::Name,
pub definition: Option<Definition<'db>>,
pub value: Type<'db>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, salsa::Update)]
pub enum TypeAliasType<'db> {
PEP695(PEP695TypeAliasType<'db>),
Bare(BareTypeAliasType<'db>),
}
impl<'db> TypeAliasType<'db> {
pub(crate) fn name(self, db: &'db dyn Db) -> &'db str {
match self {
TypeAliasType::PEP695(type_alias) => type_alias.name(db),
TypeAliasType::Bare(type_alias) => type_alias.name(db).as_str(),
}
}
pub(crate) fn definition(self, db: &'db dyn Db) -> Option<Definition<'db>> {
match self {
TypeAliasType::PEP695(type_alias) => Some(type_alias.definition(db)),
TypeAliasType::Bare(type_alias) => type_alias.definition(db),
}
}
pub(crate) fn value_type(self, db: &'db dyn Db) -> Type<'db> {
match self {
TypeAliasType::PEP695(type_alias) => type_alias.value_type(db),
TypeAliasType::Bare(type_alias) => type_alias.value(db),
}
}
}
/// Either the explicit `metaclass=` keyword of the class, or the inferred metaclass of one of its base classes.
#[derive(Debug, Clone, PartialEq, Eq, salsa::Update)]
pub(super) struct MetaclassCandidate<'db> {
@ -8004,6 +8080,10 @@ impl<'db> TupleType<'db> {
.to_specialized_instance(db, [UnionType::from_elements(db, self.elements(db))])
}
pub(crate) fn empty(db: &'db dyn Db) -> Type<'db> {
Type::Tuple(TupleType::new(db, Box::<[Type<'db>]>::from([])))
}
pub(crate) fn from_elements<T: Into<Type<'db>>>(
db: &'db dyn Db,
types: impl IntoIterator<Item = T>,

View file

@ -41,6 +41,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
registry.register_lint(&INVALID_EXCEPTION_CAUGHT);
registry.register_lint(&INVALID_GENERIC_CLASS);
registry.register_lint(&INVALID_LEGACY_TYPE_VARIABLE);
registry.register_lint(&INVALID_TYPE_ALIAS_TYPE);
registry.register_lint(&INVALID_METACLASS);
registry.register_lint(&INVALID_OVERLOAD);
registry.register_lint(&INVALID_PARAMETER_DEFAULT);
@ -591,6 +592,27 @@ declare_lint! {
}
}
declare_lint! {
/// ## What it does
/// Checks for the creation of invalid `TypeAliasType`s
///
/// ## Why is this bad?
/// There are several requirements that you must follow when creating a `TypeAliasType`.
///
/// ## Examples
/// ```python
/// from typing import TypeAliasType
///
/// IntOrStr = TypeAliasType("IntOrStr", int | str) # okay
/// NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name must be a string literal
/// ```
pub(crate) static INVALID_TYPE_ALIAS_TYPE = {
summary: "detects invalid TypeAliasType definitions",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// Checks for arguments to `metaclass=` that are invalid.

View file

@ -71,26 +71,26 @@ use crate::types::diagnostic::{
CONFLICTING_METACLASS, CYCLIC_CLASS_DEFINITION, DIVISION_BY_ZERO, INCONSISTENT_MRO,
INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE,
INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_LEGACY_TYPE_VARIABLE,
INVALID_PARAMETER_DEFAULT, INVALID_TYPE_FORM, INVALID_TYPE_VARIABLE_CONSTRAINTS,
POSSIBLY_UNBOUND_IMPORT, TypeCheckDiagnostics, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE,
UNRESOLVED_IMPORT, UNSUPPORTED_OPERATOR, report_implicit_return_type,
report_invalid_arguments_to_annotated, report_invalid_arguments_to_callable,
report_invalid_assignment, report_invalid_attribute_assignment,
report_invalid_generator_function_return_type, report_invalid_return_type,
report_possibly_unbound_attribute,
INVALID_PARAMETER_DEFAULT, INVALID_TYPE_ALIAS_TYPE, INVALID_TYPE_FORM,
INVALID_TYPE_VARIABLE_CONSTRAINTS, POSSIBLY_UNBOUND_IMPORT, TypeCheckDiagnostics,
UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_IMPORT, UNSUPPORTED_OPERATOR,
report_implicit_return_type, report_invalid_arguments_to_annotated,
report_invalid_arguments_to_callable, report_invalid_assignment,
report_invalid_attribute_assignment, report_invalid_generator_function_return_type,
report_invalid_return_type, report_possibly_unbound_attribute,
};
use crate::types::generics::GenericContext;
use crate::types::mro::MroErrorKind;
use crate::types::unpacker::{UnpackResult, Unpacker};
use crate::types::{
CallDunderError, CallableSignature, CallableType, ClassLiteral, ClassType, DataclassParams,
DynamicType, FunctionDecorators, FunctionType, GenericAlias, IntersectionBuilder,
IntersectionType, KnownClass, KnownFunction, KnownInstanceType, MemberLookupPolicy,
MetaclassCandidate, Parameter, ParameterForm, Parameters, Signature, Signatures,
StringLiteralType, SubclassOfType, Symbol, SymbolAndQualifiers, Truthiness, TupleType, Type,
TypeAliasType, TypeAndQualifiers, TypeArrayDisplay, TypeQualifiers, TypeVarBoundOrConstraints,
TypeVarInstance, TypeVarKind, TypeVarVariance, UnionBuilder, UnionType, binding_type,
todo_type,
BareTypeAliasType, CallDunderError, CallableSignature, CallableType, ClassLiteral, ClassType,
DataclassParams, DynamicType, FunctionDecorators, FunctionType, GenericAlias,
IntersectionBuilder, IntersectionType, KnownClass, KnownFunction, KnownInstanceType,
MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType, Parameter, ParameterForm,
Parameters, Signature, Signatures, StringLiteralType, SubclassOfType, Symbol,
SymbolAndQualifiers, Truthiness, TupleType, Type, TypeAliasType, TypeAndQualifiers,
TypeArrayDisplay, TypeQualifiers, TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind,
TypeVarVariance, UnionBuilder, UnionType, binding_type, todo_type,
};
use crate::unpack::{Unpack, UnpackPosition};
use crate::util::subscript::{PyIndex, PySlice};
@ -2374,12 +2374,13 @@ impl<'db> TypeInferenceBuilder<'db> {
.node_scope(NodeWithScopeRef::TypeAlias(type_alias))
.to_scope_id(self.db(), self.file());
let type_alias_ty =
Type::KnownInstance(KnownInstanceType::TypeAliasType(TypeAliasType::new(
let type_alias_ty = Type::KnownInstance(KnownInstanceType::TypeAliasType(
TypeAliasType::PEP695(PEP695TypeAliasType::new(
self.db(),
&type_alias.name.as_name_expr().unwrap().id,
rhs_scope,
)));
)),
));
self.add_declaration_with_binding(
type_alias.into(),
@ -4860,6 +4861,7 @@ impl<'db> TypeInferenceBuilder<'db> {
| KnownClass::Super
| KnownClass::TypeVar
| KnownClass::NamedTuple
| KnownClass::TypeAliasType
)
)
// temporary special-casing for all subclasses of `enum.Enum`
@ -5363,6 +5365,50 @@ impl<'db> TypeInferenceBuilder<'db> {
));
}
KnownClass::TypeAliasType => {
let assigned_to = (self.index)
.try_expression(call_expression_node)
.and_then(|expr| expr.assigned_to(self.db()));
let containing_assignment =
assigned_to.as_ref().and_then(|assigned_to| {
match assigned_to.node().targets.as_slice() {
[ast::Expr::Name(target)] => Some(
self.index.expect_single_definition(target),
),
_ => None,
}
});
let [Some(name), Some(value), ..] =
overload.parameter_types()
else {
continue;
};
if let Some(name) = name.into_string_literal() {
overload.set_return_type(Type::KnownInstance(
KnownInstanceType::TypeAliasType(
TypeAliasType::Bare(BareTypeAliasType::new(
self.db(),
ast::name::Name::new(name.value(self.db())),
containing_assignment,
value,
)),
),
));
} else {
if let Some(builder) = self.context.report_lint(
&INVALID_TYPE_ALIAS_TYPE,
call_expression,
) {
builder.into_diagnostic(format_args!(
"The name of a `typing.TypeAlias` must be a string literal",
));
}
}
}
_ => (),
}
}