Skip to content

Commit 7fbf48c

Browse files
committed
Type check Map.fetch!/2 and :maps.take/2
1 parent 77ef258 commit 7fbf48c

File tree

3 files changed

+186
-1
lines changed

3 files changed

+186
-1
lines changed

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

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,8 +245,11 @@ defmodule Module.Types.Apply do
245245

246246
## Map
247247
{:maps, :from_keys, [{[list(term()), term()], open_map()}]},
248+
{:maps, :get, [{[term(), open_map()], term()}]},
248249
{:maps, :is_key, [{[term(), open_map()], boolean()}]},
249250
{:maps, :keys, [{[open_map()], dynamic(list(term()))}]},
251+
{:maps, :take,
252+
[{[term(), open_map()], tuple([term(), open_map()]) |> union(atom([:error]))}]},
250253
{:maps, :to_list, [{[open_map()], dynamic(list(tuple([term(), term()])))}]},
251254
{:maps, :values, [{[open_map()], dynamic(list(term()))}]}
252255
] do
@@ -422,6 +425,43 @@ defmodule Module.Types.Apply do
422425
end
423426
end
424427

428+
defp remote_apply(:maps, :get, _info, [key, map] = args_types, stack) do
429+
case map_get(map, key) do
430+
{_, value} ->
431+
{:ok, return(value, args_types, stack)}
432+
433+
:badmap ->
434+
{:error, badremote(:maps, :get, 2)}
435+
436+
:error ->
437+
{:error, {:badkeydomain, map, key, []}}
438+
end
439+
end
440+
441+
defp remote_apply(:maps, :take, _info, [key, map] = args_types, stack) do
442+
case map_update(map, key, not_set()) do
443+
# We could suggest to use :maps.delete if the key always exists
444+
# but :maps.take/2 means calling Erlang directly, so we are fine.
445+
{value, descr, errors} ->
446+
result = tuple([value, descr])
447+
448+
result =
449+
if errors == [] and not gradual?(map) do
450+
result
451+
else
452+
union(result, atom([:error]))
453+
end
454+
455+
{:ok, return(result, args_types, stack)}
456+
457+
:badmap ->
458+
{:error, badremote(:maps, :take, 2)}
459+
460+
{:error, _errors} ->
461+
{:ok, return(atom([:error]), args_types, stack)}
462+
end
463+
end
464+
425465
defp remote_apply(:maps, :to_list, _info, [map], stack) do
426466
case map_to_list(map) do
427467
{:ok, list_type} -> {:ok, return(list_type, [map], stack)}
@@ -1033,6 +1073,58 @@ defmodule Module.Types.Apply do
10331073
}
10341074
end
10351075

1076+
def format_diagnostic({{:badkeydomain, map, key, errors}, _args_types, mfac, expr, context}) do
1077+
{mod, fun, arity, _converter} = mfac
1078+
traces = collect_traces(expr, context)
1079+
mfa_or_fa = if mod, do: Exception.format_mfa(mod, fun, arity), else: "#{fun}/#{arity}"
1080+
1081+
error_key =
1082+
Enum.reduce(errors, none(), fn
1083+
{:badkey, key}, acc -> union(atom([key]), acc)
1084+
{:baddomain, domain}, acc -> union(domain, acc)
1085+
end)
1086+
1087+
%{
1088+
details: %{typing_traces: traces},
1089+
message:
1090+
IO.iodata_to_binary([
1091+
"""
1092+
incompatible types given to #{mfa_or_fa}:
1093+
1094+
#{expr_to_string(expr) |> indent(4)}
1095+
1096+
""",
1097+
if errors == [] do
1098+
"""
1099+
the map:
1100+
1101+
#{to_quoted_string(map) |> indent(4)}
1102+
1103+
does not have the given keys:
1104+
1105+
#{to_quoted_string(key) |> indent(4)}
1106+
1107+
"""
1108+
else
1109+
"""
1110+
expected the map:
1111+
1112+
#{to_quoted_string(map) |> indent(4)}
1113+
1114+
to have the keys:
1115+
1116+
#{to_quoted_string(key) |> indent(4)}
1117+
1118+
but the following keys are missing:
1119+
1120+
#{to_quoted_string(error_key) |> indent(4)}
1121+
"""
1122+
end,
1123+
format_traces(traces)
1124+
])
1125+
}
1126+
end
1127+
10361128
def format_diagnostic({{:badremote, {_, domain, clauses}}, args_types, mfac, expr, context}) do
10371129
domain = domain(domain, clauses)
10381130
{mod, fun, arity, converter} = mfac

lib/elixir/lib/module/types/descr.ex

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2970,7 +2970,7 @@ defmodule Module.Types.Descr do
29702970
The gradual aspect of `key_descr` does not impact the return type.
29712971
29722972
It returns `{type, descr, errors}`, `:badmap`, `{:error, errors}`.
2973-
The list of `errors` may be empty, which implies the a bad domain.
2973+
The list of `errors` may be empty, which implies a bad domain.
29742974
The `return_type?` flag is used for optimizations purposes. If set to false,
29752975
the returned `type` should not be used, as it will be imprecise.
29762976
"""
@@ -3023,6 +3023,8 @@ defmodule Module.Types.Descr do
30233023
empty?(type)
30243024
end)
30253025

3026+
# We can exceptionally check for none() here because
3027+
# we already check for empty downstream
30263028
if dynamic_descr == none() do
30273029
{:error, static_errors ++ dynamic_errors}
30283030
else

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

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,97 @@ defmodule Module.Types.MapTest do
1111
import Module.Types.Descr
1212
defmacro domain_key(arg) when is_atom(arg), do: [arg]
1313

14+
describe ":maps.take/2" do
15+
test "checking" do
16+
assert typecheck!(:maps.take(:key, %{})) == atom([:error])
17+
18+
assert typecheck!(:maps.take(:key, %{key: 123})) |> equal?(tuple([integer(), empty_map()]))
19+
20+
assert typecheck!([x], :maps.take(:key, x))
21+
|> equal?(
22+
union(
23+
dynamic(tuple([term(), open_map(key: not_set())])),
24+
atom([:error])
25+
)
26+
)
27+
28+
assert typecheck!([condition?, x], :maps.take(if(condition?, do: :foo, else: :bar), x))
29+
|> equal?(
30+
union(
31+
dynamic(
32+
tuple([
33+
term(),
34+
union(
35+
open_map(foo: not_set()),
36+
open_map(bar: not_set())
37+
)
38+
])
39+
),
40+
atom([:error])
41+
)
42+
)
43+
44+
assert typecheck!([x], :maps.take(integer(), x))
45+
|> equal?(
46+
union(
47+
dynamic(tuple([term(), open_map()])),
48+
atom([:error])
49+
)
50+
)
51+
end
52+
53+
test "inference" do
54+
assert typecheck!(
55+
[x],
56+
(
57+
_ = :maps.take(:key, x)
58+
x
59+
)
60+
) == dynamic(open_map())
61+
end
62+
63+
test "errors" do
64+
assert typeerror!([x = []], :maps.take(:foo, x)) =~
65+
"incompatible types given to :maps.take/2"
66+
end
67+
end
68+
69+
describe "Map.fetch!/2" do
70+
test "errors" do
71+
assert typeerror!(Map.fetch!(%{}, :foo)) =~
72+
"""
73+
incompatible types given to Map.fetch!/2:
74+
75+
Map.fetch!(%{}, :foo)
76+
77+
the map:
78+
79+
empty_map()
80+
81+
does not have the given keys:
82+
83+
:foo
84+
85+
"""
86+
87+
assert typeerror!(Map.fetch!(%{}, 123)) =~
88+
"""
89+
incompatible types given to Map.fetch!/2:
90+
91+
Map.fetch!(%{}, 123)
92+
93+
the map:
94+
95+
empty_map()
96+
97+
does not have the given keys:
98+
99+
integer()
100+
101+
"""
102+
end
103+
end
104+
14105
describe "Map.to_list/1" do
15106
test "checking" do
16107
assert typecheck!([x = %{}], Map.to_list(x)) == dynamic(list(tuple([term(), term()])))

0 commit comments

Comments
 (0)