diff --git a/lib/elixir/lib/kernel/utils.ex b/lib/elixir/lib/kernel/utils.ex index 2cdcd601305..6a2e5b34361 100644 --- a/lib/elixir/lib/kernel/utils.ex +++ b/lib/elixir/lib/kernel/utils.ex @@ -208,10 +208,11 @@ defmodule Kernel.Utils do case enforce_keys -- :maps.keys(struct) do [] -> - # The __struct__ field is used for expansion and for loading remote structs + # The __struct__ attribute is public and it is used for expansion + # and for loading remote structs. :ets.insert(set, {:__struct__, struct, nil, []}) - # Store all field metadata to go into __info__(:struct) + # The complete metadata goes into __info__(:struct) mapper = fn {key, val} -> %{field: key, default: val, required: :lists.member(key, enforce_keys)} end diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index 3da9a828543..79476cf656b 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -52,8 +52,8 @@ defmodule Module.Types.Descr do def integer(), do: %{bitmap: @bit_integer} def float(), do: %{bitmap: @bit_float} def fun(), do: %{bitmap: @bit_fun} - def open_map(pairs), do: %{map: [{:open, Map.new(pairs), []}]} - def closed_map(pairs), do: %{map: [{:closed, Map.new(pairs), []}]} + def open_map(pairs), do: %{map: map_new(:open, Map.new(pairs))} + def closed_map(pairs), do: %{map: map_new(:closed, Map.new(pairs))} def open_map(), do: %{map: @map_top} def empty_map(), do: %{map: @map_empty} def non_empty_list(), do: %{bitmap: @bit_non_empty_list} @@ -416,6 +416,54 @@ defmodule Module.Types.Descr do # an empty list of atoms. It is simplified to `0` in set operations, and the key # is removed from the map. + @doc """ + Returns a set of all known atoms. + + Returns `{:ok, known_set}` if it is an atom, `:error` otherwise. + Notice `known_set` may be empty and still return positive, due to + negations. + """ + def atom_fetch(%{} = descr) do + case :maps.take(:dynamic, descr) do + :error -> + if atom_only?(descr) do + case descr do + %{atom: {:union, set}} -> {:ok, :sets.to_list(set)} + %{atom: {:negation, _}} -> {:ok, []} + %{} -> :error + end + else + :error + end + + {dynamic, static} -> + if atom_only?(static) do + case {dynamic, static} do + {%{atom: {:union, d}}, %{atom: {:union, s}}} -> + {:ok, :sets.to_list(:sets.union(d, s))} + + {_, %{atom: {:negation, _}}} -> + {:ok, []} + + {%{atom: {:negation, _}}, _} -> + {:ok, []} + + {%{atom: {:union, d}}, %{}} -> + {:ok, :sets.to_list(d)} + + {%{}, %{atom: {:union, s}}} -> + {:ok, :sets.to_list(s)} + + {%{}, %{}} -> + :error + end + else + :error + end + end + end + + defp atom_only?(descr), do: empty?(Map.delete(descr, :atom)) defp atom_new(as) when is_list(as), do: {:union, :sets.from_list(as, version: 2)} defp atom_intersection({tag1, s1}, {tag2, s2}) do @@ -447,18 +495,35 @@ defmodule Module.Types.Descr do if tag == :union and :sets.size(s) == 0, do: 0, else: {tag, s} end - defp literal(lit), do: {:__block__, [], [lit]} + defp literal_to_quoted(lit) do + if is_atom(lit) and Macro.classify_atom(lit) == :alias do + segments = + case Atom.to_string(lit) do + "Elixir" -> + [:"Elixir"] + + "Elixir." <> segments -> + segments + |> String.split(".") + |> Enum.map(&String.to_atom/1) + end + + {:__aliases__, [], segments} + else + {:__block__, [], [lit]} + end + end defp atom_to_quoted({:union, a}) do if :sets.is_subset(@boolset, a) do :sets.subtract(a, @boolset) |> :sets.to_list() |> Enum.sort() - |> Enum.reduce({:boolean, [], []}, &{:or, [], [&2, literal(&1)]}) + |> Enum.reduce({:boolean, [], []}, &{:or, [], [&2, literal_to_quoted(&1)]}) else :sets.to_list(a) |> Enum.sort() - |> Enum.map(&literal/1) + |> Enum.map(&literal_to_quoted/1) |> Enum.reduce(&{:or, [], [&2, &1]}) end |> List.wrap() @@ -584,31 +649,43 @@ defmodule Module.Types.Descr do defp map_tag_to_type(:open), do: term_or_not_set() defp map_tag_to_type(:closed), do: not_set() - defp map_descr(tag, fields), do: %{map: [{tag, fields, []}]} + defp map_new(tag, fields), do: [{tag, fields, []}] + defp map_descr(tag, fields), do: %{map: map_new(tag, fields)} @doc """ - Gets the type of the value returned by accessing `key` on `map`. + Gets the type of the value returned by accessing `key` on `map` + with the assumption that the descr is exclusively a map. - It returns a two element tuple. The first element says if the type - is optional or not, the second element is the type. In static mode, - we likely want to raise if `map.field` (or pattern matching?) is - called on an optional key. + It returns a two element tuple or `:error`. The first element says + if the type is optional or not, the second element is the type. + In static mode, we likely want to raise if `map.field` + (or pattern matching?) is called on an optional key. """ - def map_get(%{} = descr, key) do + def map_fetch(%{} = descr, key) do case :maps.take(:dynamic, descr) do :error -> - map_get_static(descr, key) + if is_map_key(descr, :map) and map_only?(descr) do + map_fetch_static(descr, key) + else + :error + end {dynamic, static} -> - {dynamic_optional?, dynamic_type} = map_get_static(dynamic, key) - {static_optional?, static_type} = map_get_static(static, key) + if (is_map_key(dynamic, :map) or is_map_key(static, :map)) and map_only?(static) do + {dynamic_optional?, dynamic_type} = map_fetch_static(dynamic, key) + {static_optional?, static_type} = map_fetch_static(static, key) - {dynamic_optional? or static_optional?, - union(intersection(dynamic(), dynamic_type), static_type)} + {dynamic_optional? or static_optional?, + union(intersection(dynamic(), dynamic_type), static_type)} + else + :error + end end end - defp map_get_static(descr, key) when is_atom(key) do + defp map_only?(descr), do: empty?(Map.delete(descr, :map)) + + defp map_fetch_static(descr, key) when is_atom(key) do case descr do %{map: map} -> map_split_on_key(map, key) @@ -832,18 +909,40 @@ defmodule Module.Types.Descr do def map_literal_to_quoted({tag, fields}) do case tag do - :closed -> {:%{}, [], map_fields_to_quoted(tag, fields)} - :open -> {:%{}, [], [{:..., [], nil} | map_fields_to_quoted(tag, fields)]} + :closed -> + with %{__struct__: struct_descr} <- fields, + {:ok, [struct]} <- atom_fetch(struct_descr) do + {:%, [], + [ + literal_to_quoted(struct), + {:%{}, [], map_fields_to_quoted(tag, Map.delete(fields, :__struct__))} + ]} + else + _ -> {:%{}, [], map_fields_to_quoted(tag, fields)} + end + + :open -> + {:%{}, [], [{:..., [], nil} | map_fields_to_quoted(tag, fields)]} end end defp map_fields_to_quoted(tag, map) do - for {key, type} <- Enum.sort(map), + sorted = Enum.sort(map) + keyword? = Inspect.List.keyword?(sorted) + + for {key, type} <- sorted, not (tag == :open and optional?(type) and term?(type)) do + key = + if keyword? do + {:__block__, [format: :keyword], [key]} + else + literal_to_quoted(key) + end + cond do - not optional?(type) -> {literal(key), to_quoted(type)} - empty?(type) -> {literal(key), {:not_set, [], []}} - true -> {literal(key), {:if_set, [], [to_quoted(type)]}} + not optional?(type) -> {key, to_quoted(type)} + empty?(type) -> {key, {:not_set, [], []}} + true -> {key, {:if_set, [], [to_quoted(type)]}} end end end diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index eb4e7b695c8..6682c8271a0 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -113,15 +113,16 @@ defmodule Module.Types.Expr do end end - # TODO: %Struct{map | ...} def of_expr( - {:%, meta, [module, {:%{}, _, [{:|, _, [_, _]}]} = update]}, + {:%, _, [module, {:%{}, _, [{:|, _, [map, args]}]}]} = expr, stack, context ) do - with {:ok, _, context} <- Of.struct(module, meta, stack, context), - {:ok, _, context} <- of_expr(update, stack, context) do - {:ok, open_map(), context} + with {:ok, type, context} <- Of.struct(expr, module, args, true, stack, context, &of_expr/3), + {:ok, _, context} <- of_expr(map, stack, context) do + # TODO: We need to validate that map is actually compatible with struct. + # Perhaps a simple validation over the struct name? + {:ok, type, context} end end @@ -130,12 +131,8 @@ defmodule Module.Types.Expr do Of.closed_map(args, stack, context, &of_expr/3) end - # TODO: %Struct{...} - def of_expr({:%, meta1, [module, {:%{}, _meta2, args}]}, stack, context) do - with {:ok, _, context} <- Of.struct(module, meta1, stack, context), - {:ok, _, context} <- Of.open_map(args, stack, context, &of_expr/3) do - {:ok, open_map(), context} - end + def of_expr({:%, _, [module, {:%{}, _, args}]} = expr, stack, context) do + Of.struct(expr, module, args, false, stack, context, &of_expr/3) end # () @@ -278,40 +275,39 @@ defmodule Module.Types.Expr do end end - # TODO: expr.key_or_fun - def of_expr({{:., _meta1, [expr1, _key_or_fun]}, meta2, []}, stack, context) - when not is_atom(expr1) do - if Keyword.get(meta2, :no_parens, false) do - with {:ok, _, context} <- of_expr(expr1, stack, context) do - {:ok, dynamic(), context} - end - else - with {:ok, _, context} <- of_expr(expr1, stack, context) do + def of_expr({{:., _, [callee, key_or_fun]}, meta, []} = expr, stack, context) + when not is_atom(callee) and is_atom(key_or_fun) do + with {:ok, type, context} <- of_expr(callee, stack, context) do + if Keyword.get(meta, :no_parens, false) do + Of.map_fetch(expr, type, key_or_fun, stack, context) + else + {_mods, context} = Of.remote(expr, type, key_or_fun, 0, [:dot], meta, stack, context) + # TODO: Return the proper type {:ok, dynamic(), context} end end end # TODO: expr.fun(arg) - def of_expr({{:., _meta1, [expr1, fun]}, meta2, args}, stack, context) do - context = Of.remote(expr1, fun, length(args), meta2, stack, context) - - with {:ok, _expr_type, context} <- of_expr(expr1, stack, context), - {:ok, _arg_types, context} <- - map_reduce_ok(args, context, &of_expr(&1, stack, &2)) do + def of_expr({{:., _, [remote, fun]}, meta, args} = expr, stack, context) do + with {:ok, remote_type, context} <- of_expr(remote, stack, context), + {:ok, _arg_types, context} <- map_reduce_ok(args, context, &of_expr(&1, stack, &2)) do + {_mods, context} = Of.remote(expr, remote_type, fun, length(args), [], meta, stack, context) {:ok, dynamic(), context} end end # TODO: &Foo.bar/1 def of_expr( - {:&, _, [{:/, _, [{{:., _, [module, fun]}, meta, []}, arity]}]}, + {:&, _, [{:/, _, [{{:., _, [remote, fun]}, meta, []}, arity]}]} = expr, stack, context ) - when is_atom(module) and is_atom(fun) do - context = Of.remote(module, fun, arity, meta, stack, context) - {:ok, dynamic(), context} + when is_atom(fun) and is_integer(arity) do + with {:ok, remote_type, context} <- of_expr(remote, stack, context) do + {_mods, context} = Of.remote(expr, remote_type, fun, arity, [], meta, stack, context) + {:ok, dynamic(), context} + end end # &foo/1 diff --git a/lib/elixir/lib/module/types/helpers.ex b/lib/elixir/lib/module/types/helpers.ex index ff4d6dbd4e2..2377620d3ac 100644 --- a/lib/elixir/lib/module/types/helpers.ex +++ b/lib/elixir/lib/module/types/helpers.ex @@ -29,6 +29,13 @@ defmodule Module.Types.Helpers do is an integer. Pass a modifier, such as <> or <>, \ to change the default behavior. """ + + :dot -> + """ + + #{hint()} "var.field" (without parentheses) means "var" is a map() while \ + "var.fun()" (with parentheses) means "var" is an atom() + """ end) end diff --git a/lib/elixir/lib/module/types/of.ex b/lib/elixir/lib/module/types/of.ex index cb2386bed54..4f6dcb18726 100644 --- a/lib/elixir/lib/module/types/of.ex +++ b/lib/elixir/lib/module/types/of.ex @@ -15,20 +15,25 @@ defmodule Module.Types.Of do @float float() @binary binary() - # There are important assumptions on how we work with maps. - # - # First, the keys in the map must be ordered by subtyping. - # - # Second, optional keys must be a superset of the required - # keys, i.e. %{required(atom) => integer, optional(:foo) => :bar} - # is forbidden. - # - # Third, in order to preserve co/contra-variance, a supertype - # must satisfy its subtypes. I.e. %{foo: :bar, atom() => :baz} - # is forbidden, it must be %{foo: :bar, atom() => :baz | :bar}. - # - # Once we support user declared maps, we need to validate these - # assumptions. + @doc """ + Handles fetching a map key. + """ + def map_fetch(expr, type, field, stack, context) when is_atom(field) do + case map_fetch(type, field) do + :error -> + {:ok, dynamic(), + warn({:badmap, expr, type, field, context}, elem(expr, 1), stack, context)} + + # TODO: on static type checking, we want check it is not optional. + {_optional?, value_type} -> + if empty?(value_type) do + {:ok, dynamic(), + warn({:badkey, expr, type, field, context}, elem(expr, 1), stack, context)} + else + {:ok, value_type, context} + end + end + end @doc """ Handles open maps. @@ -59,9 +64,31 @@ defmodule Module.Types.Of do @doc """ Handles structs. """ - def struct(struct, meta, stack, context) do - context = remote(struct, :__struct__, 0, meta, stack, context) - {:ok, open_map(), context} + def struct({:%, meta, _}, struct, args, defaults?, stack, context, of_fun) + when is_atom(struct) do + context = preload_and_maybe_check_export(struct, :__struct__, 0, meta, stack, context) + + # The compiler has already checked the keys are atoms and which ones are required. + # TODO: Type check and do not assume dynamic for non-specified keys. + with {:ok, pairs, context} <- + map_reduce_ok(args, context, fn {key, value}, context when is_atom(key) -> + with {:ok, type, context} <- of_fun.(value, stack, context) do + {:ok, {key, type}, context} + end + end) do + defaults = + if defaults? do + dynamic = dynamic() + + for key <- Map.keys(struct.__struct__()), key != :__struct__ do + {key, dynamic} + end + else + [] + end + + {:ok, closed_map([{:__struct__, atom([struct])} | defaults] ++ pairs), context} + end end ## Binary @@ -140,10 +167,28 @@ defmodule Module.Types.Of do ## Remote - @doc """ - Handles remote calls. - """ - def remote(module, fun, arity, meta, stack, context) when is_atom(module) do + def remote(expr, type, fun, arity, hints, meta, stack, context) do + case atom_fetch(type) do + {:ok, mods} -> + context = + Enum.reduce(mods, context, fn mod, context -> + preload_and_maybe_check_export(mod, fun, arity, meta, stack, context) + end) + + {mods, context} + + :error -> + warning = {:badmodule, expr, type, fun, arity, hints, context} + {[], warn(warning, meta, stack, context)} + end + end + + def remote(module, fun, arity, meta, stack, context) do + preload_and_maybe_check_export(module, fun, arity, meta, stack, context) + end + + defp preload_and_maybe_check_export(module, fun, arity, meta, stack, context) + when is_atom(module) do if Keyword.get(meta, :runtime_module, false) do context else @@ -152,8 +197,6 @@ defmodule Module.Types.Of do end end - def remote(_module, _fun, _arity, _meta, _stack, context), do: context - defp check_export(module, fun, arity, meta, stack, context) do case ParallelChecker.fetch_export(stack.cache, module, fun, arity) do {:ok, mode, :def, reason} -> @@ -267,6 +310,63 @@ defmodule Module.Types.Of do ] end + def format_warning({:badmap, expr, type, key, context}) do + {traces, trace_hints} = Module.Types.Pattern.format_traces(expr, context) + + [ + """ + expected a map or struct when accessing .#{key} in expression: + + #{Macro.to_string(expr)} + + but got type: + + #{to_quoted_string(type)} + """, + traces, + format_hints([:dot | trace_hints]), + "\ntyping violation found at:" + ] + end + + def format_warning({:badkey, expr, type, key, context}) do + {traces, trace_hints} = Module.Types.Pattern.format_traces(expr, context) + + [ + """ + missing key .#{key} in expression: + + #{Macro.to_string(expr)} + + the given type does not have the given key: + + #{to_quoted_string(type)} + """, + traces, + format_hints([:dot | trace_hints]), + "\ntyping violation found at:" + ] + end + + def format_warning({:badmodule, expr, type, fun, arity, hints, context}) do + {traces, trace_hints} = Module.Types.Pattern.format_traces(expr, context) + + [ + """ + expected a module (an atom) when invoking #{fun}/#{arity} in expression: + + #{Macro.to_string(expr)} + + but got type: + + #{to_quoted_string(type)} + """, + traces, + format_hints(hints ++ trace_hints), + "\ntyping violation found at:" + ] + end + def format_warning({:undefined_module, module, fun, arity}) do [ Exception.format_mfa(module, fun, arity), diff --git a/lib/elixir/lib/module/types/pattern.ex b/lib/elixir/lib/module/types/pattern.ex index 2251de5d02c..e66d81100a0 100644 --- a/lib/elixir/lib/module/types/pattern.ex +++ b/lib/elixir/lib/module/types/pattern.ex @@ -102,22 +102,18 @@ defmodule Module.Types.Pattern do end # %var{...} and %^var{...} - def of_pattern({:%, _meta1, [var, {:%{}, _meta2, args}]}, _expected_expr, stack, context) + def of_pattern({:%, _meta1, [var, {:%{}, _meta2, args}]} = expr, _expected_expr, stack, context) when not is_atom(var) do - # TODO: validate var is an atom - with {:ok, _, context} = of_pattern(var, stack, context), + with {:ok, _, context} <- of_pattern(var, {atom(), expr}, stack, context), {:ok, _, context} <- Of.open_map(args, stack, context, &of_pattern/3) do {:ok, open_map(), context} end end # %Struct{...} - def of_pattern({:%, meta1, [module, {:%{}, _meta2, args}]}, _expected_expr, stack, context) + def of_pattern({:%, _, [module, {:%{}, _, args}]} = expr, _expected_expr, stack, context) when is_atom(module) do - with {:ok, _, context} <- Of.struct(module, meta1, stack, context), - {:ok, _, context} <- Of.open_map(args, stack, context, &of_pattern/3) do - {:ok, open_map(), context} - end + Of.struct(expr, module, args, true, stack, context, &of_pattern/3) end # %{...} diff --git a/lib/elixir/src/elixir_map.erl b/lib/elixir/src/elixir_map.erl index 85cf462e802..421a05155c2 100644 --- a/lib/elixir/src/elixir_map.erl +++ b/lib/elixir/src/elixir_map.erl @@ -23,14 +23,14 @@ expand_struct(Meta, Left, {'%{}', MapMeta, MapArgs}, S, #{context := Context} = case extract_struct_assocs(Meta, ERight, E) of {expand, MapMeta, Assocs} when Context /= match -> %% Expand AssocKeys = [K || {K, _} <- Assocs], - Struct = load_struct(Meta, ELeft, [Assocs], AssocKeys, EE), + Struct = load_struct(Meta, ELeft, [Assocs], Assocs, EE), Keys = ['__struct__'] ++ AssocKeys, WithoutKeys = lists:sort(maps:to_list(maps:without(Keys, Struct))), StructAssocs = elixir_quote:escape(WithoutKeys, none, false), {{'%', Meta, [ELeft, {'%{}', MapMeta, StructAssocs ++ Assocs}]}, SE, EE}; {_, _, Assocs} -> %% Update or match - _ = load_struct(Meta, ELeft, [], [K || {K, _} <- Assocs], EE), + _ = load_struct(Meta, ELeft, [], Assocs, EE), {{'%', Meta, [ELeft, ERight]}, SE, EE} end; @@ -125,7 +125,12 @@ validate_struct({Var, _Meta, Ctx}, match) when is_atom(Var), is_atom(Ctx) -> tru validate_struct(Atom, _) when is_atom(Atom) -> true; validate_struct(_, _) -> false. -load_struct(Meta, Name, Args, Keys, E) -> +load_struct(Meta, Name, Args, Assocs, E) -> + Keys = [begin + is_atom(K) orelse function_error(Meta, E, ?MODULE, {invalid_key_for_struct, K}), + K + end || {K, _} <- Assocs], + case maybe_load_struct(Meta, Name, Args, Keys, E) of {ok, Struct} -> Struct; {error, Desc} -> file_error(Meta, E, ?MODULE, Desc) @@ -258,5 +263,8 @@ format_error({undefined_struct, Module, Arity}) -> format_error({unknown_key_for_struct, Module, Key}) -> io_lib:format("unknown key ~ts for struct ~ts", ['Elixir.Macro':to_string(Key), elixir_aliases:inspect(Module)]); +format_error({invalid_key_for_struct, Key}) -> + io_lib:format("invalid key ~ts for struct, struct keys must be atoms, got: ", + ['Elixir.Macro':to_string(Key)]); format_error(ignored_struct_key_in_struct) -> "key :__struct__ is ignored when using structs". diff --git a/lib/elixir/test/elixir/kernel/expansion_test.exs b/lib/elixir/test/elixir/kernel/expansion_test.exs index 22e506d363a..62374f4914d 100644 --- a/lib/elixir/test/elixir/kernel/expansion_test.exs +++ b/lib/elixir/test/elixir/kernel/expansion_test.exs @@ -588,17 +588,25 @@ defmodule Kernel.ExpansionTest do assert expand(quote(do: %x{} = 1)) == quote(do: %x{} = 1) end - test "unknown ^keys in structs" do - message = ~r"unknown key \^my_key for struct Kernel\.ExpansionTest\.User" + test "invalid keys in structs" do + assert_compile_error(~r"invalid key :erlang\.\+\(1, 2\) for struct", fn -> + expand( + quote do + %User{(1 + 2) => :my_value} + end + ) + end) + end + + test "unknown key in structs" do + message = ~r"unknown key :foo for struct Kernel\.ExpansionTest\.User" assert_compile_error(message, fn -> - code = + expand( quote do - my_key = :my_key - %User{^my_key => :my_value} = %{} + %User{foo: :my_value} = %{} end - - expand(code) + ) end) end end diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index 0faf6c42d64..c3676751590 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -246,52 +246,53 @@ defmodule Module.Types.DescrTest do describe "map operations" do test "get field" do - assert map_get(closed_map(a: integer()), :a) == {false, integer()} - assert map_get(dynamic(), :a) == {true, dynamic()} + assert map_fetch(closed_map(a: integer()), :a) == {false, integer()} + + assert map_fetch(term(), :a) == :error + assert map_fetch(union(open_map(), integer()), :a) == :error + assert map_fetch(dynamic(), :a) == {true, dynamic()} assert intersection(dynamic(), open_map(a: integer())) - |> map_get(:a) == {false, intersection(integer(), dynamic())} + |> map_fetch(:a) == {false, intersection(integer(), dynamic())} {false, value_type} = open_map(my_map: open_map(foo: integer())) |> intersection(open_map(my_map: open_map(bar: boolean()))) - |> map_get(:my_map) + |> map_fetch(:my_map) assert equal?(value_type, open_map(foo: integer(), bar: boolean())) - assert map_get(union(closed_map(a: integer()), closed_map(a: atom())), :a) == + assert map_fetch(union(closed_map(a: integer()), closed_map(a: atom())), :a) == {false, union(integer(), atom())} - assert map_get(union(closed_map(a: integer()), closed_map(b: atom())), :a) == + assert map_fetch(union(closed_map(a: integer()), closed_map(b: atom())), :a) == {true, integer()} - assert map_get(term(), :a) == {true, term()} - {false, value_type} = closed_map(a: union(integer(), atom())) |> difference(open_map(a: integer())) - |> map_get(:a) + |> map_fetch(:a) assert equal?(value_type, atom()) {false, value_type} = closed_map(a: integer(), b: atom()) |> difference(closed_map(a: integer(), b: atom([:foo]))) - |> map_get(:a) + |> map_fetch(:a) assert equal?(value_type, integer()) {false, value_type} = closed_map(a: integer()) |> difference(closed_map(a: atom())) - |> map_get(:a) + |> map_fetch(:a) assert equal?(value_type, integer()) {false, value_type} = open_map(a: integer(), b: atom()) |> union(closed_map(a: tuple())) - |> map_get(:a) + |> map_fetch(:a) assert equal?(value_type, union(integer(), tuple())) @@ -299,24 +300,24 @@ defmodule Module.Types.DescrTest do closed_map(a: atom()) |> difference(closed_map(a: atom([:foo, :bar]))) |> difference(closed_map(a: atom([:bar]))) - |> map_get(:a) + |> map_fetch(:a) assert equal?(value_type, intersection(atom(), negation(atom([:foo, :bar])))) assert closed_map(a: union(atom(), pid()), b: integer(), c: tuple()) |> difference(open_map(a: atom(), b: integer())) |> difference(open_map(a: atom(), c: tuple())) - |> map_get(:a) == {false, pid()} + |> map_fetch(:a) == {false, pid()} assert closed_map(a: union(atom([:foo]), pid()), b: integer(), c: tuple()) |> difference(open_map(a: atom([:foo]), b: integer())) |> difference(open_map(a: atom(), c: tuple())) - |> map_get(:a) == {false, pid()} + |> map_fetch(:a) == {false, pid()} assert closed_map(a: union(atom([:foo, :bar, :baz]), integer())) |> difference(open_map(a: atom([:foo, :bar]))) |> difference(open_map(a: atom([:foo, :baz]))) - |> map_get(:a) == {false, integer()} + |> map_fetch(:a) == {false, integer()} end end @@ -340,6 +341,9 @@ defmodule Module.Types.DescrTest do assert atom([:a]) |> to_quoted_string() == ":a" assert atom([:a, :b]) |> to_quoted_string() == ":a or :b" assert difference(atom(), atom([:a])) |> to_quoted_string() == "atom() and not :a" + + assert atom([Elixir]) |> to_quoted_string() == "Elixir" + assert atom([Foo.Bar]) |> to_quoted_string() == "Foo.Bar" end test "boolean" do @@ -362,38 +366,58 @@ defmodule Module.Types.DescrTest do "dynamic() or (:bar or :foo)" assert intersection(dynamic(), closed_map(a: integer())) |> to_quoted_string() == - "dynamic() and %{:a => integer()}" + "dynamic() and %{a: integer()}" end test "map" do assert open_map() |> to_quoted_string() == "%{...}" - assert closed_map(a: integer()) |> to_quoted_string() == "%{:a => integer()}" - assert open_map(a: float()) |> to_quoted_string() == "%{..., :a => float()}" + + assert closed_map(a: integer()) |> to_quoted_string() == "%{a: integer()}" + assert open_map(a: float()) |> to_quoted_string() == "%{..., a: float()}" + + assert closed_map("Elixir.Foo.Bar": integer()) |> to_quoted_string() == + "%{Foo.Bar => integer()}" + + assert open_map("Elixir.Foo.Bar": float()) |> to_quoted_string() == + "%{..., Foo.Bar => float()}" # TODO: support this simplification # assert difference(open_map(), open_map(a: term())) |> to_quoted_string() == - # "%{..., :a => not_set()}" + # "%{..., a: not_set()}" assert closed_map(a: integer(), b: atom()) |> to_quoted_string() == - "%{:a => integer(), :b => atom()}" + "%{a: integer(), b: atom()}" assert open_map(a: float()) |> difference(closed_map(a: float())) - |> to_quoted_string() == "%{..., :a => float()} and not %{:a => float()}" + |> to_quoted_string() == "%{..., a: float()} and not %{a: float()}" assert difference(open_map(), empty_map()) |> to_quoted_string() == "%{...} and not %{}" assert closed_map(foo: union(integer(), not_set())) |> to_quoted_string() == - "%{:foo => if_set(integer())}" + "%{foo: if_set(integer())}" assert difference(open_map(a: integer()), closed_map(b: boolean())) |> to_quoted_string() == - "%{..., :a => integer()}" + "%{..., a: integer()}" assert open_map(a: integer(), b: atom()) |> difference(open_map(b: atom())) |> union(open_map(a: integer())) - |> to_quoted_string() == "%{..., :a => integer()}" + |> to_quoted_string() == "%{..., a: integer()}" + end + + test "structs" do + assert open_map(__struct__: atom([URI])) |> to_quoted_string() == "%{..., __struct__: URI}" + + assert closed_map(__struct__: atom([URI])) |> to_quoted_string() == + "%URI{}" + + assert closed_map(__struct__: atom([URI]), path: atom([nil])) |> to_quoted_string() == + "%URI{path: nil}" + + assert closed_map(__struct__: atom([URI, Another])) |> to_quoted_string() == + "%{__struct__: Another or URI}" end end end diff --git a/lib/elixir/test/elixir/module/types/expr_test.exs b/lib/elixir/test/elixir/module/types/expr_test.exs index 1dc9df695c2..29388f77364 100644 --- a/lib/elixir/test/elixir/module/types/expr_test.exs +++ b/lib/elixir/test/elixir/module/types/expr_test.exs @@ -1,5 +1,9 @@ Code.require_file("type_helper.exs", __DIR__) +defmodule Point do + defstruct [:x, :y, z: 0] +end + defmodule Module.Types.ExprTest do use ExUnit.Case, async: true @@ -19,8 +23,12 @@ defmodule Module.Types.ExprTest do assert typecheck!(%{}) == open_map() end - describe "undefined functions" do - test "warnings" do + describe "remotes" do + test "dynamic calls" do + assert typecheck!([%x{}], x.foo_bar()) == dynamic() + end + + test "undefined function warnings" do assert typewarn!(URI.unknown("foo")) == {dynamic(), "URI.unknown/1 is undefined or private"} @@ -30,6 +38,74 @@ defmodule Module.Types.ExprTest do assert typewarn!(try(do: :ok, after: URI.unknown("foo"))) == {dynamic(), "URI.unknown/1 is undefined or private"} end + + test "calling a nullary function on non atoms" do + assert typewarn!([<>], x.foo_bar()) == + {dynamic(), + ~l""" + expected a module (an atom) when invoking foo_bar/0 in expression: + + x.foo_bar() + + but got type: + + integer() + + where "x" was given the type: + + # type: integer() + # from: types_test.ex:LINE-2 + <> + + #{hints(:dot)} + + typing violation found at:\ + """} + end + + test "calling a function on non atoms with arguments" do + assert typewarn!([<>], x.foo_bar(1, 2)) == + {dynamic(), + ~l""" + expected a module (an atom) when invoking foo_bar/2 in expression: + + x.foo_bar(1, 2) + + but got type: + + integer() + + where "x" was given the type: + + # type: integer() + # from: types_test.ex:LINE-2 + <> + + typing violation found at:\ + """} + end + + test "capture a function with non atoms" do + assert typewarn!([<>], &x.foo_bar/2) == + {dynamic(), + ~l""" + expected a module (an atom) when invoking foo_bar/2 in expression: + + &x.foo_bar/2 + + but got type: + + integer() + + where "x" was given the type: + + # type: integer() + # from: types_test.ex:LINE-2 + <> + + typing violation found at:\ + """} + end end describe "binaries" do @@ -111,4 +187,70 @@ defmodule Module.Types.ExprTest do """} end end + + describe "maps/structs" do + test "matching struct name" do + assert typecheck!([%x{}], x) == atom() + end + + test "creating structs" do + assert typecheck!(%Point{}) == + closed_map(__struct__: atom([Point]), x: atom([nil]), y: atom([nil]), z: integer()) + + assert typecheck!(%Point{x: :zero}) == + closed_map( + __struct__: atom([Point]), + x: atom([:zero]), + y: atom([nil]), + z: integer() + ) + end + + test "updating structs" do + assert typecheck!([x], %Point{x | x: :zero}) == + closed_map(__struct__: atom([Point]), x: atom([:zero]), y: dynamic(), z: dynamic()) + end + + test "accessing a field on not a map" do + assert typewarn!([<>], x.foo_bar) == + {dynamic(), + ~l""" + expected a map or struct when accessing .foo_bar in expression: + + x.foo_bar + + but got type: + + integer() + + where "x" was given the type: + + # type: integer() + # from: types_test.ex:LINE-2 + <> + + #{hints(:dot)} + + typing violation found at:\ + """} + end + + test "accessing an unknown field on struct" do + assert typewarn!(%Point{}.foo_bar) == + {dynamic(), + ~l""" + missing key .foo_bar in expression: + + %Point{x: nil, y: nil, z: 0}.foo_bar + + the given type does not have the given key: + + %Point{x: nil, y: nil, z: integer()} + + #{hints(:dot)} + + typing violation found at:\ + """} + end + end end