Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix[ux]: error messages relating to initializer issues #3831

Merged
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"
charles-cooper marked this conversation as resolved.
Show resolved Hide resolved
raise InitializerException(msg, node, hint=hint)

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