From b6923820928460e90b10ed7eda055751f148a56c Mon Sep 17 00:00:00 2001 From: Alex Gubarev Date: Tue, 1 Sep 2026 14:57:50 +0300 Subject: [PATCH 1/3] Improve defstruct @enfore_keys --- lib/elixir/lib/kernel.ex | 3 + lib/elixir/lib/kernel/utils.ex | 19 ++----- lib/elixir/test/elixir/kernel/errors_test.exs | 2 +- lib/elixir/test/elixir/map_test.exs | 55 +++++++++++++++---- 4 files changed, 53 insertions(+), 26 deletions(-) diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex index e8f58373b4e..fb8c441d4cd 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 139541c581e..af665161e13 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 6f2c1efaffc..e469347ef73 100644 --- a/lib/elixir/test/elixir/kernel/errors_test.exs +++ b/lib/elixir/test/elixir/kernel/errors_test.exs @@ -423,7 +423,7 @@ defmodule Kernel.ErrorsTest do end assert_raise ArgumentError, - "@enforce_keys required keys ([:fo, :bar]) that are not defined in defstruct: [foo: nil]", + "unknown or duplicate keys given to @enforce_keys, got: [:fo, :bar]", fn -> defmodule EnforceKeysError do @enforce_keys [:foo, :fo, :bar] diff --git a/lib/elixir/test/elixir/map_test.exs b/lib/elixir/test/elixir/map_test.exs index 2db2e297ead..df4f3ac7f5c 100644 --- a/lib/elixir/test/elixir/map_test.exs +++ b/lib/elixir/test/elixir/map_test.exs @@ -422,17 +422,50 @@ 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 -> + Code.eval_string(""" + 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 -> + Code.eval_string(""" + 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 -> + Code.eval_string(""" + defmodule TestMod do + @enforce_keys [:unknown] + defstruct [:foo] + end + """) + end + + assert_raise ArgumentError, + "unknown or duplicate keys given to @enforce_keys, got: [:__struct__]", + fn -> + Code.eval_string(""" + defmodule TestMod do + @enforce_keys [:__struct__] + defstruct [:foo] + end + """) + end end test "struct always expands context module" do From 4961f1d0d90c8f84d9d077be954c781e18efdd28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 2 Sep 2026 21:20:07 +0200 Subject: [PATCH 2/3] Update errors_test.exs --- lib/elixir/test/elixir/kernel/errors_test.exs | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/lib/elixir/test/elixir/kernel/errors_test.exs b/lib/elixir/test/elixir/kernel/errors_test.exs index e469347ef73..957cbe31234 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, - "unknown or duplicate keys given to @enforce_keys, got: [:fo, :bar]", - fn -> - defmodule EnforceKeysError do - @enforce_keys [:foo, :fo, :bar] - defstruct(foo: nil) - end - end - end end test "invalid unquote" do From 36a727b5dacd892cc8d94ab4caceaa4505765dc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 2 Sep 2026 21:21:09 +0200 Subject: [PATCH 3/3] Apply suggestion from @josevalim --- lib/elixir/test/elixir/map_test.exs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/lib/elixir/test/elixir/map_test.exs b/lib/elixir/test/elixir/map_test.exs index df4f3ac7f5c..14d00791b69 100644 --- a/lib/elixir/test/elixir/map_test.exs +++ b/lib/elixir/test/elixir/map_test.exs @@ -426,45 +426,37 @@ defmodule MapTest do assert_raise ArgumentError, "unknown or duplicate keys given to @enforce_keys, got: [\"foo\"]", fn -> - Code.eval_string(""" 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 -> - Code.eval_string(""" 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 -> - Code.eval_string(""" defmodule TestMod do @enforce_keys [:unknown] defstruct [:foo] end - """) end assert_raise ArgumentError, "unknown or duplicate keys given to @enforce_keys, got: [:__struct__]", fn -> - Code.eval_string(""" defmodule TestMod do @enforce_keys [:__struct__] defstruct [:foo] end - """) end end