Previously, the internal Elixir expansion pass worked directly on
Macro.Env. However, this poised an issue, if we want to track
more information in the pass, we ended up exposing it on
Macro.Env, making it larger, and potentially slowing down
operations such as __ENV__ serialization.
This commit refactors the expansion pass to work with two
structures, the Macro.Env struct and a #elixir_ex{} record.
254 lines
7.0 KiB
Elixir
254 lines
7.0 KiB
Elixir
# 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_line(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) do
|
|
undefined =
|
|
for {pair, _, meta, _} <- all_defined,
|
|
{local, line, macro_dispatch?} <- out_neighbours(bag, {:local, pair}),
|
|
error = undefined_local_error(set, local, macro_dispatch?),
|
|
do: {build_meta(line, meta), local, error}
|
|
|
|
: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 build_meta(nil, _meta), do: []
|
|
|
|
# We need to transform any file annotation in the function
|
|
# definition into a keep annotation that is used by the
|
|
# error handling system in order to respect line/file.
|
|
defp build_meta(line, meta) do
|
|
case Keyword.get(meta, :file) do
|
|
{file, _} -> [keep: {file, line}]
|
|
_ -> [line: line]
|
|
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
|