Skip to content

Commit

Permalink
[mypyc] Reject instance attribute access through class object (#10798)
Browse files Browse the repository at this point in the history
Accessing an instance attribute through a native class object results in
unexpected behavior at runtime (e.g. <attribute 'x' of 'C' objects>)
so reject these during compilation.

Also produce a note that suggests how to work around the issue.

Fixes mypyc/mypyc#814.
  • Loading branch information
JukkaL committed Jul 10, 2021
1 parent 24f3ba0 commit a37c388
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 2 deletions.
34 changes: 33 additions & 1 deletion mypyc/irbuild/expression.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
AssignmentExpr,
Var, RefExpr, MypyFile, TypeInfo, TypeApplication, LDEF, ARG_POS
)
from mypy.types import TupleType, get_proper_type, Instance
from mypy.types import TupleType, Instance, TypeType, ProperType, get_proper_type

from mypyc.common import MAX_SHORT_INT
from mypyc.ir.ops import (
Expand Down Expand Up @@ -133,9 +133,41 @@ def transform_member_expr(builder: IRBuilder, expr: MemberExpr) -> Value:
if expr.name in fields:
index = builder.builder.load_int(fields.index(expr.name))
return builder.gen_method_call(obj, '__getitem__', [index], rtype, expr.line)

check_instance_attribute_access_through_class(builder, expr, typ)

return builder.builder.get_attr(obj, expr.name, rtype, expr.line)


def check_instance_attribute_access_through_class(builder: IRBuilder,
expr: MemberExpr,
typ: Optional[ProperType]) -> None:
"""Report error if accessing an instance attribute through class object."""
if isinstance(expr.expr, RefExpr):
node = expr.expr.node
if isinstance(typ, TypeType) and isinstance(typ.item, Instance):
# TODO: Handle other item types
node = typ.item.type
if isinstance(node, TypeInfo):
class_ir = builder.mapper.type_to_ir.get(node)
if class_ir is not None and class_ir.is_ext_class:
sym = node.get(expr.name)
if (sym is not None
and isinstance(sym.node, Var)
and not sym.node.is_classvar
and not sym.node.is_final):
builder.error(
'Cannot access instance attribute "{}" through class object'.format(
expr.name),
expr.line
)
builder.note(
'(Hint: Use "x: Final = ..." or "x: ClassVar = ..." to define '
'a class attribute)',
expr.line
)


def transform_super_expr(builder: IRBuilder, o: SuperExpr) -> Value:
# warning(builder, 'can not optimize super() expression', o.line)
sup_val = builder.load_module_attr_by_fullname('builtins.super', o.line)
Expand Down
38 changes: 38 additions & 0 deletions mypyc/test-data/irbuild-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -1144,3 +1144,41 @@ class DeletableFinal2:
X: Final = 0 # E: Deletable attribute cannot be final

__deletable__ = ['X']

[case testNeedAnnotateClassVar]
from typing import Final, ClassVar, Type

class C:
a = 'A'
b: str = 'B'
f: Final = 'F'
c: ClassVar = 'C'

class D(C):
pass

def f() -> None:
C.a # E: Cannot access instance attribute "a" through class object \
# N: (Hint: Use "x: Final = ..." or "x: ClassVar = ..." to define a class attribute)
C.b # E: Cannot access instance attribute "b" through class object \
# N: (Hint: Use "x: Final = ..." or "x: ClassVar = ..." to define a class attribute)
C.f
C.c

D.a # E: Cannot access instance attribute "a" through class object \
# N: (Hint: Use "x: Final = ..." or "x: ClassVar = ..." to define a class attribute)
D.b # E: Cannot access instance attribute "b" through class object \
# N: (Hint: Use "x: Final = ..." or "x: ClassVar = ..." to define a class attribute)
D.f
D.c

def g(c: Type[C], d: Type[D]) -> None:
c.a # E: Cannot access instance attribute "a" through class object \
# N: (Hint: Use "x: Final = ..." or "x: ClassVar = ..." to define a class attribute)
c.f
c.c

d.a # E: Cannot access instance attribute "a" through class object \
# N: (Hint: Use "x: Final = ..." or "x: ClassVar = ..." to define a class attribute)
d.f
d.c
11 changes: 10 additions & 1 deletion mypyc/test-data/run-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -225,8 +225,15 @@ class Overload:
def get(c: Overload, s: str) -> str:
return c.get(s)

@decorator
class Var:
x = 'xy'

def get_class_var() -> str:
return Var.x

[file driver.py]
from native import A, Overload, get
from native import A, Overload, get, get_class_var
a = A()
assert a.a == 1
assert a.b == 2
Expand All @@ -240,6 +247,8 @@ o = Overload()
assert get(o, "test") == "test"
assert o.get(20) == 20

assert get_class_var() == 'xy'

[case testEnum]
from enum import Enum

Expand Down

0 comments on commit a37c388

Please sign in to comment.