From 0ec2ad2fa573610a13acdfdea190ad912b86a07e Mon Sep 17 00:00:00 2001 From: med1844 <51191710+med1844@users.noreply.github.com> Date: Mon, 30 Jun 2025 01:06:01 -0700 Subject: [PATCH] [ty] Emit error for invalid binary operations in type expressions (#18991) ## Summary This PR adds diagnostic for invalid binary operators in type expressions. It should close https://github.com/astral-sh/ty/issues/706 if merged. Please feel free to suggest better wordings for the diagnostic message. ## Test Plan I modified `mdtest/annotations/invalid.md` and added a test for each binary operator, and fixed tests that was broken by the new diagnostic. --- .../resources/mdtest/annotations/invalid.md | 34 +++++++++++++++++++ .../resources/mdtest/annotations/string.md | 1 + .../type_properties/is_disjoint_from.md | 2 +- crates/ty_python_semantic/src/types/infer.rs | 11 +++++- 4 files changed, 46 insertions(+), 2 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/invalid.md b/crates/ty_python_semantic/resources/mdtest/annotations/invalid.md index 5392e38c77..9dca3893b7 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/invalid.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/invalid.md @@ -91,6 +91,40 @@ async def outer(): # avoid unrelated syntax errors on yield, yield from, and aw reveal_type(p) # revealed: Unknown reveal_type(q) # revealed: int | Unknown reveal_type(r) # revealed: @Todo(unknown type subscript) + +class Mat: + def __init__(self, value: int): + self.value = value + + def __matmul__(self, other) -> int: + return 42 + +def invalid_binary_operators( + a: "1" + "2", # error: [invalid-type-form] "Invalid binary operator `+` in type annotation" + b: 3 - 5.0, # error: [invalid-type-form] "Invalid binary operator `-` in type annotation" + c: 4 * -2, # error: [invalid-type-form] "Invalid binary operator `*` in type annotation" + d: Mat(4) @ Mat(2), # error: [invalid-type-form] "Invalid binary operator `@` in type annotation" + e: 10 / 2, # error: [invalid-type-form] "Invalid binary operator `/` in type annotation" + f: 10 % 3, # error: [invalid-type-form] "Invalid binary operator `%` in type annotation" + g: 2**-0.5, # error: [invalid-type-form] "Invalid binary operator `**` in type annotation" + h: 10 // 3, # error: [invalid-type-form] "Invalid binary operator `//` in type annotation" + i: 1 << 2, # error: [invalid-type-form] "Invalid binary operator `<<` in type annotation" + j: 4 >> 42, # error: [invalid-type-form] "Invalid binary operator `>>` in type annotation" + k: 5 ^ 3, # error: [invalid-type-form] "Invalid binary operator `^` in type annotation" + l: 5 & 3, # error: [invalid-type-form] "Invalid binary operator `&` in type annotation" +): + reveal_type(a) # revealed: Unknown + reveal_type(b) # revealed: Unknown + reveal_type(c) # revealed: Unknown + reveal_type(d) # revealed: Unknown + reveal_type(e) # revealed: Unknown + reveal_type(f) # revealed: Unknown + reveal_type(g) # revealed: Unknown + reveal_type(h) # revealed: Unknown + reveal_type(i) # revealed: Unknown + reveal_type(j) # revealed: Unknown + reveal_type(k) # revealed: Unknown + reveal_type(l) # revealed: Unknown ``` ## Invalid Collection based AST nodes diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/string.md b/crates/ty_python_semantic/resources/mdtest/annotations/string.md index f44c000855..5777070441 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/string.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/string.md @@ -154,6 +154,7 @@ shouldn't panic. ```py a: "1 or 2" b: "(x := 1)" +# error: [invalid-type-form] c: "1 + 2" d: "lambda x: x" e: "x if True else y" diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md index 9441cc1334..81231b4ad5 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md @@ -251,7 +251,7 @@ static_assert(is_disjoint_from(Intersection[int, Any], Not[int])) static_assert(is_disjoint_from(Not[int], Intersection[int, Any])) # TODO https://github.com/astral-sh/ty/issues/216 -static_assert(is_disjoint_from(AlwaysFalsy, LiteralString & ~Literal[""])) # error: [static-assert-error] +static_assert(is_disjoint_from(AlwaysFalsy, Intersection[LiteralString, Not[Literal[""]]])) # error: [static-assert-error] ``` ## Special types diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 689dbcf068..f7702b66f3 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -8416,8 +8416,17 @@ impl<'db> TypeInferenceBuilder<'db, '_> { UnionType::from_elements(self.db(), [left_ty, right_ty]) } // anything else is an invalid annotation: - _ => { + op => { self.infer_binary_expression(binary); + if let Some(mut diag) = self.report_invalid_type_expression( + expression, + format_args!( + "Invalid binary operator `{}` in type annotation", + op.as_str() + ), + ) { + diag.info("Did you mean to use `|`?"); + } Type::unknown() } }