Current section

Files

Jump to
dune lib dune parser atom_encoder.ex
Raw

lib/dune/parser/atom_encoder.ex

defmodule Dune.Parser.AtomEncoder do
@moduledoc false
alias Dune.AtomMapping
@type atom_category :: :alias | :private_var | :public_var | :other
@atom_categories 4
# TODO Remove when dropping support for Elixir 1.16
extra_modules =
if System.version() |> Version.compare("1.17.0-rc.0") != :lt, do: [Duration], else: []
@elixir_modules [
Kernel,
Kernel.SpecialForms,
Atom,
Base,
Bitwise,
Date,
DateTime,
Duration,
Exception,
Float,
Function,
Integer,
Module,
NaiveDateTime,
Record,
Regex,
String,
Time,
Tuple,
URI,
Version,
Version.Requirement,
Access,
Date.Range,
Enum,
Keyword,
List,
Map,
MapSet,
Range,
Stream,
File,
File.Stat,
File.Stream,
IO,
IO.ANSI,
IO.Stream,
OptionParser,
Path,
Port,
StringIO,
System,
Calendar,
Calendar.ISO,
Calendar.TimeZoneDatabase,
Calendar.UTCOnlyTimeZoneDatabase,
Agent,
Application,
Config,
Config.Provider,
Config.Reader,
DynamicSupervisor,
GenServer,
Node,
Process,
Registry,
Supervisor,
Task,
Task.Supervisor,
Collectable,
Enumerable,
Inspect,
Inspect.Algebra,
Inspect.Opts,
List.Chars,
Protocol,
String.Chars,
Code,
Kernel.ParallelCompiler,
Macro,
Macro.Env,
Behaviour,
Dict,
GenEvent,
HashDict,
HashSet,
Set,
Supervisor.Spec,
ArgumentError,
ArithmeticError,
BadArityError,
BadBooleanError,
BadFunctionError,
BadMapError,
BadStructError,
CaseClauseError,
Code.LoadError,
CompileError,
CondClauseError,
Enum.EmptyError,
Enum.OutOfBoundsError,
ErlangError,
File.CopyError,
File.Error,
File.LinkError,
File.RenameError,
FunctionClauseError,
IO.StreamError,
Inspect.Error,
KeyError,
MatchError,
Module.Types.Error,
OptionParser.ParseError,
Protocol.UndefinedError,
Regex.CompileError,
RuntimeError,
SyntaxError,
SystemLimitError,
TokenMissingError,
TryClauseError,
DuneRestrictedError,
UnicodeConversionError,
Version.InvalidRequirementError,
Version.InvalidVersionError,
WithClauseError
] ++ extra_modules
@module_reprs @elixir_modules
|> Enum.flat_map(&Module.split/1)
|> Map.new(&{&1, String.to_existing_atom(&1)})
@spec load_atom_mapping(AtomMapping.t() | nil) :: :ok
def load_atom_mapping(nil), do: :ok
def load_atom_mapping(%AtomMapping{atoms: atoms}) do
count = Enum.count(atoms)
Process.put(:__Dune_atom_count__, count)
Enum.each(atoms, fn {atom, binary} ->
Process.put({:__Dune_atom__, binary}, atom)
end)
end
@spec static_atoms_encoder(String.t(), non_neg_integer()) :: {:ok, atom} | {:error, String.t()}
def static_atoms_encoder(binary, pool_size)
when is_binary(binary) and is_integer(pool_size) do
case @module_reprs do
%{^binary => atom} ->
{:ok, atom}
_ ->
if binary =~ "Dune" do
{:error, "Atoms containing `Dune` are restricted for safety"}
else
atom_category = categorize_atom_binary(binary)
do_static_atoms_encoder(binary, atom_category, pool_size)
end
end
end
@spec categorize_atom_binary(binary) :: atom_category
def categorize_atom_binary(atom_binary) do
charlist = String.to_charlist(atom_binary)
case {Code.Fragment.cursor_context(charlist), atom_binary} do
{{:alias, ^charlist}, _} -> :alias
{{:local_or_var, ^charlist}, "_" <> _} -> :private_var
{{:local_or_var, ^charlist}, _} -> :public_var
_ -> :other
end
end
defp do_static_atoms_encoder("Elixir." <> rest, :alias, pool_size) do
rest
|> String.split(".")
|> encode_many_atoms(pool_size, [])
end
defp do_static_atoms_encoder(binary, atom_category, pool_size) do
process_key = {:__Dune_atom__, binary}
case Process.get(process_key, nil) do
nil -> do_static_atoms_encoder(binary, atom_category, process_key, pool_size)
atom when is_atom(atom) -> {:ok, atom}
end
end
defp do_static_atoms_encoder(binary, atom_category, process_key, pool_size) do
{:ok, String.to_existing_atom(binary)}
rescue
ArgumentError ->
case new_atom(atom_category, pool_size) do
{:ok, atom} ->
Process.put(process_key, atom)
if atom_category == :other do
Process.put({:__Dune_atom_extra_info__, atom}, :wrapped)
end
{:ok, atom}
{:error, error} ->
{:error, error}
end
end
defp encode_many_atoms([], _pool_size, acc) do
{:ok, {:__aliases__, [], [Elixir | Enum.reverse(acc)]}}
end
defp encode_many_atoms([head | tail], pool_size, acc) do
case do_static_atoms_encoder(head, :alias, pool_size) do
{:ok, atom} -> encode_many_atoms(tail, pool_size, [atom | acc])
{:error, error} -> {:error, error}
end
end
@spec plain_atom_mapping :: AtomMapping.t()
def plain_atom_mapping() do
atoms =
for {{:__Dune_atom__, binary}, atom} <- Process.get() do
{atom, binary}
end
extra_info =
for {{:__Dune_atom_extra_info__, atom}, info} <- Process.get() do
{atom, info}
end
AtomMapping.from_atoms(atoms, extra_info)
end
defp new_atom(atom_category, pool_size) do
count = Process.get(:__Dune_atom_count__, 0) + 1
if count * @atom_categories > pool_size do
{:error, "atom_pool_size exceeded, failed to parse atom"}
else
Process.put(:__Dune_atom_count__, count)
atom = do_new_atom(atom_category, count)
{:ok, atom}
end
end
defp do_new_atom(:alias, count) do
:"Dune_Atom_#{count}__"
end
defp do_new_atom(:public_var, count) do
:"a__Dune_atom_#{count}__"
end
defp do_new_atom(category, count) when category in [:private_var, :other] do
:"__Dune_atom_#{count}__"
end
@spec encode_modules(Macro.t(), AtomMapping.t(), AtomMapping.t() | nil) ::
{Macro.t(), AtomMapping.t()}
def encode_modules(ast, plain_atom_mapping, existing_mapping) do
initial_acc = get_module_acc(existing_mapping)
{new_ast, acc} =
Macro.postwalk(ast, initial_acc, fn
{:__aliases__, ctx, atoms}, acc ->
{modules, new_acc} = remove_elixir_prefix(atoms) |> map_modules_ast(acc)
{{:__aliases__, ctx, modules}, new_acc}
other, acc ->
{other, acc}
end)
atom_mapping = build_module_mapping(acc, plain_atom_mapping)
{new_ast, atom_mapping}
end
defp get_module_acc(nil), do: %{}
defp get_module_acc(%AtomMapping{atoms: atoms, modules: modules}) do
reverse_atoms = Map.new(atoms, fn {atom, string} -> {string, atom} end)
Map.new(modules, fn {atom, string} ->
atoms = String.split(string, ".") |> Enum.map(&Map.fetch!(reverse_atoms, &1))
{atoms, atom}
end)
end
defp remove_elixir_prefix(atoms = [Elixir, Elixir | _]), do: atoms
defp remove_elixir_prefix([Elixir | atoms]) when atoms != [], do: atoms
defp remove_elixir_prefix(atoms), do: atoms
defp map_modules_ast(atoms, acc) do
case acc do
%{^atoms => module_name} ->
{[module_name], acc}
_ ->
try do
atoms |> Enum.join(".") |> then(&"Elixir.#{&1}") |> String.to_existing_atom()
rescue
ArgumentError ->
module_name = :"Dune_Module_#{map_size(acc) + 1}__"
{[module_name], Map.put(acc, atoms, module_name)}
else
_ ->
{atoms, acc}
end
end
end
defp build_module_mapping(acc, plain_atom_mapping) do
modules =
Enum.map(acc, fn {atoms, module_name} ->
string = Enum.map_join(atoms, ".", &AtomMapping.to_string(plain_atom_mapping, &1))
module = Module.concat([module_name])
{module, string}
end)
AtomMapping.add_modules(plain_atom_mapping, modules)
end
end