Current section
Files
Jump to
Current section
Files
lib/dune/allowlist/spec.ex
defmodule Dune.Allowlist.Spec do
@moduledoc false
alias Dune.Allowlist
alias Dune.Parser.RealModule
@type t :: %__MODULE__{
modules: %{optional(module) => [{atom, non_neg_integer, Allowlist.status()}]}
}
@enforce_keys [:modules]
defstruct @enforce_keys
def new do
%__MODULE__{modules: %{}}
end
@spec list_fun_statuses(t) :: list({module, atom, Allowlist.status()})
def list_fun_statuses(%__MODULE__{modules: modules}) do
for {module, funs} <- modules, {fun_name, _arity, status} <- funs do
{module, fun_name, status}
end
|> Enum.sort()
|> Enum.dedup()
end
@spec list_ordered_modules(t) :: list({module, {atom, Allowlist.status()}})
def list_ordered_modules(%__MODULE__{modules: modules}) do
modules
|> Enum.map(fn {module, funs} ->
{module, group_funs_by_status(funs)}
end)
|> Enum.sort()
end
defp group_funs_by_status(funs) do
Enum.group_by(
funs,
fn {_fun, _arity, status} -> extract_status_atom(status) end,
fn {fun, arity, _status} -> {fun, arity} end
)
|> Enum.map(fn {status, funs} -> {status, Enum.sort(funs) |> Enum.dedup_by(&elem(&1, 0))} end)
|> Enum.sort_by(fn {status, _} -> status_sort(status) end)
end
defp extract_status_atom(:restricted), do: :restricted
defp extract_status_atom(:allowed), do: :allowed
defp extract_status_atom({:shimmed, _, _}), do: :shimmed
defp status_sort(:allowed), do: 1
defp status_sort(:shimmed), do: 2
defp status_sort(:restricted), do: 3
@spec add_new_module(t, module, :all) :: t
def add_new_module(%__MODULE__{modules: modules}, module, _status)
when :erlang.map_get(module, modules) != nil do
# TODO proper error type
raise "ModuleConflict: module #{inspect(module)} already defined"
end
def add_new_module(spec = %__MODULE__{modules: modules}, module, status) when is_atom(module) do
Code.ensure_compiled!(module)
functions =
RealModule.list_functions(module)
|> classify_functions(status)
%{spec | modules: Map.put(modules, module, functions)}
end
defp classify_functions(functions, :all) do
Enum.map(functions, fn {fun_name, arity} -> {fun_name, arity, :allowed} end)
end
defp classify_functions(functions, only: only) when is_list(only) do
do_classify(functions, only, :allowed, :restricted)
end
defp classify_functions(functions, except: except) when is_list(except) do
do_classify(functions, except, :restricted, :allowed)
end
defp classify_functions(functions, opts) do
case Keyword.pop(opts, :shims) do
{shims, remaining_opts} when is_list(shims) ->
new_opts =
case remaining_opts do
[] -> :all
other -> other
end
classify_functions(functions, new_opts) |> shim_functions(shims)
{nil, _} ->
raise "Invalid opts #{inspect(opts)}"
end
end
defp do_classify(functions, set_list, member_atom, non_member_atom) do
set = to_atom_set(set_list)
functions
|> Enum.map_reduce(set, fn {fun_name, arity}, acc ->
case set do
%{^fun_name => _} -> {{fun_name, arity, member_atom}, Map.delete(acc, fun_name)}
_ -> {{fun_name, arity, non_member_atom}, acc}
end
end)
|> unwrap_classify()
end
defp to_atom_set(list) do
Enum.each(list, fn atom when is_atom(atom) -> :ok end)
:maps.from_keys(list, nil)
end
defp unwrap_classify({result, remaining}) when remaining == %{}, do: result
defp unwrap_classify({_, remaining}) do
[{key, _}] = Enum.take(remaining, 1)
raise "Unknown function #{key}"
end
defp shim_functions(functions, shims) do
# TODO validate shims
Enum.map(functions, fn fun = {fun_name, arity, _status} ->
case Keyword.get(shims, fun_name) do
nil ->
fun
{shim_module, shim_fun_name} ->
validate_shim!(shim_module, shim_fun_name, arity + 1)
{fun_name, arity, {:shimmed, shim_module, shim_fun_name}}
end
end)
end
defp validate_shim!(module, fun_name, arity) do
Code.ensure_compiled!(module)
unless function_exported?(module, fun_name, arity) or macro_exported?(module, fun_name, arity) do
raise "Invalid shim: function #{inspect(module)}.#{fun_name}/#{arity} doesn't exist!"
end
end
end