Skip to content

Commit c0cf436

Browse files
feat[lang]!: make @external modifier optional in .vyi files (#4178)
make `@external` visibility in `.vyi` files optional. additionally, fix a panic when functions in an interface have the wrong visibility --------- Co-authored-by: Charles Cooper <cooper.charles.m@gmail.com>
1 parent 85269b0 commit c0cf436

File tree

5 files changed

+145
-13
lines changed

5 files changed

+145
-13
lines changed

tests/functional/codegen/test_interfaces.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -695,3 +695,34 @@ def test_call(a: address, b: {type_str}) -> {type_str}:
695695
make_file("jsonabi.json", json.dumps(convert_v1_abi(abi)))
696696
c3 = get_contract(code, input_bundle=input_bundle)
697697
assert c3.test_call(c1.address, value) == value
698+
699+
700+
def test_interface_function_without_visibility(make_input_bundle, get_contract):
701+
interface_code = """
702+
def foo() -> uint256:
703+
...
704+
705+
@external
706+
def bar() -> uint256:
707+
...
708+
"""
709+
710+
code = """
711+
import a as FooInterface
712+
713+
implements: FooInterface
714+
715+
@external
716+
def foo() -> uint256:
717+
return 1
718+
719+
@external
720+
def bar() -> uint256:
721+
return 1
722+
"""
723+
724+
input_bundle = make_input_bundle({"a.vyi": interface_code})
725+
726+
c = get_contract(code, input_bundle=input_bundle)
727+
728+
assert c.foo() == c.bar() == 1

tests/functional/syntax/test_interfaces.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,3 +484,81 @@ def baz():
484484
"""
485485

486486
assert compiler.compile_code(code, input_bundle=input_bundle) is not None
487+
488+
489+
invalid_visibility_code = [
490+
"""
491+
import foo as Foo
492+
implements: Foo
493+
@external
494+
def foobar():
495+
pass
496+
""",
497+
"""
498+
import foo as Foo
499+
implements: Foo
500+
@internal
501+
def foobar():
502+
pass
503+
""",
504+
"""
505+
import foo as Foo
506+
implements: Foo
507+
def foobar():
508+
pass
509+
""",
510+
]
511+
512+
513+
@pytest.mark.parametrize("code", invalid_visibility_code)
514+
def test_internal_visibility_in_interface(make_input_bundle, code):
515+
interface_code = """
516+
@internal
517+
def foobar():
518+
...
519+
"""
520+
521+
input_bundle = make_input_bundle({"foo.vyi": interface_code})
522+
523+
with pytest.raises(FunctionDeclarationException) as e:
524+
compiler.compile_code(code, input_bundle=input_bundle)
525+
526+
assert e.value._message == "Interface functions can only be marked as `@external`"
527+
528+
529+
external_visibility_interface = [
530+
"""
531+
@external
532+
def foobar():
533+
...
534+
def bar():
535+
...
536+
""",
537+
"""
538+
def foobar():
539+
...
540+
@external
541+
def bar():
542+
...
543+
""",
544+
]
545+
546+
547+
@pytest.mark.parametrize("iface", external_visibility_interface)
548+
def test_internal_implemenatation_of_external_interface(make_input_bundle, iface):
549+
input_bundle = make_input_bundle({"foo.vyi": iface})
550+
551+
code = """
552+
import foo as Foo
553+
implements: Foo
554+
@internal
555+
def foobar():
556+
pass
557+
def bar():
558+
pass
559+
"""
560+
561+
with pytest.raises(InterfaceViolation) as e:
562+
compiler.compile_code(code, input_bundle=input_bundle)
563+
564+
assert e.value.message == "Contract does not implement all interface functions: bar(), foobar()"

vyper/semantics/analysis/module.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ def __init__(
193193
self._imported_modules: dict[PurePath, vy_ast.VyperNode] = {}
194194

195195
# keep track of exported functions to prevent duplicate exports
196-
self._exposed_functions: dict[ContractFunctionT, vy_ast.VyperNode] = {}
196+
self._all_functions: dict[ContractFunctionT, vy_ast.VyperNode] = {}
197197

198198
self._events: list[EventT] = []
199199

@@ -414,7 +414,7 @@ def visit_ImplementsDecl(self, node):
414414
raise StructureException(msg, node.annotation, hint=hint)
415415

416416
# grab exposed functions
417-
funcs = self._exposed_functions
417+
funcs = {fn_t: node for fn_t, node in self._all_functions.items() if fn_t.is_external}
418418
type_.validate_implements(node, funcs)
419419

420420
node._metadata["interface_type"] = type_
@@ -608,10 +608,10 @@ def _self_t(self):
608608
def _add_exposed_function(self, func_t, node, relax=True):
609609
# call this before self._self_t.typ.add_member() for exception raising
610610
# priority
611-
if not relax and (prev_decl := self._exposed_functions.get(func_t)) is not None:
611+
if not relax and (prev_decl := self._all_functions.get(func_t)) is not None:
612612
raise StructureException("already exported!", node, prev_decl=prev_decl)
613613

614-
self._exposed_functions[func_t] = node
614+
self._all_functions[func_t] = node
615615

616616
def visit_VariableDecl(self, node):
617617
# postcondition of VariableDecl.validate

vyper/semantics/types/function.py

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,23 @@ def from_vyi(cls, funcdef: vy_ast.FunctionDef) -> "ContractFunctionT":
330330
function_visibility, state_mutability, nonreentrant = _parse_decorators(funcdef)
331331

332332
if nonreentrant:
333-
raise FunctionDeclarationException("`@nonreentrant` not allowed in interfaces", funcdef)
333+
# TODO: refactor so parse_decorators returns the AST location
334+
decorator = next(d for d in funcdef.decorator_list if d.id == "nonreentrant")
335+
raise FunctionDeclarationException(
336+
"`@nonreentrant` not allowed in interfaces", decorator
337+
)
338+
339+
# it's redundant to specify visibility in vyi - always should be external
340+
if function_visibility is None:
341+
function_visibility = FunctionVisibility.EXTERNAL
342+
343+
if function_visibility != FunctionVisibility.EXTERNAL:
344+
nonexternal = next(
345+
d for d in funcdef.decorator_list if d.id in FunctionVisibility.values()
346+
)
347+
raise FunctionDeclarationException(
348+
"Interface functions can only be marked as `@external`", nonexternal
349+
)
334350

335351
if funcdef.name == "__init__":
336352
raise FunctionDeclarationException("Constructors cannot appear in interfaces", funcdef)
@@ -381,6 +397,10 @@ def from_FunctionDef(cls, funcdef: vy_ast.FunctionDef) -> "ContractFunctionT":
381397
"""
382398
function_visibility, state_mutability, nonreentrant = _parse_decorators(funcdef)
383399

400+
# it's redundant to specify internal visibility - it's implied by not being external
401+
if function_visibility is None:
402+
function_visibility = FunctionVisibility.INTERNAL
403+
384404
positional_args, keyword_args = _parse_args(funcdef)
385405

386406
return_type = _parse_return_type(funcdef)
@@ -419,6 +439,10 @@ def from_FunctionDef(cls, funcdef: vy_ast.FunctionDef) -> "ContractFunctionT":
419439
raise FunctionDeclarationException(
420440
"Constructor may not use default arguments", funcdef.args.defaults[0]
421441
)
442+
if nonreentrant:
443+
decorator = next(d for d in funcdef.decorator_list if d.id == "nonreentrant")
444+
msg = "`@nonreentrant` decorator disallowed on `__init__`"
445+
raise FunctionDeclarationException(msg, decorator)
422446

423447
return cls(
424448
funcdef.name,
@@ -495,6 +519,8 @@ def implements(self, other: "ContractFunctionT") -> bool:
495519
if not self.is_external: # pragma: nocover
496520
raise CompilerPanic("unreachable!")
497521

522+
assert self.visibility == other.visibility
523+
498524
arguments, return_type = self._iface_sig
499525
other_arguments, other_return_type = other._iface_sig
500526

@@ -700,7 +726,7 @@ def _parse_return_type(funcdef: vy_ast.FunctionDef) -> Optional[VyperType]:
700726

701727
def _parse_decorators(
702728
funcdef: vy_ast.FunctionDef,
703-
) -> tuple[FunctionVisibility, StateMutability, bool]:
729+
) -> tuple[Optional[FunctionVisibility], StateMutability, bool]:
704730
function_visibility = None
705731
state_mutability = None
706732
nonreentrant_node = None
@@ -719,10 +745,6 @@ def _parse_decorators(
719745
if nonreentrant_node is not None:
720746
raise StructureException("nonreentrant decorator is already set", nonreentrant_node)
721747

722-
if funcdef.name == "__init__":
723-
msg = "`@nonreentrant` decorator disallowed on `__init__`"
724-
raise FunctionDeclarationException(msg, decorator)
725-
726748
nonreentrant_node = decorator
727749

728750
elif isinstance(decorator, vy_ast.Name):
@@ -733,6 +755,7 @@ def _parse_decorators(
733755
decorator,
734756
hint="only one visibility decorator is allowed per function",
735757
)
758+
736759
function_visibility = FunctionVisibility(decorator.id)
737760

738761
elif StateMutability.is_valid_value(decorator.id):
@@ -755,9 +778,6 @@ def _parse_decorators(
755778
else:
756779
raise StructureException("Bad decorator syntax", decorator)
757780

758-
if function_visibility is None:
759-
function_visibility = FunctionVisibility.INTERNAL
760-
761781
if state_mutability is None:
762782
# default to nonpayable
763783
state_mutability = StateMutability.NONPAYABLE

vyper/semantics/types/module.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ def _ctor_modifiability_for_call(self, node: vy_ast.Call, modifiability: Modifia
107107
def validate_implements(
108108
self, node: vy_ast.ImplementsDecl, functions: dict[ContractFunctionT, vy_ast.VyperNode]
109109
) -> None:
110+
# only external functions can implement interfaces
110111
fns_by_name = {fn_t.name: fn_t for fn_t in functions.keys()}
111112

112113
unimplemented = []
@@ -116,7 +117,9 @@ def _is_function_implemented(fn_name, fn_type):
116117
return False
117118

118119
to_compare = fns_by_name[fn_name]
120+
assert to_compare.is_external
119121
assert isinstance(to_compare, ContractFunctionT)
122+
assert isinstance(fn_type, ContractFunctionT)
120123

121124
return to_compare.implements(fn_type)
122125

0 commit comments

Comments
 (0)