diff --git a/mypy/typeanal.py b/mypy/typeanal.py index daf7ab1951ea..00d0027d0bb3 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -1354,7 +1354,49 @@ def visit_union_type(self, t: UnionType) -> Type: and not self.options.python_version >= (3, 10) ): self.fail("X | Y syntax for unions requires Python 3.10", t, code=codes.SYNTAX) - return UnionType(self.anal_array(t.items), t.line, uses_pep604_syntax=t.uses_pep604_syntax) + items = self.anal_array(t.items) + # Check for invalid quoted type inside union syntax. + if ( + t.original_str_expr is None + and not self.api.is_stub_file + and (not self.api.is_future_flag_set("annotations") or self.defining_alias) + ): + # Whether each type can be OR'd with a string. + item_ors_with_str = [ + # Is a TypeVar? + isinstance(typ, (TypeVarType, UnboundType)) + # Is a 'typing._UnionGenericAlias'? + or ( + isinstance(typ, TypeAliasType) + and typ.alias is not None + # "type: ignore" comment needed to satisfy the ProperTypePlugin. + # Calling get_proper_type here would give the wrong result in the edge + # case where a non-PEP-695 alias has a PEP-695 alias as its target. + and isinstance(typ.alias.target, UnionType) # type: ignore[misc] + and typ.alias.alias_tvars + and not typ.alias.python_3_12_type_alias + ) + for typ in items + ] + for idx, itm in enumerate(t.items): + if ( + isinstance(itm, UnboundType) + # Comes from a quoted expression. + and itm.original_str_expr is not None + # Not preceded by type that makes OR-ing with string valid. + and not any(item_ors_with_str[:idx]) + # Not the first item immediately followed by a type that + # can be OR'd with a string. + # Accessing index 1 is safe because union syntax guarantees + # that there are at least 2 items. + and not (idx == 0 and item_ors_with_str[1]) + ): + self.fail( + "X | Y syntax for unions cannot use quoted operands; use quotes" + " around the entire expression instead", + itm, + ) + return UnionType(items, t.line, uses_pep604_syntax=t.uses_pep604_syntax) def visit_partial_type(self, t: PartialType) -> Type: assert False, "Internal error: Unexpected partial type" diff --git a/test-data/unit/check-python312.test b/test-data/unit/check-python312.test index c5c8ada1aae1..6aa5dac13aba 100644 --- a/test-data/unit/check-python312.test +++ b/test-data/unit/check-python312.test @@ -1949,3 +1949,31 @@ class D: class G[Q]: def g(self, x: Q): ... d: G[str] + +[case testPEP695TypeAliasInUnionOrSyntaxWithQuotedOperands] +import types + +type A1 = int +type A2 = int | str +type A3[T] = list[T] +type A4[T] = T | str + +x1: A1 | "bytes" # E: X | Y syntax for unions cannot use quoted operands; use quotes around the entire expression instead +x2: A2 | "bytes" # E: X | Y syntax for unions cannot use quoted operands; use quotes around the entire expression instead +x3: A3[int] | "bytes" # E: X | Y syntax for unions cannot use quoted operands; use quotes around the entire expression instead +x4: A4[int] | "bytes" # E: X | Y syntax for unions cannot use quoted operands; use quotes around the entire expression instead +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + +[case testPEP695TypeAliasInUnionOrSyntaxWithQuotedOperandsNestedAlias] +import types +from typing import TypeVar +from typing_extensions import TypeAlias + +T = TypeVar("T") +type A1[T] = T | str +A2: TypeAlias = A1[T] + +x: A2[int] | "bytes" # E: X | Y syntax for unions cannot use quoted operands; use quotes around the entire expression instead +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] diff --git a/test-data/unit/check-union-or-syntax.test b/test-data/unit/check-union-or-syntax.test index fcf679fff401..8e99e32b2195 100644 --- a/test-data/unit/check-union-or-syntax.test +++ b/test-data/unit/check-union-or-syntax.test @@ -251,3 +251,65 @@ foo: ReadableBuffer [file was_mmap.py] from was_builtins import * class mmap: ... + +[case testUnionOrSyntaxWithQuotedOperandsNotAllowed] +# flags: --python-version 3.10 +from typing import Union, assert_type + +x1: "int" | str # E: X | Y syntax for unions cannot use quoted operands; use quotes around the entire expression instead +x2: int | "str" # E: X | Y syntax for unions cannot use quoted operands; use quotes around the entire expression instead +x3: int | str | "bytes" # E: X | Y syntax for unions cannot use quoted operands; use quotes around the entire expression instead +assert_type(x1, Union[int, str]) +assert_type(x2, Union[int, str]) +assert_type(x3, Union[int, str, bytes]) + +[case testUnionOrSyntaxWithQuotedOperandsWithTypeVar] +# flags: --python-version 3.10 +import types +from typing import TypeVar, Union, assert_type +from typing_extensions import TypeAlias + +T = TypeVar("T") + +ok1: TypeAlias = T | "int" +ok2: TypeAlias = "int" | T +ok3: TypeAlias = int | T | "str" +ok4: TypeAlias = "int" | T | "str" +ok5: TypeAlias = T | "int" | str +ok6: TypeAlias = T | int | "str" +ok7: TypeAlias = list["str" | T] + +bad1: TypeAlias = "T" | "int" # E: X | Y syntax for unions cannot use quoted operands; use quotes around the entire expression instead +bad2: TypeAlias = int | "str" | T # E: X | Y syntax for unions cannot use quoted operands; use quotes around the entire expression instead +bad3: TypeAlias = list["str" | int] # E: X | Y syntax for unions cannot use quoted operands; use quotes around the entire expression instead +[builtins fixtures/tuple.pyi] + +[case testUnionOrSyntaxWithQuotedOperandsWithAlias] +# flags: --python-version 3.10 +import types +from typing import TypeVar +from typing_extensions import TypeAlias + +T = TypeVar("T") + +A1: TypeAlias = int +A2: TypeAlias = int | str +A3: TypeAlias = list[T] +A4: TypeAlias = T | str + +x1: A1 | "bytes" # E: X | Y syntax for unions cannot use quoted operands; use quotes around the entire expression instead +x2: A2 | "bytes" # E: X | Y syntax for unions cannot use quoted operands; use quotes around the entire expression instead +x3: A3[int] | "bytes" # E: X | Y syntax for unions cannot use quoted operands; use quotes around the entire expression instead +x4: A4[int] | "bytes" # ok +[builtins fixtures/tuple.pyi] + +[case testUnionOrSyntaxWithQuotedOperandsFutureAnnotations] +# flags: --python-version 3.10 +from __future__ import annotations +import types +from typing_extensions import TypeAlias + +x1: int | "str" # ok +def f(x: int | "str"): pass # ok +x2: TypeAlias = int | "str" # E: X | Y syntax for unions cannot use quoted operands; use quotes around the entire expression instead +[builtins fixtures/tuple.pyi]