Skip to content

Commit a9aac52

Browse files
committed
Type check Map.put/3 and Map.delete/2
1 parent d831a00 commit a9aac52

File tree

2 files changed

+122
-16
lines changed

2 files changed

+122
-16
lines changed

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

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -244,12 +244,14 @@ defmodule Module.Types.Apply do
244244
{:erlang, :tuple_to_list, [{[open_tuple([])], dynamic(list(term()))}]},
245245

246246
## Map
247+
{:maps, :remove, [{[term(), open_map()], open_map()}]},
247248
{:maps, :from_keys, [{[list(term()), term()], open_map()}]},
248249
{:maps, :find,
249250
[{[term(), open_map()], tuple([atom([:ok]), term()]) |> union(atom([:error]))}]},
250251
{:maps, :get, [{[term(), open_map()], term()}]},
251252
{:maps, :is_key, [{[term(), open_map()], boolean()}]},
252253
{:maps, :keys, [{[open_map()], dynamic(list(term()))}]},
254+
{:maps, :put, [{[term(), term(), open_map()], open_map()}]},
253255
{:maps, :take,
254256
[{[term(), open_map()], tuple([term(), open_map()]) |> union(atom([:error]))}]},
255257
{:maps, :to_list, [{[open_map()], dynamic(list(tuple([term(), term()])))}]},
@@ -425,17 +427,11 @@ defmodule Module.Types.Apply do
425427
end
426428
end
427429

428-
defp remote_apply(:maps, :keys, _info, [map], stack) do
429-
case map_to_list(map, fn key, _value -> key end) do
430-
{:ok, list_type} -> {:ok, return(list_type, [map], stack)}
431-
:badmap -> {:error, badremote(:maps, :keys, 1)}
432-
end
433-
end
434-
435-
defp remote_apply(:maps, :values, _info, [map], stack) do
436-
case map_to_list(map, fn _key, value -> value end) do
437-
{:ok, list_type} -> {:ok, return(list_type, [map], stack)}
438-
:badmap -> {:error, badremote(:maps, :keys, 1)}
430+
defp remote_apply(:maps, :remove, _info, [key, map] = args_types, stack) do
431+
case map_update(map, key, not_set(), false, true) do
432+
{_value, descr, _errors} -> {:ok, return(descr, args_types, stack)}
433+
:badmap -> {:error, badremote(:maps, :remove, 2)}
434+
{:error, _errors} -> {:error, {:badkeydomain, map, key, nil}}
439435
end
440436
end
441437

@@ -461,10 +457,17 @@ defmodule Module.Types.Apply do
461457
end
462458
end
463459

464-
defp remote_apply(:maps, :update, _info, [key, value, map] = args_types, stack) do
465-
case map_update(map, key, value, false, false) do
460+
defp remote_apply(:maps, :keys, _info, [map], stack) do
461+
case map_to_list(map, fn key, _value -> key end) do
462+
{:ok, list_type} -> {:ok, return(list_type, [map], stack)}
463+
:badmap -> {:error, badremote(:maps, :keys, 1)}
464+
end
465+
end
466+
467+
defp remote_apply(:maps, :put, _info, [key, value, map] = args_types, stack) do
468+
case map_update(map, key, value, false, true) do
466469
{_value, descr, _errors} -> {:ok, return(descr, args_types, stack)}
467-
:badmap -> {:error, badremote(:maps, :update, 3)}
470+
:badmap -> {:error, badremote(:maps, :put, 3)}
468471
{:error, _errors} -> {:error, {:badkeydomain, map, key, nil}}
469472
end
470473
end
@@ -500,6 +503,21 @@ defmodule Module.Types.Apply do
500503
end
501504
end
502505

506+
defp remote_apply(:maps, :update, _info, [key, value, map] = args_types, stack) do
507+
case map_update(map, key, value, false, false) do
508+
{_value, descr, _errors} -> {:ok, return(descr, args_types, stack)}
509+
:badmap -> {:error, badremote(:maps, :update, 3)}
510+
{:error, _errors} -> {:error, {:badkeydomain, map, key, nil}}
511+
end
512+
end
513+
514+
defp remote_apply(:maps, :values, _info, [map], stack) do
515+
case map_to_list(map, fn _key, value -> value end) do
516+
{:ok, list_type} -> {:ok, return(list_type, [map], stack)}
517+
:badmap -> {:error, badremote(:maps, :keys, 1)}
518+
end
519+
end
520+
503521
defp remote_apply(_mod, _fun, info, args_types, stack) do
504522
remote_apply(info, args_types, stack)
505523
end

lib/elixir/test/elixir/module/types/map_test.exs

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,54 @@ defmodule Module.Types.MapTest do
8080
end
8181
end
8282

83+
describe "Map.delete/2" do
84+
test "checking" do
85+
assert typecheck!(Map.delete(%{}, :key)) ==
86+
closed_map(key: not_set())
87+
88+
assert typecheck!(Map.delete(%{key: 123}, :key)) ==
89+
closed_map(key: not_set())
90+
91+
assert typecheck!([x], Map.delete(x, :key)) ==
92+
dynamic(open_map(key: not_set()))
93+
94+
# If one of them succeeds, we are still fine!
95+
assert typecheck!(
96+
[condition?],
97+
Map.delete(%{foo: 123}, if(condition?, do: :foo, else: :bar))
98+
) ==
99+
union(
100+
closed_map(foo: not_set()),
101+
closed_map(foo: integer(), bar: not_set())
102+
)
103+
104+
assert typecheck!([x], Map.delete(x, 123)) == dynamic(open_map())
105+
end
106+
107+
test "inference" do
108+
assert typecheck!(
109+
[x],
110+
(
111+
_ = Map.delete(x, :key)
112+
x
113+
)
114+
) == dynamic(open_map())
115+
end
116+
117+
test "errors" do
118+
assert typeerror!([x = []], Map.delete(x, :key)) =~
119+
"incompatible types given to Map.delete/2"
120+
end
121+
122+
test "combined with put" do
123+
assert typecheck!([x], x |> Map.delete(:key) |> Map.put(:key, "123")) ==
124+
dynamic(open_map(key: binary()))
125+
126+
assert typecheck!([x, y], x |> Map.delete(:key) |> Map.put(String.to_atom(y), "123")) ==
127+
dynamic(open_map(key: if_set(binary())))
128+
end
129+
end
130+
83131
describe "Map.fetch/2" do
84132
test "checking" do
85133
assert typecheck!(Map.fetch(%{key: 123}, :key)) ==
@@ -190,6 +238,46 @@ defmodule Module.Types.MapTest do
190238
end
191239
end
192240

241+
describe "Map.put/3" do
242+
test "checking" do
243+
assert typecheck!(Map.put(%{}, :key, :value)) ==
244+
closed_map(key: atom([:value]))
245+
246+
assert typecheck!(Map.put(%{key: 123}, :key, :value)) ==
247+
closed_map(key: atom([:value]))
248+
249+
assert typecheck!([x], Map.put(x, :key, :value)) ==
250+
dynamic(open_map(key: atom([:value])))
251+
252+
# If one of them succeeds, we are still fine!
253+
assert typecheck!(
254+
[condition?],
255+
Map.put(%{foo: 123}, if(condition?, do: :foo, else: :bar), "123")
256+
) ==
257+
union(
258+
closed_map(foo: binary()),
259+
closed_map(foo: integer(), bar: binary())
260+
)
261+
262+
assert typecheck!([x], Map.put(x, 123, 456)) == dynamic(open_map())
263+
end
264+
265+
test "inference" do
266+
assert typecheck!(
267+
[x],
268+
(
269+
_ = Map.put(x, :key, :value)
270+
x
271+
)
272+
) == dynamic(open_map())
273+
end
274+
275+
test "errors" do
276+
assert typeerror!([x = []], Map.put(x, :key, :value)) =~
277+
"incompatible types given to Map.put/3"
278+
end
279+
end
280+
193281
describe "Map.replace!/3" do
194282
test "checking" do
195283
assert typecheck!(Map.replace!(%{key: 123}, :key, :value)) ==
@@ -201,8 +289,8 @@ defmodule Module.Types.MapTest do
201289
# If one of them succeeds, we are still fine!
202290
assert typecheck!(
203291
[condition?],
204-
Map.replace!(%{foo: 123}, if(condition?, do: :foo, else: :bar), :value)
205-
) == closed_map(foo: atom([:value]))
292+
Map.replace!(%{foo: 123}, if(condition?, do: :foo, else: :bar), "123")
293+
) == closed_map(foo: binary())
206294

207295
assert typecheck!([x], Map.replace!(x, 123, 456)) == dynamic(open_map())
208296
end

0 commit comments

Comments
 (0)