Skip to content

Commit a6ba0cb

Browse files
committed
Type check Map.replace!/3
1 parent 18c81d6 commit a6ba0cb

File tree

3 files changed

+68
-4
lines changed

3 files changed

+68
-4
lines changed

lib/elixir/lib/map.ex

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -372,9 +372,7 @@ defmodule Map do
372372
end
373373
"""
374374
@spec fetch!(map, key) :: value
375-
def fetch!(map, key) do
376-
:maps.get(key, map)
377-
end
375+
def fetch!(map, key), do: :maps.get(key, map)
378376

379377
@doc """
380378
Puts the given `value` under `key` unless the entry `key`
@@ -432,7 +430,11 @@ defmodule Map do
432430
@doc """
433431
Puts a value under `key` only if the `key` already exists in `map`.
434432
435-
If `key` is not present in `map`, a `KeyError` exception is raised.
433+
The exclamation mark (`!`) implies this function can raise a `KeyError`
434+
exception at runtime if `map` doesn't contain `key`. If the type system
435+
can verify this function will always raise (which means the key is never
436+
available), then it will emit a warning at compile-time. See the "Type
437+
checking" section in `Map.fetch!/2` for more information.
436438
437439
Inlined by the compiler.
438440

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,7 @@ defmodule Module.Types.Apply do
253253
{:maps, :take,
254254
[{[term(), open_map()], tuple([term(), open_map()]) |> union(atom([:error]))}]},
255255
{:maps, :to_list, [{[open_map()], dynamic(list(tuple([term(), term()])))}]},
256+
{:maps, :update, [{[term(), term(), open_map()], open_map()}]},
256257
{:maps, :values, [{[open_map()], dynamic(list(term()))}]}
257258
] do
258259
[arity] = Enum.map(clauses, fn {args, _return} -> length(args) end) |> Enum.uniq()
@@ -342,6 +343,12 @@ defmodule Module.Types.Apply do
342343
{{:strong, nil, [{domain, term()}]}, domain, context}
343344
end
344345

346+
def remote_domain(:maps, :update, [key, _, _], _expected, _meta, _stack, context)
347+
when is_atom(key) do
348+
domain = [term(), term(), open_map([{key, term()}])]
349+
{{:strong, nil, [{domain, open_map()}]}, domain, context}
350+
end
351+
345352
def remote_domain(mod, fun, args, expected, meta, stack, context) do
346353
arity = length(args)
347354

@@ -459,6 +466,14 @@ defmodule Module.Types.Apply do
459466
end
460467
end
461468

469+
defp remote_apply(:maps, :update, _info, [key, value, map] = args_types, stack) do
470+
case map_update(map, key, value, false) do
471+
{_value, descr, _errors} -> {:ok, return(descr, args_types, stack)}
472+
:badmap -> {:error, badremote(:maps, :update, 3)}
473+
{:error, _errors} -> {:error, {:badkeydomain, map, key, nil}}
474+
end
475+
end
476+
462477
defp remote_apply(:maps, :take, _info, [key, map] = args_types, stack) do
463478
case map_update(map, key, not_set()) do
464479
# We could suggest to use :maps.delete if the key always exists

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

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,53 @@ defmodule Module.Types.MapTest do
190190
end
191191
end
192192

193+
describe "Map.replace!/3" do
194+
test "checking" do
195+
assert typecheck!(Map.replace!(%{key: 123}, :key, :value)) ==
196+
closed_map(key: atom([:value]))
197+
198+
assert typecheck!([x], Map.replace!(x, :key, :value)) ==
199+
dynamic(open_map(key: atom([:value])))
200+
201+
# If one of them succeeds, we are still fine!
202+
assert typecheck!(
203+
[condition?],
204+
Map.replace!(%{foo: 123}, if(condition?, do: :foo, else: :bar), :value)
205+
) == closed_map(foo: atom([:value]))
206+
207+
assert typecheck!([x], Map.replace!(x, 123, 456)) == dynamic(open_map())
208+
end
209+
210+
test "inference" do
211+
assert typecheck!(
212+
[x],
213+
(
214+
_ = Map.replace!(x, :key, :value)
215+
x
216+
)
217+
) == dynamic(open_map(key: term()))
218+
end
219+
220+
test "errors" do
221+
assert typeerror!(Map.replace!(%{}, :key, :value)) =~
222+
"""
223+
incompatible types given to Map.replace!/3:
224+
225+
Map.replace!(%{}, :key, :value)
226+
227+
the map:
228+
229+
empty_map()
230+
231+
does not have all required keys:
232+
233+
:key
234+
235+
therefore this function will always raise
236+
"""
237+
end
238+
end
239+
193240
describe "Map.to_list/1" do
194241
test "checking" do
195242
assert typecheck!([x = %{}], Map.to_list(x)) == dynamic(list(tuple([term(), term()])))

0 commit comments

Comments
 (0)