mirror of
https://github.com/astral-sh/ruff
synced 2026-01-21 05:20:49 -05:00
[ty] Simple syntactic validation for PEP-613 type aliases (#22652)
This commit is contained in:
@@ -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]
|
||||
```
|
||||
|
||||
@@ -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) =
|
||||
|
||||
Reference in New Issue
Block a user