defmodule Octopus.Eval do
@moduledoc """
The module evaluates strings with Elixir code. Used in Transform module.
"""
require Logger
@spec eval_string(String.t(), Keyword.t()) :: any() | String.t()
def eval_string(string, args) when is_binary(string) do
{:ok, do_eval_string(string, args)}
rescue
error ->
{:error, inspect(error)}
end
def eval_string(arg, _args), do: {:error, "#{arg} is not a string"}
defp do_eval_string(string, args) do
{value, _} =
string
|> String.replace("'", "\"")
|> Code.string_to_quoted!(existing_atoms_only: true)
|> locals_calls_only()
|> limit_kernel_calls()
|> import_helpers(Keyword.get(args, :helpers, []))
|> Code.eval_quoted(args)
value
end
defp locals_calls_only(ast) do
ast
|> Macro.prewalk(fn
{{:., _, [Access, _]}, _, _} = code ->
code
{{:., _, [{:__aliases__, _, [:Access]}, _]}, _, _} = code ->
code
{{:., _, _}, _, _} = bad ->
raise("Non local call #{inspect(bad)}")
{:eval, _, args} when is_list(args) ->
raise("No eval")
code ->
code
end)
end
def limit_kernel_calls(ast) do
quote do
import Kernel,
only: [
!=: 2,
!==: 2,
*: 2,
**: 2,
+: 1,
+: 2,
++: 2,
-: 1,
-: 2,
--: 2,
/: 2,
<: 2,
<=: 2,
<>: 2,
==: 2,
===: 2,
=~: 2,
>: 2,
>=: 2,
abs: 1,
byte_size: 1,
ceil: 1,
div: 2,
elem: 2,
floor: 1,
get_in: 2,
hd: 1,
if: 2,
inspect: 1,
inspect: 2,
is_atom: 1,
is_binary: 1,
is_bitstring: 1,
is_boolean: 1,
is_float: 1,
is_function: 1,
is_function: 2,
is_integer: 1,
is_list: 1,
is_map: 1,
is_map_key: 2,
is_number: 1,
is_pid: 1,
is_port: 1,
is_reference: 1,
is_tuple: 1,
length: 1,
map_size: 1,
max: 2,
min: 2,
pop_in: 2,
put_elem: 3,
put_in: 3,
rem: 2,
round: 1,
tl: 1,
to_string: 1,
trunc: 1,
tuple_size: 1,
unless: 2,
update_in: 3
]
unquote(ast)
end
end
defp import_helpers(ast, []) do
quote do
unquote(ast)
end
end
defp import_helpers(ast, modules) do
Enum.reduce(modules, ast, fn module, acc ->
quote do
import unquote(module)
unquote(acc)
end
end)
end
end