Skip to content

Commit 549a867

Browse files
committed
Warn on underived derives, closes #11799
1 parent bd49ad6 commit 549a867

File tree

5 files changed

+54
-11
lines changed

5 files changed

+54
-11
lines changed

lib/elixir/lib/kernel.ex

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5178,9 +5178,7 @@ defmodule Kernel do
51785178
"""
51795179
defmacro defstruct(fields) do
51805180
quote bind_quoted: [fields: fields, bootstrapped?: bootstrapped?(Enum)] do
5181-
{struct, keys, derive, body} = Kernel.Utils.defstruct(__MODULE__, fields, bootstrapped?)
5182-
@__struct__ struct
5183-
@enforce_keys keys
5181+
{struct, derive, body} = Kernel.Utils.defstruct(__MODULE__, fields, bootstrapped?)
51845182

51855183
case derive do
51865184
[] -> :ok

lib/elixir/lib/kernel/utils.ex

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -164,9 +164,9 @@ defmodule Kernel.Utils do
164164
_ ->
165165
quote do
166166
{map, keys} =
167-
Enum.reduce(var!(kv), {@__struct__, @enforce_keys}, fn {key, val},
168-
{map, keys} ->
169-
{Map.replace!(map, key, val), List.delete(keys, key)}
167+
Enum.reduce(var!(kv), {@__struct__, unquote(enforce_keys)}, fn
168+
{key, val}, {map, keys} ->
169+
{Map.replace!(map, key, val), List.delete(keys, key)}
170170
end)
171171

172172
case keys do
@@ -183,8 +183,6 @@ defmodule Kernel.Utils do
183183

184184
false ->
185185
quote do
186-
_ = @enforce_keys
187-
188186
:lists.foldl(
189187
fn {key, val}, acc -> Map.replace!(acc, key, val) end,
190188
@__struct__,
@@ -195,7 +193,10 @@ defmodule Kernel.Utils do
195193

196194
case enforce_keys -- :maps.keys(struct) do
197195
[] ->
198-
{struct, enforce_keys, Module.get_attribute(module, :derive), body}
196+
derive = Module.get_attribute(module, :derive)
197+
Module.put_attribute(module, :__struct__, struct)
198+
Module.put_attribute(module, :__derived__, derive)
199+
{struct, Module.get_attribute(module, :derive), body}
199200

200201
error_keys ->
201202
raise ArgumentError,

lib/elixir/lib/module.ex

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1787,7 +1787,8 @@ defmodule Module do
17871787
defp args_count([], total, defaults), do: {total, defaults}
17881788

17891789
@doc false
1790-
def check_behaviours_and_impls(env, _set, bag, all_definitions) do
1790+
def check_derive_behaviours_and_impls(env, set, bag, all_definitions) do
1791+
check_derive(env, set, bag)
17911792
behaviours = bag_lookup_element(bag, {:accumulate, :behaviour}, 2)
17921793
impls = bag_lookup_element(bag, :impls, 2)
17931794
callbacks = check_behaviours(env, behaviours)
@@ -1805,6 +1806,30 @@ defmodule Module do
18051806
:ok
18061807
end
18071808

1809+
defp check_derive(env, set, bag) do
1810+
case bag_lookup_element(bag, {:accumulate, :derive}, 2) do
1811+
[] ->
1812+
:ok
1813+
1814+
to_derive ->
1815+
case :ets.lookup(set, :__derived__) do
1816+
[{_, derived, _}] ->
1817+
if to_derive != :lists.reverse(derived) do
1818+
message =
1819+
"warning: module attribute @derive was set after defstruct, all @derive calls must come before defstruct"
1820+
1821+
IO.warn(message, Macro.Env.stacktrace(env))
1822+
end
1823+
1824+
[] ->
1825+
message =
1826+
"warning: module attribute @derive was set but never used (it must come before defstruct)"
1827+
1828+
IO.warn(message, Macro.Env.stacktrace(env))
1829+
end
1830+
end
1831+
end
1832+
18081833
defp check_behaviours(env, behaviours) do
18091834
Enum.reduce(behaviours, %{}, fn behaviour, acc ->
18101835
cond do

lib/elixir/src/elixir_module.erl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ compile(Line, Module, Block, Vars, E) ->
127127
make_readonly(Module),
128128

129129
(not elixir_config:is_bootstrap()) andalso
130-
'Elixir.Module':check_behaviours_and_impls(E, DataSet, DataBag, AllDefinitions),
130+
'Elixir.Module':check_derive_behaviours_and_impls(E, DataSet, DataBag, AllDefinitions),
131131

132132
RawCompileOpts = bag_lookup_element(DataBag, {accumulate, compile}, 2),
133133
CompileOpts = validate_compile_opts(RawCompileOpts, AllDefinitions, Unreachable, File, Line),

lib/elixir/test/elixir/protocol_test.exs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,25 @@ defmodule ProtocolTest do
332332
assert message =~
333333
"cannot define @optional_callbacks inside protocol, all of the protocol definitions are required"
334334
end
335+
336+
test "when deriving after struct" do
337+
assert capture_io(:stderr, fn ->
338+
defmodule DeriveTooLate do
339+
defstruct []
340+
@derive [{Derivable, :ok}]
341+
end
342+
end) =~
343+
"module attribute @derive was set after defstruct, all @derive calls must come before defstruct"
344+
end
345+
346+
test "when deriving with no struct" do
347+
assert capture_io(:stderr, fn ->
348+
defmodule DeriveNeverUsed do
349+
@derive [{Derivable, :ok}]
350+
end
351+
end) =~
352+
"module attribute @derive was set but never used (it must come before defstruct)"
353+
end
335354
end
336355

337356
describe "errors" do

0 commit comments

Comments
 (0)