Skip to content

Commit

Permalink
Emit warning for invalid quoted types in union syntax
Browse files Browse the repository at this point in the history
  • Loading branch information
brianschubert committed Nov 24, 2024
1 parent 499adae commit ce34db6
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 1 deletion.
44 changes: 43 additions & 1 deletion mypy/typeanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
28 changes: 28 additions & 0 deletions test-data/unit/check-python312.test
Original file line number Diff line number Diff line change
Expand Up @@ -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]
62 changes: 62 additions & 0 deletions test-data/unit/check-union-or-syntax.test
Original file line number Diff line number Diff line change
Expand Up @@ -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]

0 comments on commit ce34db6

Please sign in to comment.