diff --git a/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md index 29b13ee4b3..7251d6f62c 100644 --- a/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md @@ -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] +``` diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 8aa106a6ee..85e2bb2a7e 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -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) =