diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex index e8f58373b4..fb8c441d4c 100644 --- a/lib/elixir/lib/kernel.ex +++ b/lib/elixir/lib/kernel.ex @@ -5589,6 +5589,9 @@ defmodule Kernel do defstruct name: nil, age: 10 + 11 end + `@enforce_keys` must be set to an atom or a list of unique atoms, + all of which must name fields defined by `defstruct/1` + Now trying to build a struct without the name key will fail: %User{age: 21} diff --git a/lib/elixir/lib/kernel/utils.ex b/lib/elixir/lib/kernel/utils.ex index 139541c581..af665161e1 100644 --- a/lib/elixir/lib/kernel/utils.ex +++ b/lib/elixir/lib/kernel/utils.ex @@ -162,16 +162,8 @@ defmodule Kernel.Utils do # TODO: Make it raise on v2.0 warn_on_duplicate_struct_key(:lists.keysort(1, fields), env) - foreach = fn - key when is_atom(key) -> - :ok - - key -> - raise ArgumentError, "keys given to @enforce_keys must be atoms, got: #{inspect(key)}" - end - - :lists.foreach(foreach, enforce_keys) - struct = :maps.from_list([__struct__: module] ++ fields) + field_map = :maps.from_list(fields) + struct = :maps.put(:__struct__, module, field_map) escaped_struct = :elixir_quote.escape(struct, {:struct, module}, false) body = @@ -215,7 +207,7 @@ defmodule Kernel.Utils do end end - case enforce_keys -- :maps.keys(struct) do + case enforce_keys -- :maps.keys(field_map) do [] -> mapper = fn {key, val} -> %{field: key, default: val, required: :lists.member(key, enforce_keys)} @@ -225,10 +217,9 @@ defmodule Kernel.Utils do derive = :lists.map(fn {_, value} -> value end, :ets.take(bag, {:accumulate, :derive})) {struct, :lists.reverse(derive), escaped_struct, quote(do: kv), body} - error_keys -> + invalid_keys -> raise ArgumentError, - "@enforce_keys required keys (#{inspect(error_keys)}) that are not defined in defstruct: " <> - "#{inspect(fields)}" + "unknown or duplicate keys given to @enforce_keys, got: #{inspect(invalid_keys)}" end end diff --git a/lib/elixir/test/elixir/kernel/errors_test.exs b/lib/elixir/test/elixir/kernel/errors_test.exs index 6f2c1efaff..957cbe3123 100644 --- a/lib/elixir/test/elixir/kernel/errors_test.exs +++ b/lib/elixir/test/elixir/kernel/errors_test.exs @@ -401,7 +401,7 @@ defmodule Kernel.ErrorsTest do end end - test "good struct" do + test "valid struct" do defmodule GoodStruct do defstruct name: "john" end @@ -415,22 +415,6 @@ defmodule Kernel.ErrorsTest do ~c"%#{GoodStruct}{age: 27} = %{}" ) end - - test "enforce @enforce_keys" do - defmodule EnforceKeys do - @enforce_keys [:foo] - defstruct(foo: nil) - end - - assert_raise ArgumentError, - "@enforce_keys required keys ([:fo, :bar]) that are not defined in defstruct: [foo: nil]", - fn -> - defmodule EnforceKeysError do - @enforce_keys [:foo, :fo, :bar] - defstruct(foo: nil) - end - end - end end test "invalid unquote" do diff --git a/lib/elixir/test/elixir/map_test.exs b/lib/elixir/test/elixir/map_test.exs index 2db2e297ea..14d00791b6 100644 --- a/lib/elixir/test/elixir/map_test.exs +++ b/lib/elixir/test/elixir/map_test.exs @@ -422,17 +422,42 @@ defmodule MapTest do end end - test "defstruct raises on invalid enforce_keys" do - message = "keys given to @enforce_keys must be atoms, got: \"foo\"" - - assert_raise ArgumentError, message, fn -> - Code.eval_string(""" - defmodule TestMod do - @enforce_keys "foo" - defstruct [:foo] - end - """) - end + test "defstruct raises on unknown or duplicate enforce_keys" do + assert_raise ArgumentError, + "unknown or duplicate keys given to @enforce_keys, got: [\"foo\"]", + fn -> + defmodule TestMod do + @enforce_keys "foo" + defstruct [:foo] + end + end + + assert_raise ArgumentError, + "unknown or duplicate keys given to @enforce_keys, got: [:foo, :bar]", + fn -> + defmodule TestMod do + @enforce_keys [:foo, :bar, :foo, :bar] + defstruct [:foo, :bar] + end + end + + assert_raise ArgumentError, + "unknown or duplicate keys given to @enforce_keys, got: [:unknown]", + fn -> + defmodule TestMod do + @enforce_keys [:unknown] + defstruct [:foo] + end + end + + assert_raise ArgumentError, + "unknown or duplicate keys given to @enforce_keys, got: [:__struct__]", + fn -> + defmodule TestMod do + @enforce_keys [:__struct__] + defstruct [:foo] + end + end end test "struct always expands context module" do