[ty] Simple syntactic validation for PEP-613 type aliases (#22652)

This commit is contained in:
Alex Waygood
2026-01-17 15:43:50 +00:00
committed by GitHub
parent 2e4774623c
commit 6b16931169
2 changed files with 132 additions and 4 deletions

View File

@@ -395,3 +395,38 @@ from typing import TypeAlias
# error: [invalid-type-form]
Empty: TypeAlias
```
## Simple syntactic validation
We don't yet do full validation for the right-hand side of a `TypeAlias` assignment, but we do
simple syntactic validation:
```toml
[environment]
python-version = "3.11"
```
```py
from typing_extensions import Annotated, Literal, TypeAlias
GoodTypeAlias: TypeAlias = Annotated[int, (1, 3.14, lambda x: x)]
GoodTypeAlias: TypeAlias = tuple[int, *tuple[str, ...]]
BadTypeAlias1: TypeAlias = eval("".join(map(chr, [105, 110, 116]))) # error: [invalid-type-form]
BadTypeAlias2: TypeAlias = [int, str] # error: [invalid-type-form]
BadTypeAlias3: TypeAlias = ((int, str),) # error: [invalid-type-form]
BadTypeAlias4: TypeAlias = [int for i in range(1)] # error: [invalid-type-form]
BadTypeAlias5: TypeAlias = {"a": "b"} # error: [invalid-type-form]
BadTypeAlias6: TypeAlias = (lambda: int)() # error: [invalid-type-form]
BadTypeAlias7: TypeAlias = [int][0] # error: [invalid-type-form]
BadTypeAlias8: TypeAlias = int if 1 < 3 else str # error: [invalid-type-form]
BadTypeAlias10: TypeAlias = True # error: [invalid-type-form]
BadTypeAlias11: TypeAlias = 1 # error: [invalid-type-form]
BadTypeAlias12: TypeAlias = list or set # error: [invalid-type-form]
BadTypeAlias13: TypeAlias = f"{'int'}" # error: [invalid-type-form]
BadTypeAlias14: TypeAlias = Literal[-3.14] # error: [invalid-type-form]
# error: [invalid-type-form]
# error: [invalid-type-form]
BadTypeAlias14: TypeAlias = Literal[3.14]
```

View File

@@ -7454,6 +7454,85 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
assignment: &'db AnnotatedAssignmentDefinitionKind,
definition: Definition<'db>,
) {
/// Simple syntactic validation for the right-hand sides of PEP-613 type aliases.
///
/// TODO: this is far from exhaustive and should be improved.
const fn alias_syntax_validation(expr: &ast::Expr) -> bool {
const fn inner(expr: &ast::Expr, allow_context_dependent: bool) -> bool {
match expr {
ast::Expr::Name(_)
| ast::Expr::StringLiteral(_)
| ast::Expr::NoneLiteral(_) => true,
ast::Expr::Attribute(ast::ExprAttribute {
value,
attr: _,
node_index: _,
range: _,
ctx: _,
}) => inner(value, allow_context_dependent),
ast::Expr::Subscript(ast::ExprSubscript {
value,
slice,
node_index: _,
range: _,
ctx: _,
}) => {
if !inner(value, allow_context_dependent) {
return false;
}
match &**slice {
ast::Expr::Tuple(ast::ExprTuple { elts, .. }) => {
match elts.as_slice() {
[first, ..] => inner(first, true),
_ => true,
}
}
_ => inner(slice, true),
}
}
ast::Expr::BinOp(ast::ExprBinOp {
left,
op,
right,
range: _,
node_index: _,
}) => {
op.is_bit_or()
&& inner(left, allow_context_dependent)
&& inner(right, allow_context_dependent)
}
ast::Expr::UnaryOp(ast::ExprUnaryOp {
op,
operand,
range: _,
node_index: _,
}) => {
allow_context_dependent
&& matches!(op, ast::UnaryOp::UAdd | ast::UnaryOp::USub)
&& matches!(
&**operand,
ast::Expr::NumberLiteral(ast::ExprNumberLiteral {
value: ast::Number::Int(_),
..
})
)
}
ast::Expr::NumberLiteral(ast::ExprNumberLiteral {
value,
node_index: _,
range: _,
}) => allow_context_dependent && value.is_int(),
ast::Expr::EllipsisLiteral(_)
| ast::Expr::BytesLiteral(_)
| ast::Expr::BooleanLiteral(_)
| ast::Expr::Starred(_)
| ast::Expr::List(_) => allow_context_dependent,
_ => false,
}
}
inner(expr, false)
}
let annotation = assignment.annotation(self.module());
let target = assignment.target(self.module());
let value = assignment.value(self.module());
@@ -7463,6 +7542,24 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
DeferredExpressionState::from(self.defer_annotations()),
);
let is_pep_613_type_alias = declared.inner_type().is_typealias_special_form();
if is_pep_613_type_alias
&& let Some(value) = value
&& !alias_syntax_validation(value)
&& let Some(builder) = self.context.report_lint(
&INVALID_TYPE_FORM,
definition.full_range(self.db(), self.module()),
)
{
// TODO: better error message; full type-expression validation; etc.
let mut diagnostic = builder
.into_diagnostic("Invalid right-hand side for `typing.TypeAlias` assignment");
diagnostic.help(
"See https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions",
);
}
if !declared.qualifiers.is_empty() {
let current_scope_id = self.scope().file_scope_id(self.db());
let current_scope = self.index.scope(current_scope_id);
@@ -7509,10 +7606,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
declared.inner = Type::BooleanLiteral(true);
}
// Check if this is a PEP 613 `TypeAlias`. (This must come below the SpecialForm handling
// immediately below, since that can overwrite the type to be `TypeAlias`.)
let is_pep_613_type_alias = declared.inner_type().is_typealias_special_form();
// Handle various singletons.
if let Some(name_expr) = target.as_name_expr()
&& let Some(special_form) =