Skip to content

Commit a0ef7f0

Browse files
committed
Move struct validation in patterns and updates to type checker
This means changing a struct definition only cases recompilation when structs are created, no longer on matches and updates. Closes #15098.
1 parent 34a5000 commit a0ef7f0

18 files changed

Lines changed: 457 additions & 283 deletions

File tree

lib/elixir/lib/kernel/parallel_compiler.ex

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -460,7 +460,7 @@ defmodule Kernel.ParallelCompiler do
460460
modules = write_module_binaries(result, state.output, state)
461461
profile(state, "after compile callback", state.after_compile)
462462

463-
runtime_warnings =
463+
{runtime_warnings, errors} =
464464
if state.verification? do
465465
profile(
466466
state,
@@ -471,11 +471,19 @@ defmodule Kernel.ParallelCompiler do
471471
fn -> Module.ParallelChecker.verify(state.checker, dependent_modules) end
472472
)
473473
else
474-
[]
474+
{[], []}
475475
end
476476

477477
info = %{compile_warnings: Enum.reverse(compile_warnings), runtime_warnings: runtime_warnings}
478-
{{:ok, modules, info}, state}
478+
479+
case errors do
480+
[] ->
481+
{{:ok, modules, info}, state}
482+
483+
_ ->
484+
IO.puts(:stderr, "== Type checking failed with errors ==")
485+
{{:error, errors, info}, state}
486+
end
479487
end
480488

481489
defp profile_init(:time), do: {:time, System.monotonic_time(), 0}

lib/elixir/lib/macro.ex

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -971,9 +971,15 @@ defmodule Macro do
971971
}
972972
]
973973
def struct_info!(module, env) when is_atom(module) do
974-
case :elixir_map.maybe_load_struct_info([line: env.line], module, [], true, env) do
975-
{:ok, info} -> info
976-
{:error, desc} -> raise ArgumentError, List.to_string(:elixir_map.format_error(desc))
974+
meta = [line: env.line]
975+
976+
case :elixir_map.maybe_load_struct_info(meta, module, env) do
977+
{:ok, info} ->
978+
:elixir_env.trace({:struct_expansion, meta, module, []}, env)
979+
info
980+
981+
{:error, desc} ->
982+
raise ArgumentError, List.to_string(:elixir_map.format_error(desc))
977983
end
978984
end
979985

lib/elixir/lib/module/parallel_checker.ex

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -120,14 +120,14 @@ defmodule Module.ParallelChecker do
120120
# Set the compiler info so we can collect warnings
121121
:erlang.put(:elixir_compiler_info, {pid, self()})
122122

123-
warnings =
123+
{warnings, errors} =
124124
if module_tuple do
125125
check_module(module_tuple, {checker, table}, log?)
126126
else
127-
[]
127+
{[], []}
128128
end
129129

130-
send(pid, {__MODULE__, module, warnings})
130+
send(pid, {__MODULE__, module, warnings, errors})
131131
send(checker, {__MODULE__, :done, module})
132132
end
133133

@@ -194,29 +194,34 @@ defmodule Module.ParallelChecker do
194194
end
195195

196196
count = :gen_server.call(checker, :start, :infinity)
197-
diagnostics = collect_results(count, [])
197+
{warnings, errors} = collect_results(count, [], [])
198198

199199
case :erlang.get(:elixir_code_diagnostics) do
200200
:undefined -> :ok
201-
{tail, log?} -> :erlang.put(:elixir_code_diagnostics, {diagnostics ++ tail, log?})
201+
{tail, log?} -> :erlang.put(:elixir_code_diagnostics, {errors ++ warnings ++ tail, log?})
202202
end
203203

204-
diagnostics
204+
{warnings, errors}
205205
end
206206

207-
defp collect_results(0, diagnostics) do
208-
diagnostics
207+
defp collect_results(0, warnings, errors) do
208+
{warnings, errors}
209209
end
210210

211-
defp collect_results(count, diagnostics) do
211+
defp collect_results(count, warnings, errors) do
212212
receive do
213213
{:diagnostic, %{file: file} = diagnostic, read_snippet} ->
214214
:elixir_errors.print_diagnostic(diagnostic, read_snippet)
215215
diagnostic = %{diagnostic | file: file && Path.absname(file)}
216-
collect_results(count, [diagnostic | diagnostics])
217216

218-
{__MODULE__, _module, new_diagnostics} ->
219-
collect_results(count - 1, new_diagnostics ++ diagnostics)
217+
if Map.get(diagnostic, :severity, :warning) == :error do
218+
collect_results(count, warnings, [diagnostic | errors])
219+
else
220+
collect_results(count, [diagnostic | warnings], errors)
221+
end
222+
223+
{__MODULE__, _module, new_warnings, new_errors} ->
224+
collect_results(count - 1, new_warnings ++ warnings, new_errors ++ errors)
220225
end
221226
end
222227

@@ -270,18 +275,19 @@ defmodule Module.ParallelChecker do
270275
definitions
271276
)
272277

273-
diagnostics =
278+
{warnings, errors} =
274279
module
275280
|> Module.Types.warnings(file, attrs, definitions, no_warn_undefined, cache)
276281
|> Kernel.++(behaviour_warnings)
277-
|> group_warnings()
278-
|> emit_warnings(file, log?)
282+
|> group_diagnostics()
283+
|> emit_diagnostics(file, log?)
284+
|> Enum.split_with(&(&1.severity == :warning))
279285

280286
Enum.each(after_verify, fn {verify_mod, verify_fun} ->
281287
apply(verify_mod, verify_fun, [module])
282288
end)
283289

284-
diagnostics
290+
{warnings, errors}
285291
end
286292

287293
defp module_map_to_module_tuple(module_map) do
@@ -332,16 +338,16 @@ defmodule Module.ParallelChecker do
332338

333339
## Warning helpers
334340

335-
defp group_warnings(warnings) do
341+
defp group_diagnostics(triplets) do
336342
{ungrouped, grouped} =
337-
Enum.reduce(warnings, {[], %{}}, fn {module, warning, location}, {ungrouped, grouped} ->
338-
%{message: _} = diagnostic = module.format_diagnostic(warning)
343+
Enum.reduce(triplets, {[], %{}}, fn {module, term, location}, {ungrouped, grouped} ->
344+
%{message: _} = diagnostic = module.format_diagnostic(term)
339345

340346
if Map.get(diagnostic, :group, false) do
341347
locations = MapSet.new([location])
342348

343349
grouped =
344-
Map.update(grouped, warning, {locations, diagnostic}, fn
350+
Map.update(grouped, term, {locations, diagnostic}, fn
345351
{locations, diagnostic} -> {MapSet.put(locations, location), diagnostic}
346352
end)
347353

@@ -359,7 +365,7 @@ defmodule Module.ParallelChecker do
359365
Enum.sort(ungrouped ++ grouped)
360366
end
361367

362-
defp emit_warnings(warnings, file, log?) do
368+
defp emit_diagnostics(warnings, file, log?) do
363369
Enum.flat_map(warnings, fn {locations, diagnostic} ->
364370
diagnostics = Enum.map(locations, &to_diagnostic(diagnostic, file, &1))
365371
log? and print_diagnostics(diagnostics)

lib/elixir/lib/module/types/apply.ex

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1953,17 +1953,10 @@ defmodule Module.Types.Apply do
19531953
end
19541954

19551955
def format_diagnostic({:undefined, :badmodule, module, fun, arity}) do
1956-
top =
1957-
if fun == :__struct__ and arity == 0 do
1958-
"struct #{inspect(module)}"
1959-
else
1960-
Exception.format_mfa(module, fun, arity)
1961-
end
1962-
19631956
%{
19641957
message:
19651958
IO.iodata_to_binary([
1966-
top,
1959+
Exception.format_mfa(module, fun, arity),
19671960
" is undefined (module ",
19681961
inspect(module),
19691962
" is not available or is yet to be defined)",
@@ -1973,14 +1966,6 @@ defmodule Module.Types.Apply do
19731966
}
19741967
end
19751968

1976-
def format_diagnostic({:undefined, {:badfunction, _}, module, :__struct__, 0}) do
1977-
%{
1978-
message:
1979-
"struct #{inspect(module)} is undefined (there is such module but it does not define a struct)",
1980-
group: true
1981-
}
1982-
end
1983-
19841969
def format_diagnostic({:undefined, {:badfunction, _}, module, fun, arity}) do
19851970
_ = Code.ensure_loaded(module)
19861971

lib/elixir/lib/module/types/expr.ex

Lines changed: 35 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -210,28 +210,41 @@ defmodule Module.Types.Expr do
210210
stack,
211211
context
212212
) do
213-
# We pass the expected type as `term()` because the struct update
214-
# operator already expects it to be a map at this point.
215-
{map_type, context} = of_expr(map, term(), struct, stack, context)
213+
{info, context} = Of.struct_info(module, :expr, meta, stack, context)
216214

217-
context =
218-
with {false, struct_key_type} <- map_fetch_key(map_type, :__struct__),
219-
{:finite, [^module]} <- atom_fetch(struct_key_type) do
220-
context
221-
else
222-
_ ->
223-
error(__MODULE__, {:badupdate, map_type, struct, context}, meta, stack, context)
224-
end
215+
if info do
216+
# We pass the expected type as `term()` because the struct update
217+
# operator already expects it to be a map at this point.
218+
{map_type, context} = of_expr(map, term(), struct, stack, context)
225219

226-
Enum.reduce(pairs, {map_type, context}, fn {key, value}, {acc, context} ->
227-
# TODO: Once we support typed structs, we need to type check them here
228-
{type, context} = of_expr(value, term(), expr, stack, context)
220+
context =
221+
with {false, struct_key_type} <- map_fetch_key(map_type, :__struct__),
222+
{:finite, [^module]} <- atom_fetch(struct_key_type) do
223+
context
224+
else
225+
_ ->
226+
error(__MODULE__, {:badupdate, map_type, struct, context}, meta, stack, context)
227+
end
229228

230-
case map_put_key(acc, key, type) do
231-
{:ok, acc} -> {acc, context}
232-
_ -> {acc, context}
233-
end
234-
end)
229+
Enum.reduce(pairs, {map_type, context}, fn {key, value}, {acc, context} ->
230+
context =
231+
if Enum.any?(info, &(&1.field == key)) do
232+
context
233+
else
234+
Of.unknown_struct_field(module, key, :expr, meta, stack, context)
235+
end
236+
237+
# TODO: Once we support typed structs, we need to type check them here
238+
{type, context} = of_expr(value, term(), expr, stack, context)
239+
240+
case map_put_key(acc, key, type) do
241+
{:ok, acc} -> {acc, context}
242+
_ -> {acc, context}
243+
end
244+
end)
245+
else
246+
{error_type(), context}
247+
end
235248
end
236249

237250
# %{...}
@@ -543,12 +556,11 @@ defmodule Module.Types.Expr do
543556
Enum.map_reduce(exceptions, original, fn exception, context ->
544557
# Exceptions are not validated in the compiler,
545558
# to avoid export dependencies. So we do it here.
546-
if Code.ensure_loaded?(exception) and function_exported?(exception, :__struct__, 0) do
547-
{info, context} = Of.struct_info(exception, meta, stack, context)
559+
{info, context} = Of.struct_info(exception, :expr, meta, stack, context)
560+
561+
if info do
548562
{Of.struct_type(exception, info, args), context}
549563
else
550-
# If the exception cannot be found or is invalid, fetch the signature to emit warnings.
551-
{_, context} = Apply.signature(exception, :__struct__, 0, meta, stack, context)
552564
{error_type(), context}
553565
end
554566
end)

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

Lines changed: 53 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -434,11 +434,17 @@ defmodule Module.Types.Of do
434434

435435
@doc """
436436
Handles instantiation of a new struct.
437+
438+
This is expanded and validated by the compiler, so don't need to check the fields.
437439
"""
438440
# TODO: Type check the fields match the struct
439441
def struct_instance(struct, args, expected, meta, stack, context, of_fun)
440442
when is_atom(struct) do
441-
{_info, context} = struct_info(struct, meta, stack, context)
443+
{info, context} = struct_info(struct, :expr, meta, stack, context)
444+
445+
if is_nil(info) do
446+
raise "expected #{inspect(struct)} to return struct metadata, but got none"
447+
end
442448

443449
# The compiler has already checked the keys are atoms and which ones are required.
444450
{args_types, context} =
@@ -459,23 +465,28 @@ defmodule Module.Types.Of do
459465
@doc """
460466
Returns `__info__(:struct)` information about a struct.
461467
"""
462-
def struct_info(struct, meta, stack, context) do
468+
def struct_info(struct, kind, meta, stack, context) do
463469
case stack.no_warn_undefined do
464470
%Macro.Env{} = env ->
465-
case :elixir_map.maybe_load_struct_info(meta, struct, [], false, env) do
471+
case :elixir_map.maybe_load_struct_info(meta, struct, env) do
466472
{:ok, info} -> {info, context}
467-
{:error, desc} -> raise ArgumentError, List.to_string(:elixir_map.format_error(desc))
473+
{:error, _desc} -> {nil, context}
468474
end
469475

470476
_ ->
471-
# Fetch the signature to validate for warnings.
472-
{_, context} = Module.Types.Apply.signature(struct, :__struct__, 0, meta, stack, context)
473-
474477
info =
475-
struct.__info__(:struct) ||
476-
raise "expected #{inspect(struct)} to return struct metadata, but got none"
478+
Code.ensure_loaded?(struct) and function_exported?(struct, :__info__, 1) and
479+
struct.__info__(:struct)
480+
481+
if info do
482+
{_, context} =
483+
Module.Types.Apply.signature(struct, :__struct__, 0, meta, stack, context)
477484

478-
{info, context}
485+
{info, context}
486+
else
487+
error = {:unknown_struct, kind, struct}
488+
{nil, error(error, meta, stack, context)}
489+
end
479490
end
480491
end
481492

@@ -492,6 +503,14 @@ defmodule Module.Types.Of do
492503
closed_map(pairs)
493504
end
494505

506+
@doc """
507+
Returns shared error for unknown struct field.
508+
"""
509+
def unknown_struct_field(struct, field, kind, meta, stack, context) do
510+
error = {:unknown_struct_field, kind, struct, field}
511+
error(error, meta, stack, context)
512+
end
513+
495514
## Bitstrings
496515

497516
@doc """
@@ -802,6 +821,30 @@ defmodule Module.Types.Of do
802821
}
803822
end
804823

824+
def format_diagnostic({:unknown_struct, kind, module}) do
825+
message =
826+
if Code.ensure_loaded?(module) do
827+
"struct #{inspect(module)} is undefined (there is such module but it does not define a struct)"
828+
else
829+
"struct #{inspect(module)} is undefined " <>
830+
"(module #{inspect(module)} is not available or is yet to be defined)"
831+
end
832+
833+
%{
834+
message: message,
835+
group: true,
836+
severity: if(kind == :pattern, do: :error, else: :warning)
837+
}
838+
end
839+
840+
def format_diagnostic({:unknown_struct_field, kind, module, field}) do
841+
%{
842+
message: "unknown key #{inspect(field)} for struct #{inspect(module)}",
843+
group: true,
844+
severity: if(kind == :pattern, do: :error, else: :warning)
845+
}
846+
end
847+
805848
defp dot_var?(expr) do
806849
match?({{:., _, [var, _fun]}, _, _args} when is_var(var), expr)
807850
end

0 commit comments

Comments
 (0)