-- Test cases for always defined attributes.
--
-- If class C has attributes x and y that are always defined, the output will
-- have a line like this:
--
--   C: [x, y]

[case testAlwaysDefinedSimple]
class C:
    def __init__(self, x: int) -> None:
        self.x = x
[out]
C: [x]

[case testAlwaysDefinedFail]
class MethodCall:
    def __init__(self, x: int) -> None:
        self.f()
        self.x = x

    def f(self) -> None:
        pass

class FuncCall:
    def __init__(self, x: int) -> None:
        f(x)
        self.x = x
        f(self)
        self.y = x

class GetAttr:
    x: int
    def __init__(self, x: int) -> None:
        a = self.x
        self.x = x

class _Base:
    def  __init__(self) -> None:
        f(self)

class CallSuper(_Base):
    def __init__(self, x: int) -> None:
        super().__init__()
        self.x = x

class Lambda:
    def __init__(self, x: int) -> None:
        f = lambda x: x + 1
        self.x = x
        g = lambda x: self
        self.y = x

class If:
    def __init__(self, x: int) -> None:
        self.a = 1
        if x:
            self.x = x
        else:
            self.y = 1

class Deletable:
    __deletable__ = ('x', 'y')

    def __init__(self) -> None:
        self.x = 0
        self.y = 1
        self.z = 2

class PrimitiveWithSelf:
    def __init__(self, s: str) -> None:
        self.x = getattr(self, s)

def f(a) -> None: pass
[out]
MethodCall: []
FuncCall: [x]
GetAttr: []
CallSuper: []
Lambda: []
If: [a]
Deletable: [z]
PrimitiveWithSelf: []

[case testAlwaysDefinedConditional]
class IfAlways:
    def __init__(self, x: int, y: int) -> None:
        if x:
            self.x = x
            self.y = y
        elif y:
            self.x = y
            self.y = x
        else:
            self.x = 0
            self.y = 0
        self.z = 0

class IfSometimes1:
    def __init__(self, x: int, y: int) -> None:
        if x:
            self.x = x
            self.y = y
        elif y:
            self.z = y
            self.y = x
        else:
            self.y = 0
            self.a = 0

class IfSometimes2:
    def __init__(self, x: int, y: int) -> None:
        if x:
            self.x = x
        self.y = y

class IfStopAnalysis1:
    def __init__(self, x: int, y: int) -> None:
        if x:
            self.x = x
            f(self)
        else:
            self.x = x
        self.y = y

class IfStopAnalysis2:
    def __init__(self, x: int, y: int) -> None:
        if x:
            self.x = x
        else:
            self.x = x
            f(self)
        self.y = y

class IfStopAnalysis3:
    def __init__(self, x: int, y: int) -> None:
        if x:
            self.x = x
        else:
            f(self)
            self.x = x
        self.y = y

class IfConditionalAndNonConditional1:
    def __init__(self, x: int) -> None:
        self.x = 0
        if x:
            self.x = x

class IfConditionalAndNonConditional2:
    def __init__(self, x: int) -> None:
        # x is not considered always defined, since the second assignment may
        # either initialize or update.
        if x:
            self.x = x
        self.x = 0

def f(a) -> None: pass
[out]
IfAlways: [x, y, z]
IfSometimes1: [y]
IfSometimes2: [y]
IfStopAnalysis1: [x]
IfStopAnalysis2: [x]
IfStopAnalysis3: []
IfConditionalAndNonConditional1: [x]
IfConditionalAndNonConditional2: []

[case testAlwaysDefinedExpressions]
from typing import Dict, List, Set, Optional, cast
from typing_extensions import Final

import other

class C: pass

class Collections:
    def __init__(self, x: int) -> None:
        self.l = [x]
        self.d: Dict[str, str] = {}
        self.s: Set[int] = set()
        self.d2 = {'x': x}
        self.s2 = {x}
        self.l2 = [f(), None] * x
        self.t = tuple(self.l2)

class Comparisons:
    def __init__(self, y: int, c: C, s: str, o: Optional[str]) -> None:
        self.n1 = y < 5
        self.n2 = y == 5
        self.c1 = y is c
        self.c2 = y is not c
        self.o1 = o is None
        self.o2 = o is not None
        self.s = s < 'x'

class BinaryOps:
    def __init__(self, x: int, s: str) -> None:
        self.a = x + 2
        self.b = x & 2
        self.c = x * 2
        self.d = -x
        self.e = 'x' + s
        self.f = x << x

g = 2

class LocalsAndGlobals:
    def __init__(self, x: int) -> None:
        t = x + 1
        self.a = t - t
        self.g = g

class Booleans:
    def __init__(self, x: int, b: bool) -> None:
        self.a = True
        self.b = False
        self.c = not b
        self.d = b or b
        self.e = b and b

F: Final = 3

class ModuleFinal:
    def __init__(self) -> None:
        self.a = F
        self.b = other.Y

class ClassFinal:
    F: Final = 3

    def __init__(self) -> None:
        self.a = ClassFinal.F

class Literals:
    def __init__(self) -> None:
        self.a = 'x'
        self.b = b'x'
        self.c = 2.2

class ListComprehension:
    def __init__(self, x: List[int]) -> None:
        self.a = [i + 1 for i in x]

class Helper:
    def __init__(self, arg) -> None:
        self.x = 0

    def foo(self, arg) -> int:
        return 1

class AttrAccess:
    def __init__(self, o: Helper) -> None:
        self.x = o.x
        o.x = o.x + 1
        self.y = o.foo(self.x)
        o.foo(self)
        self.z = 1

class Construct:
    def __init__(self) -> None:
        self.x = Helper(1)
        self.y = Helper(self)

class IsInstance:
    def __init__(self, x: object) -> None:
        if isinstance(x, str):
            self.x = 0
        elif isinstance(x, Helper):
            self.x = 1
        elif isinstance(x, (list, tuple)):
            self.x = 2
        else:
            self.x = 3

class Cast:
    def __init__(self, x: object) -> None:
        self.x = cast(int, x)
        self.s = cast(str, x)
        self.c = cast(Cast, x)

class PropertyAccessGetter:
    def __init__(self, other: PropertyAccessGetter) -> None:
        self.x = other.p
        self.y = 1
        self.z = self.p

    @property
    def p(self) -> int:
        return 0

class PropertyAccessSetter:
    def __init__(self, other: PropertyAccessSetter) -> None:
        other.p = 1
        self.y = 1
        self.z = self.p

    @property
    def p(self) -> int:
        return 0

    @p.setter
    def p(self, x: int) -> None:
        pass

def f() -> int:
    return 0

[file other.py]
# Not compiled
from typing_extensions import Final

Y: Final = 3

[out]
C: []
Collections: [d, d2, l, l2, s, s2, t]
Comparisons: [c1, c2, n1, n2, o1, o2, s]
BinaryOps: [a, b, c, d, e, f]
LocalsAndGlobals: [a, g]
Booleans: [a, b, c, d, e]
ModuleFinal: [a, b]
ClassFinal: [F, a]
Literals: [a, b, c]
ListComprehension: [a]
Helper: [x]
AttrAccess: [x, y]
Construct: [x]
IsInstance: [x]
Cast: [c, s, x]
PropertyAccessGetter: [x, y]
PropertyAccessSetter: [y]

[case testAlwaysDefinedExpressions2]
from typing import List, Tuple

class C:
    def __init__(self) -> None:
        self.x = 0

class AttributeRef:
    def __init__(self, c: C) -> None:
        self.aa = c.x
        self.bb = self.aa
        if c is not None:
            self.z = 0
        self.cc = 0
        self.dd = self.z

class ListOps:
    def __init__(self, x: List[int], n: int) -> None:
        self.a = len(x)
        self.b = x[n]
        self.c = [y + 1 for y in x]

class TupleOps:
    def __init__(self, t: Tuple[int, str]) -> None:
        x, y = t
        self.x = x
        self.y = t[0]
        s = x, y
        self.z = s

class IfExpr:
    def __init__(self, x: int) -> None:
        self.a = 1 if x < 5 else 2

class Base:
    def __init__(self, x: int) -> None:
        self.x = x

class Derived1(Base):
    def __init__(self, y: int) -> None:
        self.aa = y
        super().__init__(y)
        self.bb = y

class Derived2(Base):
    pass

class Conditionals:
    def __init__(self, b: bool, n: int) -> None:
        if not (n == 5 or n >= n + 1):
            self.a = b
        else:
            self.a = not b
        if b:
            self.b = 2
        else:
            self.b = 4

[out]
C: [x]
AttributeRef: [aa, bb, cc, dd]
ListOps: [a, b, c]
TupleOps: [x, y, z]
IfExpr: [a]
Base: [x]
Derived1: [aa, bb, x]
Derived2: [x]
Conditionals: [a, b]

[case testAlwaysDefinedStatements]
from typing import Any, List, Optional, Iterable

class Return:
    def __init__(self, x: int) -> None:
        self.x = x
        if x > 5:
            self.y = 1
            return
        self.y = 2
        self.z = x

class While:
    def __init__(self, x: int) -> None:
        n = 2
        while x > 0:
            n *=2
            x -= 1
        self.a = n
        while x < 5:
            self.b = 1
            self.b += 1

class Try:
    def __init__(self, x: List[int]) -> None:
        self.a = 0
        try:
            self.b = x[0]
        except:
            self.c = x
        self.d = 0
        try:
            self.e = x[0]
        except:
            self.e = 1

class TryFinally:
    def __init__(self, x: List[int]) -> None:
        self.a = 0
        try:
            self.b = x[0]
        finally:
            self.c = x
        self.d = 0
        try:
            self.e = x[0]
        finally:
            self.e = 1

class Assert:
    def __init__(self, x: Optional[str], y: int) -> None:
        assert x is not None
        assert y < 5
        self.a = x

class For:
    def __init__(self, it: Iterable[int]) -> None:
        self.x = 0
        for x in it:
            self.x += x
        for x in it:
            self.y = x

class Assignment1:
    def __init__(self, other: Assignment1) -> None:
        self.x = 0
        self = other  # Give up after assignment to self
        self.y = 1

class Assignment2:
    def __init__(self) -> None:
        self.x = 0
        other = self  # Give up after self is aliased
        self.y = other.x

class With:
    def __init__(self, x: Any) -> None:
        self.a = 0
        with x:
            self.b = 1
        self.c = 2

def f() -> None:
    pass

[out]
Return: [x, y]
While: [a]
-- We could infer 'e' as always defined, but this is tricky, since always defined attribute
-- analysis must be performed earlier than exception handling transform. This would be
-- easy to infer *after* exception handling transform.
Try: [a, d]
-- Again, 'e' could be always defined, but it would be a bit tricky to do it.
TryFinally: [a, c, d]
Assert: [a]
For: [x]
Assignment1: [x]
Assignment2: [x]
-- TODO: Why is not 'b' included?
With: [a, c]

[case testAlwaysDefinedAttributeDefaults]
class Basic:
    x = 0

class ClassBodyAndInit:
    x = 0
    s = 'x'

    def __init__(self, n: int) -> None:
        self.n = 0

class AttrWithDefaultAndInit:
    x = 0

    def __init__(self, x: int) -> None:
        self.x = x

class Base:
    x = 0
    y = 1

class Derived(Base):
    y = 2
    z = 3
[out]
Basic: [x]
ClassBodyAndInit: [n, s, x]
AttrWithDefaultAndInit: [x]
Base: [x, y]
Derived: [x, y, z]

[case testAlwaysDefinedWithInheritance]
class Base:
    def __init__(self, x: int) -> None:
        self.x = x

class Deriv1(Base):
    def __init__(self, x: int, y: str) -> None:
        super().__init__(x)
        self.y = y

class Deriv2(Base):
    def __init__(self, x: int, y: str) -> None:
        self.y = y
        super().__init__(x)

class Deriv22(Deriv2):
    def __init__(self, x: int, y: str, z: bool) -> None:
        super().__init__(x, y)
        self.z = False

class Deriv3(Base):
    def __init__(self) -> None:
        super().__init__(1)

class Deriv4(Base):
    def __init__(self) -> None:
        self.y = 1
        self.x = 2

def f(a): pass

class BaseUnsafe:
    def __init__(self, x: int, y: int) -> None:
        self.x = x
        f(self)  # Unknown function
        self.y = y

class DerivUnsafe(BaseUnsafe):
    def __init__(self, z: int, zz: int) -> None:
        self.z = z
        super().__init__(1, 2)  # Calls unknown function
        self.zz = zz

class BaseWithDefault:
    x = 1

    def __init__(self) -> None:
        self.y = 1

class DerivedWithDefault(BaseWithDefault):
    def __init__(self) -> None:
        super().__init__()
        self.z = 1

class AlwaysDefinedInBase:
    def __init__(self) -> None:
        self.x = 1
        self.y = 1

class UndefinedInDerived(AlwaysDefinedInBase):
    def __init__(self, x: bool) -> None:
        self.x = 1
        if x:
            self.y = 2

class UndefinedInDerived2(UndefinedInDerived):
    def __init__(self, x: bool):
        if x:
            self.y = 2
[out]
Base: [x]
Deriv1: [x, y]
Deriv2: [x, y]
Deriv22: [x, y, z]
Deriv3: [x]
Deriv4: [x, y]
BaseUnsafe: [x]
DerivUnsafe: [x, z]
BaseWithDefault: [x, y]
DerivedWithDefault: [x, y, z]
AlwaysDefinedInBase: []
UndefinedInDerived: []
UndefinedInDerived2: []

[case testAlwaysDefinedWithInheritance2]
from mypy_extensions import trait, mypyc_attr

from interpreted import PythonBase

class BasePartiallyDefined:
    def __init__(self, x: int) -> None:
        self.a = 0
        if x:
            self.x = x

class Derived1(BasePartiallyDefined):
    def __init__(self, x: int) -> None:
        super().__init__(x)
        self.y = x

class BaseUndefined:
    x: int

class DerivedAlwaysDefined(BaseUndefined):
    def __init__(self) -> None:
        super().__init__()
        self.z = 0
        self.x = 2

@trait
class MyTrait:
    def f(self) -> None: pass

class SimpleTraitImpl(MyTrait):
    def __init__(self) -> None:
        super().__init__()
        self.x = 0

@trait
class TraitWithAttr:
    x: int
    y: str

class TraitWithAttrImpl(TraitWithAttr):
    def __init__(self) -> None:
        self.y = 'x'

@trait
class TraitWithAttr2:
    z: int

class TraitWithAttrImpl2(TraitWithAttr, TraitWithAttr2):
    def __init__(self) -> None:
        self.y = 'x'
        self.z = 2

@mypyc_attr(allow_interpreted_subclasses=True)
class BaseWithGeneralSubclassing:
    x = 0
    y: int
    def __init__(self, s: str) -> None:
        self.s = s

class Derived2(BaseWithGeneralSubclassing):
    def __init__(self) -> None:
        super().__init__('x')
        self.z = 0

class SubclassPythonclass(PythonBase):
    def __init__(self) -> None:
        self.y = 1

class BaseWithSometimesDefined:
    def __init__(self, b: bool) -> None:
        if b:
            self.x = 0

class Derived3(BaseWithSometimesDefined):
    def __init__(self, b: bool) -> None:
        super().__init__(b)
        self.x = 1

[file interpreted.py]
class PythonBase:
    def __init__(self) -> None:
        self.x = 0

[out]
BasePartiallyDefined: [a]
Derived1: [a, y]
BaseUndefined: []
DerivedAlwaysDefined: [x, z]
MyTrait: []
SimpleTraitImpl: [x]
TraitWithAttr: []
TraitWithAttrImpl: [y]
TraitWithAttr2: []
TraitWithAttrImpl2: [y, z]
BaseWithGeneralSubclassing: []
-- TODO: 's' could also be always defined
Derived2: [x, z]
-- Always defined attribute analysis is turned off when inheriting a non-native class.
SubclassPythonclass: []
BaseWithSometimesDefined: []
-- TODO: 'x' could also be always defined, but it is a bit tricky to support
Derived3: []

[case testAlwaysDefinedWithNesting]
class NestedFunc:
    def __init__(self) -> None:
        self.x = 0
        def f() -> None:
            self.y = 0
        f()
        self.z = 1
[out]
-- TODO: Support nested functions.
NestedFunc: []
f___init___NestedFunc_obj: []
