Files
n8n-openai-adapter/lib/elixir/lib/module/locals_tracker.ex
T
José Valim 739ad53fed Break Macro.Env apart (#11164)
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.
2021-08-03 13:35:15 +02:00

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