Perform return type inference and application across local calls (#13984)
This commit is contained in:
+6
-4
@@ -6,13 +6,15 @@ TODO.
|
||||
|
||||
* Type inference of patterns (typing inference of guards will be part of an upcoming release)
|
||||
|
||||
* [Support for tuples and lists as composite types](https://elixir-lang.org/blog/2024/08/28/typing-lists-and-tuples/) as well as type checking of their basic operations
|
||||
|
||||
* Type checking of all language constructs, except `for`, `with`, and closures
|
||||
* Type checking of all language constructs, including local and remote calls, except `for`, `with`, and closures
|
||||
|
||||
* Type checking of all `Kernel` and conversion functions inlined by the compiler
|
||||
|
||||
* Detection of clauses and patterns that never match
|
||||
* [Support for tuples and lists as composite types](https://elixir-lang.org/blog/2024/08/28/typing-lists-and-tuples/) as well as type checking of their basic operations
|
||||
|
||||
* Detection of clauses and patterns that will never match from `case`, `cond`, and `=`
|
||||
|
||||
* Detection of unused clauses from private functions
|
||||
|
||||
## ExUnit improvements
|
||||
|
||||
|
||||
@@ -797,15 +797,8 @@ defmodule Time do
|
||||
@doc since: "1.5.0"
|
||||
@spec convert!(Calendar.time(), Calendar.calendar()) :: t
|
||||
def convert!(time, calendar) do
|
||||
case convert(time, calendar) do
|
||||
{:ok, value} ->
|
||||
value
|
||||
|
||||
{:error, reason} ->
|
||||
raise ArgumentError,
|
||||
"cannot convert #{inspect(time)} to target calendar #{inspect(calendar)}, " <>
|
||||
"reason: #{inspect(reason)}"
|
||||
end
|
||||
{:ok, value} = convert(time, calendar)
|
||||
value
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
||||
@@ -1649,7 +1649,7 @@ defmodule Code do
|
||||
|
||||
* `:infer_signatures` (since v1.18.0) - when `false`, it disables module-local
|
||||
signature inference used when type checking remote calls to the compiled
|
||||
module. Type checking will be executed regardless of this value of this option.
|
||||
module. Type checking will be executed regardless of the value of this option.
|
||||
Defaults to `true`.
|
||||
|
||||
`mix test` automatically disables this option via the `:test_elixirc_options`
|
||||
|
||||
@@ -302,7 +302,7 @@ defmodule Kernel do
|
||||
@compile {:inline, bootstrapped?: 1}
|
||||
case :code.ensure_loaded(Kernel) do
|
||||
{:module, _} ->
|
||||
defp bootstrapped?(_), do: true
|
||||
defp bootstrapped?(module), do: is_atom(module)
|
||||
|
||||
{:error, _} ->
|
||||
defp bootstrapped?(module), do: :code.ensure_loaded(module) == {:module, module}
|
||||
@@ -3646,7 +3646,7 @@ defmodule Kernel do
|
||||
:ok
|
||||
|
||||
true ->
|
||||
pos = :elixir_locals.cache_env(__CALLER__)
|
||||
pos = :elixir_module.cache_env(__CALLER__)
|
||||
%{line: line, file: file, module: module} = __CALLER__
|
||||
|
||||
quote do
|
||||
@@ -5323,7 +5323,7 @@ defmodule Kernel do
|
||||
key
|
||||
end
|
||||
|
||||
pos = :elixir_locals.cache_env(env)
|
||||
pos = :elixir_module.cache_env(env)
|
||||
|
||||
quote do
|
||||
:elixir_def.store_definition(unquote(kind), unquote(store), unquote(pos))
|
||||
|
||||
@@ -196,7 +196,7 @@ defmodule Kernel.CLI do
|
||||
end
|
||||
|
||||
@elixir_internals [:elixir, :elixir_aliases, :elixir_expand, :elixir_compiler, :elixir_module] ++
|
||||
[:elixir_clauses, :elixir_lexical, :elixir_def, :elixir_map, :elixir_locals] ++
|
||||
[:elixir_clauses, :elixir_lexical, :elixir_def, :elixir_map] ++
|
||||
[:elixir_erl, :elixir_erl_clauses, :elixir_erl_compiler, :elixir_erl_pass] ++
|
||||
[Kernel.ErrorHandler, Module.ParallelChecker]
|
||||
|
||||
|
||||
@@ -138,7 +138,7 @@ defmodule Kernel.Typespec do
|
||||
case type_to_signature(expr) do
|
||||
{name, arity} = signature when signature in @reserved_signatures ->
|
||||
compile_error(
|
||||
:elixir_locals.get_cached_env(pos),
|
||||
:elixir_module.get_cached_env(pos),
|
||||
"type #{name}/#{arity} is a reserved type and it cannot be defined"
|
||||
)
|
||||
|
||||
@@ -247,7 +247,7 @@ defmodule Kernel.Typespec do
|
||||
|
||||
defp collect_defined_type_pairs(type_typespecs) do
|
||||
fun = fn {_kind, expr, pos}, type_pairs ->
|
||||
%{file: file, line: line} = env = :elixir_locals.get_cached_env(pos)
|
||||
%{file: file, line: line} = env = :elixir_module.get_cached_env(pos)
|
||||
|
||||
case type_to_signature(expr) do
|
||||
{name, arity} = type_pair ->
|
||||
@@ -292,7 +292,7 @@ defmodule Kernel.Typespec do
|
||||
|
||||
defp translate_type({kind, {:"::", _, [{name, meta, args}, definition]}, pos}, state)
|
||||
when is_list(meta) do
|
||||
caller = :elixir_locals.get_cached_env(pos)
|
||||
caller = :elixir_module.get_cached_env(pos)
|
||||
state = clean_local_state(state)
|
||||
|
||||
args =
|
||||
@@ -349,12 +349,12 @@ defmodule Kernel.Typespec do
|
||||
defp underspecified?(_kind, _arity, _spec), do: false
|
||||
|
||||
defp translate_spec({kind, {:when, _meta, [spec, guard]}, pos}, state) do
|
||||
caller = :elixir_locals.get_cached_env(pos)
|
||||
caller = :elixir_module.get_cached_env(pos)
|
||||
translate_spec(kind, spec, guard, caller, state)
|
||||
end
|
||||
|
||||
defp translate_spec({kind, spec, pos}, state) do
|
||||
caller = :elixir_locals.get_cached_env(pos)
|
||||
caller = :elixir_module.get_cached_env(pos)
|
||||
translate_spec(kind, spec, [], caller, state)
|
||||
end
|
||||
|
||||
|
||||
@@ -1415,15 +1415,7 @@ defmodule Module do
|
||||
def delete_definition(module, {name, arity})
|
||||
when is_atom(module) and is_atom(name) and is_integer(arity) do
|
||||
assert_not_readonly!(__ENV__.function, module)
|
||||
|
||||
case :elixir_def.take_definition(module, {name, arity}) do
|
||||
false ->
|
||||
false
|
||||
|
||||
_ ->
|
||||
:elixir_locals.yank({name, arity}, module)
|
||||
true
|
||||
end
|
||||
:elixir_def.take_definition(module, {name, arity}) != false
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -1452,8 +1444,7 @@ defmodule Module do
|
||||
"overridable because it was not defined"
|
||||
|
||||
clause ->
|
||||
neighbours = :elixir_locals.yank(tuple, module)
|
||||
:elixir_overridable.record_overridable(module, tuple, clause, neighbours)
|
||||
:elixir_overridable.record_overridable(module, tuple, clause)
|
||||
end
|
||||
|
||||
other ->
|
||||
|
||||
@@ -1,258 +0,0 @@
|
||||
# This is an Elixir module responsible for tracking
|
||||
# calls in order to extract Elixir modules' behaviour
|
||||
# during compilation time.
|
||||
#
|
||||
# ## Implementation
|
||||
#
|
||||
# The implementation uses ETS to track all dependencies
|
||||
# resembling a graph. The keys and what they point to are:
|
||||
#
|
||||
# * `:reattach` points to `{name, arity}`
|
||||
# * `{:local, {name, arity}}` points to `{{name, arity}, line, macro_dispatch?}`
|
||||
# * `{:import, {name, arity}}` points to `Module`
|
||||
#
|
||||
# This is built on top of the internal module tables.
|
||||
defmodule Module.LocalsTracker do
|
||||
@moduledoc false
|
||||
|
||||
@defmacros [:defmacro, :defmacrop]
|
||||
|
||||
@doc """
|
||||
Adds and tracks defaults for a definition into the tracker.
|
||||
"""
|
||||
def add_defaults({_set, bag}, kind, {name, arity} = pair, defaults, meta) do
|
||||
for i <- :lists.seq(arity - defaults, arity - 1) do
|
||||
put_edge(bag, {:local, {name, i}}, {pair, get_line(meta), kind in @defmacros})
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
Adds a local dispatch from-to the given target.
|
||||
"""
|
||||
def add_local({_set, bag}, from, to, meta, macro_dispatch?)
|
||||
when is_tuple(from) and is_tuple(to) and is_boolean(macro_dispatch?) do
|
||||
put_edge(bag, {:local, from}, {to, get_position(meta), macro_dispatch?})
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
Adds an import dispatch to the given target.
|
||||
"""
|
||||
def add_import({set, _bag}, function, module, imported)
|
||||
when is_tuple(function) and is_atom(module) do
|
||||
put_edge(set, {:import, imported}, module)
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
Yanks a local node. Returns its in and out vertices in a tuple.
|
||||
"""
|
||||
def yank({_set, bag}, local) do
|
||||
:lists.usort(take_out_neighbours(bag, {:local, local}))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Reattach a previously yanked node.
|
||||
"""
|
||||
def reattach({_set, bag}, tuple, kind, function, out_neighbours, meta) do
|
||||
for out_neighbour <- out_neighbours do
|
||||
put_edge(bag, {:local, function}, out_neighbour)
|
||||
end
|
||||
|
||||
# Make a call from the old function to the new one
|
||||
if function != tuple do
|
||||
put_edge(bag, {:local, function}, {tuple, get_line(meta), kind in @defmacros})
|
||||
end
|
||||
|
||||
# Finally marked the new one as reattached
|
||||
put_edge(bag, :reattach, tuple)
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
Collect all conflicting imports with the given functions
|
||||
"""
|
||||
def collect_imports_conflicts({set, _bag}, all_defined) do
|
||||
for {pair, _, meta, _} <- all_defined, n = out_neighbour(set, {:import, pair}) do
|
||||
{meta, {n, pair}}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Collect all unused definitions based on the private
|
||||
given, also accounting the expected number of default
|
||||
clauses a private function have.
|
||||
"""
|
||||
def collect_unused_locals({_set, bag}, all_defined, private) do
|
||||
reachable =
|
||||
Enum.reduce(all_defined, %{}, fn {pair, kind, _, _}, acc ->
|
||||
if kind in [:def, :defmacro] do
|
||||
reachable_from(bag, pair, acc)
|
||||
else
|
||||
acc
|
||||
end
|
||||
end)
|
||||
|
||||
reattached = :lists.usort(out_neighbours(bag, :reattach))
|
||||
{unreachable(reachable, reattached, private), collect_warnings(reachable, private)}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Collect undefined functions based on local calls and existing definitions.
|
||||
"""
|
||||
def collect_undefined_locals({set, bag}, all_defined, file) do
|
||||
undefined =
|
||||
for {pair, _, meta, _} <- all_defined,
|
||||
{{local_name, _} = local, position, macro_dispatch?} <-
|
||||
out_neighbours(bag, {:local, pair}),
|
||||
error = undefined_local_error(set, local, macro_dispatch?) do
|
||||
file =
|
||||
case Keyword.get(meta, :file) do
|
||||
{keep_file, _keep_line} -> keep_file
|
||||
nil -> file
|
||||
end
|
||||
|
||||
meta = build_meta(position, local_name)
|
||||
{pair, meta, file, local, error}
|
||||
end
|
||||
|
||||
:lists.usort(undefined)
|
||||
end
|
||||
|
||||
defp undefined_local_error(set, local, true) do
|
||||
case :ets.member(set, {:def, local}) do
|
||||
true -> false
|
||||
false -> :undefined_function
|
||||
end
|
||||
end
|
||||
|
||||
defp undefined_local_error(set, local, false) do
|
||||
try do
|
||||
if :ets.lookup_element(set, {:def, local}, 2) in @defmacros do
|
||||
:incorrect_dispatch
|
||||
else
|
||||
false
|
||||
end
|
||||
catch
|
||||
_, _ -> :undefined_function
|
||||
end
|
||||
end
|
||||
|
||||
defp unreachable(reachable, reattached, private) do
|
||||
for {tuple, kind, _, _} <- private,
|
||||
not reachable?(tuple, kind, reachable, reattached),
|
||||
do: tuple
|
||||
end
|
||||
|
||||
defp reachable?(tuple, :defmacrop, reachable, reattached) do
|
||||
# All private macros are unreachable unless they have been
|
||||
# reattached and they are reachable.
|
||||
:lists.member(tuple, reattached) and Map.has_key?(reachable, tuple)
|
||||
end
|
||||
|
||||
defp reachable?(tuple, :defp, reachable, _reattached) do
|
||||
Map.has_key?(reachable, tuple)
|
||||
end
|
||||
|
||||
defp collect_warnings(reachable, private) do
|
||||
:lists.foldl(&collect_warnings(&1, &2, reachable), [], private)
|
||||
end
|
||||
|
||||
defp collect_warnings({_, _, false, _}, acc, _reachable) do
|
||||
acc
|
||||
end
|
||||
|
||||
defp collect_warnings({tuple, kind, meta, 0}, acc, reachable) do
|
||||
if Map.has_key?(reachable, tuple) do
|
||||
acc
|
||||
else
|
||||
[{meta, {:unused_def, tuple, kind}} | acc]
|
||||
end
|
||||
end
|
||||
|
||||
defp collect_warnings({tuple, kind, meta, default}, acc, reachable) when default > 0 do
|
||||
{name, arity} = tuple
|
||||
min = arity - default
|
||||
max = arity
|
||||
|
||||
case min_reachable_default(max, min, :none, name, reachable) do
|
||||
:none -> [{meta, {:unused_def, tuple, kind}} | acc]
|
||||
^min -> acc
|
||||
^max -> [{meta, {:unused_args, tuple}} | acc]
|
||||
diff -> [{meta, {:unused_args, tuple, diff}} | acc]
|
||||
end
|
||||
end
|
||||
|
||||
defp min_reachable_default(max, min, last, name, reachable) when max >= min do
|
||||
case Map.has_key?(reachable, {name, max}) do
|
||||
true -> min_reachable_default(max - 1, min, max, name, reachable)
|
||||
false -> min_reachable_default(max - 1, min, last, name, reachable)
|
||||
end
|
||||
end
|
||||
|
||||
defp min_reachable_default(_max, _min, last, _name, _reachable) do
|
||||
last
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns all local nodes reachable from `vertex`.
|
||||
|
||||
By default, all public functions are reachable.
|
||||
A private function is only reachable if it has
|
||||
a public function that it invokes directly.
|
||||
"""
|
||||
def reachable_from({_, bag}, local) do
|
||||
bag
|
||||
|> reachable_from(local, %{})
|
||||
|> Map.keys()
|
||||
end
|
||||
|
||||
defp reachable_from(bag, local, vertices) do
|
||||
vertices = Map.put(vertices, local, true)
|
||||
|
||||
Enum.reduce(out_neighbours(bag, {:local, local}), vertices, fn {local, _line, _}, acc ->
|
||||
case acc do
|
||||
%{^local => true} -> acc
|
||||
_ -> reachable_from(bag, local, acc)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp get_line(meta), do: Keyword.get(meta, :line)
|
||||
defp get_position(meta), do: {get_line(meta), meta[:column]}
|
||||
|
||||
defp build_meta(position, function_name) do
|
||||
case position do
|
||||
{line, nil} -> [line: line]
|
||||
{line, col} -> :elixir_env.calculate_span([line: line, column: col], function_name)
|
||||
end
|
||||
end
|
||||
|
||||
## Lightweight digraph implementation
|
||||
|
||||
defp put_edge(d, from, to) do
|
||||
:ets.insert(d, {from, to})
|
||||
end
|
||||
|
||||
defp out_neighbour(d, from) do
|
||||
try do
|
||||
:ets.lookup_element(d, from, 2)
|
||||
catch
|
||||
:error, :badarg -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp out_neighbours(d, from) do
|
||||
try do
|
||||
:ets.lookup_element(d, from, 2)
|
||||
catch
|
||||
:error, :badarg -> []
|
||||
end
|
||||
end
|
||||
|
||||
defp take_out_neighbours(d, from) do
|
||||
Keyword.values(:ets.take(d, from))
|
||||
end
|
||||
end
|
||||
@@ -5,7 +5,6 @@ defmodule Module.ParallelChecker do
|
||||
|
||||
@type cache() :: {pid(), :ets.tid()}
|
||||
@type warning() :: term()
|
||||
@type kind() :: :def | :defmacro
|
||||
@type mode() :: :elixir | :erlang
|
||||
|
||||
@doc """
|
||||
@@ -48,7 +47,12 @@ defmodule Module.ParallelChecker do
|
||||
@doc """
|
||||
Spawns a process that runs the parallel checker.
|
||||
"""
|
||||
def spawn({pid, checker}, module, info, log?) do
|
||||
def spawn({pid, checker}, module, module_map, log?) do
|
||||
ets = :gen_server.call(checker, :ets, :infinity)
|
||||
inner_spawn(pid, checker, module, cache_from_module_map(ets, module_map), log?)
|
||||
end
|
||||
|
||||
defp inner_spawn(pid, checker, module, info, log?) do
|
||||
ref = make_ref()
|
||||
|
||||
spawned =
|
||||
@@ -59,17 +63,22 @@ defmodule Module.ParallelChecker do
|
||||
{^ref, :cache, ets} ->
|
||||
Process.link(pid)
|
||||
|
||||
module_map =
|
||||
if is_map(info) do
|
||||
info
|
||||
else
|
||||
case File.read(info) do
|
||||
{:ok, binary} -> maybe_module_map(binary, module)
|
||||
{:error, _} -> nil
|
||||
end
|
||||
module_tuple =
|
||||
cond do
|
||||
is_tuple(info) ->
|
||||
info
|
||||
|
||||
is_binary(info) ->
|
||||
with {:ok, binary} <- File.read(info),
|
||||
{:ok, {_, [debug_info: chunk]}} <- :beam_lib.chunks(binary, [:debug_info]),
|
||||
{:debug_info_v1, backend, data} = chunk,
|
||||
{:ok, module_map} <- backend.debug_info(:elixir_v1, module, data, []) do
|
||||
cache_from_module_map(ets, module_map)
|
||||
else
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
module_map && cache_from_module_map(ets, module_map)
|
||||
send(checker, {ref, :cached})
|
||||
|
||||
receive do
|
||||
@@ -78,8 +87,8 @@ defmodule Module.ParallelChecker do
|
||||
:erlang.put(:elixir_compiler_info, {pid, self()})
|
||||
|
||||
warnings =
|
||||
if module_map do
|
||||
check_module(module_map, {checker, ets}, log?)
|
||||
if module_tuple do
|
||||
check_module(module_tuple, {checker, ets}, log?)
|
||||
else
|
||||
[]
|
||||
end
|
||||
@@ -147,7 +156,7 @@ defmodule Module.ParallelChecker do
|
||||
log? = not match?({_, false}, value)
|
||||
|
||||
for {module, file} <- runtime_files do
|
||||
spawn({self(), checker}, module, file, log?)
|
||||
inner_spawn(self(), checker, module, file, log?)
|
||||
end
|
||||
|
||||
count = :gen_server.call(checker, :start, :infinity)
|
||||
@@ -212,29 +221,9 @@ defmodule Module.ParallelChecker do
|
||||
|
||||
## Module checking
|
||||
|
||||
defp check_module(module_map, cache, log?) do
|
||||
%{
|
||||
module: module,
|
||||
file: file,
|
||||
compile_opts: compile_opts,
|
||||
definitions: definitions,
|
||||
attributes: attributes,
|
||||
impls: impls
|
||||
} = module_map
|
||||
|
||||
# TODO: Match on anno directly in Elixir v1.22+
|
||||
line =
|
||||
case module_map do
|
||||
%{anno: anno} -> :erl_anno.line(anno)
|
||||
%{line: line} -> line
|
||||
end
|
||||
|
||||
behaviours = for {:behaviour, module} <- attributes, do: module
|
||||
|
||||
no_warn_undefined =
|
||||
compile_opts
|
||||
|> extract_no_warn_undefined()
|
||||
|> merge_compiler_no_warn_undefined()
|
||||
defp check_module(module_tuple, cache, log?) do
|
||||
{module, file, line, definitions, no_warn_undefined, behaviours, impls, after_verify} =
|
||||
module_tuple
|
||||
|
||||
behaviour_warnings =
|
||||
Module.Behaviour.check_behaviours_and_impls(
|
||||
@@ -253,13 +242,41 @@ defmodule Module.ParallelChecker do
|
||||
|> group_warnings()
|
||||
|> emit_warnings(log?)
|
||||
|
||||
module_map
|
||||
|> Map.get(:after_verify, [])
|
||||
|> Enum.each(fn {verify_mod, verify_fun} -> apply(verify_mod, verify_fun, [module]) end)
|
||||
Enum.each(after_verify, fn {verify_mod, verify_fun} ->
|
||||
apply(verify_mod, verify_fun, [module])
|
||||
end)
|
||||
|
||||
diagnostics
|
||||
end
|
||||
|
||||
defp module_map_to_module_tuple(module_map) do
|
||||
%{
|
||||
module: module,
|
||||
file: file,
|
||||
compile_opts: compile_opts,
|
||||
definitions: definitions,
|
||||
attributes: attributes,
|
||||
impls: impls,
|
||||
after_verify: after_verify
|
||||
} = module_map
|
||||
|
||||
# TODO: Match on anno directly in Elixir v1.22+
|
||||
line =
|
||||
case module_map do
|
||||
%{anno: anno} -> :erl_anno.line(anno)
|
||||
%{line: line} -> line
|
||||
end
|
||||
|
||||
behaviours = for {:behaviour, module} <- attributes, do: module
|
||||
|
||||
no_warn_undefined =
|
||||
compile_opts
|
||||
|> extract_no_warn_undefined()
|
||||
|> merge_compiler_no_warn_undefined()
|
||||
|
||||
{module, file, line, definitions, no_warn_undefined, behaviours, impls, after_verify}
|
||||
end
|
||||
|
||||
defp extract_no_warn_undefined(compile_opts) do
|
||||
for(
|
||||
{:no_warn_undefined, values} <- compile_opts,
|
||||
@@ -400,18 +417,6 @@ defmodule Module.ParallelChecker do
|
||||
_ -> %{}
|
||||
end
|
||||
|
||||
defp maybe_module_map(binary, module) when is_binary(binary) do
|
||||
# If a module was compiled without debug_info,
|
||||
# then there is no module_map for further verification.
|
||||
with {:ok, {_, [debug_info: chunk]}} <- :beam_lib.chunks(binary, [:debug_info]),
|
||||
{:debug_info_v1, backend, data} = chunk,
|
||||
{:ok, module_map} <- backend.debug_info(:elixir_v1, module, data, []) do
|
||||
module_map
|
||||
else
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp cache_from_module_map(ets, map) do
|
||||
exports =
|
||||
[{:__info__, 1}] ++
|
||||
@@ -419,6 +424,7 @@ defmodule Module.ParallelChecker do
|
||||
for({function, :def, _meta, _clauses} <- map.definitions, do: function)
|
||||
|
||||
cache_info(ets, map.module, exports, Map.new(map.deprecated), map.signatures, :elixir)
|
||||
module_map_to_module_tuple(map)
|
||||
end
|
||||
|
||||
defp cache_info(ets, module, exports, deprecated, sigs, mode) do
|
||||
|
||||
+314
-82
@@ -1,87 +1,284 @@
|
||||
defmodule Module.Types do
|
||||
@moduledoc false
|
||||
alias Module.Types.{Descr, Expr, Pattern, Helpers}
|
||||
|
||||
alias Module.Types.{Descr, Expr, Pattern}
|
||||
# The mode controls what happens on function application when
|
||||
# there are gradual arguments. Non-gradual arguments always
|
||||
# perform subtyping and return its output (OUT).
|
||||
#
|
||||
# * :strict - Requires types signatures (not implemented).
|
||||
# * Strong arrows with gradual performs subtyping and returns OUT
|
||||
# * Weak arrows with gradual performs subtyping and returns OUT
|
||||
#
|
||||
# * :static - Type signatures have been given.
|
||||
# * Strong arrows with gradual performs compatibility and returns OUT
|
||||
# * Weak arrows with gradual performs compatibility and returns dynamic()
|
||||
#
|
||||
# * :dynamic - Type signatures have not been given.
|
||||
# * Strong arrows with gradual performs compatibility and returns dynamic(OUT)
|
||||
# * Weak arrows with gradual performs compatibility and returns dynamic()
|
||||
#
|
||||
# * :infer - Same as :dynamic but skips remote calls.
|
||||
#
|
||||
# * :traversal - Focused mostly on traversing AST, skips most type system
|
||||
# operations. Used by macros and when skipping inference.
|
||||
#
|
||||
# The mode may also control exhaustiveness checks in the future (to be decided).
|
||||
# We may also want for applications with subtyping in dynamic mode to always
|
||||
# intersect with dynamic, but this mode may be too lax (to be decided based on
|
||||
# feedback).
|
||||
@modes [:static, :dynamic, :infer, :traversal]
|
||||
|
||||
# These functions are not inferred because they are added/managed by the compiler
|
||||
@no_infer [__protocol__: 1, behaviour_info: 1]
|
||||
|
||||
@doc false
|
||||
def infer(module, file, defs, env) do
|
||||
context = context()
|
||||
def infer(module, file, defs, private, defmacrop, env) do
|
||||
infer_signatures? = :elixir_config.get(:infer_signatures)
|
||||
defmacrop = Map.from_keys(defmacrop, [])
|
||||
|
||||
for {{fun, arity}, :def, _meta, clauses} <- defs,
|
||||
{fun, arity} not in @no_infer,
|
||||
into: %{} do
|
||||
stack = stack(:infer, file, module, {fun, arity}, :all, env)
|
||||
expected = List.duplicate(Descr.dynamic(), arity)
|
||||
finder =
|
||||
fn fun_arity ->
|
||||
case :lists.keyfind(fun_arity, 1, defs) do
|
||||
{_, kind, _, _} = clause -> {infer_mode(kind, infer_signatures?), clause}
|
||||
false -> false
|
||||
end
|
||||
end
|
||||
|
||||
handler = fn meta, fun_arity, stack, context ->
|
||||
case local_handler(meta, fun_arity, stack, context, finder) do
|
||||
false ->
|
||||
undefined_function!(:undefined_function, meta, fun_arity, stack, env)
|
||||
false
|
||||
|
||||
{kind, _, _} = triplet ->
|
||||
if (kind == :defmacro or kind == :defmacrop) and not Keyword.has_key?(meta, :super) do
|
||||
undefined_function!(:incorrect_dispatch, meta, fun_arity, stack, env)
|
||||
false
|
||||
else
|
||||
triplet
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
stack = stack(:infer, file, module, {:__info__, 1}, :all, env, handler)
|
||||
|
||||
{types, %{local_sigs: local_sigs}} =
|
||||
for {fun_arity, kind, meta, clauses} = def <- defs, reduce: {[], context()} do
|
||||
{types, context} ->
|
||||
cond do
|
||||
kind in [:def, :defmacro] ->
|
||||
finder = fn _ -> {infer_mode(kind, infer_signatures?), def} end
|
||||
{_kind, inferred, context} = local_handler(meta, fun_arity, stack, context, finder)
|
||||
|
||||
if infer_signatures? and kind == :def and fun_arity not in @no_infer do
|
||||
{[{fun_arity, inferred} | types], context}
|
||||
else
|
||||
{types, context}
|
||||
end
|
||||
|
||||
kind == :defmacrop and is_map_key(defmacrop, fun_arity) ->
|
||||
# Bypass the caching structure for defmacrop, that's because
|
||||
# we don't need them stored in the signatures when we perform
|
||||
# unreachable checks. This may cause defmacrop to be traversed
|
||||
# twice if it uses default arguments (which is the only way
|
||||
# to refer to another defmacrop in definitions) but that should
|
||||
# be cheap anyway.
|
||||
{_kind, _inferred, context} =
|
||||
local_handler(fun_arity, kind, meta, clauses, :traversal, stack, context)
|
||||
|
||||
{types, context}
|
||||
|
||||
true ->
|
||||
{types, context}
|
||||
end
|
||||
end
|
||||
|
||||
unreachable =
|
||||
for {fun_arity, _kind, _meta, _defaults} = info <- private,
|
||||
warn_unused_def(info, local_sigs, defmacrop, env),
|
||||
not is_map_key(local_sigs, fun_arity),
|
||||
do: fun_arity
|
||||
|
||||
{Map.new(types), unreachable}
|
||||
end
|
||||
|
||||
defp infer_mode(kind, infer_signatures?) do
|
||||
if infer_signatures? and kind in [:def, :defp], do: :infer, else: :traversal
|
||||
end
|
||||
|
||||
defp undefined_function!(reason, meta, {fun, arity}, stack, env) do
|
||||
env = %{env | function: stack.function, file: stack.file}
|
||||
tuple = {reason, {fun, arity}, stack.module}
|
||||
:elixir_errors.module_error(Helpers.with_span(meta, fun), env, __MODULE__, tuple)
|
||||
end
|
||||
|
||||
defp warn_unused_def({_fun_arity, _kind, false, _}, _reachable, _used, _env) do
|
||||
:ok
|
||||
end
|
||||
|
||||
defp warn_unused_def({fun_arity, kind, meta, 0}, reachable, used, env) do
|
||||
case is_map_key(reachable, fun_arity) or is_map_key(used, fun_arity) do
|
||||
true -> :ok
|
||||
false -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_def, fun_arity, kind})
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp warn_unused_def({tuple, kind, meta, default}, reachable, used, env) when default > 0 do
|
||||
{name, arity} = tuple
|
||||
min = arity - default
|
||||
max = arity
|
||||
|
||||
case min_reachable_default(max, min, :none, name, reachable, used) do
|
||||
:none -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_def, tuple, kind})
|
||||
^min -> :ok
|
||||
^max -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_args, tuple})
|
||||
diff -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_args, tuple, diff})
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp min_reachable_default(max, min, last, name, reachable, used) when max >= min do
|
||||
fun_arity = {name, max}
|
||||
|
||||
case is_map_key(reachable, fun_arity) or is_map_key(used, fun_arity) do
|
||||
true -> min_reachable_default(max - 1, min, max, name, reachable, used)
|
||||
false -> min_reachable_default(max - 1, min, last, name, reachable, used)
|
||||
end
|
||||
end
|
||||
|
||||
defp min_reachable_default(_max, _min, last, _name, _reachable, _used) do
|
||||
last
|
||||
end
|
||||
|
||||
@doc false
|
||||
def warnings(module, file, defs, no_warn_undefined, cache) do
|
||||
finder = fn fun_arity ->
|
||||
case :lists.keyfind(fun_arity, 1, defs) do
|
||||
{_, _, _, _} = clause -> {:dynamic, clause}
|
||||
false -> false
|
||||
end
|
||||
end
|
||||
|
||||
handler = &local_handler(&1, &2, &3, &4, finder)
|
||||
stack = stack(:dynamic, file, module, {:__info__, 1}, no_warn_undefined, cache, handler)
|
||||
|
||||
context =
|
||||
Enum.reduce(defs, context(), fn {fun_arity, _kind, meta, _clauses} = def, context ->
|
||||
finder = fn _ -> {:dynamic, def} end
|
||||
{_kind, _inferred, context} = local_handler(meta, fun_arity, stack, context, finder)
|
||||
context
|
||||
end)
|
||||
|
||||
context = warn_unused_clauses(defs, stack, context)
|
||||
context.warnings
|
||||
end
|
||||
|
||||
defp warn_unused_clauses(defs, stack, context) do
|
||||
for {fun_arity, pending} <- context.local_used, pending != [], reduce: context do
|
||||
context ->
|
||||
{_fun_arity, kind, _meta, clauses} = List.keyfind(defs, fun_arity, 0)
|
||||
{_kind, _inferred, mapping} = Map.fetch!(context.local_sigs, fun_arity)
|
||||
|
||||
clauses_indexes =
|
||||
for type_index <- pending, {clause_index, ^type_index} <- mapping, do: clause_index
|
||||
|
||||
Enum.reduce(clauses_indexes, context, fn clause_index, context ->
|
||||
{meta, _args, _guards, _body} = Enum.fetch!(clauses, clause_index)
|
||||
stack = %{stack | function: fun_arity}
|
||||
Helpers.warn(__MODULE__, {:unused_clause, kind, fun_arity}, meta, stack, context)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
defp local_handler(_meta, fun_arity, stack, context, finder) do
|
||||
case context.local_sigs do
|
||||
%{^fun_arity => {kind, inferred, _mapping}} ->
|
||||
{kind, inferred, context}
|
||||
|
||||
%{^fun_arity => kind} when is_atom(kind) ->
|
||||
{kind, :none, context}
|
||||
|
||||
local_sigs ->
|
||||
case finder.(fun_arity) do
|
||||
{mode, {fun_arity, kind, meta, clauses}} ->
|
||||
context = put_in(context.local_sigs, Map.put(local_sigs, fun_arity, kind))
|
||||
|
||||
{inferred, mapping, context} =
|
||||
local_handler(fun_arity, kind, meta, clauses, mode, stack, context)
|
||||
|
||||
context =
|
||||
update_in(context.local_sigs, &Map.put(&1, fun_arity, {kind, inferred, mapping}))
|
||||
|
||||
{kind, inferred, context}
|
||||
|
||||
false ->
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp local_handler({fun, arity} = fun_arity, kind, meta, clauses, mode, stack, context) do
|
||||
expected = List.duplicate(Descr.dynamic(), arity)
|
||||
stack = stack |> fresh_stack(mode, fun_arity) |> with_file_meta(meta)
|
||||
|
||||
{_, _, mapping, clauses_types, clauses_context} =
|
||||
Enum.reduce(clauses, {0, 0, [], [], context}, fn
|
||||
{meta, args, guards, body}, {index, total, mapping, inferred, context} ->
|
||||
context = fresh_context(context)
|
||||
|
||||
pair_types =
|
||||
Enum.reduce(clauses, [], fn {meta, args, guards, body}, inferred ->
|
||||
try do
|
||||
{args, context} =
|
||||
{args_types, context} =
|
||||
Pattern.of_head(args, guards, expected, :default, meta, stack, context)
|
||||
|
||||
{return, _context} = Expr.of_expr(body, stack, context)
|
||||
add_inferred(inferred, args, return, [])
|
||||
rescue
|
||||
e -> internal_error!(e, __STACKTRACE__, :def, meta, module, fun, args, guards, body)
|
||||
end
|
||||
end)
|
||||
{return_type, context} =
|
||||
Expr.of_expr(body, stack, context)
|
||||
|
||||
# TODO: Reuse context from patterns and guards
|
||||
{{fun, arity}, {:infer, Enum.reverse(pair_types)}}
|
||||
end
|
||||
{type_index, inferred} =
|
||||
add_inferred(inferred, args_types, return_type, total - 1, [])
|
||||
|
||||
if type_index == -1 do
|
||||
{index + 1, total + 1, [{index, total} | mapping], inferred, context}
|
||||
else
|
||||
{index + 1, total, [{index, type_index} | mapping], inferred, context}
|
||||
end
|
||||
rescue
|
||||
e ->
|
||||
internal_error!(e, __STACKTRACE__, kind, meta, fun, args, guards, body, stack)
|
||||
end
|
||||
end)
|
||||
|
||||
inferred = {:infer, Enum.reverse(clauses_types)}
|
||||
{inferred, mapping, restore_context(context, clauses_context)}
|
||||
end
|
||||
|
||||
# We check for term equality of types as an optimization
|
||||
# to reduce the amount of check we do at runtime.
|
||||
defp add_inferred([{args, existing_return} | tail], args, return, acc),
|
||||
do: Enum.reverse(acc, [{args, Descr.union(existing_return, return)} | tail])
|
||||
defp add_inferred([{args, existing_return} | tail], args, return, index, acc),
|
||||
do: {index, Enum.reverse(acc, [{args, Descr.union(existing_return, return)} | tail])}
|
||||
|
||||
defp add_inferred([head | tail], args, return, acc),
|
||||
do: add_inferred(tail, args, return, [head | acc])
|
||||
defp add_inferred([head | tail], args, return, index, acc),
|
||||
do: add_inferred(tail, args, return, index - 1, [head | acc])
|
||||
|
||||
defp add_inferred([], args, return, acc),
|
||||
do: [{args, return} | Enum.reverse(acc)]
|
||||
defp add_inferred([], args, return, -1, acc),
|
||||
do: {-1, [{args, return} | Enum.reverse(acc)]}
|
||||
|
||||
@doc false
|
||||
def warnings(module, file, defs, no_warn_undefined, cache) do
|
||||
context = context()
|
||||
|
||||
Enum.flat_map(defs, fn {{fun, arity}, kind, meta, clauses} ->
|
||||
file = with_file_meta(meta, file)
|
||||
stack = stack(:dynamic, file, module, {fun, arity}, no_warn_undefined, cache)
|
||||
expected = List.duplicate(Descr.dynamic(), arity)
|
||||
|
||||
Enum.flat_map(clauses, fn {meta, args, guards, body} ->
|
||||
try do
|
||||
{_types, context} =
|
||||
Pattern.of_head(args, guards, expected, :default, meta, stack, context)
|
||||
|
||||
{_type, context} = Expr.of_expr(body, stack, context)
|
||||
context.warnings
|
||||
rescue
|
||||
e ->
|
||||
internal_error!(e, __STACKTRACE__, kind, meta, module, fun, args, guards, body)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
defp with_file_meta(meta, file) do
|
||||
defp with_file_meta(stack, meta) do
|
||||
case Keyword.fetch(meta, :file) do
|
||||
{:ok, {meta_file, _}} -> meta_file
|
||||
:error -> file
|
||||
{:ok, {meta_file, _}} -> %{stack | file: meta_file}
|
||||
:error -> stack
|
||||
end
|
||||
end
|
||||
|
||||
defp internal_error!(e, stack, kind, meta, module, fun, args, guards, body) do
|
||||
defp internal_error!(e, trace, kind, meta, fun, args, guards, body, stack) do
|
||||
def_expr = {kind, meta, [guards_to_expr(guards, {fun, [], args}), [do: body]]}
|
||||
|
||||
exception =
|
||||
RuntimeError.exception("""
|
||||
found error while checking types for #{Exception.format_mfa(module, fun, length(args))}:
|
||||
found error while checking types for #{Exception.format_mfa(stack.module, fun, length(args))}:
|
||||
|
||||
#{Exception.format_banner(:error, e, stack)}\
|
||||
|
||||
@@ -92,7 +289,7 @@ defmodule Module.Types do
|
||||
Please report this bug at: https://github.com/elixir-lang/elixir/issues
|
||||
""")
|
||||
|
||||
reraise exception, stack
|
||||
reraise exception, trace
|
||||
end
|
||||
|
||||
defp guards_to_expr([], left) do
|
||||
@@ -104,8 +301,8 @@ defmodule Module.Types do
|
||||
end
|
||||
|
||||
@doc false
|
||||
def stack(mode, file, module, function, no_warn_undefined, cache)
|
||||
when mode in [:static, :dynamic, :infer] do
|
||||
def stack(mode, file, module, function, no_warn_undefined, cache, handler)
|
||||
when mode in @modes do
|
||||
%{
|
||||
# The fallback meta used for literals in patterns and guards
|
||||
meta: [],
|
||||
@@ -119,29 +316,10 @@ defmodule Module.Types do
|
||||
no_warn_undefined: no_warn_undefined,
|
||||
# A tuple with cache information or a Macro.Env struct indicating no remote traversals
|
||||
cache: cache,
|
||||
# The mode controls what happens on function application when
|
||||
# there are gradual arguments. Non-gradual arguments always
|
||||
# perform subtyping and return its output (OUT).
|
||||
#
|
||||
# * :strict - Requires types signatures (not implemented).
|
||||
# * Strong arrows with gradual performs subtyping and returns OUT
|
||||
# * Weak arrows with gradual performs subtyping and returns OUT
|
||||
#
|
||||
# * :static - Type signatures have been given.
|
||||
# * Strong arrows with gradual performs compatibility and returns OUT
|
||||
# * Weak arrows with gradual performs compatibility and returns dynamic()
|
||||
#
|
||||
# * :dynamic - Type signatures have not been given.
|
||||
# * Strong arrows with gradual performs compatibility and returns dynamic(OUT)
|
||||
# * Weak arrows with gradual performs compatibility and returns dynamic()
|
||||
#
|
||||
# * :infer - Same as :dynamic but skips remote calls.
|
||||
#
|
||||
# The mode may also control exhaustiveness checks in the future (to be decided).
|
||||
# We may also want for applications with subtyping in dynamic mode to always
|
||||
# intersect with dynamic, but this mode may be too lax (to be decided based on
|
||||
# feedback).
|
||||
mode: mode
|
||||
# The mode to be used, see the @modes attribute
|
||||
mode: mode,
|
||||
# The function for handling local calls
|
||||
local_handler: handler
|
||||
}
|
||||
end
|
||||
|
||||
@@ -155,7 +333,61 @@ defmodule Module.Types do
|
||||
# Variables and arguments from patterns
|
||||
pattern_info: nil,
|
||||
# If type checking has found an error/failure
|
||||
failed: false
|
||||
failed: false,
|
||||
# Local signatures used by local handler
|
||||
local_sigs: %{},
|
||||
# Track which clauses have been used across private local calls
|
||||
local_used: %{}
|
||||
}
|
||||
end
|
||||
|
||||
defp fresh_stack(stack, mode, function) when mode in @modes do
|
||||
%{stack | mode: mode, function: function}
|
||||
end
|
||||
|
||||
defp fresh_context(context) do
|
||||
%{context | vars: %{}, failed: false}
|
||||
end
|
||||
|
||||
defp restore_context(%{vars: vars, failed: failed}, later_context) do
|
||||
%{later_context | vars: vars, failed: failed}
|
||||
end
|
||||
|
||||
## Diagnostics
|
||||
|
||||
def format_diagnostic({:unused_clause, kind, {fun, arity}}) do
|
||||
%{
|
||||
message: "this clause of #{kind} #{fun}/#{arity} is never used"
|
||||
}
|
||||
end
|
||||
|
||||
## Module errors
|
||||
|
||||
def format_error({:unused_args, {name, arity}}),
|
||||
do: "default values for the optional arguments in #{name}/#{arity} are never used"
|
||||
|
||||
def format_error({:unused_args, {name, arity}, count}) when arity - count == 1,
|
||||
do: "the default value for the last optional argument in #{name}/#{arity} is never used"
|
||||
|
||||
def format_error({:unused_args, {name, arity}, count}),
|
||||
do:
|
||||
"the default values for the last #{arity - count} optional arguments in #{name}/#{arity} are never used"
|
||||
|
||||
def format_error({:unused_def, {name, arity}, :defp}),
|
||||
do: "function #{name}/#{arity} is unused"
|
||||
|
||||
def format_error({:unused_def, {name, arity}, :defmacrop}),
|
||||
do: "macro #{name}/#{arity} is unused"
|
||||
|
||||
def format_error({:undefined_function, {f, a}, _})
|
||||
when {f, a} in [__info__: 1, behaviour_info: 1, module_info: 1, module_info: 0],
|
||||
do:
|
||||
"undefined function #{f}/#{a} (this function is auto-generated by the compiler and must always be called as a remote, as in __MODULE__.#{f}/#{a})"
|
||||
|
||||
def format_error({:undefined_function, {f, a}, module}),
|
||||
do:
|
||||
"undefined function #{f}/#{a} (expected #{inspect(module)} to define such a function or for it to be imported, but none are available)"
|
||||
|
||||
def format_error({:incorrect_dispatch, {f, a}, _module}),
|
||||
do: "cannot invoke macro #{f}/#{a} before its definition"
|
||||
end
|
||||
|
||||
@@ -0,0 +1,955 @@
|
||||
defmodule Module.Types.Apply do
|
||||
# Typing functionality shared between Expr and Pattern.
|
||||
# Generic AST and Enum helpers go to Module.Types.Helpers.
|
||||
@moduledoc false
|
||||
|
||||
# We limit the size of the union for two reasons:
|
||||
# To avoid really large outputs in reports and to
|
||||
# reduce the computation cost of inferred code.
|
||||
@max_clauses 16
|
||||
|
||||
alias Module.ParallelChecker
|
||||
import Module.Types.{Helpers, Descr}
|
||||
|
||||
## Signatures
|
||||
|
||||
# Define strong arrows found in the standard library.
|
||||
# A strong arrow means that, if a type outside of its
|
||||
# domain is given, an error is raised. We are also
|
||||
# ensuring that domains for the same function have
|
||||
# no overlaps.
|
||||
|
||||
# Remote for callback info functions
|
||||
|
||||
kw = fn kw ->
|
||||
kw
|
||||
|> Enum.map(fn {key, type} when is_atom(key) ->
|
||||
tuple([atom([key]), type])
|
||||
end)
|
||||
|> Enum.reduce(&union/2)
|
||||
|> list()
|
||||
end
|
||||
|
||||
fas = list(tuple([atom(), integer()]))
|
||||
|
||||
shared_info = [
|
||||
attributes: list(tuple([atom(), list(term())])),
|
||||
compile: kw.(version: list(integer()), source: list(integer()), options: list(term())),
|
||||
exports: fas,
|
||||
md5: binary(),
|
||||
module: atom()
|
||||
]
|
||||
|
||||
infos =
|
||||
%{
|
||||
behaviour_info: [
|
||||
callbacks: fas,
|
||||
optional_callbacks: fas
|
||||
],
|
||||
module_info: [functions: fas, nifs: fas] ++ shared_info,
|
||||
__info__:
|
||||
[
|
||||
deprecated: list(tuple([tuple([atom(), integer()]), binary()])),
|
||||
exports_md5: binary(),
|
||||
functions: fas,
|
||||
macros: fas,
|
||||
struct:
|
||||
list(closed_map(default: term(), field: atom(), required: boolean()))
|
||||
|> union(atom([nil]))
|
||||
] ++ shared_info,
|
||||
__protocol__: [
|
||||
module: atom(),
|
||||
functions: fas,
|
||||
consolidated?: boolean(),
|
||||
impls: union(atom([:not_consolidated]), tuple([atom([:consolidated]), list(atom())]))
|
||||
]
|
||||
}
|
||||
|
||||
for {name, clauses} <- infos do
|
||||
domain = atom(Keyword.keys(clauses))
|
||||
clauses = Enum.map(clauses, fn {key, return} -> {[atom([key])], return} end)
|
||||
|
||||
defp signature(unquote(name), 1) do
|
||||
{:strong, [unquote(Macro.escape(domain))], unquote(Macro.escape(clauses))}
|
||||
end
|
||||
end
|
||||
|
||||
defp signature(:module_info, 0) do
|
||||
{:strong, nil, [{[], unquote(Macro.escape(kw.(infos.module_info)))}]}
|
||||
end
|
||||
|
||||
defp signature(_, _), do: :none
|
||||
|
||||
# Remote for compiler functions
|
||||
|
||||
mfargs = [atom(), atom(), list(term())]
|
||||
|
||||
send_destination =
|
||||
pid()
|
||||
|> union(reference())
|
||||
|> union(port())
|
||||
|> union(atom())
|
||||
|> union(tuple([atom(), atom()]))
|
||||
|
||||
basic_arith_2_args_clauses = [
|
||||
{[integer(), integer()], integer()},
|
||||
{[integer(), float()], float()},
|
||||
{[float(), integer()], float()},
|
||||
{[float(), float()], float()}
|
||||
]
|
||||
|
||||
is_clauses = [{[term()], boolean()}]
|
||||
|
||||
args_or_arity = union(list(term()), integer())
|
||||
args_or_none = union(list(term()), atom([:none]))
|
||||
extra_info = kw.(file: list(integer()), line: integer(), error_info: open_map())
|
||||
|
||||
raise_stacktrace =
|
||||
list(
|
||||
tuple([atom(), atom(), args_or_arity, extra_info])
|
||||
|> union(tuple([atom(), atom(), args_or_arity]))
|
||||
|> union(tuple([fun(), args_or_arity, extra_info]))
|
||||
|> union(tuple([fun(), args_or_arity]))
|
||||
)
|
||||
|
||||
and_signature =
|
||||
for left <- [true, false], right <- [true, false] do
|
||||
{[atom([left]), atom([right])], atom([left and right])}
|
||||
end
|
||||
|
||||
or_signature =
|
||||
for left <- [true, false], right <- [true, false] do
|
||||
{[atom([left]), atom([right])], atom([left or right])}
|
||||
end
|
||||
|
||||
for {mod, fun, clauses} <- [
|
||||
# :binary
|
||||
{:binary, :copy, [{[binary(), integer()], binary()}]},
|
||||
|
||||
# :erlang
|
||||
{:erlang, :+, [{[integer()], integer()}, {[float()], float()}]},
|
||||
{:erlang, :+, basic_arith_2_args_clauses},
|
||||
{:erlang, :-, [{[integer()], integer()}, {[float()], float()}]},
|
||||
{:erlang, :-, basic_arith_2_args_clauses},
|
||||
{:erlang, :*, basic_arith_2_args_clauses},
|
||||
{:erlang, :/, [{[union(integer(), float()), union(integer(), float())], float()}]},
|
||||
{:erlang, :"/=", [{[term(), term()], boolean()}]},
|
||||
{:erlang, :"=/=", [{[term(), term()], boolean()}]},
|
||||
{:erlang, :<, [{[term(), term()], boolean()}]},
|
||||
{:erlang, :"=<", [{[term(), term()], boolean()}]},
|
||||
{:erlang, :==, [{[term(), term()], boolean()}]},
|
||||
{:erlang, :"=:=", [{[term(), term()], boolean()}]},
|
||||
{:erlang, :>, [{[term(), term()], boolean()}]},
|
||||
{:erlang, :>=, [{[term(), term()], boolean()}]},
|
||||
{:erlang, :abs, [{[integer()], integer()}, {[float()], float()}]},
|
||||
{:erlang, :and, and_signature},
|
||||
{:erlang, :atom_to_binary, [{[atom()], binary()}]},
|
||||
{:erlang, :atom_to_list, [{[atom()], list(integer())}]},
|
||||
{:erlang, :band, [{[integer(), integer()], integer()}]},
|
||||
{:erlang, :binary_part, [{[binary(), integer(), integer()], binary()}]},
|
||||
{:erlang, :binary_to_atom, [{[binary()], atom()}]},
|
||||
{:erlang, :binary_to_existing_atom, [{[binary()], atom()}]},
|
||||
{:erlang, :binary_to_integer, [{[binary()], integer()}]},
|
||||
{:erlang, :binary_to_integer, [{[binary(), integer()], integer()}]},
|
||||
{:erlang, :binary_to_float, [{[binary()], float()}]},
|
||||
{:erlang, :bit_size, [{[binary()], integer()}]},
|
||||
{:erlang, :bnot, [{[integer()], integer()}]},
|
||||
{:erlang, :bor, [{[integer(), integer()], integer()}]},
|
||||
{:erlang, :bsl, [{[integer(), integer()], integer()}]},
|
||||
{:erlang, :bsr, [{[integer(), integer()], integer()}]},
|
||||
{:erlang, :bxor, [{[integer(), integer()], integer()}]},
|
||||
{:erlang, :byte_size, [{[binary()], integer()}]},
|
||||
{:erlang, :ceil, [{[union(integer(), float())], integer()}]},
|
||||
{:erlang, :div, [{[integer(), integer()], integer()}]},
|
||||
{:erlang, :error, [{[term()], none()}]},
|
||||
{:erlang, :error, [{[term(), args_or_none], none()}]},
|
||||
{:erlang, :error, [{[term(), args_or_none, kw.(error_info: open_map())], none()}]},
|
||||
{:erlang, :floor, [{[union(integer(), float())], integer()}]},
|
||||
{:erlang, :function_exported, [{[atom(), atom(), integer()], boolean()}]},
|
||||
{:erlang, :integer_to_binary, [{[integer()], binary()}]},
|
||||
{:erlang, :integer_to_binary, [{[integer(), integer()], binary()}]},
|
||||
{:erlang, :integer_to_list, [{[integer()], non_empty_list(integer())}]},
|
||||
{:erlang, :integer_to_list, [{[integer(), integer()], non_empty_list(integer())}]},
|
||||
{:erlang, :is_atom, is_clauses},
|
||||
{:erlang, :is_binary, is_clauses},
|
||||
{:erlang, :is_bitstring, is_clauses},
|
||||
{:erlang, :is_boolean, is_clauses},
|
||||
{:erlang, :is_float, is_clauses},
|
||||
{:erlang, :is_function, is_clauses},
|
||||
{:erlang, :is_function, [{[term(), integer()], boolean()}]},
|
||||
{:erlang, :is_integer, is_clauses},
|
||||
{:erlang, :is_list, is_clauses},
|
||||
{:erlang, :is_map, is_clauses},
|
||||
{:erlang, :is_map_key, [{[term(), open_map()], boolean()}]},
|
||||
{:erlang, :is_number, is_clauses},
|
||||
{:erlang, :is_pid, is_clauses},
|
||||
{:erlang, :is_port, is_clauses},
|
||||
{:erlang, :is_reference, is_clauses},
|
||||
{:erlang, :is_tuple, is_clauses},
|
||||
{:erlang, :length, [{[list(term())], integer()}]},
|
||||
{:erlang, :list_to_atom, [{[list(integer())], atom()}]},
|
||||
{:erlang, :list_to_existing_atom, [{[list(integer())], atom()}]},
|
||||
{:erlang, :list_to_float, [{[non_empty_list(integer())], float()}]},
|
||||
{:erlang, :list_to_integer, [{[non_empty_list(integer())], integer()}]},
|
||||
{:erlang, :list_to_integer, [{[non_empty_list(integer()), integer()], integer()}]},
|
||||
{:erlang, :make_ref, [{[], reference()}]},
|
||||
{:erlang, :map_size, [{[open_map()], integer()}]},
|
||||
{:erlang, :node, [{[], atom()}]},
|
||||
{:erlang, :node, [{[pid() |> union(reference()) |> union(port())], atom()}]},
|
||||
{:erlang, :not, [{[atom([false])], atom([true])}, {[atom([true])], atom([false])}]},
|
||||
{:erlang, :or, or_signature},
|
||||
{:erlang, :raise, [{[atom([:error, :exit, :throw]), term(), raise_stacktrace], none()}]},
|
||||
{:erlang, :rem, [{[integer(), integer()], integer()}]},
|
||||
{:erlang, :round, [{[union(integer(), float())], integer()}]},
|
||||
{:erlang, :self, [{[], pid()}]},
|
||||
{:erlang, :spawn, [{[fun()], pid()}]},
|
||||
{:erlang, :spawn, [{mfargs, pid()}]},
|
||||
{:erlang, :spawn_link, [{[fun()], pid()}]},
|
||||
{:erlang, :spawn_link, [{mfargs, pid()}]},
|
||||
{:erlang, :spawn_monitor, [{[fun()], tuple([reference(), pid()])}]},
|
||||
{:erlang, :spawn_monitor, [{mfargs, tuple([reference(), pid()])}]},
|
||||
{:erlang, :tuple_size, [{[open_tuple([])], integer()}]},
|
||||
{:erlang, :trunc, [{[union(integer(), float())], integer()}]},
|
||||
|
||||
# TODO: Replace term()/dynamic() by parametric types
|
||||
{:erlang, :++, [{[list(term()), term()], dynamic(list(term(), term()))}]},
|
||||
{:erlang, :--, [{[list(term()), list(term())], dynamic(list(term()))}]},
|
||||
{:erlang, :andalso, [{[boolean(), term()], dynamic()}]},
|
||||
{:erlang, :delete_element, [{[integer(), open_tuple([])], dynamic(open_tuple([]))}]},
|
||||
{:erlang, :hd, [{[non_empty_list(term(), term())], dynamic()}]},
|
||||
{:erlang, :element, [{[integer(), open_tuple([])], dynamic()}]},
|
||||
{:erlang, :insert_element,
|
||||
[{[integer(), open_tuple([]), term()], dynamic(open_tuple([]))}]},
|
||||
{:erlang, :list_to_tuple, [{[list(term())], dynamic(open_tuple([]))}]},
|
||||
{:erlang, :max, [{[term(), term()], dynamic()}]},
|
||||
{:erlang, :min, [{[term(), term()], dynamic()}]},
|
||||
{:erlang, :orelse, [{[boolean(), term()], dynamic()}]},
|
||||
{:erlang, :send, [{[send_destination, term()], dynamic()}]},
|
||||
{:erlang, :setelement, [{[integer(), open_tuple([]), term()], dynamic(open_tuple([]))}]},
|
||||
{:erlang, :tl, [{[non_empty_list(term(), term())], dynamic()}]},
|
||||
{:erlang, :tuple_to_list, [{[open_tuple([])], dynamic(list(term()))}]}
|
||||
] do
|
||||
[arity] = Enum.map(clauses, fn {args, _return} -> length(args) end) |> Enum.uniq()
|
||||
|
||||
true =
|
||||
Code.ensure_loaded?(mod) and
|
||||
(function_exported?(mod, fun, arity) or fun in [:orelse, :andalso])
|
||||
|
||||
domain_clauses =
|
||||
case clauses do
|
||||
[_] ->
|
||||
{:strong, nil, clauses}
|
||||
|
||||
_ ->
|
||||
domain =
|
||||
clauses
|
||||
|> Enum.map(fn {args, _} -> args end)
|
||||
|> Enum.zip_with(fn types -> Enum.reduce(types, &union/2) end)
|
||||
|
||||
{:strong, domain, clauses}
|
||||
end
|
||||
|
||||
defp signature(unquote(mod), unquote(fun), unquote(arity)),
|
||||
do: unquote(Macro.escape(domain_clauses))
|
||||
end
|
||||
|
||||
defp signature(_mod, _fun, _arity), do: :none
|
||||
|
||||
@doc """
|
||||
Applies a function in unknown modules.
|
||||
|
||||
Used only by info functions.
|
||||
"""
|
||||
def remote(_name, _args_types, _expr, %{mode: :traversal}, context) do
|
||||
{dynamic(), context}
|
||||
end
|
||||
|
||||
def remote(name, args_types, expr, stack, context) do
|
||||
arity = length(args_types)
|
||||
|
||||
case signature(name, arity) do
|
||||
:none -> {dynamic(), context}
|
||||
info -> apply_remote(info, args_types, expr, stack, context)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Applies a function in a given module.
|
||||
"""
|
||||
def remote(_module, _fun, _args_types, _expr, %{mode: :traversal}, context) do
|
||||
{dynamic(), context}
|
||||
end
|
||||
|
||||
def remote(:erlang, :element, [_, tuple], {_, meta, [index, _]} = expr, stack, context)
|
||||
when is_integer(index) do
|
||||
case tuple_fetch(tuple, index - 1) do
|
||||
{_optional?, value_type} ->
|
||||
{value_type, context}
|
||||
|
||||
:badtuple ->
|
||||
{error_type(), badremote_error(expr, [integer(), tuple], stack, context)}
|
||||
|
||||
reason ->
|
||||
{error_type(), error({reason, expr, tuple, index - 1, context}, meta, stack, context)}
|
||||
end
|
||||
end
|
||||
|
||||
def remote(
|
||||
:erlang,
|
||||
:insert_element,
|
||||
[_, tuple, value],
|
||||
{_, meta, [index, _, _]} = expr,
|
||||
stack,
|
||||
context
|
||||
)
|
||||
when is_integer(index) do
|
||||
case tuple_insert_at(tuple, index - 1, value) do
|
||||
value_type when is_descr(value_type) ->
|
||||
{value_type, context}
|
||||
|
||||
:badtuple ->
|
||||
{error_type(), badremote_error(expr, [integer(), tuple, value], stack, context)}
|
||||
|
||||
reason ->
|
||||
{error_type(), error({reason, expr, tuple, index - 2, context}, meta, stack, context)}
|
||||
end
|
||||
end
|
||||
|
||||
def remote(:erlang, :delete_element, [_, tuple], {_, meta, [index, _]} = expr, stack, context)
|
||||
when is_integer(index) do
|
||||
case tuple_delete_at(tuple, index - 1) do
|
||||
value_type when is_descr(value_type) ->
|
||||
{value_type, context}
|
||||
|
||||
:badtuple ->
|
||||
{error_type(), badremote_error(expr, [integer(), tuple], stack, context)}
|
||||
|
||||
reason ->
|
||||
{error_type(), error({reason, expr, tuple, index - 1, context}, meta, stack, context)}
|
||||
end
|
||||
end
|
||||
|
||||
def remote(:erlang, :make_tuple, [_, elem], {_, _meta, [size, _]}, _stack, context)
|
||||
when is_integer(size) and size >= 0 do
|
||||
{tuple(List.duplicate(elem, size)), context}
|
||||
end
|
||||
|
||||
def remote(:erlang, :hd, [list], expr, stack, context) do
|
||||
case list_hd(list) do
|
||||
{_, value_type} ->
|
||||
{value_type, context}
|
||||
|
||||
:badnonemptylist ->
|
||||
{error_type(), badremote_error(expr, [list], stack, context)}
|
||||
end
|
||||
end
|
||||
|
||||
def remote(:erlang, :tl, [list], expr, stack, context) do
|
||||
case list_tl(list) do
|
||||
{_, value_type} ->
|
||||
{value_type, context}
|
||||
|
||||
:badnonemptylist ->
|
||||
{error_type(), badremote_error(expr, [list], stack, context)}
|
||||
end
|
||||
end
|
||||
|
||||
def remote(:erlang, name, [left, right] = args_types, expr, stack, context)
|
||||
when name in [:>=, :"=<", :>, :<, :min, :max] do
|
||||
context =
|
||||
cond do
|
||||
stack.mode == :infer ->
|
||||
context
|
||||
|
||||
match?({false, _}, map_fetch(left, :__struct__)) or
|
||||
match?({false, _}, map_fetch(right, :__struct__)) ->
|
||||
warning = {:struct_comparison, expr, context}
|
||||
warn(__MODULE__, warning, elem(expr, 1), stack, context)
|
||||
|
||||
number_type?(left) and number_type?(right) ->
|
||||
context
|
||||
|
||||
disjoint?(left, right) ->
|
||||
warning = {:mismatched_comparison, expr, context}
|
||||
warn(__MODULE__, warning, elem(expr, 1), stack, context)
|
||||
|
||||
true ->
|
||||
context
|
||||
end
|
||||
|
||||
if name in [:min, :max] do
|
||||
{union(left, right), context}
|
||||
else
|
||||
{return(boolean(), args_types, stack), context}
|
||||
end
|
||||
end
|
||||
|
||||
def remote(:erlang, name, [left, right] = args_types, expr, stack, context)
|
||||
when name in [:==, :"/=", :"=:=", :"=/="] do
|
||||
context =
|
||||
cond do
|
||||
stack.mode == :infer ->
|
||||
context
|
||||
|
||||
name in [:==, :"/="] and number_type?(left) and number_type?(right) ->
|
||||
context
|
||||
|
||||
disjoint?(left, right) ->
|
||||
warning = {:mismatched_comparison, expr, context}
|
||||
warn(__MODULE__, warning, elem(expr, 1), stack, context)
|
||||
|
||||
true ->
|
||||
context
|
||||
end
|
||||
|
||||
{return(boolean(), args_types, stack), context}
|
||||
end
|
||||
|
||||
def remote(mod, name, args_types, expr, stack, context) do
|
||||
arity = length(args_types)
|
||||
|
||||
case :elixir_rewrite.inline(mod, name, arity) do
|
||||
{mod, name} ->
|
||||
remote(mod, name, args_types, expr, stack, context)
|
||||
|
||||
false ->
|
||||
{info, context} = signature(mod, name, arity, elem(expr, 1), stack, context)
|
||||
apply_remote(info, args_types, expr, stack, context)
|
||||
end
|
||||
end
|
||||
|
||||
defp apply_remote(info, args_types, expr, stack, context) do
|
||||
case apply_signature(info, args_types, stack) do
|
||||
{:ok, _indexes, type} ->
|
||||
{type, context}
|
||||
|
||||
{:error, domain, clauses} ->
|
||||
error = {:badremote, expr, args_types, domain, clauses, context}
|
||||
{error_type(), error(error, elem(expr, 1), stack, context)}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a mfa signature.
|
||||
|
||||
It returns either a tuple with the remote information and the context.
|
||||
The remote information may be one of:
|
||||
|
||||
* `:none` - no typing information found.
|
||||
|
||||
* `{:infer, clauses}` - clauses from inferences. You must check all
|
||||
all clauses and return the union between them. They are dynamic
|
||||
and they can only be converted into arrows by computing the union
|
||||
of all arguments.
|
||||
|
||||
* `{:strong, domain or nil, clauses}` - clauses from signatures. So far
|
||||
these are strong arrows with non-overlapping domains
|
||||
|
||||
"""
|
||||
def signature(module, fun, arity, meta, stack, context) when is_atom(module) do
|
||||
if Keyword.get(meta, :runtime_module, false) do
|
||||
{:none, context}
|
||||
else
|
||||
case signature(module, fun, arity) do
|
||||
:none -> export(module, fun, arity, meta, stack, context)
|
||||
clauses -> {clauses, context}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp export(_module, :module_info, arity, _meta, _stack, context) when arity in [0, 1] do
|
||||
{signature(:module_info, arity), context}
|
||||
end
|
||||
|
||||
defp export(_module, _fun, _arity, _meta, %{cache: %Macro.Env{}}, context) do
|
||||
{:none, context}
|
||||
end
|
||||
|
||||
defp export(module, fun, arity, meta, stack, context) do
|
||||
case ParallelChecker.fetch_export(stack.cache, module, fun, arity) do
|
||||
{:ok, mode, reason, info} ->
|
||||
info = if info == :none, do: signature(fun, arity), else: info
|
||||
{info, check_deprecated(mode, module, fun, arity, reason, meta, stack, context)}
|
||||
|
||||
{:error, type} ->
|
||||
context =
|
||||
if warn_undefined?(module, fun, arity, stack) do
|
||||
warn(__MODULE__, {:undefined, type, module, fun, arity}, meta, stack, context)
|
||||
else
|
||||
context
|
||||
end
|
||||
|
||||
{:none, context}
|
||||
end
|
||||
end
|
||||
|
||||
defp check_deprecated(:elixir, module, fun, arity, reason, meta, stack, context) do
|
||||
if reason do
|
||||
warn(__MODULE__, {:deprecated, module, fun, arity, reason}, meta, stack, context)
|
||||
else
|
||||
context
|
||||
end
|
||||
end
|
||||
|
||||
defp check_deprecated(:erlang, module, fun, arity, _reason, meta, stack, context) do
|
||||
case :otp_internal.obsolete(module, fun, arity) do
|
||||
{:deprecated, string} when is_list(string) ->
|
||||
reason = string |> List.to_string() |> :string.titlecase()
|
||||
warn(__MODULE__, {:deprecated, module, fun, arity, reason}, meta, stack, context)
|
||||
|
||||
{:deprecated, string, removal} when is_list(string) and is_list(removal) ->
|
||||
reason = string |> List.to_string() |> :string.titlecase()
|
||||
reason = "It will be removed in #{removal}. #{reason}"
|
||||
warn(__MODULE__, {:deprecated, module, fun, arity, reason}, meta, stack, context)
|
||||
|
||||
_ ->
|
||||
context
|
||||
end
|
||||
end
|
||||
|
||||
defp warn_undefined?(_, _, _, %{no_warn_undefined: :all}) do
|
||||
false
|
||||
end
|
||||
|
||||
defp warn_undefined?(module, fun, arity, stack) do
|
||||
not Enum.any?(stack.no_warn_undefined, &(&1 == module or &1 == {module, fun, arity}))
|
||||
end
|
||||
|
||||
## Local
|
||||
|
||||
@doc """
|
||||
Deal with local functions.
|
||||
"""
|
||||
def local(fun, args_types, {_, meta, _} = expr, stack, context) do
|
||||
fun_arity = {fun, length(args_types)}
|
||||
|
||||
case stack.local_handler.(meta, fun_arity, stack, context) do
|
||||
false ->
|
||||
{dynamic(), context}
|
||||
|
||||
{_kind, _info, context} when stack.mode == :traversal ->
|
||||
{dynamic(), context}
|
||||
|
||||
{kind, info, context} ->
|
||||
case apply_signature(info, args_types, stack) do
|
||||
{:ok, indexes, type} ->
|
||||
context =
|
||||
if stack.mode != :infer and kind == :defp do
|
||||
update_in(context.local_used[fun_arity], fn current ->
|
||||
if info == :none do
|
||||
[]
|
||||
else
|
||||
(current || used_from_clauses(info)) -- indexes
|
||||
end
|
||||
end)
|
||||
else
|
||||
context
|
||||
end
|
||||
|
||||
{type, context}
|
||||
|
||||
{:error, domain, clauses} ->
|
||||
error = {:badlocal, expr, args_types, domain, clauses, context}
|
||||
{error_type(), error(error, with_span(meta, fun), stack, context)}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp used_from_clauses({:infer, clauses}),
|
||||
do: Enum.with_index(clauses, fn _, i -> i end)
|
||||
|
||||
defp used_from_clauses({:strong, _, clauses}),
|
||||
do: Enum.with_index(clauses, fn _, i -> i end)
|
||||
|
||||
@doc """
|
||||
Deal with local captures.
|
||||
"""
|
||||
def local_capture(fun, arity, meta, stack, context) do
|
||||
fun_arity = {fun, arity}
|
||||
|
||||
case stack.local_handler.(meta, fun_arity, stack, context) do
|
||||
false ->
|
||||
{dynamic(fun()), context}
|
||||
|
||||
{_kind, _info, context} when stack.mode == :traversal ->
|
||||
{fun(), context}
|
||||
|
||||
{kind, _info, context} ->
|
||||
if stack.mode != :infer and kind == :defp do
|
||||
# Mark all clauses as used, as the function is being exported.
|
||||
{fun(), put_in(context.local_used[fun_arity], [])}
|
||||
else
|
||||
{fun(), context}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
## Application helpers
|
||||
|
||||
defp return(type, args_types, stack) do
|
||||
cond do
|
||||
stack.mode == :static -> type
|
||||
Enum.any?(args_types, &gradual?/1) -> dynamic(type)
|
||||
true -> type
|
||||
end
|
||||
end
|
||||
|
||||
defp apply_signature(:none, _args_types, _stack) do
|
||||
{:ok, [], dynamic()}
|
||||
end
|
||||
|
||||
defp apply_signature({:strong, nil, [{expected, return}] = clauses}, args_types, stack) do
|
||||
# Optimize single clauses as the domain is the single clause args.
|
||||
case zip_compatible?(args_types, expected) do
|
||||
true -> {:ok, [0], return(return, args_types, stack)}
|
||||
false -> {:error, expected, clauses}
|
||||
end
|
||||
end
|
||||
|
||||
defp apply_signature({:strong, domain, clauses}, args_types, stack) do
|
||||
# If the type is only gradual, the compatibility check is the same
|
||||
# as a non disjoint check. So we skip checking compatibility twice.
|
||||
with true <- zip_compatible_or_only_gradual?(args_types, domain),
|
||||
{count, used, returns} when count > 0 <- apply_clauses(clauses, args_types, 0, 0, [], []) do
|
||||
{:ok, used, returns |> Enum.reduce(&union/2) |> return(args_types, stack)}
|
||||
else
|
||||
_ -> {:error, domain, clauses}
|
||||
end
|
||||
end
|
||||
|
||||
defp apply_signature({:infer, clauses}, args_types, _stack) do
|
||||
case apply_clauses(clauses, args_types, 0, 0, [], []) do
|
||||
{0, [], []} ->
|
||||
domain =
|
||||
clauses
|
||||
|> Enum.map(fn {args, _} -> args end)
|
||||
|> Enum.zip_with(fn types -> Enum.reduce(types, &union/2) end)
|
||||
|
||||
{:error, domain, clauses}
|
||||
|
||||
{count, used, _returns} when count > @max_clauses ->
|
||||
{:ok, used, dynamic()}
|
||||
|
||||
{_count, used, returns} ->
|
||||
{:ok, used, returns |> Enum.reduce(&union/2) |> dynamic()}
|
||||
end
|
||||
end
|
||||
|
||||
defp apply_clauses([{expected, return} | clauses], args_types, index, count, used, returns) do
|
||||
if zip_not_disjoint?(args_types, expected) do
|
||||
apply_clauses(clauses, args_types, index + 1, count + 1, [index | used], [return | returns])
|
||||
else
|
||||
apply_clauses(clauses, args_types, index + 1, count, used, returns)
|
||||
end
|
||||
end
|
||||
|
||||
defp apply_clauses([], _args_types, _index, count, used, returns) do
|
||||
{count, used, returns}
|
||||
end
|
||||
|
||||
defp zip_compatible_or_only_gradual?([actual | actuals], [expected | expecteds]) do
|
||||
(only_gradual?(actual) or compatible?(actual, expected)) and
|
||||
zip_compatible_or_only_gradual?(actuals, expecteds)
|
||||
end
|
||||
|
||||
defp zip_compatible_or_only_gradual?([], []), do: true
|
||||
|
||||
defp zip_compatible?([actual | actuals], [expected | expecteds]) do
|
||||
compatible?(actual, expected) and zip_compatible?(actuals, expecteds)
|
||||
end
|
||||
|
||||
defp zip_compatible?([], []), do: true
|
||||
|
||||
defp zip_not_disjoint?([actual | actuals], [expected | expecteds]) do
|
||||
not disjoint?(actual, expected) and zip_not_disjoint?(actuals, expecteds)
|
||||
end
|
||||
|
||||
defp zip_not_disjoint?([], []), do: true
|
||||
|
||||
## Error handling
|
||||
|
||||
defp error(warning, meta, stack, context) do
|
||||
error(__MODULE__, warning, meta, stack, context)
|
||||
end
|
||||
|
||||
defp badremote_error({{:., _, [mod, fun]}, meta, _} = expr, args_types, stack, context) do
|
||||
{_type, domain, [{args, _} | _] = clauses} = signature(mod, fun, length(args_types))
|
||||
error({:badremote, expr, args_types, domain || args, clauses, context}, meta, stack, context)
|
||||
end
|
||||
|
||||
## Diagnosstics
|
||||
|
||||
def format_diagnostic({:badindex, expr, type, index, context}) do
|
||||
traces = collect_traces(expr, context)
|
||||
|
||||
%{
|
||||
details: %{typing_traces: traces},
|
||||
message:
|
||||
IO.iodata_to_binary([
|
||||
"""
|
||||
expected a tuple with at least #{pluralize(index + 1, "element", "elements")} in #{format_mfa(expr)}:
|
||||
|
||||
#{expr_to_string(expr) |> indent(4)}
|
||||
|
||||
the given type does not have the given index:
|
||||
|
||||
#{to_quoted_string(type) |> indent(4)}
|
||||
""",
|
||||
format_traces(traces)
|
||||
])
|
||||
}
|
||||
end
|
||||
|
||||
def format_diagnostic({:badlocal, expr, args_types, domain, clauses, context}) do
|
||||
traces = collect_traces(expr, context)
|
||||
converter = &Function.identity/1
|
||||
{fun, _, _} = expr
|
||||
|
||||
explanation =
|
||||
empty_arg_reason(args_types) ||
|
||||
"""
|
||||
but expected one of:
|
||||
#{clauses_args_to_quoted_string(clauses, converter)}
|
||||
"""
|
||||
|
||||
%{
|
||||
details: %{typing_traces: traces},
|
||||
message:
|
||||
IO.iodata_to_binary([
|
||||
"""
|
||||
incompatible types given to #{fun}/#{length(args_types)}:
|
||||
|
||||
#{expr_to_string(expr) |> indent(4)}
|
||||
|
||||
given types:
|
||||
|
||||
#{args_to_quoted_string(args_types, domain, converter) |> indent(4)}
|
||||
|
||||
""",
|
||||
explanation,
|
||||
format_traces(traces)
|
||||
])
|
||||
}
|
||||
end
|
||||
|
||||
def format_diagnostic({:badremote, expr, args_types, domain, clauses, context}) do
|
||||
traces = collect_traces(expr, context)
|
||||
{{:., _, [mod, fun]}, _, args} = expr
|
||||
{mod, fun, args, converter} = :elixir_rewrite.erl_to_ex(mod, fun, args)
|
||||
|
||||
explanation =
|
||||
empty_arg_reason(converter.(args_types)) ||
|
||||
"""
|
||||
but expected one of:
|
||||
#{clauses_args_to_quoted_string(clauses, converter)}
|
||||
"""
|
||||
|
||||
%{
|
||||
details: %{typing_traces: traces},
|
||||
message:
|
||||
IO.iodata_to_binary([
|
||||
"""
|
||||
incompatible types given to #{Exception.format_mfa(mod, fun, length(args))}:
|
||||
|
||||
#{expr_to_string(expr) |> indent(4)}
|
||||
|
||||
given types:
|
||||
|
||||
#{args_to_quoted_string(args_types, domain, converter) |> indent(4)}
|
||||
|
||||
""",
|
||||
explanation,
|
||||
format_traces(traces)
|
||||
])
|
||||
}
|
||||
end
|
||||
|
||||
def format_diagnostic({:mismatched_comparison, expr, context}) do
|
||||
traces = collect_traces(expr, context)
|
||||
|
||||
%{
|
||||
details: %{typing_traces: traces},
|
||||
message:
|
||||
IO.iodata_to_binary([
|
||||
"""
|
||||
comparison between distinct types found:
|
||||
|
||||
#{expr_to_string(expr) |> indent(4)}
|
||||
""",
|
||||
format_traces(traces),
|
||||
"""
|
||||
|
||||
While Elixir can compare across all types, you are comparing \
|
||||
across types which are always disjoint, and the result is either \
|
||||
always true or always false
|
||||
"""
|
||||
])
|
||||
}
|
||||
end
|
||||
|
||||
def format_diagnostic({:struct_comparison, expr, context}) do
|
||||
traces = collect_traces(expr, context)
|
||||
|
||||
%{
|
||||
details: %{typing_traces: traces},
|
||||
message:
|
||||
IO.iodata_to_binary([
|
||||
"""
|
||||
comparison with structs found:
|
||||
|
||||
#{expr_to_string(expr) |> indent(4)}
|
||||
""",
|
||||
format_traces(traces),
|
||||
"""
|
||||
|
||||
Comparison operators (>, <, >=, <=, min, and max) perform structural \
|
||||
and not semantic comparison. Comparing with a struct won't give meaningful \
|
||||
results. Structs that can be compared typically define a compare/2 function \
|
||||
within their modules that can be used for semantic comparison.
|
||||
"""
|
||||
])
|
||||
}
|
||||
end
|
||||
|
||||
def format_diagnostic({:undefined, :module, module, fun, arity}) do
|
||||
top =
|
||||
if fun == :__struct__ and arity == 0 do
|
||||
"struct #{inspect(module)}"
|
||||
else
|
||||
Exception.format_mfa(module, fun, arity)
|
||||
end
|
||||
|
||||
%{
|
||||
message:
|
||||
IO.iodata_to_binary([
|
||||
top,
|
||||
" is undefined (module ",
|
||||
inspect(module),
|
||||
" is not available or is yet to be defined)",
|
||||
UndefinedFunctionError.hint_for_missing_module(module, fun, arity)
|
||||
]),
|
||||
group: true
|
||||
}
|
||||
end
|
||||
|
||||
def format_diagnostic({:undefined, :function, module, :__struct__, 0}) do
|
||||
%{
|
||||
message:
|
||||
"struct #{inspect(module)} is undefined (there is such module but it does not define a struct)",
|
||||
group: true
|
||||
}
|
||||
end
|
||||
|
||||
def format_diagnostic({:undefined, :function, module, fun, arity}) do
|
||||
%{
|
||||
message:
|
||||
IO.iodata_to_binary([
|
||||
Exception.format_mfa(module, fun, arity),
|
||||
" is undefined or private",
|
||||
UndefinedFunctionError.hint_for_loaded_module(module, fun, arity)
|
||||
]),
|
||||
group: true
|
||||
}
|
||||
end
|
||||
|
||||
def format_diagnostic({:deprecated, module, fun, arity, reason}) do
|
||||
%{
|
||||
message:
|
||||
IO.iodata_to_binary([
|
||||
Exception.format_mfa(module, fun, arity),
|
||||
" is deprecated. ",
|
||||
reason
|
||||
]),
|
||||
group: true
|
||||
}
|
||||
end
|
||||
|
||||
defp empty_arg_reason(args_types) do
|
||||
if i = Enum.find_index(args_types, &empty?/1) do
|
||||
"""
|
||||
the #{integer_to_ordinal(i + 1)} argument is empty (often represented as none()), \
|
||||
most likely because it is the result of an expression that always fails, such as \
|
||||
a `raise` or a previous invalid call. This causes any function called with this \
|
||||
value to fail
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
||||
defp pluralize(1, singular, _), do: "1 #{singular}"
|
||||
defp pluralize(i, _, plural), do: "#{i} #{plural}"
|
||||
|
||||
defp format_mfa({{:., _, [mod, fun]}, _, args}) do
|
||||
{mod, fun, args, _} = :elixir_rewrite.erl_to_ex(mod, fun, args)
|
||||
Exception.format_mfa(mod, fun, length(args))
|
||||
end
|
||||
|
||||
## Algebra helpers
|
||||
|
||||
alias Inspect.Algebra, as: IA
|
||||
|
||||
defp clauses_args_to_quoted_string([{args, _return}], converter) do
|
||||
"\n " <> (clause_args_to_quoted_string(args, converter) |> indent(4))
|
||||
end
|
||||
|
||||
defp clauses_args_to_quoted_string(clauses, converter) do
|
||||
clauses
|
||||
|> Enum.with_index(fn {args, _return}, index ->
|
||||
"""
|
||||
|
||||
##{index + 1}
|
||||
#{clause_args_to_quoted_string(args, converter)}\
|
||||
"""
|
||||
|> indent(4)
|
||||
end)
|
||||
|> Enum.join("\n")
|
||||
end
|
||||
|
||||
defp clause_args_to_quoted_string(args, converter) do
|
||||
docs = Enum.map(args, &(&1 |> to_quoted() |> Code.Formatter.to_algebra()))
|
||||
args_docs_to_quoted_string(converter.(docs))
|
||||
end
|
||||
|
||||
defp args_to_quoted_string(args_types, domain, converter) do
|
||||
ansi? = IO.ANSI.enabled?()
|
||||
|
||||
docs =
|
||||
Enum.zip_with(args_types, domain, fn actual, expected ->
|
||||
doc = actual |> to_quoted() |> Code.Formatter.to_algebra()
|
||||
|
||||
cond do
|
||||
compatible?(actual, expected) -> doc
|
||||
ansi? -> IA.concat(IA.color(doc, IO.ANSI.red()), IA.color(IA.empty(), IO.ANSI.reset()))
|
||||
true -> IA.concat(["-", doc, "-"])
|
||||
end
|
||||
end)
|
||||
|
||||
args_docs_to_quoted_string(converter.(docs))
|
||||
end
|
||||
|
||||
defp args_docs_to_quoted_string(docs) do
|
||||
doc = IA.fold(docs, fn doc, acc -> IA.glue(IA.concat(doc, ","), acc) end)
|
||||
|
||||
wrapped_docs =
|
||||
case docs do
|
||||
[_] -> IA.concat("(", IA.concat(doc, ")"))
|
||||
_ -> IA.group(IA.glue(IA.nest(IA.glue("(", "", doc), 2), "", ")"))
|
||||
end
|
||||
|
||||
wrapped_docs
|
||||
|> IA.format(98)
|
||||
|> IO.iodata_to_binary()
|
||||
|> case do
|
||||
"(\n" <> _ = multiple_lines -> multiple_lines
|
||||
single_line -> binary_slice(single_line, 1..-2//1)
|
||||
end
|
||||
end
|
||||
|
||||
defp integer_to_ordinal(i) do
|
||||
case rem(i, 10) do
|
||||
1 when rem(i, 100) != 11 -> "#{i}st"
|
||||
2 when rem(i, 100) != 12 -> "#{i}nd"
|
||||
3 when rem(i, 100) != 13 -> "#{i}rd"
|
||||
_ -> "#{i}th"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,7 +1,7 @@
|
||||
defmodule Module.Types.Expr do
|
||||
@moduledoc false
|
||||
|
||||
alias Module.Types.{Of, Pattern}
|
||||
alias Module.Types.{Apply, Of, Pattern}
|
||||
import Module.Types.{Helpers, Descr}
|
||||
|
||||
14 = length(Macro.Env.__info__(:struct))
|
||||
@@ -78,14 +78,35 @@ defmodule Module.Types.Expr do
|
||||
{prefix, suffix} = unpack_list(list, [])
|
||||
{prefix, context} = Enum.map_reduce(prefix, context, &of_expr(&1, stack, &2))
|
||||
{suffix, context} = of_expr(suffix, stack, context)
|
||||
{non_empty_list(Enum.reduce(prefix, &union/2), suffix), context}
|
||||
|
||||
if stack.mode == :traversal do
|
||||
{dynamic(), context}
|
||||
else
|
||||
{non_empty_list(Enum.reduce(prefix, &union/2), suffix), context}
|
||||
end
|
||||
end
|
||||
|
||||
# {left, right}
|
||||
def of_expr({left, right}, stack, context) do
|
||||
{left, context} = of_expr(left, stack, context)
|
||||
{right, context} = of_expr(right, stack, context)
|
||||
{tuple([left, right]), context}
|
||||
|
||||
if stack.mode == :traversal do
|
||||
{dynamic(), context}
|
||||
else
|
||||
{tuple([left, right]), context}
|
||||
end
|
||||
end
|
||||
|
||||
# {...}
|
||||
def of_expr({:{}, _meta, exprs}, stack, context) do
|
||||
{types, context} = Enum.map_reduce(exprs, context, &of_expr(&1, stack, &2))
|
||||
|
||||
if stack.mode == :traversal do
|
||||
{dynamic(), context}
|
||||
else
|
||||
{tuple(types), context}
|
||||
end
|
||||
end
|
||||
|
||||
# <<...>>>
|
||||
@@ -103,12 +124,6 @@ defmodule Module.Types.Expr do
|
||||
{@stacktrace, context}
|
||||
end
|
||||
|
||||
# {...}
|
||||
def of_expr({:{}, _meta, exprs}, stack, context) do
|
||||
{types, context} = Enum.map_reduce(exprs, context, &of_expr(&1, stack, &2))
|
||||
{tuple(types), context}
|
||||
end
|
||||
|
||||
# left = right
|
||||
def of_expr({:=, _, [left_expr, right_expr]} = expr, stack, context) do
|
||||
{left_expr, right_expr} = repack_match(left_expr, right_expr)
|
||||
@@ -221,7 +236,7 @@ defmodule Module.Types.Expr do
|
||||
{head_type, context} = of_expr(head, stack, context)
|
||||
|
||||
context =
|
||||
if stack.mode == :infer do
|
||||
if stack.mode in [:infer, :traversal] do
|
||||
context
|
||||
else
|
||||
case truthness(head_type) do
|
||||
@@ -386,32 +401,47 @@ defmodule Module.Types.Expr do
|
||||
)
|
||||
when is_atom(name) and is_integer(arity) do
|
||||
{remote_type, context} = of_expr(remote, stack, context)
|
||||
|
||||
# TODO: We cannot return the unions of functions. Do we forbid this?
|
||||
# Do we check it is always the same return type? Do we simply say it is a function?
|
||||
{mods, context} = Of.modules(remote_type, name, arity, expr, meta, stack, context)
|
||||
|
||||
context =
|
||||
Enum.reduce(mods, context, &(Of.remote(&1, name, arity, meta, stack, &2) |> elem(1)))
|
||||
Enum.reduce(
|
||||
mods,
|
||||
context,
|
||||
&(Apply.signature(&1, name, arity, meta, stack, &2) |> elem(1))
|
||||
)
|
||||
|
||||
{fun(), context}
|
||||
{dynamic(fun()), context}
|
||||
end
|
||||
|
||||
# &foo/1
|
||||
# TODO: & &1
|
||||
def of_expr({:&, _meta, _arg}, _stack, context) do
|
||||
{fun(), context}
|
||||
# TODO: &foo/1
|
||||
def of_expr({:&, _meta, [{:/, _, [{fun, meta, _}, arity]}]}, stack, context) do
|
||||
Apply.local_capture(fun, arity, meta, stack, context)
|
||||
end
|
||||
|
||||
# TODO: local_call(arg)
|
||||
def of_expr({fun, _meta, args}, stack, context)
|
||||
# Super
|
||||
def of_expr({:super, meta, args} = expr, stack, context) when is_list(args) do
|
||||
{_kind, fun} = Keyword.fetch!(meta, :super)
|
||||
{args_types, context} = Enum.map_reduce(args, context, &of_expr(&1, stack, &2))
|
||||
Apply.local(fun, args_types, expr, stack, context)
|
||||
end
|
||||
|
||||
# Local calls
|
||||
def of_expr({fun, _meta, args} = expr, stack, context)
|
||||
when is_atom(fun) and is_list(args) do
|
||||
{_arg_types, context} = Enum.map_reduce(args, context, &of_expr(&1, stack, &2))
|
||||
{dynamic(), context}
|
||||
{args_types, context} = Enum.map_reduce(args, context, &of_expr(&1, stack, &2))
|
||||
Apply.local(fun, args_types, expr, stack, context)
|
||||
end
|
||||
|
||||
# var
|
||||
def of_expr(var, _stack, context) when is_var(var) do
|
||||
{Of.var(var, context), context}
|
||||
def of_expr(var, stack, context) when is_var(var) do
|
||||
if stack.mode == :traversal do
|
||||
{dynamic(), context}
|
||||
else
|
||||
{Of.var(var, context), context}
|
||||
end
|
||||
end
|
||||
|
||||
## Try
|
||||
@@ -427,9 +457,8 @@ defmodule Module.Types.Expr do
|
||||
{info, context} = Of.struct_info(exception, meta, stack, context)
|
||||
{Of.struct_type(exception, info, args), context}
|
||||
else
|
||||
# If the exception cannot be found or is invalid,
|
||||
# we call Of.remote/5 to emit a warning.
|
||||
{_, context} = Of.remote(exception, :__struct__, 0, meta, stack, context)
|
||||
# If the exception cannot be found or is invalid, fetch the signature to emit warnings.
|
||||
{_, context} = Apply.signature(exception, :__struct__, 0, meta, stack, context)
|
||||
{error_type(), context}
|
||||
end
|
||||
end)
|
||||
@@ -521,17 +550,17 @@ defmodule Module.Types.Expr do
|
||||
## General helpers
|
||||
|
||||
defp apply_many([], function, args_types, expr, stack, context) do
|
||||
Of.apply(function, args_types, expr, stack, context)
|
||||
Apply.remote(function, args_types, expr, stack, context)
|
||||
end
|
||||
|
||||
defp apply_many([mod], function, args_types, expr, stack, context) do
|
||||
Of.apply(mod, function, args_types, expr, stack, context)
|
||||
Apply.remote(mod, function, args_types, expr, stack, context)
|
||||
end
|
||||
|
||||
defp apply_many(mods, function, args_types, expr, stack, context) do
|
||||
{returns, context} =
|
||||
Enum.map_reduce(mods, context, fn mod, context ->
|
||||
Of.apply(mod, function, args_types, expr, stack, context)
|
||||
Apply.remote(mod, function, args_types, expr, stack, context)
|
||||
end)
|
||||
|
||||
{Enum.reduce(returns, &union/2), context}
|
||||
@@ -546,7 +575,7 @@ defmodule Module.Types.Expr do
|
||||
defp dynamic_unless_static({_, _} = output, %{mode: :static}), do: output
|
||||
defp dynamic_unless_static({type, context}, %{mode: _}), do: {dynamic(type), context}
|
||||
|
||||
defp of_clauses(clauses, expected, info, stack, {acc, context}) do
|
||||
defp of_clauses(clauses, expected, info, %{mode: mode} = stack, {acc, context}) do
|
||||
%{failed: failed?} = context
|
||||
|
||||
Enum.reduce(clauses, {acc, context}, fn {:->, meta, [head, body]}, {acc, context} ->
|
||||
@@ -554,7 +583,13 @@ defmodule Module.Types.Expr do
|
||||
{patterns, guards} = extract_head(head)
|
||||
{_types, context} = Pattern.of_head(patterns, guards, expected, info, meta, stack, context)
|
||||
{body, context} = of_expr(body, stack, context)
|
||||
{union(acc, body), set_failed(context, failed?)}
|
||||
context = set_failed(context, failed?)
|
||||
|
||||
if mode == :traversal do
|
||||
{dynamic(), context}
|
||||
else
|
||||
{union(acc, body), context}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
|
||||
@@ -38,6 +38,13 @@ defmodule Module.Types.Helpers do
|
||||
def get_meta({_, meta, _}), do: meta
|
||||
def get_meta(_other), do: []
|
||||
|
||||
@doc """
|
||||
Attaches span information.
|
||||
"""
|
||||
def with_span(meta, name) do
|
||||
:elixir_env.calculate_span(meta, name)
|
||||
end
|
||||
|
||||
## Warnings
|
||||
|
||||
@doc """
|
||||
|
||||
@@ -2,8 +2,6 @@ defmodule Module.Types.Of do
|
||||
# Typing functionality shared between Expr and Pattern.
|
||||
# Generic AST and Enum helpers go to Module.Types.Helpers.
|
||||
@moduledoc false
|
||||
|
||||
alias Module.ParallelChecker
|
||||
import Module.Types.{Helpers, Descr}
|
||||
|
||||
@prefix quote(do: ...)
|
||||
@@ -117,6 +115,17 @@ defmodule Module.Types.Of do
|
||||
@doc """
|
||||
Builds permutation of maps according to the given keys.
|
||||
"""
|
||||
def permutate_map(pairs, %{mode: :traversal} = stack, context, of_fun, _of_map) do
|
||||
context =
|
||||
Enum.reduce(pairs, context, fn {key, value}, context ->
|
||||
{_, context} = of_fun.(key, stack, context)
|
||||
{_, context} = of_fun.(value, stack, context)
|
||||
context
|
||||
end)
|
||||
|
||||
{dynamic(), context}
|
||||
end
|
||||
|
||||
def permutate_map(pairs, stack, context, of_fun, of_map) do
|
||||
{dynamic?, fallback, single, multiple, assert, context} =
|
||||
Enum.reduce(pairs, {false, none(), [], [], [], context}, fn
|
||||
@@ -215,7 +224,8 @@ defmodule Module.Types.Of do
|
||||
end
|
||||
|
||||
_ ->
|
||||
{_, context} = export(struct, :__struct__, 0, meta, stack, context)
|
||||
# Fetch the signature to validate for warnings.
|
||||
{_, context} = Module.Types.Apply.signature(struct, :__struct__, 0, meta, stack, context)
|
||||
|
||||
info =
|
||||
struct.__info__(:struct) ||
|
||||
@@ -333,7 +343,7 @@ defmodule Module.Types.Of do
|
||||
## Modules
|
||||
|
||||
@doc """
|
||||
Returns the modules.
|
||||
Returns modules in a type.
|
||||
|
||||
The call information is used on report reporting.
|
||||
"""
|
||||
@@ -348,568 +358,6 @@ defmodule Module.Types.Of do
|
||||
end
|
||||
end
|
||||
|
||||
## Remotes
|
||||
|
||||
# Define strong arrows found in the standard library.
|
||||
# A strong arrow means that, if a type outside of its
|
||||
# domain is given, an error is raised. We are also
|
||||
# ensuring that domains for the same function have
|
||||
# no overlaps.
|
||||
|
||||
# Remote for callback info functions
|
||||
|
||||
kw = fn kw ->
|
||||
kw
|
||||
|> Enum.map(fn {key, type} when is_atom(key) ->
|
||||
tuple([atom([key]), type])
|
||||
end)
|
||||
|> Enum.reduce(&union/2)
|
||||
|> list()
|
||||
end
|
||||
|
||||
fas = list(tuple([atom(), integer()]))
|
||||
|
||||
shared_info = [
|
||||
attributes: list(tuple([atom(), list(term())])),
|
||||
compile: kw.(version: list(integer()), source: list(integer()), options: list(term())),
|
||||
exports: fas,
|
||||
md5: binary(),
|
||||
module: atom()
|
||||
]
|
||||
|
||||
infos =
|
||||
%{
|
||||
behaviour_info: [
|
||||
callbacks: fas,
|
||||
optional_callbacks: fas
|
||||
],
|
||||
module_info: [functions: fas, nifs: fas] ++ shared_info,
|
||||
__info__:
|
||||
[
|
||||
deprecated: list(tuple([tuple([atom(), integer()]), binary()])),
|
||||
exports_md5: binary(),
|
||||
functions: fas,
|
||||
macros: fas,
|
||||
struct:
|
||||
list(closed_map(default: term(), field: atom(), required: boolean()))
|
||||
|> union(atom([nil]))
|
||||
] ++ shared_info,
|
||||
__protocol__: [
|
||||
module: atom(),
|
||||
functions: fas,
|
||||
consolidated?: boolean(),
|
||||
impls: union(atom([:not_consolidated]), tuple([atom([:consolidated]), list(atom())]))
|
||||
]
|
||||
}
|
||||
|
||||
for {name, clauses} <- infos do
|
||||
domain = atom(Keyword.keys(clauses))
|
||||
clauses = Enum.map(clauses, fn {key, return} -> {[atom([key])], return} end)
|
||||
|
||||
defp remote(unquote(name), 1) do
|
||||
{:strong, [unquote(Macro.escape(domain))], unquote(Macro.escape(clauses))}
|
||||
end
|
||||
end
|
||||
|
||||
defp remote(:module_info, 0) do
|
||||
{:strong, nil, [{[], unquote(Macro.escape(kw.(infos.module_info)))}]}
|
||||
end
|
||||
|
||||
defp remote(_, _), do: :none
|
||||
|
||||
# Remote for compiler functions
|
||||
|
||||
mfargs = [atom(), atom(), list(term())]
|
||||
|
||||
send_destination =
|
||||
pid()
|
||||
|> union(reference())
|
||||
|> union(port())
|
||||
|> union(atom())
|
||||
|> union(tuple([atom(), atom()]))
|
||||
|
||||
basic_arith_2_args_clauses = [
|
||||
{[integer(), integer()], integer()},
|
||||
{[integer(), float()], float()},
|
||||
{[float(), integer()], float()},
|
||||
{[float(), float()], float()}
|
||||
]
|
||||
|
||||
is_clauses = [{[term()], boolean()}]
|
||||
|
||||
args_or_arity = union(list(term()), integer())
|
||||
args_or_none = union(list(term()), atom([:none]))
|
||||
extra_info = kw.(file: list(integer()), line: integer(), error_info: open_map())
|
||||
|
||||
raise_stacktrace =
|
||||
list(
|
||||
tuple([atom(), atom(), args_or_arity, extra_info])
|
||||
|> union(tuple([atom(), atom(), args_or_arity]))
|
||||
|> union(tuple([fun(), args_or_arity, extra_info]))
|
||||
|> union(tuple([fun(), args_or_arity]))
|
||||
)
|
||||
|
||||
and_signature =
|
||||
for left <- [true, false], right <- [true, false] do
|
||||
{[atom([left]), atom([right])], atom([left and right])}
|
||||
end
|
||||
|
||||
or_signature =
|
||||
for left <- [true, false], right <- [true, false] do
|
||||
{[atom([left]), atom([right])], atom([left or right])}
|
||||
end
|
||||
|
||||
for {mod, fun, clauses} <- [
|
||||
# :binary
|
||||
{:binary, :copy, [{[binary(), integer()], binary()}]},
|
||||
|
||||
# :erlang
|
||||
{:erlang, :+, [{[integer()], integer()}, {[float()], float()}]},
|
||||
{:erlang, :+, basic_arith_2_args_clauses},
|
||||
{:erlang, :-, [{[integer()], integer()}, {[float()], float()}]},
|
||||
{:erlang, :-, basic_arith_2_args_clauses},
|
||||
{:erlang, :*, basic_arith_2_args_clauses},
|
||||
{:erlang, :/, [{[union(integer(), float()), union(integer(), float())], float()}]},
|
||||
{:erlang, :"/=", [{[term(), term()], boolean()}]},
|
||||
{:erlang, :"=/=", [{[term(), term()], boolean()}]},
|
||||
{:erlang, :<, [{[term(), term()], boolean()}]},
|
||||
{:erlang, :"=<", [{[term(), term()], boolean()}]},
|
||||
{:erlang, :==, [{[term(), term()], boolean()}]},
|
||||
{:erlang, :"=:=", [{[term(), term()], boolean()}]},
|
||||
{:erlang, :>, [{[term(), term()], boolean()}]},
|
||||
{:erlang, :>=, [{[term(), term()], boolean()}]},
|
||||
{:erlang, :abs, [{[integer()], integer()}, {[float()], float()}]},
|
||||
{:erlang, :and, and_signature},
|
||||
{:erlang, :atom_to_binary, [{[atom()], binary()}]},
|
||||
{:erlang, :atom_to_list, [{[atom()], list(integer())}]},
|
||||
{:erlang, :band, [{[integer(), integer()], integer()}]},
|
||||
{:erlang, :binary_part, [{[binary(), integer(), integer()], binary()}]},
|
||||
{:erlang, :binary_to_atom, [{[binary()], atom()}]},
|
||||
{:erlang, :binary_to_existing_atom, [{[binary()], atom()}]},
|
||||
{:erlang, :binary_to_integer, [{[binary()], integer()}]},
|
||||
{:erlang, :binary_to_integer, [{[binary(), integer()], integer()}]},
|
||||
{:erlang, :binary_to_float, [{[binary()], float()}]},
|
||||
{:erlang, :bit_size, [{[binary()], integer()}]},
|
||||
{:erlang, :bnot, [{[integer()], integer()}]},
|
||||
{:erlang, :bor, [{[integer(), integer()], integer()}]},
|
||||
{:erlang, :bsl, [{[integer(), integer()], integer()}]},
|
||||
{:erlang, :bsr, [{[integer(), integer()], integer()}]},
|
||||
{:erlang, :bxor, [{[integer(), integer()], integer()}]},
|
||||
{:erlang, :byte_size, [{[binary()], integer()}]},
|
||||
{:erlang, :ceil, [{[union(integer(), float())], integer()}]},
|
||||
{:erlang, :div, [{[integer(), integer()], integer()}]},
|
||||
{:erlang, :error, [{[term()], none()}]},
|
||||
{:erlang, :error, [{[term(), args_or_none], none()}]},
|
||||
{:erlang, :error, [{[term(), args_or_none, kw.(error_info: open_map())], none()}]},
|
||||
{:erlang, :floor, [{[union(integer(), float())], integer()}]},
|
||||
{:erlang, :function_exported, [{[atom(), atom(), integer()], boolean()}]},
|
||||
{:erlang, :integer_to_binary, [{[integer()], binary()}]},
|
||||
{:erlang, :integer_to_binary, [{[integer(), integer()], binary()}]},
|
||||
{:erlang, :integer_to_list, [{[integer()], non_empty_list(integer())}]},
|
||||
{:erlang, :integer_to_list, [{[integer(), integer()], non_empty_list(integer())}]},
|
||||
{:erlang, :is_atom, is_clauses},
|
||||
{:erlang, :is_binary, is_clauses},
|
||||
{:erlang, :is_bitstring, is_clauses},
|
||||
{:erlang, :is_boolean, is_clauses},
|
||||
{:erlang, :is_float, is_clauses},
|
||||
{:erlang, :is_function, is_clauses},
|
||||
{:erlang, :is_function, [{[term(), integer()], boolean()}]},
|
||||
{:erlang, :is_integer, is_clauses},
|
||||
{:erlang, :is_list, is_clauses},
|
||||
{:erlang, :is_map, is_clauses},
|
||||
{:erlang, :is_map_key, [{[term(), open_map()], boolean()}]},
|
||||
{:erlang, :is_number, is_clauses},
|
||||
{:erlang, :is_pid, is_clauses},
|
||||
{:erlang, :is_port, is_clauses},
|
||||
{:erlang, :is_reference, is_clauses},
|
||||
{:erlang, :is_tuple, is_clauses},
|
||||
{:erlang, :length, [{[list(term())], integer()}]},
|
||||
{:erlang, :list_to_atom, [{[list(integer())], atom()}]},
|
||||
{:erlang, :list_to_existing_atom, [{[list(integer())], atom()}]},
|
||||
{:erlang, :list_to_float, [{[non_empty_list(integer())], float()}]},
|
||||
{:erlang, :list_to_integer, [{[non_empty_list(integer())], integer()}]},
|
||||
{:erlang, :list_to_integer, [{[non_empty_list(integer()), integer()], integer()}]},
|
||||
{:erlang, :list_to_tuple, [{[list(term())], dynamic(open_tuple([]))}]},
|
||||
{:erlang, :make_ref, [{[], reference()}]},
|
||||
{:erlang, :map_size, [{[open_map()], integer()}]},
|
||||
{:erlang, :node, [{[], atom()}]},
|
||||
{:erlang, :node, [{[pid() |> union(reference()) |> union(port())], atom()}]},
|
||||
{:erlang, :not, [{[atom([false])], atom([true])}, {[atom([true])], atom([false])}]},
|
||||
{:erlang, :or, or_signature},
|
||||
{:erlang, :raise, [{[atom([:error, :exit, :throw]), term(), raise_stacktrace], none()}]},
|
||||
{:erlang, :rem, [{[integer(), integer()], integer()}]},
|
||||
{:erlang, :round, [{[union(integer(), float())], integer()}]},
|
||||
{:erlang, :self, [{[], pid()}]},
|
||||
{:erlang, :spawn, [{[fun()], pid()}]},
|
||||
{:erlang, :spawn, [{mfargs, pid()}]},
|
||||
{:erlang, :spawn_link, [{[fun()], pid()}]},
|
||||
{:erlang, :spawn_link, [{mfargs, pid()}]},
|
||||
{:erlang, :spawn_monitor, [{[fun()], tuple([reference(), pid()])}]},
|
||||
{:erlang, :spawn_monitor, [{mfargs, tuple([reference(), pid()])}]},
|
||||
{:erlang, :tuple_size, [{[open_tuple([])], integer()}]},
|
||||
{:erlang, :trunc, [{[union(integer(), float())], integer()}]},
|
||||
|
||||
# TODO: Replace term()/dynamic() by parametric types
|
||||
{:erlang, :++, [{[list(term()), term()], dynamic(list(term(), term()))}]},
|
||||
{:erlang, :--, [{[list(term()), list(term())], dynamic(list(term()))}]},
|
||||
{:erlang, :andalso, [{[boolean(), term()], dynamic()}]},
|
||||
{:erlang, :delete_element, [{[integer(), open_tuple([])], dynamic(open_tuple([]))}]},
|
||||
{:erlang, :hd, [{[non_empty_list(term(), term())], dynamic()}]},
|
||||
{:erlang, :element, [{[integer(), open_tuple([])], dynamic()}]},
|
||||
{:erlang, :insert_element,
|
||||
[{[integer(), open_tuple([]), term()], dynamic(open_tuple([]))}]},
|
||||
{:erlang, :max, [{[term(), term()], dynamic()}]},
|
||||
{:erlang, :min, [{[term(), term()], dynamic()}]},
|
||||
{:erlang, :orelse, [{[boolean(), term()], dynamic()}]},
|
||||
{:erlang, :send, [{[send_destination, term()], dynamic()}]},
|
||||
{:erlang, :setelement, [{[integer(), open_tuple([]), term()], dynamic(open_tuple([]))}]},
|
||||
{:erlang, :tl, [{[non_empty_list(term(), term())], dynamic()}]},
|
||||
{:erlang, :tuple_to_list, [{[open_tuple([])], dynamic(list(term()))}]}
|
||||
] do
|
||||
[arity] = Enum.map(clauses, fn {args, _return} -> length(args) end) |> Enum.uniq()
|
||||
|
||||
true =
|
||||
Code.ensure_loaded?(mod) and
|
||||
(function_exported?(mod, fun, arity) or fun in [:orelse, :andalso])
|
||||
|
||||
domain_clauses =
|
||||
case clauses do
|
||||
[_] ->
|
||||
{:strong, nil, clauses}
|
||||
|
||||
_ ->
|
||||
domain =
|
||||
clauses
|
||||
|> Enum.map(fn {args, _} -> args end)
|
||||
|> Enum.zip_with(fn types -> Enum.reduce(types, &union/2) end)
|
||||
|
||||
{:strong, domain, clauses}
|
||||
end
|
||||
|
||||
defp remote(unquote(mod), unquote(fun), unquote(arity)),
|
||||
do: unquote(Macro.escape(domain_clauses))
|
||||
end
|
||||
|
||||
defp remote(_mod, _fun, _arity), do: :none
|
||||
|
||||
@doc """
|
||||
Checks a module is a valid remote.
|
||||
|
||||
It returns either a tuple with the remote information and the context.
|
||||
The remote information may be one of:
|
||||
|
||||
* `:none` - no typing information found.
|
||||
|
||||
* `{:infer, clauses}` - clauses from inferences. You must check all
|
||||
all clauses and return the union between them. They are dynamic
|
||||
and they can only be converted into arrows by computing the union
|
||||
of all arguments.
|
||||
|
||||
* `{:strong, domain or nil, clauses}` - clauses from signatures. So far
|
||||
these are strong arrows with non-overlapping domains
|
||||
|
||||
"""
|
||||
def remote(module, fun, arity, meta, stack, context) when is_atom(module) do
|
||||
if Keyword.get(meta, :runtime_module, false) do
|
||||
{:none, context}
|
||||
else
|
||||
case remote(module, fun, arity) do
|
||||
:none -> export(module, fun, arity, meta, stack, context)
|
||||
clauses -> {clauses, context}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Applies a function in unknown modules.
|
||||
|
||||
Used only by info functions.
|
||||
"""
|
||||
def apply(name, args_types, expr, stack, context) do
|
||||
arity = length(args_types)
|
||||
|
||||
case remote(name, arity) do
|
||||
:none -> {dynamic(), context}
|
||||
info -> apply_remote(info, args_types, expr, stack, context)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Applies a function in a given module.
|
||||
"""
|
||||
def apply(:erlang, :element, [_, tuple], {_, meta, [index, _]} = expr, stack, context)
|
||||
when is_integer(index) do
|
||||
case tuple_fetch(tuple, index - 1) do
|
||||
{_optional?, value_type} ->
|
||||
{value_type, context}
|
||||
|
||||
:badtuple ->
|
||||
{error_type(), to_badapply_error(expr, [integer(), tuple], stack, context)}
|
||||
|
||||
reason ->
|
||||
{error_type(), error({reason, expr, tuple, index - 1, context}, meta, stack, context)}
|
||||
end
|
||||
end
|
||||
|
||||
def apply(
|
||||
:erlang,
|
||||
:insert_element,
|
||||
[_, tuple, value],
|
||||
{_, meta, [index, _, _]} = expr,
|
||||
stack,
|
||||
context
|
||||
)
|
||||
when is_integer(index) do
|
||||
case tuple_insert_at(tuple, index - 1, value) do
|
||||
value_type when is_descr(value_type) ->
|
||||
{value_type, context}
|
||||
|
||||
:badtuple ->
|
||||
{error_type(), to_badapply_error(expr, [integer(), tuple, value], stack, context)}
|
||||
|
||||
reason ->
|
||||
{error_type(), error({reason, expr, tuple, index - 2, context}, meta, stack, context)}
|
||||
end
|
||||
end
|
||||
|
||||
def apply(:erlang, :delete_element, [_, tuple], {_, meta, [index, _]} = expr, stack, context)
|
||||
when is_integer(index) do
|
||||
case tuple_delete_at(tuple, index - 1) do
|
||||
value_type when is_descr(value_type) ->
|
||||
{value_type, context}
|
||||
|
||||
:badtuple ->
|
||||
{error_type(), to_badapply_error(expr, [integer(), tuple], stack, context)}
|
||||
|
||||
reason ->
|
||||
{error_type(), error({reason, expr, tuple, index - 1, context}, meta, stack, context)}
|
||||
end
|
||||
end
|
||||
|
||||
def apply(:erlang, :make_tuple, [_, elem], {_, _meta, [size, _]}, _stack, context)
|
||||
when is_integer(size) and size >= 0 do
|
||||
{tuple(List.duplicate(elem, size)), context}
|
||||
end
|
||||
|
||||
def apply(:erlang, :hd, [list], expr, stack, context) do
|
||||
case list_hd(list) do
|
||||
{_, value_type} ->
|
||||
{value_type, context}
|
||||
|
||||
:badnonemptylist ->
|
||||
{error_type(), to_badapply_error(expr, [list], stack, context)}
|
||||
end
|
||||
end
|
||||
|
||||
def apply(:erlang, :tl, [list], expr, stack, context) do
|
||||
case list_tl(list) do
|
||||
{_, value_type} ->
|
||||
{value_type, context}
|
||||
|
||||
:badnonemptylist ->
|
||||
{error_type(), to_badapply_error(expr, [list], stack, context)}
|
||||
end
|
||||
end
|
||||
|
||||
def apply(:erlang, name, [left, right] = args_types, expr, stack, context)
|
||||
when name in [:>=, :"=<", :>, :<, :min, :max] do
|
||||
context =
|
||||
cond do
|
||||
stack.mode == :infer ->
|
||||
context
|
||||
|
||||
match?({false, _}, map_fetch(left, :__struct__)) or
|
||||
match?({false, _}, map_fetch(right, :__struct__)) ->
|
||||
warning = {:struct_comparison, expr, context}
|
||||
warn(__MODULE__, warning, elem(expr, 1), stack, context)
|
||||
|
||||
number_type?(left) and number_type?(right) ->
|
||||
context
|
||||
|
||||
disjoint?(left, right) ->
|
||||
warning = {:mismatched_comparison, expr, context}
|
||||
warn(__MODULE__, warning, elem(expr, 1), stack, context)
|
||||
|
||||
true ->
|
||||
context
|
||||
end
|
||||
|
||||
if name in [:min, :max] do
|
||||
{union(left, right), context}
|
||||
else
|
||||
{remote_return(boolean(), args_types, stack), context}
|
||||
end
|
||||
end
|
||||
|
||||
def apply(:erlang, name, [left, right] = args_types, expr, stack, context)
|
||||
when name in [:==, :"/=", :"=:=", :"=/="] do
|
||||
context =
|
||||
cond do
|
||||
stack.mode == :infer ->
|
||||
context
|
||||
|
||||
name in [:==, :"/="] and number_type?(left) and number_type?(right) ->
|
||||
context
|
||||
|
||||
disjoint?(left, right) ->
|
||||
warning = {:mismatched_comparison, expr, context}
|
||||
warn(__MODULE__, warning, elem(expr, 1), stack, context)
|
||||
|
||||
true ->
|
||||
context
|
||||
end
|
||||
|
||||
{remote_return(boolean(), args_types, stack), context}
|
||||
end
|
||||
|
||||
def apply(mod, name, args_types, expr, stack, context) do
|
||||
arity = length(args_types)
|
||||
|
||||
case :elixir_rewrite.inline(mod, name, arity) do
|
||||
{mod, name} ->
|
||||
apply(mod, name, args_types, expr, stack, context)
|
||||
|
||||
false ->
|
||||
{info, context} = remote(mod, name, arity, elem(expr, 1), stack, context)
|
||||
apply_remote(info, args_types, expr, stack, context)
|
||||
end
|
||||
end
|
||||
|
||||
defp remote_return(type, args_types, stack) do
|
||||
cond do
|
||||
stack.mode == :static -> type
|
||||
Enum.any?(args_types, &gradual?/1) -> dynamic(type)
|
||||
true -> type
|
||||
end
|
||||
end
|
||||
|
||||
defp apply_remote(info, args_types, expr, stack, context) do
|
||||
case apply_remote(info, args_types, stack) do
|
||||
{:ok, type} ->
|
||||
{type, context}
|
||||
|
||||
{:error, domain, clauses} ->
|
||||
error = {:badapply, expr, args_types, domain, clauses, context}
|
||||
{error_type(), error(error, elem(expr, 1), stack, context)}
|
||||
end
|
||||
end
|
||||
|
||||
defp apply_remote(:none, _args_types, _stack) do
|
||||
{:ok, dynamic()}
|
||||
end
|
||||
|
||||
defp apply_remote({:strong, nil, [{expected, return}] = clauses}, args_types, stack) do
|
||||
# Optimize single clauses as the domain is the single clause args.
|
||||
case zip_compatible?(args_types, expected) do
|
||||
true -> {:ok, remote_return(return, args_types, stack)}
|
||||
false -> {:error, expected, clauses}
|
||||
end
|
||||
end
|
||||
|
||||
defp apply_remote({:strong, domain, clauses}, args_types, stack) do
|
||||
# If the type is only gradual, the compatibility check is the same
|
||||
# as a non disjoint check. So we skip checking compatibility twice.
|
||||
with true <- zip_compatible_or_only_gradual?(args_types, domain),
|
||||
[_ | _] = returns <-
|
||||
for({expected, return} <- clauses, zip_not_disjoint?(args_types, expected), do: return) do
|
||||
{:ok, returns |> Enum.reduce(&union/2) |> remote_return(args_types, stack)}
|
||||
else
|
||||
_ -> {:error, domain, clauses}
|
||||
end
|
||||
end
|
||||
|
||||
defp apply_remote({:infer, clauses}, args_types, _stack) do
|
||||
case for({expected, return} <- clauses, zip_not_disjoint?(args_types, expected), do: return) do
|
||||
[] ->
|
||||
domain =
|
||||
clauses
|
||||
|> Enum.map(fn {args, _} -> args end)
|
||||
|> Enum.zip_with(fn types -> Enum.reduce(types, &union/2) end)
|
||||
|
||||
{:error, domain, clauses}
|
||||
|
||||
returns ->
|
||||
{:ok, returns |> Enum.reduce(&union/2) |> dynamic()}
|
||||
end
|
||||
end
|
||||
|
||||
defp zip_compatible_or_only_gradual?([actual | actuals], [expected | expecteds]) do
|
||||
(only_gradual?(actual) or compatible?(actual, expected)) and
|
||||
zip_compatible_or_only_gradual?(actuals, expecteds)
|
||||
end
|
||||
|
||||
defp zip_compatible_or_only_gradual?([], []), do: true
|
||||
|
||||
defp zip_compatible?([actual | actuals], [expected | expecteds]) do
|
||||
compatible?(actual, expected) and zip_compatible?(actuals, expecteds)
|
||||
end
|
||||
|
||||
defp zip_compatible?([], []), do: true
|
||||
|
||||
defp zip_not_disjoint?([actual | actuals], [expected | expecteds]) do
|
||||
not disjoint?(actual, expected) and zip_not_disjoint?(actuals, expecteds)
|
||||
end
|
||||
|
||||
defp zip_not_disjoint?([], []), do: true
|
||||
|
||||
defp export(_module, :module_info, arity, _meta, _stack, context) when arity in [0, 1] do
|
||||
{remote(:module_info, arity), context}
|
||||
end
|
||||
|
||||
defp export(_module, _fun, _arity, _meta, %{cache: %Macro.Env{}}, context) do
|
||||
{:none, context}
|
||||
end
|
||||
|
||||
defp export(module, fun, arity, meta, stack, context) do
|
||||
case ParallelChecker.fetch_export(stack.cache, module, fun, arity) do
|
||||
{:ok, mode, reason, info} ->
|
||||
info = if info == :none, do: remote(fun, arity), else: info
|
||||
{info, check_deprecated(mode, module, fun, arity, reason, meta, stack, context)}
|
||||
|
||||
{:error, type} ->
|
||||
context =
|
||||
if warn_undefined?(module, fun, arity, stack) do
|
||||
warn(__MODULE__, {:undefined, type, module, fun, arity}, meta, stack, context)
|
||||
else
|
||||
context
|
||||
end
|
||||
|
||||
{:none, context}
|
||||
end
|
||||
end
|
||||
|
||||
defp check_deprecated(:elixir, module, fun, arity, reason, meta, stack, context) do
|
||||
if reason do
|
||||
warn(__MODULE__, {:deprecated, module, fun, arity, reason}, meta, stack, context)
|
||||
else
|
||||
context
|
||||
end
|
||||
end
|
||||
|
||||
defp check_deprecated(:erlang, module, fun, arity, _reason, meta, stack, context) do
|
||||
case :otp_internal.obsolete(module, fun, arity) do
|
||||
{:deprecated, string} when is_list(string) ->
|
||||
reason = string |> List.to_string() |> :string.titlecase()
|
||||
warn(__MODULE__, {:deprecated, module, fun, arity, reason}, meta, stack, context)
|
||||
|
||||
{:deprecated, string, removal} when is_list(string) and is_list(removal) ->
|
||||
reason = string |> List.to_string() |> :string.titlecase()
|
||||
reason = "It will be removed in #{removal}. #{reason}"
|
||||
warn(__MODULE__, {:deprecated, module, fun, arity, reason}, meta, stack, context)
|
||||
|
||||
_ ->
|
||||
context
|
||||
end
|
||||
end
|
||||
|
||||
defp warn_undefined?(_, _, _, %{no_warn_undefined: :all}) do
|
||||
false
|
||||
end
|
||||
|
||||
defp warn_undefined?(module, fun, arity, stack) do
|
||||
not Enum.any?(stack.no_warn_undefined, &(&1 == module or &1 == {module, fun, arity}))
|
||||
end
|
||||
|
||||
## Warning helpers
|
||||
|
||||
@doc """
|
||||
@@ -987,6 +435,30 @@ defmodule Module.Types.Of do
|
||||
}
|
||||
end
|
||||
|
||||
def format_diagnostic({:badmodule, expr, type, fun, arity, hints, context}) do
|
||||
traces = collect_traces(expr, context)
|
||||
|
||||
%{
|
||||
details: %{typing_traces: traces},
|
||||
message:
|
||||
IO.iodata_to_binary([
|
||||
"""
|
||||
expected a module (an atom) when invoking #{fun}/#{arity} in expression:
|
||||
|
||||
#{expr_to_string(expr) |> indent(4)}
|
||||
""",
|
||||
empty_if(dot_var?(expr), """
|
||||
|
||||
but got type:
|
||||
|
||||
#{to_quoted_string(type) |> indent(4)}
|
||||
"""),
|
||||
format_traces(traces),
|
||||
format_hints(hints)
|
||||
])
|
||||
}
|
||||
end
|
||||
|
||||
def format_diagnostic({:badmap, expr, type, key, context}) do
|
||||
traces = collect_traces(expr, context)
|
||||
|
||||
@@ -1035,279 +507,11 @@ defmodule Module.Types.Of do
|
||||
}
|
||||
end
|
||||
|
||||
def format_diagnostic({:badindex, expr, type, index, context}) do
|
||||
traces = collect_traces(expr, context)
|
||||
|
||||
%{
|
||||
details: %{typing_traces: traces},
|
||||
message:
|
||||
IO.iodata_to_binary([
|
||||
"""
|
||||
expected a tuple with at least #{pluralize(index + 1, "element", "elements")} in #{format_mfa(expr)}:
|
||||
|
||||
#{expr_to_string(expr) |> indent(4)}
|
||||
|
||||
the given type does not have the given index:
|
||||
|
||||
#{to_quoted_string(type) |> indent(4)}
|
||||
""",
|
||||
format_traces(traces)
|
||||
])
|
||||
}
|
||||
end
|
||||
|
||||
def format_diagnostic({:badmodule, expr, type, fun, arity, hints, context}) do
|
||||
traces = collect_traces(expr, context)
|
||||
|
||||
%{
|
||||
details: %{typing_traces: traces},
|
||||
message:
|
||||
IO.iodata_to_binary([
|
||||
"""
|
||||
expected a module (an atom) when invoking #{fun}/#{arity} in expression:
|
||||
|
||||
#{expr_to_string(expr) |> indent(4)}
|
||||
""",
|
||||
empty_if(dot_var?(expr), """
|
||||
|
||||
but got type:
|
||||
|
||||
#{to_quoted_string(type) |> indent(4)}
|
||||
"""),
|
||||
format_traces(traces),
|
||||
format_hints(hints)
|
||||
])
|
||||
}
|
||||
end
|
||||
|
||||
def format_diagnostic({:badapply, expr, args_types, domain, clauses, context}) do
|
||||
traces = collect_traces(expr, context)
|
||||
{{:., _, [mod, fun]}, _, args} = expr
|
||||
{mod, fun, args, converter} = :elixir_rewrite.erl_to_ex(mod, fun, args)
|
||||
|
||||
explanation =
|
||||
if i = Enum.find_index(args_types, &empty?/1) do
|
||||
"""
|
||||
the #{integer_to_ordinal(i + 1)} argument is empty (often represented as none()), \
|
||||
most likely because it is the result of an expression that always fails, such as \
|
||||
a `raise` or a previous invalid call. This causes any function called with this \
|
||||
value to fail
|
||||
"""
|
||||
else
|
||||
"""
|
||||
but expected one of:
|
||||
#{clauses_args_to_quoted_string(clauses, converter)}
|
||||
"""
|
||||
end
|
||||
|
||||
%{
|
||||
details: %{typing_traces: traces},
|
||||
message:
|
||||
IO.iodata_to_binary([
|
||||
"""
|
||||
incompatible types given to #{Exception.format_mfa(mod, fun, length(args))}:
|
||||
|
||||
#{expr_to_string(expr) |> indent(4)}
|
||||
|
||||
given types:
|
||||
|
||||
#{args_to_quoted_string(args_types, domain, converter) |> indent(4)}
|
||||
|
||||
""",
|
||||
explanation,
|
||||
format_traces(traces)
|
||||
])
|
||||
}
|
||||
end
|
||||
|
||||
def format_diagnostic({:mismatched_comparison, expr, context}) do
|
||||
traces = collect_traces(expr, context)
|
||||
|
||||
%{
|
||||
details: %{typing_traces: traces},
|
||||
message:
|
||||
IO.iodata_to_binary([
|
||||
"""
|
||||
comparison between distinct types found:
|
||||
|
||||
#{expr_to_string(expr) |> indent(4)}
|
||||
""",
|
||||
format_traces(traces),
|
||||
"""
|
||||
|
||||
While Elixir can compare across all types, you are comparing \
|
||||
across types which are always disjoint, and the result is either \
|
||||
always true or always false
|
||||
"""
|
||||
])
|
||||
}
|
||||
end
|
||||
|
||||
def format_diagnostic({:struct_comparison, expr, context}) do
|
||||
traces = collect_traces(expr, context)
|
||||
|
||||
%{
|
||||
details: %{typing_traces: traces},
|
||||
message:
|
||||
IO.iodata_to_binary([
|
||||
"""
|
||||
comparison with structs found:
|
||||
|
||||
#{expr_to_string(expr) |> indent(4)}
|
||||
""",
|
||||
format_traces(traces),
|
||||
"""
|
||||
|
||||
Comparison operators (>, <, >=, <=, min, and max) perform structural \
|
||||
and not semantic comparison. Comparing with a struct won't give meaningful \
|
||||
results. Structs that can be compared typically define a compare/2 function \
|
||||
within their modules that can be used for semantic comparison.
|
||||
"""
|
||||
])
|
||||
}
|
||||
end
|
||||
|
||||
def format_diagnostic({:undefined, :module, module, fun, arity}) do
|
||||
top =
|
||||
if fun == :__struct__ and arity == 0 do
|
||||
"struct #{inspect(module)}"
|
||||
else
|
||||
Exception.format_mfa(module, fun, arity)
|
||||
end
|
||||
|
||||
%{
|
||||
message:
|
||||
IO.iodata_to_binary([
|
||||
top,
|
||||
" is undefined (module ",
|
||||
inspect(module),
|
||||
" is not available or is yet to be defined)",
|
||||
UndefinedFunctionError.hint_for_missing_module(module, fun, arity)
|
||||
]),
|
||||
group: true
|
||||
}
|
||||
end
|
||||
|
||||
def format_diagnostic({:undefined, :function, module, :__struct__, 0}) do
|
||||
%{
|
||||
message:
|
||||
"struct #{inspect(module)} is undefined (there is such module but it does not define a struct)",
|
||||
group: true
|
||||
}
|
||||
end
|
||||
|
||||
def format_diagnostic({:undefined, :function, module, fun, arity}) do
|
||||
%{
|
||||
message:
|
||||
IO.iodata_to_binary([
|
||||
Exception.format_mfa(module, fun, arity),
|
||||
" is undefined or private",
|
||||
UndefinedFunctionError.hint_for_loaded_module(module, fun, arity)
|
||||
]),
|
||||
group: true
|
||||
}
|
||||
end
|
||||
|
||||
def format_diagnostic({:deprecated, module, fun, arity, reason}) do
|
||||
%{
|
||||
message:
|
||||
IO.iodata_to_binary([
|
||||
Exception.format_mfa(module, fun, arity),
|
||||
" is deprecated. ",
|
||||
reason
|
||||
]),
|
||||
group: true
|
||||
}
|
||||
end
|
||||
|
||||
defp pluralize(1, singular, _), do: "1 #{singular}"
|
||||
defp pluralize(i, _, plural), do: "#{i} #{plural}"
|
||||
|
||||
defp dot_var?(expr) do
|
||||
match?({{:., _, [var, _fun]}, _, _args} when is_var(var), expr)
|
||||
end
|
||||
|
||||
defp to_badapply_error({{:., _, [mod, fun]}, meta, _} = expr, args_types, stack, context) do
|
||||
{_type, domain, [{args, _} | _] = clauses} = remote(mod, fun, length(args_types))
|
||||
error({:badapply, expr, args_types, domain || args, clauses, context}, meta, stack, context)
|
||||
end
|
||||
|
||||
defp empty_if(condition, content) do
|
||||
if condition, do: "", else: content
|
||||
end
|
||||
|
||||
defp format_mfa({{:., _, [mod, fun]}, _, args}) do
|
||||
{mod, fun, args, _} = :elixir_rewrite.erl_to_ex(mod, fun, args)
|
||||
Exception.format_mfa(mod, fun, length(args))
|
||||
end
|
||||
|
||||
## Algebra helpers
|
||||
|
||||
alias Inspect.Algebra, as: IA
|
||||
|
||||
defp clauses_args_to_quoted_string([{args, _return}], converter) do
|
||||
"\n " <> (clause_args_to_quoted_string(args, converter) |> indent(4))
|
||||
end
|
||||
|
||||
defp clauses_args_to_quoted_string(clauses, converter) do
|
||||
clauses
|
||||
|> Enum.with_index(fn {args, _return}, index ->
|
||||
"""
|
||||
|
||||
##{index + 1}
|
||||
#{clause_args_to_quoted_string(args, converter)}\
|
||||
"""
|
||||
|> indent(4)
|
||||
end)
|
||||
|> Enum.join("\n")
|
||||
end
|
||||
|
||||
defp clause_args_to_quoted_string(args, converter) do
|
||||
docs = Enum.map(args, &(&1 |> to_quoted() |> Code.Formatter.to_algebra()))
|
||||
args_docs_to_quoted_string(converter.(docs))
|
||||
end
|
||||
|
||||
defp args_to_quoted_string(args_types, domain, converter) do
|
||||
ansi? = IO.ANSI.enabled?()
|
||||
|
||||
docs =
|
||||
Enum.zip_with(args_types, domain, fn actual, expected ->
|
||||
doc = actual |> to_quoted() |> Code.Formatter.to_algebra()
|
||||
|
||||
cond do
|
||||
compatible?(actual, expected) -> doc
|
||||
ansi? -> IA.concat(IA.color(doc, IO.ANSI.red()), IA.color(IA.empty(), IO.ANSI.reset()))
|
||||
true -> IA.concat(["-", doc, "-"])
|
||||
end
|
||||
end)
|
||||
|
||||
args_docs_to_quoted_string(converter.(docs))
|
||||
end
|
||||
|
||||
defp args_docs_to_quoted_string(docs) do
|
||||
doc = IA.fold(docs, fn doc, acc -> IA.glue(IA.concat(doc, ","), acc) end)
|
||||
|
||||
wrapped_docs =
|
||||
case docs do
|
||||
[_] -> IA.concat("(", IA.concat(doc, ")"))
|
||||
_ -> IA.group(IA.glue(IA.nest(IA.glue("(", "", doc), 2), "", ")"))
|
||||
end
|
||||
|
||||
wrapped_docs
|
||||
|> IA.format(98)
|
||||
|> IO.iodata_to_binary()
|
||||
|> case do
|
||||
"(\n" <> _ = multiple_lines -> multiple_lines
|
||||
single_line -> binary_slice(single_line, 1..-2//1)
|
||||
end
|
||||
end
|
||||
|
||||
defp integer_to_ordinal(i) do
|
||||
case rem(i, 10) do
|
||||
1 when rem(i, 100) != 11 -> "#{i}st"
|
||||
2 when rem(i, 100) != 12 -> "#{i}nd"
|
||||
3 when rem(i, 100) != 13 -> "#{i}rd"
|
||||
_ -> "#{i}th"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -25,6 +25,10 @@ defmodule Module.Types.Pattern do
|
||||
is refined, we restart at step 2.
|
||||
|
||||
"""
|
||||
def of_head(_patterns, _guards, expected, _tag, _meta, %{mode: :traversal}, context) do
|
||||
{expected, context}
|
||||
end
|
||||
|
||||
def of_head(patterns, guards, expected, tag, meta, stack, context) do
|
||||
stack = %{stack | meta: meta}
|
||||
|
||||
@@ -98,7 +102,13 @@ defmodule Module.Types.Pattern do
|
||||
This version tracks the whole expression in tracing,
|
||||
instead of only the pattern.
|
||||
"""
|
||||
def of_match(pattern, guards \\ [], expected, expr, tag, stack, context) do
|
||||
def of_match(pattern, guards \\ [], expected, expr, tag, stack, context)
|
||||
|
||||
def of_match(_pattern, _guards, expected, _expr, _tag, %{mode: :traversal}, context) do
|
||||
{expected, context}
|
||||
end
|
||||
|
||||
def of_match(pattern, guards, expected, expr, tag, stack, context) do
|
||||
context = init_pattern_info(context)
|
||||
{tree, context} = of_pattern(pattern, [{:arg, 0, expr}], stack, context)
|
||||
|
||||
@@ -114,32 +124,39 @@ defmodule Module.Types.Pattern do
|
||||
{type, context}
|
||||
end
|
||||
|
||||
defp all_single_path?(vars, info, index) do
|
||||
info
|
||||
|> Map.get(index, [])
|
||||
|> Enum.all?(fn version -> match?([_], Map.fetch!(vars, version)) end)
|
||||
end
|
||||
|
||||
defp of_pattern_recur(types, tag, stack, context, callback) do
|
||||
%{pattern_info: {pattern_vars, pattern_info, _counter}} = context
|
||||
%{pattern_info: {vars, info, _counter}} = context
|
||||
context = nilify_pattern_info(context)
|
||||
pattern_vars = Map.to_list(pattern_vars)
|
||||
changed = :lists.seq(0, length(types) - 1)
|
||||
|
||||
# If all variables in a given index have a single path,
|
||||
# then there are no changes to propagate
|
||||
unchangeable = for index <- changed, all_single_path?(vars, info, index), do: index
|
||||
|
||||
vars = Map.to_list(vars)
|
||||
|
||||
try do
|
||||
case callback.(types, changed, context) do
|
||||
{:ok, types, context} ->
|
||||
of_pattern_recur(types, pattern_vars, pattern_info, tag, stack, context, callback)
|
||||
of_pattern_recur(types, unchangeable, vars, info, tag, stack, context, callback)
|
||||
|
||||
{:error, context} ->
|
||||
{types, error_vars(pattern_vars, context)}
|
||||
{types, error_vars(vars, context)}
|
||||
end
|
||||
catch
|
||||
{types, context} -> {types, error_vars(pattern_vars, context)}
|
||||
{types, context} -> {types, error_vars(vars, context)}
|
||||
end
|
||||
end
|
||||
|
||||
defp of_pattern_recur(types, vars, info, tag, stack, context, callback) do
|
||||
%{vars: context_vars} = context
|
||||
|
||||
defp of_pattern_recur(types, unchangeable, vars, info, tag, stack, context, callback) do
|
||||
{changed, context} =
|
||||
Enum.reduce(vars, {[], context}, fn {version, paths}, {changed, context} ->
|
||||
current_type = context_vars[version][:type]
|
||||
|
||||
{var_changed?, context} =
|
||||
Enum.reduce(paths, {false, context}, fn
|
||||
[var, {:arg, index, expr} | path], {var_changed?, context} ->
|
||||
@@ -147,18 +164,20 @@ defmodule Module.Types.Pattern do
|
||||
|
||||
case of_pattern_var(path, actual, true, info, context) do
|
||||
{type, reachable_var?} ->
|
||||
case Of.refine_var(var, type, expr, stack, context) do
|
||||
{:ok, type, context} ->
|
||||
{var_changed? or
|
||||
(reachable_var? and
|
||||
(current_type == nil or not equal?(current_type, type))), context}
|
||||
|
||||
{:error, _type, context} ->
|
||||
throw({types, context})
|
||||
# If current type is already a subtype, there is nothing to refine.
|
||||
with %{^version => %{type: current_type}} <- context.vars,
|
||||
true <- subtype?(current_type, type) do
|
||||
{var_changed?, context}
|
||||
else
|
||||
_ ->
|
||||
case Of.refine_var(var, type, expr, stack, context) do
|
||||
{:ok, _type, context} -> {var_changed? or reachable_var?, context}
|
||||
{:error, _type, context} -> throw({types, context})
|
||||
end
|
||||
end
|
||||
|
||||
:error ->
|
||||
throw({types, to_badpattern_error(expr, tag, stack, context)})
|
||||
throw({types, badpattern_error(expr, tag, stack, context)})
|
||||
end
|
||||
end)
|
||||
|
||||
@@ -167,23 +186,12 @@ defmodule Module.Types.Pattern do
|
||||
{changed, context}
|
||||
|
||||
true ->
|
||||
case paths do
|
||||
# A single change, check if there are other variables in this index.
|
||||
[[_var, {:arg, index, _} | _]] ->
|
||||
case info do
|
||||
%{^index => true} -> {[index | changed], context}
|
||||
%{^index => false} -> {changed, context}
|
||||
end
|
||||
|
||||
# Several changes, we have to recompute all indexes.
|
||||
_ ->
|
||||
var_changed = Enum.map(paths, fn [_var, {:arg, index, _} | _] -> index end)
|
||||
{var_changed ++ changed, context}
|
||||
end
|
||||
var_changed = Enum.map(paths, fn [_var, {:arg, index, _} | _] -> index end)
|
||||
{var_changed ++ changed, context}
|
||||
end
|
||||
end)
|
||||
|
||||
case :lists.usort(changed) do
|
||||
case :lists.usort(changed) -- unchangeable do
|
||||
[] ->
|
||||
{types, context}
|
||||
|
||||
@@ -194,7 +202,7 @@ defmodule Module.Types.Pattern do
|
||||
{types, context}
|
||||
|
||||
{:ok, types, context} ->
|
||||
of_pattern_recur(types, vars, info, tag, stack, context, callback)
|
||||
of_pattern_recur(types, unchangeable, vars, info, tag, stack, context, callback)
|
||||
|
||||
{:error, context} ->
|
||||
{types, error_vars(vars, context)}
|
||||
@@ -208,7 +216,7 @@ defmodule Module.Types.Pattern do
|
||||
end)
|
||||
end
|
||||
|
||||
defp to_badpattern_error(expr, tag, stack, context) do
|
||||
defp badpattern_error(expr, tag, stack, context) do
|
||||
meta =
|
||||
if meta = get_meta(expr) do
|
||||
meta ++ Keyword.take(stack.meta, [:generated, :line])
|
||||
@@ -224,7 +232,7 @@ defmodule Module.Types.Pattern do
|
||||
type = intersection(actual, expected)
|
||||
|
||||
if empty?(type) do
|
||||
{:error, to_badpattern_error(expr, tag, stack, context)}
|
||||
{:error, badpattern_error(expr, tag, stack, context)}
|
||||
else
|
||||
{:ok, type, context}
|
||||
end
|
||||
@@ -251,8 +259,8 @@ defmodule Module.Types.Pattern do
|
||||
end
|
||||
|
||||
# TODO: Implement domain key types
|
||||
defp of_pattern_var([{:key, _key} | rest], _type, reachable_var?, info, context) do
|
||||
of_pattern_var(rest, dynamic(), reachable_var?, info, context)
|
||||
defp of_pattern_var([{:key, _key} | rest], _type, _reachable_var?, info, context) do
|
||||
of_pattern_var(rest, dynamic(), false, info, context)
|
||||
end
|
||||
|
||||
defp of_pattern_var([{:head, counter} | rest], type, _reachable_var?, info, context) do
|
||||
@@ -489,14 +497,8 @@ defmodule Module.Types.Pattern do
|
||||
paths = [[var | path] | Map.get(vars, version, [])]
|
||||
vars = Map.put(vars, version, paths)
|
||||
|
||||
# Our goal here is to compute if an argument has more than one variable.
|
||||
info =
|
||||
case info do
|
||||
%{^arg => false} -> %{info | arg => true}
|
||||
%{^arg => true} -> info
|
||||
%{} -> Map.put(info, arg, false)
|
||||
end
|
||||
|
||||
# Stores all variables used at any given argument
|
||||
info = Map.update(info, arg, [version], &[version | &1])
|
||||
{{:var, version}, %{context | pattern_info: {vars, info, counter}}}
|
||||
end
|
||||
|
||||
@@ -715,7 +717,7 @@ defmodule Module.Types.Pattern do
|
||||
{args_type, context} =
|
||||
Enum.map_reduce(args, context, &of_guard(&1, dynamic(), expr, stack, &2))
|
||||
|
||||
Of.apply(:erlang, function, args_type, call, stack, context)
|
||||
Module.Types.Apply.remote(:erlang, function, args_type, call, stack, context)
|
||||
end
|
||||
|
||||
# var
|
||||
|
||||
@@ -90,7 +90,7 @@ defmodule Set do
|
||||
if target1 == target2 do
|
||||
target1.intersection(set1, set2)
|
||||
else
|
||||
Enumerable.reduce(set1, {:cont, target1.new}, fn v, acc ->
|
||||
Enumerable.reduce(set1, {:cont, target1.new()}, fn v, acc ->
|
||||
{:cont, if(target2.member?(set2, v), do: target1.put(acc, v), else: acc)}
|
||||
end)
|
||||
|> elem(1)
|
||||
|
||||
@@ -88,9 +88,9 @@ start(_Type, _Args) ->
|
||||
|
||||
%% Compiler options
|
||||
{docs, true},
|
||||
{infer_signatures, true},
|
||||
{ignore_already_consolidated, false},
|
||||
{ignore_module_conflict, false},
|
||||
{infer_signatures, true},
|
||||
{on_undefined_variable, raise},
|
||||
{parser_options, [{columns, true}]},
|
||||
{debug_info, true},
|
||||
|
||||
@@ -50,7 +50,7 @@ define({Line, _S, #{module := Module} = E}, Kind, Call, Expr) ->
|
||||
Key
|
||||
end,
|
||||
|
||||
Args = [Kind, Store, elixir_locals:cache_env(E#{line := Line})],
|
||||
Args = [Kind, Store, elixir_module:cache_env(E#{line := Line})],
|
||||
{{'.', [], [elixir_def, store_definition]}, [], Args}.
|
||||
|
||||
unless_loaded(Fun, Args, Callback) ->
|
||||
|
||||
@@ -144,7 +144,6 @@ bootstrap() ->
|
||||
elixir_config:static(#{bootstrap => true}),
|
||||
elixir_config:put(docs, false),
|
||||
elixir_config:put(ignore_module_conflict, true),
|
||||
elixir_config:put(infer_signatures, false),
|
||||
elixir_config:put(on_undefined_variable, raise),
|
||||
elixir_config:put(parser_options, []),
|
||||
elixir_config:put(relative_paths, false),
|
||||
@@ -191,13 +190,13 @@ bootstrap_files() ->
|
||||
[
|
||||
<<"list/chars.ex">>,
|
||||
<<"bitwise.ex">>,
|
||||
<<"module/locals_tracker.ex">>,
|
||||
<<"module/parallel_checker.ex">>,
|
||||
<<"module/behaviour.ex">>,
|
||||
<<"module/types/helpers.ex">>,
|
||||
<<"module/types/descr.ex">>,
|
||||
<<"module/types/of.ex">>,
|
||||
<<"module/types/pattern.ex">>,
|
||||
<<"module/types/apply.ex">>,
|
||||
<<"module/types/expr.ex">>,
|
||||
<<"module/types.ex">>,
|
||||
<<"exception.ex">>,
|
||||
|
||||
@@ -37,6 +37,7 @@ fun_for(Meta, Module, Name, Arity, Kinds, External) ->
|
||||
{[{_, Kind, LocalMeta, _, _, _}], ClausesPairs} ->
|
||||
case (Kinds == all) orelse (lists:member(Kind, Kinds)) of
|
||||
true ->
|
||||
(Kind == defmacrop) andalso track_defmacrop(Module, Tuple),
|
||||
Local = {value, fun(Fun, Args) -> invoke_local(Meta, Module, Fun, Args, External) end},
|
||||
Clauses = [Clause || {_, Clause} <- ClausesPairs],
|
||||
elixir_erl:definition_to_anonymous(Kind, LocalMeta, Clauses, Local, External);
|
||||
@@ -60,6 +61,10 @@ invoke_local(Meta, Module, ErlName, Args, External) ->
|
||||
apply(Fun, Args)
|
||||
end.
|
||||
|
||||
track_defmacrop(Module, FunArity) ->
|
||||
{_, Bag} = elixir_module:data_tables(Module),
|
||||
ets:insert(Bag, {defmacrop_calls, FunArity}).
|
||||
|
||||
invoke_external(Meta, Mod, Name, Args, E) ->
|
||||
is_map(E) andalso elixir_env:trace({require, Meta, Mod, []}, E),
|
||||
apply(Mod, Name, Args).
|
||||
@@ -128,10 +133,10 @@ head_and_definition_meta(_, _Meta, _HeadDefaults, [{_, _, HeadMeta, _} | _]) ->
|
||||
%% Section for storing definitions
|
||||
|
||||
store_definition(Kind, {Call, Body}, Pos) ->
|
||||
E = elixir_locals:get_cached_env(Pos),
|
||||
E = elixir_module:get_cached_env(Pos),
|
||||
store_definition(Kind, false, Call, Body, E);
|
||||
store_definition(Kind, Key, Pos) ->
|
||||
#{module := Module} = E = elixir_locals:get_cached_env(Pos),
|
||||
#{module := Module} = E = elixir_module:get_cached_env(Pos),
|
||||
{Call, Body} = elixir_module:read_cache(Module, Key),
|
||||
store_definition(Kind, true, Call, Body, E).
|
||||
|
||||
@@ -210,7 +215,6 @@ store_definition(Meta, Kind, CheckClauses, Name, Arity, DefaultsArgs, Guards, Bo
|
||||
Clause <- def_to_clauses(Kind, Meta, Args, Guards, Body, E)],
|
||||
|
||||
DefaultsLength = length(Defaults),
|
||||
elixir_locals:record_defaults(Tuple, Kind, Module, DefaultsLength, Meta),
|
||||
check_previous_defaults(Meta, Module, Name, Arity, Kind, DefaultsLength, E),
|
||||
|
||||
store_definition(CheckClauses, Kind, Meta, Name, Arity, File,
|
||||
|
||||
@@ -55,7 +55,7 @@ import_function(Meta, Name, Arity, E) ->
|
||||
case find_import_by_name_arity(Meta, Tuple, [], E) of
|
||||
{function, Receiver} ->
|
||||
elixir_env:trace({imported_function, Meta, Receiver, Name, Arity}, E),
|
||||
elixir_locals:record_import(Tuple, Receiver, ?key(E, module), ?key(E, function)),
|
||||
elixir_import:record(Tuple, Receiver, ?key(E, module), ?key(E, function)),
|
||||
remote_function(Meta, Receiver, Name, Arity, E);
|
||||
{macro, _Receiver} ->
|
||||
false;
|
||||
@@ -74,7 +74,6 @@ import_function(Meta, Name, Arity, E) ->
|
||||
elixir_def:local_for(Meta, Name, Arity, [defmacro, defmacrop], E) of
|
||||
false ->
|
||||
elixir_env:trace({local_function, Meta, Name, Arity}, E),
|
||||
elixir_locals:record_local(Tuple, ?key(E, module), ?key(E, function), Meta, false),
|
||||
{local, Name, Arity};
|
||||
_ ->
|
||||
false
|
||||
@@ -183,10 +182,7 @@ expand_import(Meta, Name, Arity, E, Extra, AllowLocals, Trace) ->
|
||||
|
||||
%% Dispatch to the local.
|
||||
_ ->
|
||||
Trace andalso begin
|
||||
elixir_env:trace({local_macro, Meta, Name, Arity}, E),
|
||||
elixir_locals:record_local(Tuple, Module, ?key(E, function), Meta, true)
|
||||
end,
|
||||
Trace andalso elixir_env:trace({local_macro, Meta, Name, Arity}, E),
|
||||
{macro, Module, expander_macro_fun(Meta, Local, Module, Name, E)}
|
||||
end
|
||||
end.
|
||||
@@ -196,13 +192,13 @@ do_expand_import(Result, Meta, Name, Arity, Module, E, Trace) ->
|
||||
{function, Receiver} ->
|
||||
Trace andalso begin
|
||||
elixir_env:trace({imported_function, Meta, Receiver, Name, Arity}, E),
|
||||
elixir_locals:record_import({Name, Arity}, Receiver, Module, ?key(E, function))
|
||||
elixir_import:record({Name, Arity}, Receiver, Module, ?key(E, function))
|
||||
end,
|
||||
{function, Receiver, Name};
|
||||
{macro, Receiver} ->
|
||||
Trace andalso begin
|
||||
elixir_env:trace({imported_macro, Meta, Receiver, Name, Arity}, E),
|
||||
elixir_locals:record_import({Name, Arity}, Receiver, Module, ?key(E, function))
|
||||
elixir_import:record({Name, Arity}, Receiver, Module, ?key(E, function))
|
||||
end,
|
||||
{macro, Receiver, expander_macro_named(Meta, Receiver, Name, Arity, E)};
|
||||
{import, Receiver} ->
|
||||
|
||||
@@ -900,8 +900,7 @@ expand_local(Meta, Name, Args, S, #{module := Module, function := Function, cont
|
||||
|
||||
nil ->
|
||||
Arity = length(Args),
|
||||
elixir_env:trace({local_function, Meta, Name, Arity}, E),
|
||||
elixir_locals:record_local({Name, Arity}, Module, Function, Meta, false)
|
||||
elixir_env:trace({local_function, Meta, Name, Arity}, E)
|
||||
end,
|
||||
|
||||
{EArgs, SA, EA} = expand_args(Args, S, E),
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
%% between local functions and imports.
|
||||
%% For imports dispatch, please check elixir_dispatch.
|
||||
-module(elixir_import).
|
||||
-export([import/6, import/7, special_form/2, format_error/1]).
|
||||
-export([import/6, import/7, special_form/2,
|
||||
record/4, ensure_no_local_conflict/3,
|
||||
format_error/1]).
|
||||
-compile(inline_list_funcs).
|
||||
-include("elixir.hrl").
|
||||
|
||||
@@ -135,6 +137,31 @@ calculate_key(Meta, Key, Old, New, E, Warn) ->
|
||||
{true, FinalSet, [{Key, FinalSet} | keydelete(Key, Old)]}
|
||||
end.
|
||||
|
||||
%% Record function calls for local conflicts
|
||||
|
||||
record(_Tuple, Receiver, Module, Function)
|
||||
when Function == nil; Module == Receiver -> false;
|
||||
record(Tuple, Receiver, Module, _Function) ->
|
||||
try
|
||||
{Set, _Bag} = elixir_module:data_tables(Module),
|
||||
ets:insert(Set, {{import, Tuple}, Receiver}),
|
||||
true
|
||||
catch
|
||||
error:badarg -> false
|
||||
end.
|
||||
|
||||
ensure_no_local_conflict('Elixir.Kernel', _All, _E) ->
|
||||
ok;
|
||||
ensure_no_local_conflict(Module, AllDefinitions, E) ->
|
||||
{Set, _} = elixir_module:data_tables(Module),
|
||||
|
||||
[try
|
||||
Receiver = ets:lookup_element(Set, {import, Pair}, 2),
|
||||
elixir_errors:module_error(Meta, E, ?MODULE, {import_conflict, Receiver, Pair})
|
||||
catch
|
||||
error:badarg -> false
|
||||
end || {Pair, _, Meta, _} <- AllDefinitions].
|
||||
|
||||
%% Retrieve functions and macros from modules
|
||||
|
||||
get_functions(Module, InfoCallback) ->
|
||||
@@ -229,7 +256,11 @@ format_error({special_form_conflict, {Receiver, Name, Arity}}) ->
|
||||
[elixir_aliases:inspect(Receiver), Name, Arity]);
|
||||
|
||||
format_error({no_macros, Module}) ->
|
||||
io_lib:format("could not load macros from module ~ts", [elixir_aliases:inspect(Module)]).
|
||||
io_lib:format("could not load macros from module ~ts", [elixir_aliases:inspect(Module)]);
|
||||
|
||||
format_error({import_conflict, Receiver, {Name, Arity}}) ->
|
||||
io_lib:format("imported ~ts.~ts/~B conflicts with local function",
|
||||
[elixir_aliases:inspect(Receiver), Name, Arity]).
|
||||
|
||||
%% LIST HELPERS
|
||||
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
%% Module responsible for tracking invocations of module calls.
|
||||
-module(elixir_locals).
|
||||
-export([
|
||||
setup/1, stop/1, cache_env/1, get_cached_env/1,
|
||||
record_local/5, record_import/4, record_defaults/5,
|
||||
yank/2, reattach/6, ensure_no_import_conflict/3,
|
||||
warn_unused_local/4, ensure_no_undefined_local/3,
|
||||
format_error/1
|
||||
]).
|
||||
|
||||
-include("elixir.hrl").
|
||||
-define(cache_key, {elixir, cache_env}).
|
||||
-define(locals_key, {elixir, locals}).
|
||||
-define(locals, 'Elixir.Module.LocalsTracker').
|
||||
|
||||
setup({DataSet, _DataBag}) ->
|
||||
ets:insert(DataSet, {?cache_key, 0}),
|
||||
|
||||
case elixir_config:is_bootstrap() of
|
||||
false -> ets:insert(DataSet, {?locals_key, true});
|
||||
true -> ok
|
||||
end,
|
||||
|
||||
ok.
|
||||
|
||||
stop({DataSet, _DataBag}) ->
|
||||
ets:delete(DataSet, ?locals_key).
|
||||
|
||||
yank(Tuple, Module) ->
|
||||
if_tracker(Module, fun(Tracker) -> ?locals:yank(Tracker, Tuple) end).
|
||||
|
||||
reattach(Tuple, Kind, Module, Function, Neighbours, Meta) ->
|
||||
if_tracker(Module, fun(Tracker) -> ?locals:reattach(Tracker, Tuple, Kind, Function, Neighbours, Meta) end).
|
||||
|
||||
record_local(_Tuple, _Module, nil, _Meta, _IsMacroDispatch) ->
|
||||
ok;
|
||||
record_local(Tuple, Module, Function, Meta, IsMacroDispatch) ->
|
||||
if_tracker(Module, fun(Tracker) -> ?locals:add_local(Tracker, Function, Tuple, Meta, IsMacroDispatch), ok end).
|
||||
|
||||
record_import(_Tuple, Receiver, Module, Function)
|
||||
when Function == nil; Module == Receiver -> false;
|
||||
record_import(Tuple, Receiver, Module, Function) ->
|
||||
if_tracker(Module, fun(Tracker) -> ?locals:add_import(Tracker, Function, Receiver, Tuple), ok end).
|
||||
|
||||
record_defaults(_Tuple, _Kind, _Module, 0, _Meta) ->
|
||||
ok;
|
||||
record_defaults(Tuple, Kind, Module, Defaults, Meta) ->
|
||||
if_tracker(Module, fun(Tracker) -> ?locals:add_defaults(Tracker, Kind, Tuple, Defaults, Meta), ok end).
|
||||
|
||||
if_tracker(Module, Callback) ->
|
||||
if_tracker(Module, ok, Callback).
|
||||
|
||||
if_tracker(Module, Default, Callback) ->
|
||||
try
|
||||
{DataSet, _} = Tables = elixir_module:data_tables(Module),
|
||||
{ets:member(DataSet, ?locals_key), Tables}
|
||||
of
|
||||
{true, Tracker} -> Callback(Tracker);
|
||||
{false, _} -> Default
|
||||
catch
|
||||
error:badarg -> Default
|
||||
end.
|
||||
|
||||
%% CACHING
|
||||
|
||||
cache_env(#{line := Line, module := Module} = E) ->
|
||||
{Set, _} = elixir_module:data_tables(Module),
|
||||
Cache = elixir_env:reset_vars(E#{line := nil}),
|
||||
PrevKey = ets:lookup_element(Set, ?cache_key, 2),
|
||||
|
||||
Pos =
|
||||
case ets:lookup(Set, {cache_env, PrevKey}) of
|
||||
[{_, Cache}] ->
|
||||
PrevKey;
|
||||
_ ->
|
||||
NewKey = PrevKey + 1,
|
||||
ets:insert(Set, [{{cache_env, NewKey}, Cache}, {?cache_key, NewKey}]),
|
||||
NewKey
|
||||
end,
|
||||
|
||||
{Module, {Line, Pos}}.
|
||||
|
||||
get_cached_env({Module, {Line, Pos}}) ->
|
||||
{Set, _} = elixir_module:data_tables(Module),
|
||||
(ets:lookup_element(Set, {cache_env, Pos}, 2))#{line := Line};
|
||||
get_cached_env(Env) ->
|
||||
Env.
|
||||
|
||||
%% ERROR HANDLING
|
||||
|
||||
ensure_no_import_conflict('Elixir.Kernel', _All, _E) ->
|
||||
ok;
|
||||
ensure_no_import_conflict(Module, All, E) ->
|
||||
if_tracker(Module, ok, fun(Tracker) ->
|
||||
[elixir_errors:module_error(Meta, E, ?MODULE, {function_conflict, Error})
|
||||
|| {Meta, Error} <- ?locals:collect_imports_conflicts(Tracker, All)],
|
||||
ok
|
||||
end).
|
||||
|
||||
ensure_no_undefined_local(Module, All, E) ->
|
||||
if_tracker(Module, [], fun(Tracker) ->
|
||||
[elixir_errors:module_error(Meta, E#{function := Function, file := File}, ?MODULE, {Error, Tuple, Module})
|
||||
|| {Function, Meta, File, Tuple, Error} <- ?locals:collect_undefined_locals(Tracker, All, ?key(E, file))],
|
||||
ok
|
||||
end).
|
||||
|
||||
warn_unused_local(Module, All, Private, E) ->
|
||||
if_tracker(Module, [], fun(Tracker) ->
|
||||
{Unreachable, Warnings} = ?locals:collect_unused_locals(Tracker, All, Private),
|
||||
[elixir_errors:file_warn(Meta, E, ?MODULE, Error) || {Meta, Error} <- Warnings],
|
||||
Unreachable
|
||||
end).
|
||||
|
||||
format_error({function_conflict, {Receiver, {Name, Arity}}}) ->
|
||||
io_lib:format("imported ~ts.~ts/~B conflicts with local function",
|
||||
[elixir_aliases:inspect(Receiver), Name, Arity]);
|
||||
|
||||
format_error({unused_args, {Name, Arity}}) ->
|
||||
io_lib:format("default values for the optional arguments in ~ts/~B are never used", [Name, Arity]);
|
||||
|
||||
format_error({unused_args, {Name, Arity}, Count}) when Arity - Count == 1 ->
|
||||
io_lib:format("the default value for the last optional argument in ~ts/~B is never used", [Name, Arity]);
|
||||
|
||||
format_error({unused_args, {Name, Arity}, Count}) ->
|
||||
io_lib:format("the default values for the last ~B optional arguments in ~ts/~B are never used", [Arity - Count, Name, Arity]);
|
||||
|
||||
format_error({unused_def, {Name, Arity}, defp}) ->
|
||||
io_lib:format("function ~ts/~B is unused", [Name, Arity]);
|
||||
|
||||
format_error({unused_def, {Name, Arity}, defmacrop}) ->
|
||||
io_lib:format("macro ~ts/~B is unused", [Name, Arity]);
|
||||
|
||||
format_error({undefined_function, {F, A}, _})
|
||||
when F == '__info__', A == 1;
|
||||
F == 'behaviour_info', A == 1;
|
||||
F == 'module_info', A == 1;
|
||||
F == 'module_info', A == 0 ->
|
||||
io_lib:format("undefined function ~ts/~B (this function is auto-generated by the compiler "
|
||||
"and must always be called as a remote, as in __MODULE__.~ts/~B)", [F, A, F, A]);
|
||||
|
||||
format_error({undefined_function, {F, A}, Module}) ->
|
||||
io_lib:format("undefined function ~ts/~B (expected ~ts to define such a function or "
|
||||
"for it to be imported, but none are available)", [F, A, elixir_aliases:inspect(Module)]);
|
||||
|
||||
format_error({incorrect_dispatch, {F, A}, _Module}) ->
|
||||
io_lib:format("cannot invoke macro ~ts/~B before its definition", [F, A]).
|
||||
@@ -1,9 +1,10 @@
|
||||
-module(elixir_module).
|
||||
-export([file/1, data_tables/1, is_open/1, mode/1, delete_definition_attributes/6,
|
||||
compile/6, expand_callback/6, format_error/1, compiler_modules/0,
|
||||
write_cache/3, read_cache/2, next_counter/1, taint/1]).
|
||||
write_cache/3, read_cache/2, next_counter/1, taint/1, cache_env/1, get_cached_env/1]).
|
||||
-include("elixir.hrl").
|
||||
-define(counter_attr, {elixir, counter}).
|
||||
-define(cache_key, {elixir, cache_env}).
|
||||
|
||||
%% Stores modules currently being defined by the compiler
|
||||
|
||||
@@ -71,6 +72,29 @@ taint(Module) ->
|
||||
_:_ -> false
|
||||
end.
|
||||
|
||||
cache_env(#{line := Line, module := Module} = E) ->
|
||||
{Set, _} = data_tables(Module),
|
||||
Cache = elixir_env:reset_vars(E#{line := nil}),
|
||||
PrevKey = ets:lookup_element(Set, ?cache_key, 2),
|
||||
|
||||
Pos =
|
||||
case ets:lookup(Set, {cache_env, PrevKey}) of
|
||||
[{_, Cache}] ->
|
||||
PrevKey;
|
||||
_ ->
|
||||
NewKey = PrevKey + 1,
|
||||
ets:insert(Set, [{{cache_env, NewKey}, Cache}, {?cache_key, NewKey}]),
|
||||
NewKey
|
||||
end,
|
||||
|
||||
{Module, {Line, Pos}}.
|
||||
|
||||
get_cached_env({Module, {Line, Pos}}) ->
|
||||
{Set, _} = data_tables(Module),
|
||||
(ets:lookup_element(Set, {cache_env, Pos}, 2))#{line := Line};
|
||||
get_cached_env(Env) ->
|
||||
Env.
|
||||
|
||||
%% Compilation hook
|
||||
|
||||
compile(Meta, Module, Block, Vars, Prune, Env) ->
|
||||
@@ -146,37 +170,30 @@ compile(Meta, Module, ModuleAsCharlist, Block, Vars, Prune, E) ->
|
||||
|
||||
NifsAttribute = lists:keyfind(nifs, 1, Attributes),
|
||||
validate_nifs_attribute(NifsAttribute, AllDefinitions, Line, E),
|
||||
|
||||
Unreachable = elixir_locals:warn_unused_local(Module, AllDefinitions, NewPrivate, E),
|
||||
elixir_locals:ensure_no_undefined_local(Module, AllDefinitions, E),
|
||||
elixir_locals:ensure_no_import_conflict(Module, AllDefinitions, E),
|
||||
|
||||
%% We stop tracking locals here to avoid race conditions in case after_load
|
||||
%% evaluates code in a separate process that may write to locals table.
|
||||
elixir_locals:stop({DataSet, DataBag}),
|
||||
elixir_import:ensure_no_local_conflict(Module, AllDefinitions, E),
|
||||
make_readonly(Module),
|
||||
|
||||
(not elixir_config:is_bootstrap()) andalso
|
||||
'Elixir.Module':'__check_attributes__'(E, DataSet, DataBag),
|
||||
|
||||
RawCompileOpts = bag_lookup_element(DataBag, {accumulate, compile}, 2),
|
||||
CompileOpts = validate_compile_opts(RawCompileOpts, AllDefinitions, Unreachable, Line, E),
|
||||
Impls = bag_lookup_element(DataBag, impls, 2),
|
||||
|
||||
AfterVerify = bag_lookup_element(DataBag, {accumulate, after_verify}, 2),
|
||||
[elixir_env:trace({remote_function, [], VerifyMod, VerifyFun, 1}, CallbackE) ||
|
||||
{VerifyMod, VerifyFun} <- AfterVerify],
|
||||
|
||||
%% Compute signatures only if the module is valid.
|
||||
case ets:member(DataSet, {elixir, taint}) of
|
||||
true -> elixir_errors:compile_error(E);
|
||||
false -> ok
|
||||
end,
|
||||
%% Ensure there are no errors before we infer types
|
||||
compile_error_if_tainted(DataSet, E),
|
||||
|
||||
Signatures = case elixir_config:get(infer_signatures) of
|
||||
true -> 'Elixir.Module.Types':infer(Module, File, AllDefinitions, CallbackE);
|
||||
false -> #{}
|
||||
end,
|
||||
{Signatures, Unreachable} =
|
||||
case elixir_config:is_bootstrap() of
|
||||
true -> {#{}, []};
|
||||
false ->
|
||||
Defmacrop = bag_lookup_element(DataBag, defmacrop_calls, 2),
|
||||
'Elixir.Module.Types':infer(Module, File, AllDefinitions, NewPrivate, Defmacrop, E)
|
||||
end,
|
||||
|
||||
RawCompileOpts = bag_lookup_element(DataBag, {accumulate, compile}, 2),
|
||||
CompileOpts = validate_compile_opts(RawCompileOpts, AllDefinitions, Unreachable, Line, E),
|
||||
Impls = bag_lookup_element(DataBag, impls, 2),
|
||||
|
||||
ModuleMap = #{
|
||||
struct => get_struct(DataSet),
|
||||
@@ -186,15 +203,16 @@ compile(Meta, Module, ModuleAsCharlist, Block, Vars, Prune, E) ->
|
||||
relative_file => elixir_utils:relative_to_cwd(File),
|
||||
attributes => Attributes,
|
||||
definitions => AllDefinitions,
|
||||
unreachable => Unreachable,
|
||||
after_verify => AfterVerify,
|
||||
compile_opts => CompileOpts,
|
||||
deprecated => get_deprecated(DataBag),
|
||||
defines_behaviour => defines_behaviour(DataBag),
|
||||
impls => Impls,
|
||||
unreachable => Unreachable,
|
||||
signatures => Signatures
|
||||
},
|
||||
|
||||
compile_error_if_tainted(DataSet, E),
|
||||
Binary = elixir_erl:compile(ModuleMap),
|
||||
Autoload = proplists:get_value(autoload, CompileOpts, true),
|
||||
spawn_parallel_checker(CheckerInfo, Module, ModuleMap),
|
||||
@@ -228,6 +246,12 @@ compile(Meta, Module, ModuleAsCharlist, Block, Vars, Prune, E) ->
|
||||
elixir_code_server:call({undefmodule, Ref})
|
||||
end.
|
||||
|
||||
compile_error_if_tainted(DataSet, E) ->
|
||||
case ets:member(DataSet, {elixir, taint}) of
|
||||
true -> elixir_errors:compile_error(E);
|
||||
false -> ok
|
||||
end.
|
||||
|
||||
validate_compile_opts(Opts, Defs, Unreachable, Line, E) ->
|
||||
lists:flatmap(fun (Opt) -> validate_compile_opt(Opt, Defs, Unreachable, Line, E) end, Opts).
|
||||
|
||||
@@ -351,7 +375,6 @@ build(Module, Line, File, E) ->
|
||||
%% * {{type, Tuple}, ...}, {{opaque, Tuple}, ...}
|
||||
%% * {{callback, Tuple}, ...}, {{macrocallback, Tuple}, ...}
|
||||
%% * {{def, Tuple}, ...} (from elixir_def)
|
||||
%% * {{import, Tuple}, ...} (from elixir_locals)
|
||||
%% * {{overridable, Tuple}, ...} (from elixir_overridable)
|
||||
%%
|
||||
DataSet = ets:new(Module, [set, public]),
|
||||
@@ -367,8 +390,6 @@ build(Module, Line, File, E) ->
|
||||
%% * {overridables, ...} (from elixir_overridable)
|
||||
%% * {{default, Name}, ...} (from elixir_def)
|
||||
%% * {{clauses, Tuple}, ...} (from elixir_def)
|
||||
%% * {reattach, ...} (from elixir_locals)
|
||||
%% * {{local, Tuple}, ...} (from elixir_locals)
|
||||
%%
|
||||
DataBag = ets:new(Module, [duplicate_bag, public]),
|
||||
|
||||
@@ -395,6 +416,7 @@ build(Module, Line, File, E) ->
|
||||
{optional_callbacks, [], accumulate, []},
|
||||
|
||||
% Others
|
||||
{?cache_key, 0},
|
||||
{?counter_attr, 0}
|
||||
]),
|
||||
|
||||
@@ -411,7 +433,6 @@ build(Module, Line, File, E) ->
|
||||
%% Setup definition related modules
|
||||
Tables = {DataSet, DataBag},
|
||||
elixir_def:setup(Tables),
|
||||
elixir_locals:setup(Tables),
|
||||
Tuple = {Module, Tables, Line, File, all},
|
||||
|
||||
Ref =
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
% Holds the logic responsible for defining overridable functions and handling super.
|
||||
-module(elixir_overridable).
|
||||
-export([overridables_for/1, overridable_for/2,
|
||||
record_overridable/4, super/4,
|
||||
record_overridable/3, super/4,
|
||||
store_not_overridden/1, format_error/1]).
|
||||
-include("elixir.hrl").
|
||||
-define(overridden_pos, 5).
|
||||
-define(overridden_pos, 4).
|
||||
|
||||
overridables_for(Module) ->
|
||||
{_, Bag} = elixir_module:data_tables(Module),
|
||||
@@ -22,20 +22,20 @@ overridable_for(Module, Tuple) ->
|
||||
[] -> not_overridable
|
||||
end.
|
||||
|
||||
record_overridable(Module, Tuple, Def, Neighbours) ->
|
||||
record_overridable(Module, Tuple, Def) ->
|
||||
{Set, Bag} = elixir_module:data_tables(Module),
|
||||
|
||||
case ets:insert_new(Set, {{overridable, Tuple}, 1, Def, Neighbours, false}) of
|
||||
case ets:insert_new(Set, {{overridable, Tuple}, 1, Def, false}) of
|
||||
true ->
|
||||
ets:insert(Bag, {overridables, Tuple});
|
||||
false ->
|
||||
[{_, Count, PreviousDef, _, _}] = ets:lookup(Set, {overridable, Tuple}),
|
||||
[{_, Count, PreviousDef, _}] = ets:lookup(Set, {overridable, Tuple}),
|
||||
{{_, Kind, Meta, File, _, _}, _} = Def,
|
||||
{{_, PreviousKind, _, _, _, _}, _} = PreviousDef,
|
||||
|
||||
case is_valid_kind(Kind, PreviousKind) of
|
||||
true ->
|
||||
ets:insert(Set, {{overridable, Tuple}, Count + 1, Def, Neighbours, false});
|
||||
ets:insert(Set, {{overridable, Tuple}, Count + 1, Def, false});
|
||||
false ->
|
||||
elixir_errors:file_error(Meta, File, ?MODULE, {bad_kind, Module, Tuple, Kind})
|
||||
end
|
||||
@@ -74,7 +74,7 @@ store_not_overridden(Module) ->
|
||||
|
||||
%% Private
|
||||
|
||||
store(Set, Module, Tuple, {_, Count, Def, Neighbours, Overridden}, Hidden) ->
|
||||
store(Set, Module, Tuple, {_, Count, Def, Overridden}, Hidden) ->
|
||||
{{{def, {Name, Arity}}, Kind, Meta, File, _Check,
|
||||
{Defaults, _HasBody, _LastDefaults}}, Clauses} = Def,
|
||||
|
||||
@@ -92,8 +92,7 @@ store(Set, Module, Tuple, {_, Count, Def, Neighbours, Overridden}, Hidden) ->
|
||||
false ->
|
||||
ets:update_element(Set, {overridable, Tuple}, {?overridden_pos, true}),
|
||||
elixir_def:store_definition(none, FinalKind, Meta, FinalName, FinalArity,
|
||||
File, Module, Defaults, FinalClauses),
|
||||
elixir_locals:reattach({FinalName, FinalArity}, FinalKind, Module, Tuple, Neighbours, Meta);
|
||||
File, Module, Defaults, FinalClauses);
|
||||
true ->
|
||||
ok
|
||||
end,
|
||||
|
||||
@@ -739,8 +739,8 @@ defmodule Kernel.ErrorsTest do
|
||||
)
|
||||
|
||||
assert_compile_error(
|
||||
["nofile:1: ", "undefined function foo/1 given to @compile :inline"],
|
||||
~c"defmodule Test do @compile {:inline, foo: 1}; defmacro foo(_) end"
|
||||
["nofile:1: ", "macro foo/1 given to @compile :inline"],
|
||||
~c"defmodule Test do @compile {:inline, foo: 1}; defmacro foo(_), do: :ok end"
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
@@ -383,8 +383,6 @@ defmodule Kernel.QuoteTest.ErrorsTest do
|
||||
mod = Kernel.QuoteTest.ErrorsTest
|
||||
file = __ENV__.file |> Path.relative_to_cwd() |> String.to_charlist()
|
||||
assert [{^mod, :will_raise, 2, [file: ^file, line: @line] ++ _} | _] = __STACKTRACE__
|
||||
else
|
||||
_ -> flunk("expected failure")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -458,12 +458,10 @@ defmodule Kernel.RaiseTest do
|
||||
assert result == "no match of right hand side value: 0"
|
||||
end
|
||||
|
||||
defp empty_map(), do: %{}
|
||||
|
||||
test "bad key error" do
|
||||
result =
|
||||
try do
|
||||
%{empty_map() | foo: :bar}
|
||||
%{Process.get(:unused, %{}) | foo: :bar}
|
||||
rescue
|
||||
x in [KeyError] -> Exception.message(x)
|
||||
end
|
||||
@@ -472,7 +470,7 @@ defmodule Kernel.RaiseTest do
|
||||
|
||||
result =
|
||||
try do
|
||||
empty_map().foo
|
||||
Process.get(:unused, %{}).foo
|
||||
rescue
|
||||
x in [KeyError] -> Exception.message(x)
|
||||
end
|
||||
|
||||
@@ -68,12 +68,10 @@ defmodule Kernel.SpecialFormsTest do
|
||||
end
|
||||
end
|
||||
|
||||
def false_fun(), do: false
|
||||
|
||||
test "cond_clause error keeps line number in stacktrace" do
|
||||
try do
|
||||
cond do
|
||||
false_fun() -> :ok
|
||||
Process.get(:unused, false) -> :ok
|
||||
end
|
||||
rescue
|
||||
_ ->
|
||||
|
||||
@@ -126,7 +126,7 @@ defmodule Kernel.WithTest do
|
||||
end
|
||||
|
||||
assert_raise RuntimeError, fn ->
|
||||
with({:ok, res} <- ok(42), res = res + oops(), do: res)
|
||||
with({:ok, res} <- ok(42), oops(), do: res)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ defmodule KernelTest do
|
||||
|
||||
def id(arg), do: arg
|
||||
def id(arg1, arg2), do: {arg1, arg2}
|
||||
def empty_list(), do: []
|
||||
def empty_map, do: %{}
|
||||
|
||||
defp purge(module) do
|
||||
@@ -1359,7 +1358,7 @@ defmodule KernelTest do
|
||||
assert is_map_key(Map.new(a: 1), :a) == true
|
||||
|
||||
assert_raise BadMapError, fn ->
|
||||
is_map_key(empty_list(), :a)
|
||||
is_map_key(Process.get(:unused, []), :a)
|
||||
end
|
||||
|
||||
case Map.new(a: 1) do
|
||||
@@ -1381,7 +1380,7 @@ defmodule KernelTest do
|
||||
test "tl/1" do
|
||||
assert tl([:one]) == []
|
||||
assert tl([1, 2, 3]) == [2, 3]
|
||||
assert_raise ArgumentError, fn -> tl(empty_list()) end
|
||||
assert_raise ArgumentError, fn -> tl(Process.get(:unused, [])) end
|
||||
|
||||
assert tl([:a | :b]) == :b
|
||||
assert tl([:a, :b | :c]) == [:b | :c]
|
||||
@@ -1389,7 +1388,7 @@ defmodule KernelTest do
|
||||
|
||||
test "hd/1" do
|
||||
assert hd([1, 2, 3, 4]) == 1
|
||||
assert_raise ArgumentError, fn -> hd(empty_list()) end
|
||||
assert_raise ArgumentError, fn -> hd(Process.get(:unused, [])) end
|
||||
assert hd([1 | 2]) == 1
|
||||
end
|
||||
|
||||
|
||||
@@ -6,8 +6,7 @@ defmodule MapTest do
|
||||
doctest Map
|
||||
|
||||
@sample %{a: 1, b: 2}
|
||||
|
||||
defp sample, do: @sample
|
||||
defp sample, do: Process.get(:unused, %{a: 1, b: 2})
|
||||
|
||||
test "maps in attributes" do
|
||||
assert @sample == %{a: 1, b: 2}
|
||||
@@ -82,7 +81,7 @@ defmodule MapTest do
|
||||
|
||||
test "map_size/1" do
|
||||
assert map_size(%{}) == 0
|
||||
assert map_size(@sample) == 2
|
||||
assert map_size(sample()) == 2
|
||||
end
|
||||
|
||||
test "new/1" do
|
||||
@@ -282,8 +281,6 @@ defmodule MapTest do
|
||||
defstruct name: "john", age: 27
|
||||
end
|
||||
|
||||
defp empty_map(), do: %{}
|
||||
|
||||
test "structs" do
|
||||
assert %ExternalUser{} == %{__struct__: ExternalUser, name: "john", age: 27}
|
||||
|
||||
@@ -294,10 +291,6 @@ defmodule MapTest do
|
||||
|
||||
%ExternalUser{name: name} = %ExternalUser{}
|
||||
assert name == "john"
|
||||
|
||||
assert_raise BadStructError, "expected a struct named MapTest.ExternalUser, got: %{}", fn ->
|
||||
%ExternalUser{empty_map() | name: "meg"}
|
||||
end
|
||||
end
|
||||
|
||||
describe "structs with variable name" do
|
||||
@@ -325,12 +318,11 @@ defmodule MapTest do
|
||||
end
|
||||
end
|
||||
|
||||
defp foo(), do: "foo"
|
||||
defp destruct1(%module{}), do: module
|
||||
defp destruct2(%_{}), do: :ok
|
||||
|
||||
test "does not match" do
|
||||
invalid_struct = %{__struct__: foo()}
|
||||
invalid_struct = Process.get(:unused, %{__struct__: "foo"})
|
||||
|
||||
assert_raise CaseClauseError, fn ->
|
||||
case invalid_struct do
|
||||
@@ -345,7 +337,7 @@ defmodule MapTest do
|
||||
end
|
||||
|
||||
assert_raise CaseClauseError, fn ->
|
||||
foo = foo()
|
||||
foo = Process.get(:unused, "foo")
|
||||
|
||||
case invalid_struct do
|
||||
%^foo{} -> :ok
|
||||
@@ -370,7 +362,7 @@ defmodule MapTest do
|
||||
end
|
||||
|
||||
assert_raise MatchError, fn ->
|
||||
foo = foo()
|
||||
foo = Process.get(:unused, "foo")
|
||||
%^foo{} = invalid_struct
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
Code.require_file("../test_helper.exs", __DIR__)
|
||||
|
||||
defmodule Module.LocalsTrackerTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias Module.LocalsTracker, as: D
|
||||
|
||||
setup do
|
||||
set = :ets.new(__MODULE__, [:set, :public])
|
||||
bag = :ets.new(__MODULE__, [:duplicate_bag, :public])
|
||||
[ref: {set, bag}]
|
||||
end
|
||||
|
||||
## Locals
|
||||
|
||||
test "functions are reachable when connected through another one", config do
|
||||
D.add_local(config[:ref], {:public, 1}, {:private, 1}, [line: 1], false)
|
||||
assert {:private, 1} in D.reachable_from(config[:ref], {:public, 1})
|
||||
end
|
||||
|
||||
test "can yank and reattach nodes", config do
|
||||
D.add_local(config[:ref], {:foo, 1}, {:bar, 1}, [line: 1], false)
|
||||
|
||||
outfoo = D.yank(config[:ref], {:foo, 1})
|
||||
outbar = D.yank(config[:ref], {:bar, 1})
|
||||
|
||||
D.reattach(config[:ref], {:bar, 1}, :defp, {:bar, 1}, outbar, line: 2)
|
||||
D.reattach(config[:ref], {:foo, 1}, :def, {:foo, 1}, outfoo, line: 3)
|
||||
assert {:bar, 1} in D.reachable_from(config[:ref], {:foo, 1})
|
||||
end
|
||||
|
||||
@used [
|
||||
{{:public, 1}, :def, [], 0}
|
||||
]
|
||||
|
||||
test "unused private definitions are marked as so", config do
|
||||
D.add_local(config[:ref], {:public, 1}, {:private, 1}, [line: 1], false)
|
||||
|
||||
unused = D.collect_unused_locals(config[:ref], @used, [{{:private, 0}, :defp, [], 0}])
|
||||
assert unused == {[private: 0], [{[], {:unused_def, {:private, 0}, :defp}}]}
|
||||
|
||||
unused = D.collect_unused_locals(config[:ref], @used, [{{:private, 1}, :defp, [], 0}])
|
||||
assert unused == {[], []}
|
||||
end
|
||||
|
||||
@unused [
|
||||
{{:private, 3}, :defp, [], 3}
|
||||
]
|
||||
|
||||
test "preserves column information on retrieval", config do
|
||||
D.add_local(config[:ref], {:public, 1}, {:private, 1}, [line: 1, column: 1], false)
|
||||
|
||||
undefined = D.collect_undefined_locals(config[:ref], @used, "foo.exs")
|
||||
|
||||
assert undefined == [
|
||||
{{:public, 1}, [span: {1, 8}, line: 1, column: 1], "foo.exs", {:private, 1},
|
||||
:undefined_function}
|
||||
]
|
||||
end
|
||||
|
||||
test "private definitions with unused default arguments", config do
|
||||
unused = D.collect_unused_locals(config[:ref], @used, @unused)
|
||||
assert unused == {[private: 3], [{[], {:unused_def, {:private, 3}, :defp}}]}
|
||||
|
||||
D.add_local(config[:ref], {:public, 1}, {:private, 3}, [line: 1], false)
|
||||
unused = D.collect_unused_locals(config[:ref], @used, @unused)
|
||||
assert unused == {[], [{[], {:unused_args, {:private, 3}}}]}
|
||||
end
|
||||
|
||||
test "private definitions with some unused default arguments", config do
|
||||
D.add_local(config[:ref], {:public, 1}, {:private, 1}, [line: 1], false)
|
||||
unused = D.collect_unused_locals(config[:ref], @used, @unused)
|
||||
assert unused == {[private: 3], [{[], {:unused_args, {:private, 3}, 1}}]}
|
||||
end
|
||||
|
||||
test "private definitions with all used default arguments", config do
|
||||
D.add_local(config[:ref], {:public, 1}, {:private, 0}, [line: 1], false)
|
||||
unused = D.collect_unused_locals(config[:ref], @used, @unused)
|
||||
assert unused == {[private: 3], []}
|
||||
end
|
||||
|
||||
### Undefined functions
|
||||
|
||||
test "undefined functions are marked as so", config do
|
||||
D.add_local(config[:ref], {:public, 1}, {:private, 1}, [line: 1], false)
|
||||
|
||||
undefined = D.collect_undefined_locals(config[:ref], @used, "foo.exs")
|
||||
assert undefined == [{{:public, 1}, [line: 1], "foo.exs", {:private, 1}, :undefined_function}]
|
||||
end
|
||||
|
||||
### Incorrect dispatches
|
||||
|
||||
test "incorrect dispatches are marked as so", config do
|
||||
{set, _bag} = config[:ref]
|
||||
:ets.insert(set, {{:def, {:macro, 1}}, :defmacro, [], "nofile", false, {0, true, 0}})
|
||||
definitions = [{{:public, 1}, :def, [], 0}, {{:macro, 1}, :defmacro, [], 0}]
|
||||
|
||||
D.add_local(config[:ref], {:public, 1}, {:macro, 1}, [line: 5], false)
|
||||
|
||||
undefined = D.collect_undefined_locals(config[:ref], definitions, "foo.exs")
|
||||
assert undefined == [{{:public, 1}, [line: 5], "foo.exs", {:macro, 1}, :incorrect_dispatch}]
|
||||
end
|
||||
|
||||
## Defaults
|
||||
|
||||
test "defaults are connected to last clause only", config do
|
||||
D.add_defaults(config[:ref], :defp, {:foo, 4}, 2, line: 1)
|
||||
D.add_local(config[:ref], {:public, 1}, {:foo, 2}, [line: 2], false)
|
||||
assert {:foo, 2} in D.reachable_from(config[:ref], {:public, 1})
|
||||
refute {:foo, 3} in D.reachable_from(config[:ref], {:public, 1})
|
||||
assert {:foo, 4} in D.reachable_from(config[:ref], {:public, 1})
|
||||
end
|
||||
|
||||
## Imports
|
||||
|
||||
test "find import conflicts", config do
|
||||
entries = [{{:conflict, 1}, :def, [], []}]
|
||||
refute {[], {Module, {:conflict, 1}}} in D.collect_imports_conflicts(config[:ref], entries)
|
||||
|
||||
D.add_local(config[:ref], {:public, 1}, {:foo, 2}, [line: 1], false)
|
||||
D.add_import(config[:ref], {:foo, 2}, Module, {:conflict, 1})
|
||||
D.add_import(config[:ref], {:foo, 2}, Module, {:conflict, 1})
|
||||
assert {[], {Module, {:conflict, 1}}} in D.collect_imports_conflicts(config[:ref], entries)
|
||||
end
|
||||
|
||||
defmodule NoPrivate do
|
||||
defmacrop foo(), do: bar()
|
||||
defp bar(), do: :baz
|
||||
def baz(), do: foo()
|
||||
end
|
||||
|
||||
test "does not include unreachable locals" do
|
||||
assert NoPrivate.module_info(:functions) |> Keyword.take([:foo, :bar, :"MACRO-foo"]) == []
|
||||
end
|
||||
end
|
||||
@@ -61,4 +61,37 @@ defmodule Module.Types.InferTest do
|
||||
{[dynamic(atom([:error]))], atom([:five])}
|
||||
]}
|
||||
end
|
||||
|
||||
test "infers return types from private functions", config do
|
||||
types =
|
||||
infer config do
|
||||
def pub(x), do: priv(x)
|
||||
defp priv(:ok), do: :ok
|
||||
defp priv(:error), do: :error
|
||||
end
|
||||
|
||||
assert types[{:pub, 1}] == {:infer, [{[dynamic()], dynamic(atom([:ok, :error]))}]}
|
||||
assert types[{:priv, 1}] == nil
|
||||
end
|
||||
|
||||
test "infers return types from super functions", config do
|
||||
types =
|
||||
infer config do
|
||||
def pub(:ok), do: :ok
|
||||
def pub(:error), do: :error
|
||||
defoverridable pub: 1
|
||||
def pub(x), do: super(x)
|
||||
end
|
||||
|
||||
assert types[{:pub, 1}] == {:infer, [{[dynamic()], dynamic(atom([:ok, :error]))}]}
|
||||
end
|
||||
|
||||
test "infers return types even with loops", config do
|
||||
types =
|
||||
infer config do
|
||||
def pub(x), do: pub(x)
|
||||
end
|
||||
|
||||
assert types[{:pub, 1}] == {:infer, [{[dynamic()], dynamic()}]}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -51,8 +51,10 @@ defmodule Module.Types.IntegrationTest do
|
||||
{{:behaviour_info, 1}, %{sig: :none}}
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
test "type checks signatures" do
|
||||
describe "type checking" do
|
||||
test "inferred remote calls" do
|
||||
files = %{
|
||||
"a.ex" => """
|
||||
defmodule A do
|
||||
@@ -101,6 +103,98 @@ defmodule Module.Types.IntegrationTest do
|
||||
|
||||
assert_warnings(files, warnings)
|
||||
end
|
||||
|
||||
test "mismatched locals" do
|
||||
files = %{
|
||||
"a.ex" => """
|
||||
defmodule A do
|
||||
def error(), do: private(raise "oops")
|
||||
def public(x), do: private(List.to_tuple(x))
|
||||
defp private(:ok), do: nil
|
||||
end
|
||||
"""
|
||||
}
|
||||
|
||||
warnings = [
|
||||
"""
|
||||
warning: incompatible types given to private/1:
|
||||
|
||||
private(raise RuntimeError.exception("oops"))
|
||||
|
||||
""",
|
||||
"the 1st argument is empty (often represented as none())",
|
||||
"""
|
||||
typing violation found at:
|
||||
│
|
||||
2 │ def error(), do: private(raise "oops")
|
||||
│ ~
|
||||
│
|
||||
└─ a.ex:2:20: A.error/0
|
||||
""",
|
||||
"""
|
||||
warning: incompatible types given to private/1:
|
||||
|
||||
private(List.to_tuple(x))
|
||||
""",
|
||||
"""
|
||||
typing violation found at:
|
||||
│
|
||||
3 │ def public(x), do: private(List.to_tuple(x))
|
||||
│ ~
|
||||
│
|
||||
└─ a.ex:3:22: A.public/1
|
||||
"""
|
||||
]
|
||||
|
||||
assert_warnings(files, warnings)
|
||||
end
|
||||
|
||||
test "unused private clauses" do
|
||||
files = %{
|
||||
"a.ex" => """
|
||||
defmodule A do
|
||||
def public(x) do
|
||||
private(List.to_tuple(x))
|
||||
end
|
||||
|
||||
defp private(nil), do: nil
|
||||
defp private("foo"), do: "foo"
|
||||
defp private({:ok, ok}), do: ok
|
||||
defp private({:error, error}), do: error
|
||||
defp private("bar"), do: "bar"
|
||||
end
|
||||
"""
|
||||
}
|
||||
|
||||
warnings = [
|
||||
"""
|
||||
warning: this clause of defp private/1 is never used
|
||||
│
|
||||
6 │ defp private(nil), do: nil
|
||||
│ ~
|
||||
│
|
||||
└─ a.ex:6:8: A.private/1
|
||||
""",
|
||||
"""
|
||||
warning: this clause of defp private/1 is never used
|
||||
│
|
||||
7 │ defp private("foo"), do: "foo"
|
||||
│ ~
|
||||
│
|
||||
└─ a.ex:7:8: A.private/1
|
||||
""",
|
||||
"""
|
||||
warning: this clause of defp private/1 is never used
|
||||
│
|
||||
10 │ defp private("bar"), do: "bar"
|
||||
│ ~
|
||||
│
|
||||
└─ a.ex:10:8: A.private/1
|
||||
"""
|
||||
]
|
||||
|
||||
assert_warnings(files, warnings)
|
||||
end
|
||||
end
|
||||
|
||||
describe "undefined warnings" do
|
||||
@@ -345,42 +439,6 @@ defmodule Module.Types.IntegrationTest do
|
||||
assert_warnings(files, warnings)
|
||||
end
|
||||
|
||||
test "protocols are checked, ignoring missing built-in impls" do
|
||||
files = %{
|
||||
"a.ex" => """
|
||||
defprotocol AProtocol do
|
||||
def func(arg)
|
||||
end
|
||||
|
||||
defmodule AImplementation do
|
||||
defimpl AProtocol do
|
||||
def func(_), do: B.no_func()
|
||||
end
|
||||
end
|
||||
"""
|
||||
}
|
||||
|
||||
warnings = [
|
||||
"B.no_func/0 is undefined (module B is not available or is yet to be defined)",
|
||||
"a.ex:7:24: AProtocol.AImplementation.func/1"
|
||||
]
|
||||
|
||||
assert_warnings(files, warnings)
|
||||
end
|
||||
|
||||
test "handles Erlang ops" do
|
||||
files = %{
|
||||
"a.ex" => """
|
||||
defmodule A do
|
||||
def a(a, b), do: a and b
|
||||
def b(a, b), do: a or b
|
||||
end
|
||||
"""
|
||||
}
|
||||
|
||||
assert_no_warnings(files)
|
||||
end
|
||||
|
||||
test "hints exclude deprecated functions" do
|
||||
files = %{
|
||||
"a.ex" => """
|
||||
|
||||
@@ -138,7 +138,8 @@ defmodule TypeHelper do
|
||||
|
||||
defp new_stack(mode) do
|
||||
cache = if mode == :infer, do: :none, else: Module.ParallelChecker.test_cache()
|
||||
Types.stack(mode, "types_test.ex", TypesTest, {:test, 0}, [], cache)
|
||||
handler = fn _, _, _, _ -> raise "no local lookup" end
|
||||
Types.stack(mode, "types_test.ex", TypesTest, {:test, 0}, [], cache, handler)
|
||||
end
|
||||
|
||||
defp new_context() do
|
||||
|
||||
@@ -314,13 +314,13 @@ defmodule ExUnit.AssertionsTest do
|
||||
true = assert match?({2, 1}, Value.tuple())
|
||||
|
||||
try do
|
||||
assert match?({:ok, _}, error(true))
|
||||
assert match?({:ok, _}, Process.get(:unused, :ok))
|
||||
flunk("This should never be tested")
|
||||
rescue
|
||||
error in [ExUnit.AssertionError] ->
|
||||
"match (match?) failed" = error.message
|
||||
"assert match?({:ok, _}, error(true))" = Macro.to_string(error.expr)
|
||||
"{:error, true}" = Macro.to_string(error.right)
|
||||
"assert match?({:ok, _}, Process.get(:unused, :ok))" = Macro.to_string(error.expr)
|
||||
":ok" = Macro.to_string(error.right)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -446,10 +446,14 @@ defmodule ExUnit.CallbacksNoTests do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
setup_all do
|
||||
raise "never run"
|
||||
if :rand.uniform() >= 0 do
|
||||
raise "never run"
|
||||
end
|
||||
end
|
||||
|
||||
setup do
|
||||
raise "never run"
|
||||
if :rand.uniform() >= 0 do
|
||||
raise "never run"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user