Skip to content

Commit ce34db6

Browse files
committed
Emit warning for invalid quoted types in union syntax
1 parent 499adae commit ce34db6

File tree

3 files changed

+133
-1
lines changed

3 files changed

+133
-1
lines changed

mypy/typeanal.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1354,7 +1354,49 @@ def visit_union_type(self, t: UnionType) -> Type:
13541354
and not self.options.python_version >= (3, 10)
13551355
):
13561356
self.fail("X | Y syntax for unions requires Python 3.10", t, code=codes.SYNTAX)
1357-
return UnionType(self.anal_array(t.items), t.line, uses_pep604_syntax=t.uses_pep604_syntax)
1357+
items = self.anal_array(t.items)
1358+
# Check for invalid quoted type inside union syntax.
1359+
if (
1360+
t.original_str_expr is None
1361+
and not self.api.is_stub_file
1362+
and (not self.api.is_future_flag_set("annotations") or self.defining_alias)
1363+
):
1364+
# Whether each type can be OR'd with a string.
1365+
item_ors_with_str = [
1366+
# Is a TypeVar?
1367+
isinstance(typ, (TypeVarType, UnboundType))
1368+
# Is a 'typing._UnionGenericAlias'?
1369+
or (
1370+
isinstance(typ, TypeAliasType)
1371+
and typ.alias is not None
1372+
# "type: ignore" comment needed to satisfy the ProperTypePlugin.
1373+
# Calling get_proper_type here would give the wrong result in the edge
1374+
# case where a non-PEP-695 alias has a PEP-695 alias as its target.
1375+
and isinstance(typ.alias.target, UnionType) # type: ignore[misc]
1376+
and typ.alias.alias_tvars
1377+
and not typ.alias.python_3_12_type_alias
1378+
)
1379+
for typ in items
1380+
]
1381+
for idx, itm in enumerate(t.items):
1382+
if (
1383+
isinstance(itm, UnboundType)
1384+
# Comes from a quoted expression.
1385+
and itm.original_str_expr is not None
1386+
# Not preceded by type that makes OR-ing with string valid.
1387+
and not any(item_ors_with_str[:idx])
1388+
# Not the first item immediately followed by a type that
1389+
# can be OR'd with a string.
1390+
# Accessing index 1 is safe because union syntax guarantees
1391+
# that there are at least 2 items.
1392+
and not (idx == 0 and item_ors_with_str[1])
1393+
):
1394+
self.fail(
1395+
"X | Y syntax for unions cannot use quoted operands; use quotes"
1396+
" around the entire expression instead",
1397+
itm,
1398+
)
1399+
return UnionType(items, t.line, uses_pep604_syntax=t.uses_pep604_syntax)
13581400

13591401
def visit_partial_type(self, t: PartialType) -> Type:
13601402
assert False, "Internal error: Unexpected partial type"

test-data/unit/check-python312.test

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1949,3 +1949,31 @@ class D:
19491949
class G[Q]:
19501950
def g(self, x: Q): ...
19511951
d: G[str]
1952+
1953+
[case testPEP695TypeAliasInUnionOrSyntaxWithQuotedOperands]
1954+
import types
1955+
1956+
type A1 = int
1957+
type A2 = int | str
1958+
type A3[T] = list[T]
1959+
type A4[T] = T | str
1960+
1961+
x1: A1 | "bytes" # E: X | Y syntax for unions cannot use quoted operands; use quotes around the entire expression instead
1962+
x2: A2 | "bytes" # E: X | Y syntax for unions cannot use quoted operands; use quotes around the entire expression instead
1963+
x3: A3[int] | "bytes" # E: X | Y syntax for unions cannot use quoted operands; use quotes around the entire expression instead
1964+
x4: A4[int] | "bytes" # E: X | Y syntax for unions cannot use quoted operands; use quotes around the entire expression instead
1965+
[builtins fixtures/tuple.pyi]
1966+
[typing fixtures/typing-full.pyi]
1967+
1968+
[case testPEP695TypeAliasInUnionOrSyntaxWithQuotedOperandsNestedAlias]
1969+
import types
1970+
from typing import TypeVar
1971+
from typing_extensions import TypeAlias
1972+
1973+
T = TypeVar("T")
1974+
type A1[T] = T | str
1975+
A2: TypeAlias = A1[T]
1976+
1977+
x: A2[int] | "bytes" # E: X | Y syntax for unions cannot use quoted operands; use quotes around the entire expression instead
1978+
[builtins fixtures/tuple.pyi]
1979+
[typing fixtures/typing-full.pyi]

test-data/unit/check-union-or-syntax.test

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,3 +251,65 @@ foo: ReadableBuffer
251251
[file was_mmap.py]
252252
from was_builtins import *
253253
class mmap: ...
254+
255+
[case testUnionOrSyntaxWithQuotedOperandsNotAllowed]
256+
# flags: --python-version 3.10
257+
from typing import Union, assert_type
258+
259+
x1: "int" | str # E: X | Y syntax for unions cannot use quoted operands; use quotes around the entire expression instead
260+
x2: int | "str" # E: X | Y syntax for unions cannot use quoted operands; use quotes around the entire expression instead
261+
x3: int | str | "bytes" # E: X | Y syntax for unions cannot use quoted operands; use quotes around the entire expression instead
262+
assert_type(x1, Union[int, str])
263+
assert_type(x2, Union[int, str])
264+
assert_type(x3, Union[int, str, bytes])
265+
266+
[case testUnionOrSyntaxWithQuotedOperandsWithTypeVar]
267+
# flags: --python-version 3.10
268+
import types
269+
from typing import TypeVar, Union, assert_type
270+
from typing_extensions import TypeAlias
271+
272+
T = TypeVar("T")
273+
274+
ok1: TypeAlias = T | "int"
275+
ok2: TypeAlias = "int" | T
276+
ok3: TypeAlias = int | T | "str"
277+
ok4: TypeAlias = "int" | T | "str"
278+
ok5: TypeAlias = T | "int" | str
279+
ok6: TypeAlias = T | int | "str"
280+
ok7: TypeAlias = list["str" | T]
281+
282+
bad1: TypeAlias = "T" | "int" # E: X | Y syntax for unions cannot use quoted operands; use quotes around the entire expression instead
283+
bad2: TypeAlias = int | "str" | T # E: X | Y syntax for unions cannot use quoted operands; use quotes around the entire expression instead
284+
bad3: TypeAlias = list["str" | int] # E: X | Y syntax for unions cannot use quoted operands; use quotes around the entire expression instead
285+
[builtins fixtures/tuple.pyi]
286+
287+
[case testUnionOrSyntaxWithQuotedOperandsWithAlias]
288+
# flags: --python-version 3.10
289+
import types
290+
from typing import TypeVar
291+
from typing_extensions import TypeAlias
292+
293+
T = TypeVar("T")
294+
295+
A1: TypeAlias = int
296+
A2: TypeAlias = int | str
297+
A3: TypeAlias = list[T]
298+
A4: TypeAlias = T | str
299+
300+
x1: A1 | "bytes" # E: X | Y syntax for unions cannot use quoted operands; use quotes around the entire expression instead
301+
x2: A2 | "bytes" # E: X | Y syntax for unions cannot use quoted operands; use quotes around the entire expression instead
302+
x3: A3[int] | "bytes" # E: X | Y syntax for unions cannot use quoted operands; use quotes around the entire expression instead
303+
x4: A4[int] | "bytes" # ok
304+
[builtins fixtures/tuple.pyi]
305+
306+
[case testUnionOrSyntaxWithQuotedOperandsFutureAnnotations]
307+
# flags: --python-version 3.10
308+
from __future__ import annotations
309+
import types
310+
from typing_extensions import TypeAlias
311+
312+
x1: int | "str" # ok
313+
def f(x: int | "str"): pass # ok
314+
x2: TypeAlias = int | "str" # E: X | Y syntax for unions cannot use quoted operands; use quotes around the entire expression instead
315+
[builtins fixtures/tuple.pyi]

0 commit comments

Comments
 (0)