Skip to content

Commit 1ba39f3

Browse files
committed
Raise better error messages when expected structs disappear, closes #15472
1 parent 0f9072a commit 1ba39f3

2 files changed

Lines changed: 73 additions & 28 deletions

File tree

lib/elixir/lib/module/types/of.ex

Lines changed: 35 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -467,32 +467,32 @@ defmodule Module.Types.Of do
467467
# TODO: Type check the fields match the struct
468468
def struct_instance(struct, args, expected, meta, stack, context, of_fun)
469469
when is_atom(struct) do
470-
{info, context} = struct_info(struct, :expr, meta, stack, context)
470+
{info, context} = struct_info(struct, :expr, meta, stack, context, true)
471471

472472
if is_nil(info) do
473-
raise "expected #{inspect(struct)} to return struct metadata, but got none"
473+
{dynamic(), context}
474+
else
475+
# The compiler has already checked the keys are atoms and which ones are required.
476+
{args_types, context} =
477+
Enum.map_reduce(args, context, fn {key, value}, context when is_atom(key) ->
478+
value_type =
479+
case map_fetch_key(expected, key) do
480+
{_, expected_value_type} -> expected_value_type
481+
_ -> term()
482+
end
483+
484+
{type, context} = of_fun.(value, value_type, stack, context)
485+
{{key, type}, context}
486+
end)
487+
488+
{closed_map([{:__struct__, atom([struct])} | args_types]), context}
474489
end
475-
476-
# The compiler has already checked the keys are atoms and which ones are required.
477-
{args_types, context} =
478-
Enum.map_reduce(args, context, fn {key, value}, context when is_atom(key) ->
479-
value_type =
480-
case map_fetch_key(expected, key) do
481-
{_, expected_value_type} -> expected_value_type
482-
_ -> term()
483-
end
484-
485-
{type, context} = of_fun.(value, value_type, stack, context)
486-
{{key, type}, context}
487-
end)
488-
489-
{closed_map([{:__struct__, atom([struct])} | args_types]), context}
490490
end
491491

492492
@doc """
493493
Returns `__info__(:struct)` information about a struct.
494494
"""
495-
def struct_info(struct, kind, meta, stack, context) do
495+
def struct_info(struct, kind, meta, stack, context, must_exist? \\ false) do
496496
case stack.no_warn_undefined do
497497
%Macro.Env{} = env ->
498498
case :elixir_map.maybe_load_struct_info(meta, struct, :soft, env) do
@@ -511,7 +511,7 @@ defmodule Module.Types.Of do
511511

512512
{info, context}
513513
else
514-
error = {:unknown_struct, kind, struct}
514+
error = {:unknown_struct, kind, struct, must_exist?}
515515
{nil, error(error, meta, stack, context)}
516516
end
517517
end
@@ -848,19 +848,26 @@ defmodule Module.Types.Of do
848848
}
849849
end
850850

851-
def format_diagnostic({:unknown_struct, kind, module}) do
852-
message =
853-
if Code.ensure_loaded?(module) do
854-
"struct #{inspect(module)} is undefined (there is such module but it does not define a struct)"
855-
else
856-
"struct #{inspect(module)} is undefined " <>
857-
"(module #{inspect(module)} is not available or is yet to be defined)"
851+
def format_diagnostic({:unknown_struct, kind, module, must_exist?}) do
852+
detail =
853+
case {Code.ensure_loaded?(module), must_exist?} do
854+
{true, false} ->
855+
"there is such module but it does not define a struct"
856+
857+
{false, false} ->
858+
"module #{inspect(module)} is not available or is yet to be defined"
859+
860+
{true, true} ->
861+
"the module may have been redefined as it no longer defines a struct"
862+
863+
{false, true} ->
864+
"the module was also only available but may have been removed during compilation"
858865
end
859866

860867
%{
861-
message: message,
868+
message: "struct #{inspect(module)} is undefined (#{detail})",
862869
group: true,
863-
severity: if(kind == :pattern, do: :error, else: :warning)
870+
severity: if(kind == :pattern or must_exist?, do: :error, else: :warning)
864871
}
865872
end
866873

lib/elixir/test/elixir/module/types/integration_test.exs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -749,6 +749,44 @@ defmodule Module.Types.IntegrationTest do
749749
purge(A)
750750
end
751751

752+
test "does not crash on redefined module with newly added struct" do
753+
Code.compile_string("""
754+
defmodule RedefinedNestedStruct.Inner do
755+
def foo(), do: :nothing
756+
end
757+
""")
758+
759+
files = %{
760+
"redefined.ex" => """
761+
defmodule RedefinedNestedStruct.Inner do
762+
defstruct [:value]
763+
764+
def foo(), do: %__MODULE__{value: 1}
765+
end
766+
"""
767+
}
768+
769+
in_tmp(fn ->
770+
paths = generate_files(files)
771+
772+
{result, _stderr} =
773+
with_io(:stderr, fn ->
774+
Kernel.ParallelCompiler.compile_to_path(paths, ".", return_diagnostics: true)
775+
end)
776+
777+
assert {:error, errors, %{compile_warnings: warnings, runtime_warnings: []}} = result
778+
779+
assert [%{message: "struct RedefinedNestedStruct.Inner is undefined " <> _}] = errors
780+
781+
assert Enum.any?(
782+
warnings,
783+
&(&1.message =~ "redefining module RedefinedNestedStruct.Inner")
784+
)
785+
end)
786+
after
787+
purge(RedefinedNestedStruct.Inner)
788+
end
789+
752790
@tag :require_ast
753791
test "regressions" do
754792
files = %{

0 commit comments

Comments
 (0)