Using Code.eval_string to call other functions in the module

I have some code essentially like this:

defmodule A do

  def action(x), do: x  

  def eval(text) do
    { result, _ } = Code.eval_string(text)
    result
  end

end

I’d like for the argument to eval/1 to be able call other functions defined in the module, as in:

iex(1)> A.eval("action(:ok)")
:ok

Instead, IEx gives me this:

** (CompileError) nofile:1: undefined function action/1
    (elixir) lib/code.ex:192: Code.eval_string/3
    lib/modG.ex:6: A.eval/1

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)?

1 Like

Well you could always turn this into:

iex(1)> A.eval("A.action(:ok)")

The issue is that it’s not declaring what action to call. ^.^

You can also import A into the environment for the eval too.

And of course the usual caveats of UNSAFE/WARNING/ETC when you are eval’ing code from outside the source. :wink:

Gah my reply isn’t showing up :frowning: Trying again:

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?

hm, perhaps you were doing Code.eval_string(x, __ENV__) instead of Code.eval_string(x, _bindings = [], __ENV__) ?

With this definition of A.eval/1, I still get the UndefinedFunctionError :frowning:

  def eval(text) do
    { result, _ } = Code.eval_string(text, [], __ENV__)
    result
  end

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 :slight_smile:

1 Like

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

5 Likes

Oh that’s excellent. Thank you so much! :smiley:

Ooo yeah what @vic put is great, it’s what I suggested with some sanity checking too! :slight_smile:

It’s good if your input is safe anyway. :slight_smile: