Is there a way to get function body as a string in Elixir?

Viewed 266

Let's assume I have a function defintion like below:

def my_function(argument) do
   do_something(argument)+2
end

Now, the output I expect would be something like this:

>> function_body(&my_function/1)
"do_something(argument)+2"

Is there any way to achieve it?

2 Answers

I think if you produce a release, then the sources are no longer there to do this, but in a test environment, you could extract the AST for a given function, with something like the following:

def fetch_ast(module, fun) do
  module.__info__(:compile)[:source]
  |> to_string()
  |> File.read!()
  |> Code.string_to_quoted!()
  |> Macro.prewalk(fn
    result = {:def, _, [{^fun, _, _} | _]} -> throw(result)
    other -> other
  end)

  :error
catch
  result -> {:ok, result}
end

This essentially compiles the file for the given module again and finds the def that creates a particular function. Note that there are still some caveats here (ignored in my code), for example dealing with functions that have multiple clauses or versions with different arities. However, given the result of the function, you can either again explore the AST to check if there is a call to the function you are interested in, or convert it to string and look in the string (more hacky, but definitely simpler):

{:ok, ast} = fetch_ast(Jason, :encode)
definition = Macro.to_string(ast)
String.contains?(definition, "iodata_to_binary") # => true

While Paweł Obrok's answer answers your question and satisfies your curiosity ("Elixir has many tools for metaprogramming, thus I want to play around with them."), I suspect your question is an XY Question. You already clarified in your comment:

I want to check if certain function is called inside another, without modifying it […] It's for testing purposes.

However, I suspect even that doesn't go far enough. Rather than testing the implementation detail (Function A calls function B), in general it's better to test the actual behaviour of Function A. That means testing in terms of inputs and outputs, not side effects or implementation details. You should be able to completely change the implementation of the function, and your test should still pass as long as the outputs are the same.

Something like:

test "adds two to result of do_something/0" do
  assert my_function() == do_something() + 2
end

But even better would be to change the design of my_function/0 so that it accepts an argument, doesn't perform a side effect, and can be chained:

do_something() |> my_function()

Then the test can be much simpler:

test "adds two" do
  assert my_function(1) == 3
end
Related