Skip to content

Commit 72dc7ed

Browse files
committed
All green
1 parent b57be87 commit 72dc7ed

File tree

3 files changed

+137
-48
lines changed

3 files changed

+137
-48
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -964,7 +964,7 @@ defmodule Module.Types.Descr do
964964
case static_or_dynamic do
965965
:term -> {:infinite, []}
966966
%{atom: {:union, set}} -> {:finite, :sets.to_list(set)}
967-
%{atom: {:negation, _}} -> {:infinite, []}
967+
%{atom: {:negation, set}} -> {:infinite, :sets.to_list(set)}
968968
%{} -> :error
969969
end
970970
else
@@ -3641,7 +3641,7 @@ defmodule Module.Types.Descr do
36413641
case tag do
36423642
:closed ->
36433643
with %{__struct__: struct_descr} <- fields,
3644-
{_, [struct]} <- atom_fetch(struct_descr),
3644+
{:finite, [struct]} <- atom_fetch(struct_descr),
36453645
info when is_list(info) <- maybe_struct(struct),
36463646
true <- map_size(fields) == length(info) + 1,
36473647
true <- Enum.all?(info, &is_map_key(fields, &1.field)) do

lib/elixir/lib/module/types/of.ex

Lines changed: 88 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -191,20 +191,55 @@ defmodule Module.Types.Of do
191191

192192
{dynamic?, domain, single, multiple} =
193193
Enum.reduce(pairs_types, {false, [], [], []}, fn
194-
{key_tagged_type, dynamic_pair?, value_type}, {dynamic?, domain, single, multiple} ->
194+
{pos_neg_domain, dynamic_pair?, value_type}, {dynamic?, domain, single, multiple} ->
195195
dynamic? = dynamic? or dynamic_pair?
196196

197-
case key_tagged_type do
198-
# Because a multiple key may override single keys, we can only
199-
# collect single keys while there are no multiples.
200-
{:keys, [key]} when multiple == [] ->
201-
{dynamic?, domain, [{key, value_type} | single], multiple}
202-
203-
{:keys, keys} ->
204-
{dynamic?, domain, single, [{keys, value_type} | multiple]}
205-
206-
{:domain, keys} ->
207-
{dynamic?, [{keys, value_type} | domain], single, multiple}
197+
case pos_neg_domain do
198+
# If atom is included in domain keys, it unions all previous
199+
# single and multiple, except the ones negated:
200+
#
201+
# %{foo: :bar, term() => :baz}
202+
# #=> %{foo: :bar or :baz, term() => :baz}
203+
#
204+
# %{foo: :bar, not :foo => :baz}
205+
# #=> %{foo: :bar, term() => :baz}
206+
#
207+
# In case the negated term does not appear, we set it to none():
208+
#
209+
# %{foo: :bar, term() => :baz}
210+
# #=> %{term() => :baz, foo: :bar or :baz}
211+
#
212+
# %{not :foo => :baz}
213+
# #=> %{term() => :baz, foo: none()}
214+
#
215+
# In case we are dealing with multiple keys, we always merge the
216+
# domain. A more precise approach would be to postpone doing so
217+
# until the cartesian map is distributed but those should be very
218+
# uncommon.
219+
{[], negs, domain_keys} ->
220+
if :atom in domain_keys do
221+
{single, multiple} = union_negated(negs, value_type, single, multiple)
222+
{dynamic?, [{domain_keys, value_type} | domain], single, multiple}
223+
else
224+
{dynamic?, [{domain_keys, value_type} | domain], single, multiple}
225+
end
226+
227+
{pos, [], domain_keys} ->
228+
domain =
229+
case domain_keys do
230+
[] -> domain
231+
_ -> [{domain_keys, value_type} | domain]
232+
end
233+
234+
case pos do
235+
# Because a multiple key may override single keys, we can only
236+
# collect single keys while there are no multiples.
237+
[key] when multiple == [] ->
238+
{dynamic?, domain, [{key, value_type} | single], multiple}
239+
240+
_ ->
241+
{dynamic?, domain, single, [{pos, value_type} | multiple]}
242+
end
208243
end
209244
end)
210245

@@ -225,37 +260,63 @@ defmodule Module.Types.Of do
225260
{if(dynamic?, do: dynamic(map), else: map), context}
226261
end
227262

263+
defp union_negated([], new_type, single, multiple) do
264+
single = Enum.map(single, fn {key, old_type} -> {key, union(old_type, new_type)} end)
265+
multiple = Enum.map(multiple, fn {keys, old_type} -> {keys, union(old_type, new_type)} end)
266+
{single, multiple}
267+
end
268+
269+
defp union_negated(negated, new_type, single, multiple) do
270+
{single, matched} =
271+
Enum.map_reduce(single, [], fn {key, old_type}, matched ->
272+
if key in negated do
273+
{{key, old_type}, [key | matched]}
274+
else
275+
{{key, union(old_type, new_type)}, matched}
276+
end
277+
end)
278+
279+
multiple =
280+
Enum.map(multiple, fn {keys, old_type} ->
281+
{keys, union(old_type, new_type)}
282+
end)
283+
284+
{Enum.map(negated -- matched, fn key -> {key, none()} end) ++ single, multiple}
285+
end
286+
228287
defp pairs(pairs, expected, stack, context, of_fun) do
229288
Enum.map_reduce(pairs, context, fn {key, value}, context ->
230-
{key_tagged_type, dynamic_key?, context} = map_key_type(key, stack, context, of_fun)
289+
{pos_neg_domain, dynamic_key?, context} = map_key_type(key, stack, context, of_fun)
231290

232291
expected_value_type =
233-
with {:keys, [key]} <- key_tagged_type,
292+
with {[key], [], []} <- pos_neg_domain,
234293
{_, expected_value_type} <- map_fetch_key(expected, key) do
235294
expected_value_type
236295
else
237296
_ -> term()
238297
end
239298

240299
{value_type, context} = of_fun.(value, expected_value_type, stack, context)
241-
{{key_tagged_type, dynamic_key? or gradual?(value_type), value_type}, context}
300+
{{pos_neg_domain, dynamic_key? or gradual?(value_type), value_type}, context}
242301
end)
243302
end
244303

245304
defp map_key_type(key, _stack, context, _of_fun) when is_atom(key) do
246-
{{:keys, [key]}, false, context}
305+
{{[key], [], []}, false, context}
247306
end
248307

249308
defp map_key_type(key, stack, context, of_fun) do
250309
{key_type, context} = of_fun.(key, term(), stack, context)
310+
domain_keys = to_domain_keys(key_type)
251311

252-
# TODO: Deal with negations such that
253-
# `%{not :key => value}` => `%{atom() => value, key: none()}`
254-
# `%{:key => value, not :key => value}` => `%{atom() => value, key: value}`
255-
case atom_fetch(key_type) do
256-
{:finite, list} -> {{:keys, list}, gradual?(key_type), context}
257-
_ -> {{:domain, to_domain_keys(key_type)}, gradual?(key_type), context}
258-
end
312+
pos_neg_domain =
313+
case atom_fetch(key_type) do
314+
{:finite, list} -> {list, [], List.delete(domain_keys, :atom)}
315+
{:infinite, list} -> {[], list, domain_keys}
316+
:error -> {[], [], domain_keys}
317+
end
318+
319+
{pos_neg_domain, gradual?(key_type), context}
259320
end
260321

261322
defp cartesian_map(lists) do
@@ -457,9 +518,12 @@ defmodule Module.Types.Of do
457518
"""
458519
def modules(type, fun, arity, hints \\ [], expr, meta, stack, context) do
459520
case atom_fetch(type) do
460-
{_, mods} ->
521+
{:finite, mods} ->
461522
{mods, context}
462523

524+
{:infinite, _} ->
525+
{[], context}
526+
463527
:error ->
464528
warning = {:badmodule, expr, type, fun, arity, hints, context}
465529
{[], error(warning, meta, stack, context)}

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

Lines changed: 47 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -853,12 +853,12 @@ defmodule Module.Types.ExprTest do
853853
end
854854

855855
describe "maps/structs" do
856-
test "creating closed maps" do
856+
test "creating maps as records" do
857857
assert typecheck!(%{foo: :bar}) == closed_map(foo: atom([:bar]))
858858
assert typecheck!([x], %{key: x}) == dynamic(closed_map(key: term()))
859859
end
860860

861-
test "creating closed maps with dynamic keys" do
861+
test "creating maps as records with dynamic keys" do
862862
assert typecheck!(
863863
(
864864
foo = :foo
@@ -885,10 +885,10 @@ defmodule Module.Types.ExprTest do
885885
)
886886
end
887887

888-
test "creating open maps" do
888+
test "creating maps as dictionaries" do
889889
assert typecheck!(%{123 => 456}) == closed_map([{domain_key(:integer), integer()}])
890890

891-
# Since key cannot override :foo, we preserve it
891+
# Since key cannot override :foo based on position, we preserve it
892892
assert typecheck!([key], %{key => 456, foo: :bar}) ==
893893
dynamic(
894894
closed_map([
@@ -897,11 +897,45 @@ defmodule Module.Types.ExprTest do
897897
])
898898
)
899899

900-
# Since key can override :foo, we do not preserve it
900+
# Since key can override :foo based on position, we union it
901901
assert typecheck!([key], %{:foo => :bar, key => :baz}) ==
902902
dynamic(
903903
closed_map([
904-
{to_domain_keys(:term), atom([:baz])}
904+
{to_domain_keys(:term), atom([:baz])},
905+
{:foo, atom([:bar, :baz])}
906+
])
907+
)
908+
909+
# Since key cannot override :foo based on domain, we preserve it
910+
assert typecheck!(
911+
[arg],
912+
(
913+
key = String.to_integer(arg)
914+
%{:foo => :bar, key => :baz}
915+
)
916+
) ==
917+
closed_map([
918+
{domain_key(:integer), atom([:baz])},
919+
{:foo, atom([:bar])}
920+
])
921+
922+
# Multiple keys are fully overridden for simplicity
923+
assert typecheck!(
924+
[arg],
925+
(
926+
foo_or_bar = if String.starts_with?(arg, "0"), do: :foo, else: :bar
927+
key = String.to_integer(arg)
928+
%{foo_or_bar => :old, key => :new}
929+
)
930+
) ==
931+
union(
932+
closed_map([
933+
{domain_key(:integer), atom([:new])},
934+
{:foo, atom([:old])}
935+
]),
936+
closed_map([
937+
{domain_key(:integer), atom([:new])},
938+
{:bar, atom([:old])}
905939
])
906940
)
907941
end
@@ -924,7 +958,7 @@ defmodule Module.Types.ExprTest do
924958
)
925959
end
926960

927-
test "updating to closed maps" do
961+
test "updating to maps as records" do
928962
assert typecheck!([x], %{x | x: :zero}) ==
929963
dynamic(open_map(x: atom([:zero])))
930964

@@ -1107,32 +1141,23 @@ defmodule Module.Types.ExprTest do
11071141
"""
11081142
end
11091143

1110-
test "updating to open maps" do
1144+
test "updating to maps as dictionaries" do
11111145
assert typecheck!(
11121146
[key],
11131147
(
11141148
x = %{foo: :bar}
11151149
%{x | key => :baz}
11161150
)
1117-
) == dynamic(open_map())
1151+
) == closed_map(foo: atom([:bar, :baz]))
11181152

1119-
# Since key cannot override :foo, we preserve it
1153+
# Override based on position
11201154
assert typecheck!(
11211155
[key],
11221156
(
1123-
x = %{foo: :bar}
1124-
%{x | key => :baz, foo: :bat}
1125-
)
1126-
) == dynamic(open_map(foo: atom([:bat])))
1127-
1128-
# Since key can override :foo, we do not preserve it
1129-
assert typecheck!(
1130-
[key],
1131-
(
1132-
x = %{foo: :bar}
1133-
%{x | :foo => :baz, key => :bat}
1157+
x = %{foo: :bar, baz: :bat}
1158+
%{x | key => :old, foo: :new}
11341159
)
1135-
) == dynamic(open_map())
1160+
) == closed_map(foo: atom([:new]), baz: atom([:old, :bat]))
11361161

11371162
# The goal of this assertion is to verify we assert keys,
11381163
# even if they may be overridden later.

0 commit comments

Comments
 (0)