Skip to content

Commit 27fefd8

Browse files
eksperimentalwhatyouhide
authored andcommitted
Raise when given non-keyword-lists in Keyword.merge/3 (#5479)
This commit makes sure a valid keyword is given as both arguments. Additionally, it adds tests to make sure Keyword.merge/2,3 work exactly the same way if the function passed to merge/3 returns the value from the second keyword list. Similar to Similar to #5466.
1 parent 806804e commit 27fefd8

File tree

2 files changed

+94
-9
lines changed

2 files changed

+94
-9
lines changed

lib/elixir/lib/keyword.ex

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -618,26 +618,40 @@ defmodule Keyword do
618618
...> end)
619619
[b: 2, a: 4, d: 4, a: 8]
620620
621+
iex> Keyword.merge([a: 1, b: 2], [:a, :b], fn :a, v1, v2 ->
622+
...> v1 + v2
623+
...> end)
624+
** (ArgumentError) expected a keyword list as the second argument, got: [:a, :b]
625+
621626
"""
622627
@spec merge(t, t, (key, value, value -> value)) :: t
623-
def merge(keywords1, keywords2, fun) when is_list(keywords1) and is_list(keywords2) do
624-
do_merge(keywords2, [], keywords1, keywords1, fun)
628+
def merge(keywords1, keywords2, fun) when is_list(keywords1) and is_list(keywords2) and is_function(fun, 3) do
629+
if keyword?(keywords1) do
630+
do_merge(keywords2, [], keywords1, keywords1, fun, keywords2)
631+
else
632+
raise ArgumentError, message: "expected a keyword list as the first argument, got: #{inspect keywords1}"
633+
end
625634
end
626635

627-
defp do_merge([{k, v2} | t], acc, rest, original, fun) do
628-
case :lists.keyfind(k, 1, original) do
629-
{^k, v1} ->
630-
do_merge(t, [{k, fun.(k, v1, v2)} | acc],
631-
delete(rest, k), :lists.keydelete(k, 1, original), fun)
636+
defp do_merge([{key, value2} | tail], acc, rest, original, fun, keywords2) when is_atom(key) do
637+
case :lists.keyfind(key, 1, original) do
638+
{^key, value1} ->
639+
do_merge(tail, [{key, fun.(key, value1, value2)} | acc],
640+
delete(rest, key), :lists.keydelete(key, 1, original), fun, keywords2)
641+
632642
false ->
633-
do_merge(t, [{k, v2} | acc], rest, original, fun)
643+
do_merge(tail, [{key, value2} | acc], rest, original, fun, keywords2)
634644
end
635645
end
636646

637-
defp do_merge([], acc, rest, _original, _fun) do
647+
defp do_merge([], acc, rest, _original, _fun, _keywords2) do
638648
rest ++ :lists.reverse(acc)
639649
end
640650

651+
defp do_merge(_other, _acc, _rest, _original, _fun, keywords2) do
652+
raise ArgumentError, message: "expected a keyword list as the second argument, got: #{inspect keywords2}"
653+
end
654+
641655
@doc """
642656
Returns whether a given `key` exists in the given `keywords`.
643657

lib/elixir/test/elixir/keyword_test.exs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,75 @@ defmodule KeywordTest do
7474
# any key in keywords1 is removed if key is present in keyword2
7575
assert Keyword.merge([a: 1, b: 2, c: 3, c: 4], [c: 11, c: 12, d: 13]) == [a: 1, b: 2, c: 11, c: 12, d: 13]
7676
end
77+
78+
test "merge/3" do
79+
fun = fn _key, value1, value2 -> value1 + value2 end
80+
81+
assert Keyword.merge([a: 1, b: 2], [c: 11, d: 12], fun) == [a: 1, b: 2, c: 11, d: 12]
82+
assert Keyword.merge([], [c: 11, d: 12], fun) == [c: 11, d: 12]
83+
assert Keyword.merge([a: 1, b: 2], [], fun) == [a: 1, b: 2]
84+
85+
assert_raise ArgumentError, "expected a keyword list as the first argument, got: [1, 2]", fn ->
86+
Keyword.merge([1, 2], [c: 11, d: 12], fun)
87+
end
88+
89+
assert_raise ArgumentError, "expected a keyword list as the first argument, got: [1 | 2]", fn ->
90+
Keyword.merge([1 | 2], [c: 11, d: 12], fun)
91+
end
92+
93+
assert_raise ArgumentError, "expected a keyword list as the second argument, got: [{:x, 1}, :y, :z]", fn ->
94+
Keyword.merge([a: 1, b: 2], [{:x, 1}, :y, :z], fun)
95+
end
96+
97+
assert_raise ArgumentError, "expected a keyword list as the second argument, got: [:x | :y]", fn ->
98+
Keyword.merge([a: 1, b: 2], [:x | :y], fun)
99+
end
100+
101+
assert_raise ArgumentError, "expected a keyword list as the second argument, got: [{:x, 1} | :y]", fn ->
102+
Keyword.merge([a: 1, b: 2], [{:x, 1} | :y], fun)
103+
end
104+
105+
# duplicate keys in keywords1 are left untouched if key is not present in keywords2
106+
assert Keyword.merge([a: 1, b: 2, a: 3], [c: 11, d: 12], fun) == [a: 1, b: 2, a: 3, c: 11, d: 12]
107+
assert Keyword.merge([a: 1, b: 2, a: 3], [a: 11], fun) == [b: 2, a: 12]
108+
109+
# duplicate keys in keywords2 are always kept
110+
assert Keyword.merge([a: 1, b: 2], [c: 11, c: 12, d: 13], fun) == [a: 1, b: 2, c: 11, c: 12, d: 13]
111+
112+
# every key in keywords1 is replaced with fun result if key is present in keyword2
113+
assert Keyword.merge([a: 1, b: 2, c: 3, c: 4], [c: 11, c: 50, d: 13], fun) == [a: 1, b: 2, c: 14, c: 54, d: 13]
114+
end
115+
116+
test "merge/2 and merge/3 behave exactly the same way" do
117+
fun = fn _key, _value1, value2 -> value2 end
118+
119+
args = [
120+
{[a: 1, b: 2], [c: 11, d: 12]},
121+
{[], [c: 11, d: 12]},
122+
{[a: 1, b: 2], []},
123+
{[a: 1, b: 2, a: 3], [c: 11, d: 12]},
124+
{[a: 1, b: 2, a: 3], [a: 11]},
125+
{[a: 1, b: 2], [c: 11, c: 12, d: 13]},
126+
{[a: 1, b: 2, c: 3, c: 4], [c: 11, c: 12, d: 13]},
127+
]
128+
129+
args_error = [
130+
{[1, 2], [c: 11, d: 12]},
131+
{[1 | 2], [c: 11, d: 12]},
132+
{[a: 1, b: 2], [11, 12, 0]},
133+
{[a: 1, b: 2], [11 | 12]},
134+
{[a: 1, b: 2], [{:x, 1}, :y, :z]},
135+
{[a: 1, b: 2], [:x | :y]},
136+
{[a: 1, b: 2], [{:x, 1} | :y]},
137+
]
138+
139+
for {arg1, arg2} <- args do
140+
assert Keyword.merge(arg1, arg2) == Keyword.merge(arg1, arg2, fun)
141+
end
142+
143+
for {arg1, arg2} <- args_error do
144+
error = assert_raise ArgumentError, fn -> Keyword.merge(arg1, arg2) end
145+
assert_raise ArgumentError, error.message, fn -> Keyword.merge(arg1, arg2, fun) end
146+
end
147+
end
77148
end

0 commit comments

Comments
 (0)