diff --git a/tests/functional/syntax/modules/test_initializers.py b/tests/functional/syntax/modules/test_initializers.py index c28301301d..9825e4618f 100644 --- a/tests/functional/syntax/modules/test_initializers.py +++ b/tests/functional/syntax/modules/test_initializers.py @@ -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): @@ -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" diff --git a/vyper/semantics/analysis/global_.py b/vyper/semantics/analysis/global_.py index a632c9194c..8b3e0544a5 100644 --- a/vyper/semantics/analysis/global_.py +++ b/vyper/semantics/analysis/global_.py @@ -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 ("", "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!" diff --git a/vyper/semantics/analysis/module.py b/vyper/semantics/analysis/module.py index e0b7da0ce5..619f4e4c10 100644 --- a/vyper/semantics/analysis/module.py +++ b/vyper/semantics/analysis/module.py @@ -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() @@ -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 @@ -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