I tried supplying __ENV__ to Code.eval_string, but then get this:
** (UndefinedFunctionError) function :erl_eval.action/1 is undefined or private
(stdlib) :erl_eval.action/1
(stdlib) :erl_eval.expr/3
(elixir) lib/code.ex:186: Code.eval_string/3
lib/modG.ex:6: A.eval/1
Passing a modified __ENV__ with the functions/arities added to :functions doesn’t seem make it work either. I don’t understand the internals of Elixir enough for reading the source to help. What do I need to do for the eval string to have access to the correct context, or if that’s not possible, is there a workaround (hacky or not)?
Unfortunately the string to be eval’d is the output of a legacy tool, so I’m kind of stuck with the no-module-prefix calling convention. These are large files with tons of nesting, and I’d rather avoid doing pre-processing to tack the prefix on if I can help it.
Inside of eval/1 I can call action/1 without the module prefix, and I’m hoping there’s a way to give that context to Code.eval_string. Is there a way to capture the current context?
Yay! Turns out @before_compile is the key. Just before A compiles, we insert the get_module_environment/0 function that can return __ENV__ plus the functions from the target module:
defmodule ModuleEnvironment do
defmacro __before_compile__(_env) do
quote do
def get_module_environment() do
Map.put(
__ENV__,
:functions,
[{ __MODULE__, __info__(:functions) } | __ENV__.functions]
)
end
end
end
end
defmodule A do
@before_compile ModuleEnvironment
def action(x), do: x
def eval(text) do
env = get_module_environment()
{ result, _ } = Code.eval_string(text, [], env)
result
end
end
If anyone knows of a cleaner way, please let me know
Just like @OvermindDL1 said, you could bring the functions into scope with import. I’d also like to point as others that most of the time eval is the root of all evil :D.
The following works for me:
defmodule K do
def kill do
IO.inspect("EVIL")
end
end
defmodule A do
def action(x), do: x
def eval(text) do
{value, _} =
text
|> Code.string_to_quoted!()
|> locals_calls_only
|> in_module(__MODULE__)
|> Code.eval_quoted
value
end
defp in_module(ast, mod) do
quote do
import unquote(mod)
# prevent Kernel.exit and others to be in scope
# only allow function from the given module
import Kernel, only: []
unquote(ast)
end
end
defp locals_calls_only(ast) do
ast
|> Macro.prewalk(fn
# dont allow remote function calls
evil = {{:., _, _}, _, _} ->
IO.puts("warning: removed non local call #{inspect(evil)}")
nil
# dont allow calling to eval and prevent loops
{:eval, _, args} when is_list(args) ->
IO.puts("warning: removed call to eval")
nil
# either raise or rewrite the code
code -> code
end)
end
end
defmodule FooTest do
use ExUnit.Case
require A
test "calling action" do
assert :foo == A.eval("action(:foo)")
end
test "remove remote calls" do
assert nil == A.eval("K.kill")
end
test "remove eval calls" do
assert nil == A.eval("eval(2)")
end
test "remove kernel calls" do
assert_raise CompileError, ~r"undefined function exit/1", fn ->
A.eval("exit(:shutdown)")
end
end
end
The in_module function could be used to add other functions into scope. Edited to remove non local calls, so that only functions you have on A are valid, prevent calling eval itself and also things like Kernel.exit