Skip to content

Commit

Permalink
fix[ux]: error messages relating to initializer issues (vyperlang#3831)
Browse files Browse the repository at this point in the history
fix some error messages related to initializers, to improve UX

---------

Co-authored-by: cyberthirst <[email protected]>
  • Loading branch information
charles-cooper and cyberthirst authored Apr 7, 2024
1 parent 9d0d147 commit c54d3b1
Show file tree
Hide file tree
Showing 3 changed files with 98 additions and 6 deletions.
66 changes: 65 additions & 1 deletion tests/functional/syntax/modules/test_initializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,7 @@ def foo():
with pytest.raises(InitializerException) as e:
compile_code(main, input_bundle=input_bundle)
assert e.value._message == "`lib2` uses `lib1`, but it is not initialized with `lib1`"
assert e.value._hint == "add `lib1` to its initializer list"
assert e.value._hint == "did you mean lib2[lib1 := lib1]?"


def test_missing_uses(make_input_bundle):
Expand Down Expand Up @@ -1228,3 +1228,67 @@ def use_lib1():
compile_code(main, input_bundle=input_bundle, output_formats=["annotated_ast_dict"])
is not None
)


def test_hint_for_missing_initializer_in_list(make_input_bundle):
lib1 = """
counter: uint256
"""
lib3 = """
counter: uint256
"""
lib2 = """
import lib1
import lib3
uses: lib1
uses: lib3
counter: uint256
@internal
def foo():
lib1.counter += 1
lib3.counter += 1
"""
main = """
import lib1
import lib2
import lib3
initializes: lib2[lib1:=lib1]
initializes: lib1
initializes: lib3
"""
input_bundle = make_input_bundle({"lib1.vy": lib1, "lib2.vy": lib2, "lib3.vy": lib3})
with pytest.raises(InitializerException) as e:
compile_code(main, input_bundle=input_bundle)
assert e.value._message == "`lib2` uses `lib3`, but it is not initialized with `lib3`"
assert e.value._hint == "add `lib3 := lib3` to its initializer list"


def test_hint_for_missing_initializer_when_no_import(make_input_bundle):
lib1 = """
counter: uint256
"""
lib2 = """
import lib1
uses: lib1
counter: uint256
@internal
def foo():
lib1.counter += 1
"""
main = """
import lib2
initializes: lib2
"""
input_bundle = make_input_bundle({"lib1.vy": lib1, "lib2.vy": lib2})
with pytest.raises(InitializerException) as e:
compile_code(main, input_bundle=input_bundle)
assert e.value._message == "`lib2` uses `lib1`, but it is not initialized with `lib1`"
assert e.value._hint == "try importing lib1 first"
8 changes: 6 additions & 2 deletions vyper/semantics/analysis/global_.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,12 @@ def _validate_global_initializes_constraint(module_t: ModuleT):
if u not in all_initialized_modules:
found_module = module_t.find_module_info(u)
if found_module is not None:
hint = f"add `initializes: {found_module.alias}` to the top level of "
hint += "your main contract"
# TODO: do something about these constants
if str(module_t) in ("<unknown>", "VyperContract.vy"):
module_str = "the top level of your main contract"
else:
module_str = f"`{module_t}`"
hint = f"add `initializes: {found_module.alias}` to {module_str}"
else:
# CMC 2024-02-06 is this actually reachable?
hint = f"ensure `{module_t}` is imported in your main contract!"
Expand Down
30 changes: 27 additions & 3 deletions vyper/semantics/analysis/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,13 @@ def validate_initialized_modules(self):
msg = "not initialized!"
hint = f"add `{s.module_info.alias}.__init__()` to "
hint += "your `__init__()` function"
err_list.append(InitializerException(msg, s.node, hint=hint))

# grab the init function AST node for error message
# (it could be None, it's ok since it's just for diagnostics)
init_func_node = None
if module_t.init_function:
init_func_node = module_t.init_function.decl_node
err_list.append(InitializerException(msg, init_func_node, s.node, hint=hint))

err_list.raise_if_not_empty()

Expand Down Expand Up @@ -437,8 +443,10 @@ def visit_UsesDecl(self, node):
node._metadata["uses_info"] = UsesInfo(used_modules, node)

def visit_InitializesDecl(self, node):
module_ref = node.annotation
annotation = node.annotation

dependencies_ast = ()
module_ref = annotation
if isinstance(module_ref, vy_ast.Subscript):
dependencies_ast = vy_ast.as_tuple(module_ref.slice)
module_ref = module_ref.value
Expand Down Expand Up @@ -496,7 +504,23 @@ def visit_InitializesDecl(self, node):
item = next(iter(used_modules.values())) # just pick one
msg = f"`{module_info.alias}` uses `{item.alias}`, but it is not "
msg += f"initialized with `{item.alias}`"
hint = f"add `{item.alias}` to its initializer list"

lhs = item.alias
rhs = None
# find the alias of the uninitialized module in this contract
# to fill out the error message with.
for k, v in self.namespace.items():
if isinstance(v, ModuleInfo) and v.module_t == item.module_t:
rhs = k
break

if rhs is None:
hint = f"try importing {item.alias} first"
elif not isinstance(annotation, vy_ast.Subscript):
# it's `initializes: foo` instead of `initializes: foo[...]`
hint = f"did you mean {module_ref.id}[{lhs} := {rhs}]?"
else:
hint = f"add `{lhs} := {rhs}` to its initializer list"
raise InitializerException(msg, node, hint=hint)

# note: try to refactor. not a huge fan of mutating the
Expand Down

0 comments on commit c54d3b1

Please sign in to comment.