Textually replace function call in file

Suppose I have a module defined like this:

defmodule MyModule do
  # ...
  myfunc args do
    # ... some code
  end
  # ...
end

I want to write a mix task that replaces the function call with something else (let’s say a pre-defined string). As far as I know, there is no way to detect where the funcion call ends (that is, the location of the end keyword that closes the do block.

I could make some assumptions, like matching the first end keyword that matches the indentation of the function, but that’s not very safe.

How should I proceed? Should I use the elixir tokenizer on the file and match the do ... end pair? Is there anything in the new code formatter I can use to help me here? Should I just go with my heuristic above?

1 Like

This sounds as if you want to make myfunc a macro…

1 Like

Not exactly. A macro takes up an AST and returns another AST. What I want is to change the contents of the file so that the function/macro call is replaced by something else. It doesn’t even have to be at compile time. In fact, what I have in mind is a mix task that does the replacement independently of the compilation.

1 Like

But changing the file on disc is irreversible…

Anyway, then it sounds like snippet support in your editor or a job for sed :wink:

I have a veeeeery unusual use case in which this is desirable…

Yes, this matches my intuition. If you’re going to replace things textually just find a simple heuristic and do it. Thanks :slight_smile:

1 Like

@tmbb How about convert file contents to AST, parse it and traverse? I have created a topic on forum for that, but I did not receive good response. Last time I got an idea how I could write such traverse function myself.

Firstly you need path. With it you can read its contents using File.read/1 function. Then you can use Code.string_to_quoted and finally write a code to perform a lookup for your patterns.

It’s important to remember that same code written differently have slightly different AST, so you need to handle all of it’s cases.

def my_func, do: :ok:
# have different AST than it's equivalent
def my_func do
  :ok
end

Anyway if you know all of its cases and handle them in your traverse function then you can do it without problem.

You can translate your AST back to String using Macro.to_string.

In short:

file_path
|> File.read!()
|> Code.string_to_quoted!
|> MyApp.traverse_ast!()
|> Macro.to_string()

Hope it helps.

1 Like

If you are going to AST transform it, then you can still use a macro and ‘parse’ the file and run the macro on the relevant part only.

If I were to use this approach I’d only work on the AST of the function I want to replace, not the whole module. Doing this to the whole module removes comments from the module. And before printing tbe string you should run the code formatter through it.

But that’s a valid approach, of course.

So just don’t replace module AST and traverse deeper into specific function AST … Simpler example:

Enum.map ([:module, :function], &do_map(&1, 2))

defp do_map(item = :function, arity), do: {item, arity}
defp do_map(item, _arity), do: item

Of course Enum.map is not nested traverse and pattern match for AST is not so simple, but you can do it similarly.

Wow, I did not noticed it. Maybe @josevalim or other core developers have idea for this.

As after any edit … :smiley: