Injecting into an AST: Could anyone help me clean this up?

I’m writing a CLI tool that loads a user-defined module which I inject a module attribute into. My first solution was grabbing some GenServer state in in the __using__/1 callback of the module the user has to use, but I thought I’d avoid state and try and do it by injecting it into the AST. After several minding hours of going down the wrong paths I got it working! The satisfaction is real but it’s a bit ugly. I was wondering if anyone could help me clean it up. I’m sure when I put some more time into it later I can figure it out but my brain is burnt out and some human interaction over it would be nice :upside_down_face:

Here’s what I have:

def do_the_thing(module_attribute_value) do
  {:ok, ast} =
    Path.join(~w[path to user defined module])
    |> File.read!()
    |> Code.string_to_quoted()

  {:defmodule, line, [aliases | [[do: {:__block__, meta, body}]]]} = ast

  args = Macro.escape(module_attribute_value)
  module_attribute = quote(do: (@args unquote(args)))
  body = [module_attribute | body]

  ast = {:defmodule, line, [aliases | [[do: {:__block__, meta, body}]]]}

  [{module | _}] = Code.compile_quoted(ast)

  # do some other stuff
end

Also, are there any repercussions of the [line: n] nodes getting out of whack with my solution? Anything else, aside from being a bit ugly, that could go wrong with it?

Thanks!

PS: I’m looking for help on the AST injection specifically, I plan on adding error-handling and all that.

EDIT: It’s already causing problems, lol. So ya, how to you reliably inject a line at the top of a module?

After taking a walk with my dog I figured it out:

def do_the_thing(module_attribute_value) do
  {:ok, ast} =
    Path.join(~w[path to user defined module])
    |> File.read!()
    |> Code.string_to_quoted()

  args = Macro.escape(module_attribute_value)
  module_attribute = quote(do: (@args unquote(args)))

  ast =
    Macro.prewalk(ast, false, fn
      [do: block], false ->
        {[do: [module_attribute | [block]]], true}

      other, acc ->
        {other, acc}
    end)

  [{module | _}] = Code.compile_quoted(ast)

  # do some other stuff
end

I would still welcome feedback, of course!

1 Like

It is good when the dog knows metaprogramming :joy: .

2 Likes