diff --git a/mypy/checker.py b/mypy/checker.py index d94ab42c8479..a76bd0dce9d3 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1790,11 +1790,59 @@ def visit_class_def(self, defn: ClassDef) -> None: sig, _ = self.expr_checker.check_call(dec, [temp], [nodes.ARG_POS], defn, callable_name=fullname) + + if defn.info.is_enum and refers_to_fullname(decorator, 'enum.unique'): + self.check_enum_unique_decorator(defn, decorator) # TODO: Apply the sig to the actual TypeInfo so we can handle decorators # that completely swap out the type. (e.g. Callable[[Type[A]], Type[B]]) if typ.is_protocol and typ.defn.type_vars: self.check_protocol_variance(defn) + def check_enum_unique_decorator(self, defn: ClassDef, decorator: Expression) -> None: + # TODO: this can also check known `Literal` types in the future. + known_values = [] + + # All `Enum`s are just a series of `a = 1`, `b = 2` assignments in their bodies. + # We need raw exressions in these assignments. + for node in defn.defs.body: + if not isinstance(node, AssignmentStmt): + continue + + # Next, we need to be sure that all parts of `Enum` definition + # are just simple names and are present in `TypeInfo`. + for lvalue in node.lvalues: + if not isinstance(lvalue, NameExpr): + continue + field = defn.info.get(lvalue.name) + if field is None or not isinstance(field.node, Var): + continue + + # Validation passed, continue. + is_known, value = mypy.checkexpr.try_getting_statically_known_value( + node.rvalue) + if not is_known: + # We continue, because value was not known. + # But, others possibly still can be reported. + continue + known_values.append(value) + + # Since we might end up with unhashable objects, like `[1]` or `{}`, + # we cannot use `dict` or `Counter`. + # It should not hit us hard, because `Enum`s rarely have lots of values. + errors = [] + seen = [] + for value in known_values: + if value in seen: + errors.append(value) + if value not in seen: + seen.append(value) + + # Now, report all errors: + for error in errors: + self.fail('Duplicate value "{}" in "{}" unique enum definition'.format( + error, defn.name, + ), defn) + def check_final_deletable(self, typ: TypeInfo) -> None: # These checks are only for mypyc. Only perform some checks that are easier # to implement here than in mypyc. diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index b6dec2834449..1a85db51082e 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -5,7 +5,8 @@ from contextlib import contextmanager import itertools from typing import ( - Any, cast, Dict, Set, List, Tuple, Callable, Union, Optional, Sequence, Iterator + Any, cast, Dict, Set, List, Tuple, Callable, Union, + Optional, Sequence, Iterator, Iterable ) from typing_extensions import ClassVar, Final, overload, TypeAlias as _TypeAlias @@ -4578,3 +4579,77 @@ def get_partial_instance_type(t: Optional[Type]) -> Optional[PartialType]: if t is None or not isinstance(t, PartialType) or t.type is None: return None return t + + +def try_getting_statically_known_value(node: Expression) -> Tuple[bool, Any]: + """We try to get statically known expression's value. + + Imagine, that you have: + + class Some(enum.Enum): + one = 'one' + two = ('t', 'w', 'o') + three = None + four = method_call() + + The first boolean represent whether given node was a literal value. + The second element is literal's value if any. + + When trying to call ``try_getting_literal_expr`` on ``Some.one`` + it will return ``True, 'one'`` tuple. + On ``Some.two`` it will return ``True, ('t', 'w', 'o')`` tuple. + On ``three`` it will return ``True, None``. + On ``four`` it will return ``False, None``. + + We also recurse into nested nodes like ``TupleExpr``, ``ListExpr``, etc. + """ + if isinstance(node, (StrExpr, UnicodeExpr, IntExpr, FloatExpr, ComplexExpr)): + return True, node.value + if isinstance(node, BytesExpr): + # Since `bytes`'s value is store as `str` in `mypy`, + # we use this hack to tell them appart. + return True, f'b"{node.value}"' + if isinstance(node, NameExpr): + if node.name == 'None': + return True, None + if node.name in ('True', 'False'): + return True, node.name == 'True' + if isinstance(node, EllipsisExpr): + return True, ... + + # Recursive types: + if isinstance(node, TupleExpr): + return _recursive_statically_known_value(node.items, tuple) + if isinstance(node, ListExpr): + return _recursive_statically_known_value(node.items, list) + if isinstance(node, SetExpr): + return _recursive_statically_known_value(node.items, set) + if isinstance(node, DictExpr): + keys = [] + values = [] + for key, value in node.items: + if key is None: + return False, None # We've met `**` unpacking + + known_key, key_value = try_getting_statically_known_value(key) + if not known_key: + return False, None + known_value, value_value = try_getting_statically_known_value(value) + if not known_value: + return False, None + keys.append(key_value) + values.append(value_value) + return True, dict(zip(keys, values)) + return False, None + + +def _recursive_statically_known_value(items: Iterable[Expression], + typ: type) -> Tuple[bool, Any]: + res: List[Any] = [] + for item in items: + is_known, item_value = try_getting_statically_known_value(item) + if is_known: + res.append(item_value) + else: + return False, None + return True, typ(res) diff --git a/test-data/unit/check-enum.test b/test-data/unit/check-enum.test index 22d167f3487b..1d564c1c71ec 100644 --- a/test-data/unit/check-enum.test +++ b/test-data/unit/check-enum.test @@ -134,10 +134,11 @@ import enum @enum.unique class E(enum.Enum): x = 1 - y = 1 # NOTE: This duplicate value is not detected by mypy at the moment + y = 1 x = 1 x = E.x [out] +main:3: error: Duplicate value "1" in "E" unique enum definition main:7: error: Incompatible types in assignment (expression has type "E", variable has type "int") [case testIntEnum_assignToIntVariable] @@ -1644,3 +1645,219 @@ class A(Enum): class Inner: pass class B(A): pass # E: Cannot inherit from final class "A" [builtins fixtures/bool.pyi] + + +[case testUniqueEnumCorrectStaticValues] +from enum import Enum, IntEnum, Flag, IntFlag, unique + +@unique +class EmptyEnum(Enum): + pass +@unique +class EmptyIntEnum(IntEnum): + pass +@unique +class EmptyFlag(Flag): + pass +@unique +class EmptyIntFlag(IntFlag): + pass + +@unique +class NonEmptyEnum(Enum): + x = [1, 'a', (1, [1, 2])] + y = 2 + z = '1' + n = None + a = (1,) + b = [1] + c = {1: 1} + d = {1} + e = ... + f = False + g = True + h = 1.5 + j = 1j + k = b'1' +@unique +class NonEmptyIntEnum(IntEnum): + x = 1 + y = 2 +@unique +class NonEmptyFlag(Flag): + x = 'x' + y = 'y' +@unique +class NonEmptyIntFlag(IntFlag): + x = 1 + y = 2 +[builtins fixtures/dict.pyi] + +[case testNonEnumUnique] +from enum import unique +@unique +class A: + x = 1 + y = 1 +[builtins fixtures/bool.pyi] + +[case testEnumNonUniqueDecorator] +from enum import Enum +def dec(t): + return t +@dec +class A(Enum): + x = 1 + y = 1 +[builtins fixtures/bool.pyi] + +[case testEnumMetaUnique] +from enum import unique, EnumMeta +@unique +class A(EnumMeta): + x = 1 + y = 1 +[builtins fixtures/bool.pyi] + +[case testEnumDynamicValues] +from typing import List +from enum import Enum, unique + +# We cannot infer these yet (possibly - never): +def y() -> int: + return 1 +z = [1] + +@unique +class EmptyEnum(Enum): + a = z + b = z + c = y() + d = y() + e = z[0] + f = z[0] + +@unique +class ComplexValuesEnum(Enum): + a = (z, z) + b = (z, z) + c = {y()} + d = {y()} + e = [z, y()] + f = [z, y()] + d1 = {z: 1} + d2 = {z: 1} + d3 = {1: y()} + d4 = {1: y()} + d5 = {z: y()} + d6 = {z: y()} + +@unique +class EmptyIntEnum(Enum): # E: Duplicate value "0" in "EmptyIntEnum" unique enum definition + a = z[0] + # This still raises, we know `1` is duplicated: + b = 0 + c = 0 + # This won't: + d = int() + e = int() +[builtins fixtures/dict.pyi] + +[case testEnumUniqueSeveralLValues] +from enum import Enum, unique +@unique +class IntFloatEnum(Enum): # E: Duplicate value "1" in "IntFloatEnum" unique enum definition + x = y = 1 +[builtins fixtures/bool.pyi] + +[case testEnumUniqueUnpacking] +from enum import Enum, unique +@unique +class UniqueEnum(Enum): + x, y = (1, 2) +# TODO: should raise, but does not +# TODO: this should be handled with `Literal` types +@unique +class NonUniqueEnum(Enum): + x, y = (1, 1) +[builtins fixtures/bool.pyi] + +[case testEnumDuplocateStaticValues] +from enum import Enum, unique + +@unique +class IntFloatEnum(Enum): + x = 1 + y = 1.0 +@unique +class IntEnum(Enum): + x = 1 + y = 1 +@unique +class FloatEnum(Enum): + x = 1.0 + y = 1.0 +@unique +class ComplexEnum(Enum): + x = 1j + y = 1j +@unique +class StrEnum(Enum): + a = 'a' + b = 'a' +@unique +class BytesEnum(Enum): + a = b'a' + b = b'a' +@unique +class BytesStrEnum(Enum): + a = 'a' # ok + b = b'a' +@unique +class BoolEnum(Enum): + a = True + b = True +@unique +class BoolIntEnum(Enum): + a = True + b = 1 +@unique +class NoneEnum(Enum): + a = None + b = None +@unique +class ElipsisEnum(Enum): + a = ... + b = ... +@unique +class TupleEnum(Enum): + a = (True, 1, None) + b = (True, 1, None) +@unique +class ListEnum(Enum): + a = [1, (2, 3), [4, 5]] + b = [1, (2, 3), [4, 5]] +@unique +class SetEnum(Enum): + a = {1, True, 1.0} + b = {1} +@unique +class DictEnum(Enum): + a = {1: 'a'} + b = {1: 'a'} +[out] +main:4: error: Duplicate value "1.0" in "IntFloatEnum" unique enum definition +main:8: error: Duplicate value "1" in "IntEnum" unique enum definition +main:12: error: Duplicate value "1.0" in "FloatEnum" unique enum definition +main:16: error: Duplicate value "1j" in "ComplexEnum" unique enum definition +main:20: error: Duplicate value "a" in "StrEnum" unique enum definition +main:24: error: Duplicate value "b"a"" in "BytesEnum" unique enum definition +main:32: error: Duplicate value "True" in "BoolEnum" unique enum definition +main:36: error: Duplicate value "1" in "BoolIntEnum" unique enum definition +main:40: error: Duplicate value "None" in "NoneEnum" unique enum definition +main:44: error: Duplicate value "Ellipsis" in "ElipsisEnum" unique enum definition +main:48: error: Duplicate value "(True, 1, None)" in "TupleEnum" unique enum definition +main:52: error: Duplicate value "[1, (2, 3), [4, 5]]" in "ListEnum" unique enum definition +main:56: error: Duplicate value "{1}" in "SetEnum" unique enum definition +main:60: error: Duplicate value "{1: 'a'}" in "DictEnum" unique enum definition +[builtins fixtures/dict.pyi] diff --git a/test-data/unit/fixtures/dict.pyi b/test-data/unit/fixtures/dict.pyi index fd509de8a6c2..e7576c2cdd3b 100644 --- a/test-data/unit/fixtures/dict.pyi +++ b/test-data/unit/fixtures/dict.pyi @@ -46,6 +46,7 @@ class list(Sequence[T]): # needed by some test cases def append(self, item: T) -> None: pass class tuple(Generic[T]): pass +class set(): pass class function: pass class float: pass class complex: pass