diff --git a/lib/elixir/lib/module/types.ex b/lib/elixir/lib/module/types.ex index b712c12598f..efbd0502f8a 100644 --- a/lib/elixir/lib/module/types.ex +++ b/lib/elixir/lib/module/types.ex @@ -251,16 +251,19 @@ defmodule Module.Types do def format_warning({:unable_unify, left, right, {location, expr, traces}}) do cond do - map_type?(left) and map_type?(right) and match?({:ok, _}, missing_field(left, right)) -> - {:ok, atom} = missing_field(left, right) + map_type?(left) and map_type?(right) and match?({:ok, _, _}, missing_field(left, right)) -> + {:ok, atom, known_atoms} = missing_field(left, right) # Drop the last trace which is the expression map.foo traces = Enum.drop(traces, 1) - {traces, hints} = format_traces(traces, false) + {traces, hints} = format_traces(traces, true) [ "undefined field \"#{atom}\" ", format_expr(expr, location), + "expected one of the following fields: ", + Enum.map_join(Enum.sort(known_atoms), ", ", & &1), + "\n\n", traces, format_message_hints(hints), "Conflict found at" @@ -290,26 +293,27 @@ defmodule Module.Types do {:map, [{:required, {:atom, atom} = type, _}, {:optional, :dynamic, :dynamic}]}, {:map, fields} ) do - if List.keymember?(fields, type, 1) do - :error - else - {:ok, atom} - end + matched_missing_field(fields, type, atom) end defp missing_field( {:map, fields}, {:map, [{:required, {:atom, atom} = type, _}, {:optional, :dynamic, :dynamic}]} ) do + matched_missing_field(fields, type, atom) + end + + defp missing_field(_, _), do: :error + + defp matched_missing_field(fields, type, atom) do if List.keymember?(fields, type, 1) do :error else - {:ok, atom} + known_atoms = for {_, {:atom, atom}, _} <- fields, do: atom + {:ok, atom, known_atoms} end end - defp missing_field(_, _), do: :error - defp format_traces([], _simplify?) do {[], []} end diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index 354d28dd348..b4df5325e18 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -1,7 +1,7 @@ defmodule Module.Types.Expr do @moduledoc false - alias Module.Types.{Remote, Pattern} + alias Module.Types.{Of, Pattern} import Module.Types.{Helpers, Infer} def of_expr(expr, %{context: stack_context} = stack, context) when stack_context != :expr do @@ -35,7 +35,7 @@ defmodule Module.Types.Expr do # <<...>>> def of_expr({:<<>>, _meta, args}, stack, context) do - result = of_binary(args, stack, context, &of_expr/3) + result = Of.binary(args, stack, context, &of_expr/3) case result do {:ok, context} -> {:ok, :binary, context} @@ -142,41 +142,47 @@ defmodule Module.Types.Expr do def of_expr({:%{}, _, [{:|, _, [map, args]}]} = expr, stack, context) do stack = push_expr_stack(expr, stack) - additional_pairs = [{:optional, :dynamic, :dynamic}] - map_update(map, args, additional_pairs, stack, context) + with {:ok, map_type, context} <- of_expr(map, stack, context), + {:ok, {:map, arg_pairs}, context} <- Of.closed_map(args, stack, context, &of_expr/3), + dynamic_value_pairs = + Enum.map(arg_pairs, fn {:required, key, _value} -> {:required, key, :dynamic} end), + args_type = {:map, dynamic_value_pairs ++ [{:optional, :dynamic, :dynamic}]}, + {:ok, type, context} <- unify(args_type, map_type, stack, context) do + # Retrieve map type and overwrite with the new value types from the map update + {:map, pairs} = resolve_var(type, context) + + updated_pairs = + Enum.reduce(arg_pairs, pairs, fn {:required, key, value}, pairs -> + List.keyreplace(pairs, key, 1, {:required, key, value}) + end) + + {:ok, {:map, updated_pairs}, context} + end end # %Struct{map | ...} - def of_expr({:%, meta, [module, {:%{}, _, [{:|, _, [map, args]}]}]} = expr, stack, context) do - context = Remote.check(module, :__struct__, 0, meta, context) + def of_expr({:%, meta, [module, {:%{}, _, [{:|, _, [_, _]}]} = update]} = expr, stack, context) do stack = push_expr_stack(expr, stack) - additional_pairs = [{:required, {:atom, :__struct__}, {:atom, module}}] - map_update(map, args, additional_pairs, stack, context) + with {:ok, struct, context} <- Of.struct(module, meta, context), + {:ok, update, context} <- of_expr(update, stack, context) do + unify(update, struct, stack, context) + end end # %{...} def of_expr({:%{}, _meta, args} = expr, stack, context) do stack = push_expr_stack(expr, stack) - - case of_pairs(args, stack, context) do - {:ok, pairs, context} -> {:ok, {:map, pairs_to_unions(pairs, context)}, context} - {:error, reason} -> {:error, reason} - end + Of.closed_map(args, stack, context, &of_expr/3) end # %Struct{...} def of_expr({:%, meta1, [module, {:%{}, _meta2, args}]} = expr, stack, context) do - context = Remote.check(module, :__struct__, 0, meta1, context) stack = push_expr_stack(expr, stack) - case of_pairs(args, stack, context) do - {:ok, pairs, context} -> - pairs = [{:required, {:atom, :__struct__}, {:atom, module}} | pairs] - {:ok, {:map, pairs}, context} - - {:error, reason} -> - {:error, reason} + with {:ok, struct, context} <- Of.struct(module, meta1, context), + {:ok, map, context} <- Of.open_map(args, stack, context, &of_expr/3) do + unify(map, struct, stack, context) end end @@ -327,7 +333,7 @@ defmodule Module.Types.Expr do # expr.fun(arg) def of_expr({{:., meta1, [expr1, fun]}, _meta2, args} = expr2, stack, context) do - context = Remote.check(expr1, fun, length(args), meta1, context) + context = Of.remote(expr1, fun, length(args), meta1, context) stack = push_expr_stack(expr2, stack) with {:ok, _expr_type, context} <- of_expr(expr1, stack, context), @@ -342,7 +348,7 @@ defmodule Module.Types.Expr do # &Foo.bar/1 def of_expr({:&, meta, [{:/, _, [{{:., _, [module, fun]}, _, []}, arity]}]}, _stack, context) when is_atom(module) and is_atom(fun) do - context = Remote.check(module, fun, arity, meta, context) + context = Of.remote(module, fun, arity, meta, context) {:ok, :dynamic, context} end @@ -363,51 +369,6 @@ defmodule Module.Types.Expr do end end - defp of_pairs(pairs, stack, context) do - map_reduce_ok(pairs, context, fn {key, value}, context -> - with {:ok, key_type, context} <- of_expr(key, stack, context), - {:ok, value_type, context} <- of_expr(value, stack, context), - do: {:ok, {:required, key_type, value_type}, context} - end) - end - - defp pairs_to_unions(pairs, context) do - # We are currently creating overlapping key types - - Enum.reduce(pairs, [], fn {kind_left, key, value_left}, pairs -> - case List.keyfind(pairs, key, 1) do - {kind_right, ^key, value_right} -> - kind = unify_kinds(kind_left, kind_right) - value = to_union([value_left, value_right], context) - List.keystore(pairs, key, 1, {kind, key, value}) - - nil -> - [{kind_left, key, value_left} | pairs] - end - end) - end - - defp map_update(map, args, additional_pairs, stack, context) do - with {:ok, map_type, context} <- of_expr(map, stack, context), - {:ok, arg_pairs, context} <- of_pairs(args, stack, context), - arg_pairs = pairs_to_unions(arg_pairs, context), - # Change value types to dynamic to reuse map unification for map updates - dynamic_value_pairs = - Enum.map(arg_pairs, fn {:required, key, _value} -> {:required, key, :dynamic} end), - args_type = {:map, additional_pairs ++ dynamic_value_pairs}, - {:ok, type, context} <- unify(args_type, map_type, stack, context) do - # Retrieve map type and overwrite with the new value types from the map update - {:map, pairs} = resolve_var(type, context) - - updated_pairs = - Enum.reduce(arg_pairs, pairs, fn {:required, key, value}, pairs -> - List.keyreplace(pairs, key, 1, {:required, key, value}) - end) - - {:ok, {:map, updated_pairs}, context} - end - end - defp for_clause({:<-, _, [left, expr]}, stack, context) do {pattern, guards} = extract_head([left]) diff --git a/lib/elixir/lib/module/types/helpers.ex b/lib/elixir/lib/module/types/helpers.ex index c3a543bbe83..d54634efd51 100644 --- a/lib/elixir/lib/module/types/helpers.ex +++ b/lib/elixir/lib/module/types/helpers.ex @@ -1,11 +1,7 @@ defmodule Module.Types.Helpers do + # AST and enumeration helpers. @moduledoc false - alias Module.Types.Infer - - @prefix quote(do: ...) - @suffix quote(do: ...) - @doc """ Guard function to check if an AST node is a variable. """ @@ -157,103 +153,6 @@ defmodule Module.Types.Helpers do end end - @doc """ - Handles binaries. - - In the stack, we add nodes such as <>, <<..., expr>>, etc, - based on the position of the expression within the binary. - """ - def of_binary([], _stack, context, _fun) do - {:ok, context} - end - - def of_binary([head], stack, context, fun) do - head_stack = push_expr_stack({:<<>>, get_meta(head), [head]}, stack) - of_binary_segment(head, head_stack, context, fun) - end - - def of_binary([head | tail], stack, context, fun) do - head_stack = push_expr_stack({:<<>>, get_meta(head), [head, @suffix]}, stack) - - case of_binary_segment(head, head_stack, context, fun) do - {:ok, context} -> of_binary_many(tail, stack, context, fun) - {:error, reason} -> {:error, reason} - end - end - - defp of_binary_many([last], stack, context, fun) do - last_stack = push_expr_stack({:<<>>, get_meta(last), [@prefix, last]}, stack) - of_binary_segment(last, last_stack, context, fun) - end - - defp of_binary_many([head | tail], stack, context, fun) do - head_stack = push_expr_stack({:<<>>, get_meta(head), [@prefix, head, @suffix]}, stack) - - case of_binary_segment(head, head_stack, context, fun) do - {:ok, context} -> of_binary_many(tail, stack, context, fun) - {:error, reason} -> {:error, reason} - end - end - - defp of_binary_segment({:"::", _meta, [expr, specifiers]}, stack, context, fun) do - expected_type = - collect_binary_specifier(specifiers, &binary_type(stack.context, &1)) || :integer - - utf? = collect_binary_specifier(specifiers, &utf_type?/1) - float? = collect_binary_specifier(specifiers, &float_type?/1) - - # Special case utf and float specifiers because they can be two types as literals - # but only a specific type as a variable in a pattern - cond do - stack.context == :pattern and utf? and is_binary(expr) -> - {:ok, context} - - stack.context == :pattern and float? and is_integer(expr) -> - {:ok, context} - - true -> - with {:ok, type, context} <- fun.(expr, stack, context), - {:ok, _type, context} <- Infer.unify(type, expected_type, stack, context), - do: {:ok, context} - end - end - - # TODO: Remove this clause once we properly handle comprehensions - defp of_binary_segment({:<-, _, _}, _stack, context, _fun) do - {:ok, context} - end - - # Collect binary type specifiers, - # from `<>` collect `integer` - defp collect_binary_specifier({:-, _meta, [left, right]}, fun) do - collect_binary_specifier(left, fun) || collect_binary_specifier(right, fun) - end - - defp collect_binary_specifier(other, fun) do - fun.(other) - end - - defp binary_type(:expr, {:float, _, _}), do: :number - defp binary_type(:expr, {:utf8, _, _}), do: {:union, [:integer, :binary]} - defp binary_type(:expr, {:utf16, _, _}), do: {:union, [:integer, :binary]} - defp binary_type(:expr, {:utf32, _, _}), do: {:union, [:integer, :binary]} - defp binary_type(:pattern, {:utf8, _, _}), do: :integer - defp binary_type(:pattern, {:utf16, _, _}), do: :integer - defp binary_type(:pattern, {:utf32, _, _}), do: :integer - defp binary_type(:pattern, {:float, _, _}), do: :float - defp binary_type(_context, {:integer, _, _}), do: :integer - defp binary_type(_context, {:bits, _, _}), do: :binary - defp binary_type(_context, {:bitstring, _, _}), do: :binary - defp binary_type(_context, {:bytes, _, _}), do: :binary - defp binary_type(_context, {:binary, _, _}), do: :binary - defp binary_type(_context, _specifier), do: nil - - defp utf_type?({specifier, _, _}), do: specifier in [:utf8, :utf16, :utf32] - defp utf_type?(_), do: false - - defp float_type?({:float, _, _}), do: true - defp float_type?(_), do: false - # TODO: Remove this and let multiple when be treated as multiple clauses, # meaning they will be intersection types def guards_to_or([]) do diff --git a/lib/elixir/lib/module/types/infer.ex b/lib/elixir/lib/module/types/infer.ex index 593e7574f10..546c06e519c 100644 --- a/lib/elixir/lib/module/types/infer.ex +++ b/lib/elixir/lib/module/types/infer.ex @@ -72,10 +72,7 @@ defmodule Module.Types.Infer do end defp do_unify({:map, source_pairs}, {:map, target_pairs}, stack, context) do - case unify_structs(source_pairs, target_pairs, stack, context) do - :ok -> unify_maps(source_pairs, target_pairs, stack, context) - {:error, reason} -> {:error, reason} - end + unify_maps(source_pairs, target_pairs, stack, context) end defp do_unify(source, :dynamic, _stack, context) do @@ -143,28 +140,10 @@ defmodule Module.Types.Infer do end end - defp unify_structs(left_pairs, right_pairs, stack, context) do - with {:ok, left_module} when is_atom(left_module) <- fetch_struct_pair(left_pairs), - {:ok, right_module} when is_atom(right_module) <- fetch_struct_pair(right_pairs) do - if left_module == right_module do - :ok - else - left = {:map, [{:required, {:atom, :__struct__}, left_module}]} - right = {:map, [{:required, {:atom, :__struct__}, right_module}]} - error(:unable_unify, {left, right, stack}, context) - end - else - _ -> :ok - end - end - # * All required keys on each side need to match to the other side. # * All optional keys on each side that do not match must be discarded. defp unify_maps(source_pairs, target_pairs, stack, context) do - source_pairs = expand_struct(source_pairs) - target_pairs = expand_struct(target_pairs) - {source_required, source_optional} = split_pairs(source_pairs) {target_required, target_optional} = split_pairs(target_pairs) @@ -175,18 +154,16 @@ defmodule Module.Types.Infer do {:ok, source_optional_pairs, context} <- unify_source_optional(source_optional, target_optional, stack, context), {:ok, target_optional_pairs, context} <- - unify_target_optional(target_optional, source_optional, stack, context), - pairs = - [ - source_required_pairs, - target_required_pairs, - source_optional_pairs, - target_optional_pairs - ] - |> Enum.concat() - # Remove duplicate pairs from matching in both left and right directions - |> Enum.uniq() - |> simplify_struct() do + unify_target_optional(target_optional, source_optional, stack, context) do + # Remove duplicate pairs from matching in both left and right directions + pairs = + Enum.uniq( + source_required_pairs ++ + target_required_pairs ++ + source_optional_pairs ++ + target_optional_pairs + ) + {:ok, {:map, pairs}, context} else {:error, :unify} -> @@ -498,55 +475,5 @@ defmodule Module.Types.Infer do [] end - def unify_kinds(:required, _), do: :required - def unify_kinds(_, :required), do: :required - def unify_kinds(:optional, :optional), do: :optional - defp error(type, reason, context), do: {:error, {type, reason, context}} - - # TODO: We should check if structs have keys that do not belong to them. - # This might not be the best place to do it since it will only be - # called if the type is unified. A post-pass walking over all - # inferred types might be better. - defp expand_struct(pairs) do - case fetch_struct_pair(pairs) do - {:ok, module} -> - struct_pairs = - Enum.flat_map(Map.from_struct(module.__struct__()), fn {key, _value} -> - if List.keyfind(pairs, {:atom, key}, 1) do - [] - else - [{:required, {:atom, key}, :dynamic}] - end - end) - - pairs ++ struct_pairs - - :error -> - pairs - end - end - - defp simplify_struct(pairs) do - case fetch_struct_pair(pairs) do - {:ok, module} -> - Enum.reduce(Map.from_struct(module.__struct__()), pairs, fn {key, _value}, pairs -> - case List.keyfind(pairs, {:atom, key}, 1) do - {_, _key, :dynamic} -> List.keydelete(pairs, {:atom, key}, 1) - _ -> pairs - end - end) - - :error -> - pairs - end - end - - # TODO: Resolve type variables if %{__struct__: var, ...} - defp fetch_struct_pair(pairs) do - case Enum.find(pairs, &match?({:required, {:atom, :__struct__}, {:atom, _}}, &1)) do - {:required, {:atom, :__struct__}, {:atom, module}} -> {:ok, module} - nil -> :error - end - end end diff --git a/lib/elixir/lib/module/types/of.ex b/lib/elixir/lib/module/types/of.ex new file mode 100644 index 00000000000..0daba1f477f --- /dev/null +++ b/lib/elixir/lib/module/types/of.ex @@ -0,0 +1,285 @@ +defmodule Module.Types.Of do + # Typing functionality shared between Expr and Pattern. + # Generic AST and Enum helpers go to Module.Types.Helpers. + @moduledoc false + + @prefix quote(do: ...) + @suffix quote(do: ...) + + alias Module.Types.Infer + alias Module.ParallelChecker + + import Module.Types.Helpers + + @doc """ + Handles open maps (with dynamic => dynamic). + """ + def open_map(args, stack, context, fun) do + with {:ok, pairs, context} <- of_pairs(args, stack, context, fun) do + {:ok, {:map, pairs_to_unions(pairs, context) ++ [{:optional, :dynamic, :dynamic}]}, context} + end + end + + @doc """ + Handles closed maps (without dynamic => dynamic). + """ + def closed_map(args, stack, context, fun) do + with {:ok, pairs, context} <- of_pairs(args, stack, context, fun) do + {:ok, {:map, pairs_to_unions(pairs, context)}, context} + end + end + + defp of_pairs(pairs, stack, context, fun) do + map_reduce_ok(pairs, context, fn {key, value}, context -> + with {:ok, key_type, context} <- fun.(key, stack, context), + {:ok, value_type, context} <- fun.(value, stack, context), + do: {:ok, {:required, key_type, value_type}, context} + end) + end + + defp pairs_to_unions(pairs, context) do + # We are currently creating overlapping key types + + Enum.reduce(pairs, [], fn {kind_left, key, value_left}, pairs -> + case List.keyfind(pairs, key, 1) do + {:required, ^key, value_right} -> + value = Infer.to_union([value_left, value_right], context) + List.keystore(pairs, key, 1, {:required, key, value}) + + nil -> + [{kind_left, key, value_left} | pairs] + end + end) + end + + @doc """ + Handles structs. + """ + def struct(struct, meta, context) do + context = remote(struct, :__struct__, 0, meta, context) + + entries = + for key <- Map.keys(struct.__struct__()), key != :__struct__ do + {:required, {:atom, key}, :dynamic} + end + + {:ok, {:map, [{:required, {:atom, :__struct__}, {:atom, struct}} | entries]}, context} + end + + ## Binary + + @doc """ + Handles binaries. + + In the stack, we add nodes such as <>, <<..., expr>>, etc, + based on the position of the expression within the binary. + """ + def binary([], _stack, context, _fun) do + {:ok, context} + end + + def binary([head], stack, context, fun) do + head_stack = push_expr_stack({:<<>>, get_meta(head), [head]}, stack) + binary_segment(head, head_stack, context, fun) + end + + def binary([head | tail], stack, context, fun) do + head_stack = push_expr_stack({:<<>>, get_meta(head), [head, @suffix]}, stack) + + case binary_segment(head, head_stack, context, fun) do + {:ok, context} -> binary_many(tail, stack, context, fun) + {:error, reason} -> {:error, reason} + end + end + + defp binary_many([last], stack, context, fun) do + last_stack = push_expr_stack({:<<>>, get_meta(last), [@prefix, last]}, stack) + binary_segment(last, last_stack, context, fun) + end + + defp binary_many([head | tail], stack, context, fun) do + head_stack = push_expr_stack({:<<>>, get_meta(head), [@prefix, head, @suffix]}, stack) + + case binary_segment(head, head_stack, context, fun) do + {:ok, context} -> binary_many(tail, stack, context, fun) + {:error, reason} -> {:error, reason} + end + end + + defp binary_segment({:"::", _meta, [expr, specifiers]}, stack, context, fun) do + expected_type = + collect_binary_specifier(specifiers, &binary_type(stack.context, &1)) || :integer + + utf? = collect_binary_specifier(specifiers, &utf_type?/1) + float? = collect_binary_specifier(specifiers, &float_type?/1) + + # Special case utf and float specifiers because they can be two types as literals + # but only a specific type as a variable in a pattern + cond do + stack.context == :pattern and utf? and is_binary(expr) -> + {:ok, context} + + stack.context == :pattern and float? and is_integer(expr) -> + {:ok, context} + + true -> + with {:ok, type, context} <- fun.(expr, stack, context), + {:ok, _type, context} <- Infer.unify(type, expected_type, stack, context), + do: {:ok, context} + end + end + + # TODO: Remove this clause once we properly handle comprehensions + defp binary_segment({:<-, _, _}, _stack, context, _fun) do + {:ok, context} + end + + # Collect binary type specifiers, + # from `<>` collect `integer` + defp collect_binary_specifier({:-, _meta, [left, right]}, fun) do + collect_binary_specifier(left, fun) || collect_binary_specifier(right, fun) + end + + defp collect_binary_specifier(other, fun) do + fun.(other) + end + + defp binary_type(:expr, {:float, _, _}), do: :number + defp binary_type(:expr, {:utf8, _, _}), do: {:union, [:integer, :binary]} + defp binary_type(:expr, {:utf16, _, _}), do: {:union, [:integer, :binary]} + defp binary_type(:expr, {:utf32, _, _}), do: {:union, [:integer, :binary]} + defp binary_type(:pattern, {:utf8, _, _}), do: :integer + defp binary_type(:pattern, {:utf16, _, _}), do: :integer + defp binary_type(:pattern, {:utf32, _, _}), do: :integer + defp binary_type(:pattern, {:float, _, _}), do: :float + defp binary_type(_context, {:integer, _, _}), do: :integer + defp binary_type(_context, {:bits, _, _}), do: :binary + defp binary_type(_context, {:bitstring, _, _}), do: :binary + defp binary_type(_context, {:bytes, _, _}), do: :binary + defp binary_type(_context, {:binary, _, _}), do: :binary + defp binary_type(_context, _specifier), do: nil + + defp utf_type?({specifier, _, _}), do: specifier in [:utf8, :utf16, :utf32] + defp utf_type?(_), do: false + + defp float_type?({:float, _, _}), do: true + defp float_type?(_), do: false + + ## Remote + + @doc """ + Handles remote calls. + """ + def remote(module, fun, arity, meta, context) when is_atom(module) do + # TODO: In the future we may want to warn for modules defined + # in the local context + if Keyword.get(meta, :context_module, false) and context.module != module do + context + else + ParallelChecker.preload_module(context.cache, module) + check_export(module, fun, arity, meta, context) + end + end + + def remote(_module, _fun, _arity, _meta, context), do: context + + defp check_export(module, fun, arity, meta, context) do + case ParallelChecker.fetch_export(context.cache, module, fun, arity) do + {:ok, :def, reason} -> + check_deprecated(module, fun, arity, reason, meta, context) + + {:ok, :defmacro, reason} -> + context = warn(meta, context, {:unrequired_module, module, fun, arity}) + check_deprecated(module, fun, arity, reason, meta, context) + + {:error, :module} -> + if warn_undefined?(module, fun, arity, context) do + warn(meta, context, {:undefined_module, module, fun, arity}) + else + context + end + + {:error, :function} -> + if warn_undefined?(module, fun, arity, context) do + exports = ParallelChecker.all_exports(context.cache, module) + warn(meta, context, {:undefined_function, module, fun, arity, exports}) + else + context + end + end + end + + defp check_deprecated(module, fun, arity, reason, meta, context) do + if reason do + warn(meta, context, {:deprecated, module, fun, arity, reason}) + else + context + end + end + + # The protocol code dispatches to unknown modules, so we ignore them here. + # + # try do + # SomeProtocol.Atom.__impl__ + # rescue + # ... + # end + # + # But for protocols we don't want to traverse the protocol code anyway. + # TODO: remove this clause once we no longer traverse the protocol code. + defp warn_undefined?(_module, :__impl__, 1, _context), do: false + defp warn_undefined?(_module, :module_info, 0, _context), do: false + defp warn_undefined?(_module, :module_info, 1, _context), do: false + defp warn_undefined?(:erlang, :orelse, 2, _context), do: false + defp warn_undefined?(:erlang, :andalso, 2, _context), do: false + + defp warn_undefined?(_, _, _, %{no_warn_undefined: :all}) do + false + end + + defp warn_undefined?(module, fun, arity, context) do + not Enum.any?(context.no_warn_undefined, &(&1 == module or &1 == {module, fun, arity})) + end + + defp warn(meta, context, warning) do + {fun, arity} = context.function + location = {context.file, meta[:line] || 0, {context.module, fun, arity}} + %{context | warnings: [{__MODULE__, warning, location} | context.warnings]} + end + + ## Warning formating + + def format_warning({:undefined_module, module, fun, arity}) do + [ + Exception.format_mfa(module, fun, arity), + " is undefined (module ", + inspect(module), + " is not available or is yet to be defined)" + ] + end + + def format_warning({:undefined_function, module, fun, arity, exports}) do + [ + Exception.format_mfa(module, fun, arity), + " is undefined or private", + UndefinedFunctionError.hint_for_loaded_module(module, fun, arity, exports) + ] + end + + def format_warning({:deprecated, module, fun, arity, reason}) do + [ + Exception.format_mfa(module, fun, arity), + " is deprecated. ", + reason + ] + end + + def format_warning({:unrequired_module, module, fun, arity}) do + [ + "you must require ", + inspect(module), + " before invoking the macro ", + Exception.format_mfa(module, fun, arity) + ] + end +end diff --git a/lib/elixir/lib/module/types/pattern.ex b/lib/elixir/lib/module/types/pattern.ex index 8e46ab1941f..ac5d2a58eb3 100644 --- a/lib/elixir/lib/module/types/pattern.ex +++ b/lib/elixir/lib/module/types/pattern.ex @@ -1,7 +1,7 @@ defmodule Module.Types.Pattern do @moduledoc false - alias Module.Types.Remote + alias Module.Types.Of import Module.Types.{Helpers, Infer} @doc """ @@ -46,7 +46,7 @@ defmodule Module.Types.Pattern do # <<...>>> def of_pattern({:<<>>, _meta, args}, stack, context) do - result = of_binary(args, stack, context, &of_pattern/3) + result = Of.binary(args, stack, context, &of_pattern/3) case result do {:ok, context} -> {:ok, :binary, context} @@ -166,30 +166,17 @@ defmodule Module.Types.Pattern do # %{...} def of_pattern({:%{}, _meta, args} = expr, stack, context) do stack = push_expr_stack(expr, stack) - - case of_pairs(args, stack, context) do - {:ok, pairs, context} -> - pairs = pairs_to_unions(pairs, context) ++ [{:optional, :dynamic, :dynamic}] - {:ok, {:map, pairs}, context} - - {:error, reason} -> - {:error, reason} - end + Of.open_map(args, stack, context, &of_pattern/3) end # %Struct{...} def of_pattern({:%, meta1, [module, {:%{}, _meta2, args}]} = expr, stack, context) when is_atom(module) do - context = Remote.check(module, :__struct__, 0, meta1, context) stack = push_expr_stack(expr, stack) - case of_pairs(args, stack, context) do - {:ok, pairs, context} -> - pairs = [{:required, {:atom, :__struct__}, {:atom, module}} | pairs] - {:ok, {:map, pairs}, context} - - {:error, reason} -> - {:error, reason} + with {:ok, struct, context} <- Of.struct(module, meta1, context), + {:ok, map, context} <- Of.open_map(args, stack, context, &of_pattern/3) do + unify(map, struct, stack, context) end end @@ -202,15 +189,8 @@ defmodule Module.Types.Pattern do when is_atom(var_context) do stack = push_expr_stack(expr, stack) - case of_pairs(args, stack, context) do - {:ok, pairs, context} -> - pairs = - [{:required, {:atom, :__struct__}, :atom}] ++ pairs ++ [{:optional, :dynamic, :dynamic}] - - {:ok, {:map, pairs}, context} - - {:error, reason} -> - {:error, reason} + with {:ok, {:map, pairs}, context} <- Of.open_map(args, stack, context, &of_pattern/3) do + {:ok, {:map, [{:required, {:atom, :__struct__}, :atom} | pairs]}, context} end end @@ -223,41 +203,16 @@ defmodule Module.Types.Pattern do def of_pattern({:%, _meta1, [var, {:%{}, _meta2, args}]} = expr, stack, context) do stack = push_expr_stack(expr, stack) - with {:ok, pairs, context} <- of_pairs(args, stack, context), - {var_type, context} = new_var(var, context), - {:ok, _, context} <- unify(var_type, :atom, stack, context) do - pairs = - [{:required, {:atom, :__struct__}, var_type}] ++ - pairs ++ [{:optional, :dynamic, :dynamic}] - - {:ok, {:map, pairs}, context} + with {var_type, context} = new_var(var, context), + {:ok, _, context} <- unify(var_type, :atom, stack, context), + {:ok, {:map, pairs}, context} <- Of.open_map(args, stack, context, &of_pattern/3) do + {:ok, {:map, [{:required, {:atom, :__struct__}, var_type} | pairs]}, context} end end - defp of_pairs(pairs, stack, context) do - map_reduce_ok(pairs, context, fn {key, value}, context -> - with {:ok, key_type, context} <- of_pattern(key, stack, context), - {:ok, value_type, context} <- of_pattern(value, stack, context), - do: {:ok, {:required, key_type, value_type}, context} - end) - end - - defp pairs_to_unions(pairs, context) do - # Maps only allow simple literal keys in patterns so - # we do not have to do subtype checking - - Enum.reduce(pairs, [], fn {kind_left, key, value_left}, pairs -> - case List.keyfind(pairs, key, 1) do - {kind_right, ^key, value_right} -> - kind = unify_kinds(kind_left, kind_right) - value = to_union([value_left, value_right], context) - List.keystore(pairs, key, 1, {kind, key, value}) - - nil -> - [{kind_left, key, value_left} | pairs] - end - end) - end + def unify_kinds(:required, _), do: :required + def unify_kinds(_, :required), do: :required + def unify_kinds(:optional, :optional), do: :optional ## GUARDS diff --git a/lib/elixir/lib/module/types/remote.ex b/lib/elixir/lib/module/types/remote.ex deleted file mode 100644 index cc0789fe75e..00000000000 --- a/lib/elixir/lib/module/types/remote.ex +++ /dev/null @@ -1,116 +0,0 @@ -defmodule Module.Types.Remote do - @moduledoc false - - alias Module.ParallelChecker - - def check(module, fun, arity, meta, context) when is_atom(module) do - # TODO: In the future we may want to warn for modules defined - # in the local context - if Keyword.get(meta, :context_module, false) and context.module != module do - context - else - ParallelChecker.preload_module(context.cache, module) - check_export(module, fun, arity, meta, context) - end - end - - def check(_module, _fun, _arity, _meta, context), do: context - - defp check_export(module, fun, arity, meta, context) do - case ParallelChecker.fetch_export(context.cache, module, fun, arity) do - {:ok, :def, reason} -> - check_deprecated(module, fun, arity, reason, meta, context) - - {:ok, :defmacro, reason} -> - context = warn(meta, context, {:unrequired_module, module, fun, arity}) - check_deprecated(module, fun, arity, reason, meta, context) - - {:error, :module} -> - if warn_undefined?(module, fun, arity, context) do - warn(meta, context, {:undefined_module, module, fun, arity}) - else - context - end - - {:error, :function} -> - if warn_undefined?(module, fun, arity, context) do - exports = ParallelChecker.all_exports(context.cache, module) - warn(meta, context, {:undefined_function, module, fun, arity, exports}) - else - context - end - end - end - - defp check_deprecated(module, fun, arity, reason, meta, context) do - if reason do - warn(meta, context, {:deprecated, module, fun, arity, reason}) - else - context - end - end - - # The protocol code dispatches to unknown modules, so we ignore them here. - # - # try do - # SomeProtocol.Atom.__impl__ - # rescue - # ... - # end - # - # But for protocols we don't want to traverse the protocol code anyway. - # TODO: remove this clause once we no longer traverse the protocol code. - defp warn_undefined?(_module, :__impl__, 1, _context), do: false - defp warn_undefined?(_module, :module_info, 0, _context), do: false - defp warn_undefined?(_module, :module_info, 1, _context), do: false - defp warn_undefined?(:erlang, :orelse, 2, _context), do: false - defp warn_undefined?(:erlang, :andalso, 2, _context), do: false - - defp warn_undefined?(_, _, _, %{no_warn_undefined: :all}) do - false - end - - defp warn_undefined?(module, fun, arity, context) do - not Enum.any?(context.no_warn_undefined, &(&1 == module or &1 == {module, fun, arity})) - end - - defp warn(meta, context, warning) do - {fun, arity} = context.function - location = {context.file, meta[:line] || 0, {context.module, fun, arity}} - %{context | warnings: [{__MODULE__, warning, location} | context.warnings]} - end - - def format_warning({:undefined_module, module, fun, arity}) do - [ - Exception.format_mfa(module, fun, arity), - " is undefined (module ", - inspect(module), - " is not available or is yet to be defined)" - ] - end - - def format_warning({:undefined_function, module, fun, arity, exports}) do - [ - Exception.format_mfa(module, fun, arity), - " is undefined or private", - UndefinedFunctionError.hint_for_loaded_module(module, fun, arity, exports) - ] - end - - def format_warning({:deprecated, module, fun, arity, reason}) do - [ - Exception.format_mfa(module, fun, arity), - " is deprecated. ", - reason - ] - end - - def format_warning({:unrequired_module, module, fun, arity}) do - [ - "you must require ", - inspect(module), - " before invoking the macro ", - Exception.format_mfa(module, fun, arity) - ] - end -end diff --git a/lib/elixir/src/elixir_compiler.erl b/lib/elixir/src/elixir_compiler.erl index 378ee7e3e6c..1403e7968d1 100644 --- a/lib/elixir/src/elixir_compiler.erl +++ b/lib/elixir/src/elixir_compiler.erl @@ -165,8 +165,8 @@ bootstrap_files() -> <<"lib/elixir/lib/list/chars.ex">>, <<"lib/elixir/lib/module/locals_tracker.ex">>, <<"lib/elixir/lib/module/parallel_checker.ex">>, - <<"lib/elixir/lib/module/types/remote.ex">>, <<"lib/elixir/lib/module/types/helpers.ex">>, + <<"lib/elixir/lib/module/types/of.ex">>, <<"lib/elixir/lib/module/types/infer.ex">>, <<"lib/elixir/lib/module/types/pattern.ex">>, <<"lib/elixir/lib/module/types/expr.ex">>, diff --git a/lib/elixir/test/elixir/module/types/integration_test.exs b/lib/elixir/test/elixir/module/types/integration_test.exs index a8e78c779c6..fc4ac5569f4 100644 --- a/lib/elixir/test/elixir/module/types/integration_test.exs +++ b/lib/elixir/test/elixir/module/types/integration_test.exs @@ -1217,7 +1217,9 @@ defmodule Module.Types.IntegrationTest do # a.ex:4 map.bar - where "map" was given the type %{foo: integer()} in: + expected one of the following fields: foo + + where "map" was given the type map() in: # a.ex:3 map = %{foo: 1} @@ -1235,7 +1237,7 @@ defmodule Module.Types.IntegrationTest do "a.ex" => """ defmodule A do def a(foo) do - %File.Stat{} = foo + %URI{} = foo foo.bar end end @@ -1248,10 +1250,12 @@ defmodule Module.Types.IntegrationTest do # a.ex:4 foo.bar - where "foo" was given the type %File.Stat{} in: + expected one of the following fields: __struct__, authority, fragment, host, path, port, query, scheme, userinfo + + where "foo" was given the type %URI{} in: # a.ex:3 - %File.Stat{} = foo + %URI{} = foo Conflict found at a.ex:4: A.a/1 diff --git a/lib/elixir/test/elixir/module/types/map_test.exs b/lib/elixir/test/elixir/module/types/map_test.exs index 55060baf1ee..66c04dca79b 100644 --- a/lib/elixir/test/elixir/module/types/map_test.exs +++ b/lib/elixir/test/elixir/module/types/map_test.exs @@ -24,20 +24,20 @@ defmodule Module.Types.MapTest do {:ok, {:map, [ - {:required, {:atom, :__struct__}, {:atom, Module.Types.MapTest.Struct}}, - {:required, {:atom, :bar}, :integer}, + {:required, {:atom, :foo}, {:atom, :atom}}, {:required, {:atom, :baz}, {:map, []}}, - {:required, {:atom, :foo}, {:atom, :atom}} + {:required, {:atom, :bar}, :integer}, + {:required, {:atom, :__struct__}, {:atom, Module.Types.MapTest.Struct}} ]}} assert quoted_expr(%:"Elixir.Module.Types.MapTest.Struct"{foo: 123, bar: :atom}) == {:ok, {:map, [ - {:required, {:atom, :__struct__}, {:atom, Module.Types.MapTest.Struct}}, - {:required, {:atom, :baz}, {:map, []}}, + {:required, {:atom, :bar}, {:atom, :atom}}, {:required, {:atom, :foo}, :integer}, - {:required, {:atom, :bar}, {:atom, :atom}} + {:required, {:atom, :baz}, {:map, []}}, + {:required, {:atom, :__struct__}, {:atom, Module.Types.MapTest.Struct}} ]}} end @@ -190,11 +190,11 @@ defmodule Module.Types.MapTest do assert quoted_expr([map], %{map | foo: :b}) == {:ok, - {:map, [{:optional, :dynamic, :dynamic}, {:required, {:atom, :foo}, {:atom, :b}}]}} + {:map, [{:required, {:atom, :foo}, {:atom, :b}}, {:optional, :dynamic, :dynamic}]}} assert {:error, {:unable_unify, - {{:map, [{:optional, :dynamic, :dynamic}, {:required, {:atom, :bar}, :dynamic}]}, + {{:map, [{:required, {:atom, :bar}, :dynamic}, {:optional, :dynamic, :dynamic}]}, {:map, [{:required, {:atom, :foo}, {:atom, :a}}]}, _}}} = quoted_expr( @@ -215,17 +215,41 @@ defmodule Module.Types.MapTest do {:ok, {:map, [ - {:required, {:atom, :__struct__}, {:atom, Module.Types.MapTest.Struct2}}, - {:required, {:atom, :field}, {:atom, :b}} + {:required, {:atom, :field}, {:atom, :b}}, + {:required, {:atom, :__struct__}, {:atom, Module.Types.MapTest.Struct2}} ]}} + # TODO: improve error message to translate to MULTIPLE missing fields assert {:error, {:unable_unify, {{:map, + [ + {:required, {:atom, :field}, {:atom, :b}}, + {:required, {:atom, :foo}, {:var, 1}}, + {:optional, :dynamic, :dynamic} + ]}, + {:map, [ {:required, {:atom, :__struct__}, {:atom, Module.Types.MapTest.Struct2}}, {:required, {:atom, :field}, :dynamic} - ]}, {:map, [{:required, {:atom, :field}, {:atom, :a}}]}, + ]}, + _}}} = + quoted_expr( + [map], + ( + _ = map.foo + %Module.Types.MapTest.Struct2{map | field: :b} + ) + ) + + assert {:error, + {:unable_unify, + {{:map, [{:required, {:atom, :field}, {:atom, :b}}]}, + {:map, + [ + {:required, {:atom, :__struct__}, {:atom, Module.Types.MapTest.Struct2}}, + {:required, {:atom, :field}, :dynamic} + ]}, _}}} = quoted_expr( ( @@ -238,18 +262,18 @@ defmodule Module.Types.MapTest do {:ok, {:map, [ - {:required, {:atom, :__struct__}, {:atom, Module.Types.MapTest.Struct2}}, - {:required, {:atom, :field}, {:atom, :b}} + {:required, {:atom, :field}, {:atom, :b}}, + {:required, {:atom, :__struct__}, {:atom, Module.Types.MapTest.Struct2}} ]}} assert {:error, {:unable_unify, {{:map, - [{:optional, :dynamic, :dynamic}, {:required, {:atom, :not_field}, :dynamic}]}, + [{:required, {:atom, :not_field}, :dynamic}, {:optional, :dynamic, :dynamic}]}, {:map, [ - {:required, {:atom, :__struct__}, {:atom, Module.Types.MapTest.Struct2}}, - {:required, {:atom, :field}, {:atom, nil}} + {:required, {:atom, :field}, {:atom, nil}}, + {:required, {:atom, :__struct__}, {:atom, Module.Types.MapTest.Struct2}} ]}, _}}} = quoted_expr( diff --git a/lib/elixir/test/elixir/module/types/pattern_test.exs b/lib/elixir/test/elixir/module/types/pattern_test.exs index 20f33f14b96..0f5573d43a7 100644 --- a/lib/elixir/test/elixir/module/types/pattern_test.exs +++ b/lib/elixir/test/elixir/module/types/pattern_test.exs @@ -137,16 +137,20 @@ defmodule Module.Types.PatternTest do {:ok, {:map, [ - {:required, {:atom, :__struct__}, {:atom, Module.Types.PatternTest.Struct}} + {:required, {:atom, :__struct__}, {:atom, Module.Types.PatternTest.Struct}}, + {:required, {:atom, :bar}, :dynamic}, + {:required, {:atom, :baz}, :dynamic}, + {:required, {:atom, :foo}, :dynamic} ]}} assert quoted_pattern(%:"Elixir.Module.Types.PatternTest.Struct"{foo: 123, bar: :atom}) == {:ok, {:map, [ - {:required, {:atom, :__struct__}, {:atom, Module.Types.PatternTest.Struct}}, + {:required, {:atom, :bar}, {:atom, :atom}}, {:required, {:atom, :foo}, :integer}, - {:required, {:atom, :bar}, {:atom, :atom}} + {:required, {:atom, :__struct__}, {:atom, Module.Types.PatternTest.Struct}}, + {:required, {:atom, :baz}, :dynamic} ]}} end