sbrink

sbrink

How do I get a well-formatted string from an AST?

Hi, I want to modify elixir files based on the AST.

For the result I want to the elixir formatter.

For a prototype I tried the following and it almost works:

  File.read!("example.ex")
  |> read_contents_of_file()
  |> Code.string_to_quoted!()
  |> Macro.to_string()
  # |> Code.format_string!()
  # |> IO.iodata_to_binary

The problem are the parentheses. They’re added everywhere. An example result:

"defmodule(Example) do\n  def(output(str)) do\n    IO.inspect(str)\n  end\nend"

The documentation for Macro.to_string states “This function discards all formatting of the original code.”.

So is it possible to keep the formatting of the original code?

Thanks in advance for every piece of advice or idea :slight_smile:

  • Sascha

Most Liked

josevalim

josevalim

Creator of Elixir

The formatter works on heavily annotated AST to be able to preserve the user’s choice in some cases, that’s why it doesn’t work directly on the given AST, because it cannot guaranteed something close to a similar result. The best solution here would be to improve Macro.to_string. For example, it can skip adding parens in do/end blocks.

OvermindDL1

OvermindDL1

Look at it’s options, specifically the :locals_without_parens option, it’s default empty when run directly (the mix formatter default sets up some options).

OvermindDL1

OvermindDL1

Hmm…

iex(7)> s
"def(blah(i)) do 42 end"
iex(8)> Code.format_string!(s, locals_without_parens: [def: 1, def: 2, def: 3, def: :*])|>IO.puts()
def(blah(i)) do
  42
end
:ok

And yet the Code.format_string! docs state:

### Parens and no parens in function calls

Elixir has two syntaxes for function calls. With parens and no parens. By
default, Elixir will add parens to all calls except for:

  1. calls that have do/end blocks
  2. local calls without parens where the name and arity of the local call
     is also listed under :locals_without_parens (except for calls with arity
     0, where the compiler always require parens)

The choice of parens and no parens also affects indentation. When a function
call with parens doesn't fit on the same line, the formatter introduces a
newline around parens and indents the arguments with two spaces:

    some_call(
      arg1,                                                                                                                           
      arg2,                                                                                                                           
      arg3                                                                                                                            
    )     

On the other hand, function calls without parens are always indented by the
function call length itself, like this:

    some_call arg1,
              arg2,                                                                                                                   
              arg3                                                                                                                    

If the last argument is a data structure, such as maps and lists, and the
beginning of the data structure fits on the same line as the function call,
then no indentation happens, this allows code like this:

    Enum.reduce(some_collection, initial_value, fn element, acc ->
      # code                                                                                                                          
    end)                                                                                                                              
                                                                                                                                      
    some_function_without_parens %{                                                                                                   
      foo: :bar,                                                                                                                      
      baz: :bat                                                                                                                       
    }      

And yet it’s not working, at least in Elixir 1.9.1. Might be worth trying on master and if still not working then submitting a bug issue?

neenjaw

neenjaw

On master:

iex(1)> ast = quote do                                     
...(1)>   def add(a, b), do: a + b
...(1)> end
{:def, [context: Elixir, import: Kernel],
 [
   {:add, [context: Elixir], [{:a, [], Elixir}, {:b, [], Elixir}]},
   [
     do: {:+, [context: Elixir, import: Kernel],
      [{:a, [], Elixir}, {:b, [], Elixir}]}
   ]
 ]}
iex(2)> ast |> Macro.to_string
"def(add(a, b)) do\n  a + b\nend"
iex(3)> ast |> Macro.to_string |> Code.format_string!(locals_without_parens: [def: 1, def: 2, def: 3, def: :*])
["def", "(", "", "add", "(", "", "a", ",", " ", "b", "", ")", "", ")", " do",
 "\n  ", "a", " +", " ", "b", "\n", "end"]
iex(4)> ast |> Macro.to_string |> Code.format_string!(locals_without_parens: [def: 1, def: 2, def: 3, def: :*]) |> IO.puts()
def(add(a, b)) do
  a + b
end
:ok

I’ll file a bug report, thanks @OvermindDL1

neenjaw

neenjaw

Local without parens do not remove parens. They only keep a call without parens if they were not there in the first place. The feature that you want has to be implemented in #9291.
- Jose Valim (via github)

Turns out it’s not a bug. It doesn’t do what it seems to imply, but maybe via #9291 it will someday!

Where Next?

Popular in Questions Top

fireproofsocks
I’m working on defining a simple Ecto schema for a table (in PostGres), but I don’t see where I can define a column as NOT NULL. Conside...
New
vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
New
quazar
How to set Jason to encode all fields in ecto schema, I don’t care about security and implementing only is taking long list of attributes...
New
Jim
As a follow up to my earlier question: I have the code compiling and running but not getting a successful login from the rest server. ...
New
johnnyicon
Hi all, I've just started learning Elixir and Phoenix Framework, so please pardon my n00bness at this stage. I'm trying to use Postg...
New
skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New
idi527
I’ve been re-reading swift book again and noticed that multiline strings there don’t have a trailing line break, unlike in elixir iex(2)...
New

Other popular topics Top

belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
JDanielMartinez
Hi! May someone helps me, please! I have two apps into an umbrella project: the first one is Database, which manages queries, and the se...
New
chrisalley
ExUnit now has describe blocks which is a welcome addition coming from RSpec. In the docs, it states that nested hierarchies of describe ...
New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New
New
sacepums
Hey guys. I'm new to elixir and im really stocked about it. But I ran into a bit of problem - I need to convert a date sting, for examp...
New
script
If I have a string “1000 cfu/ml” . I want to remove the characters and / and space . So the string is like this "1000" What is the ...
New
mcarvalho
What is the difference between System.get_env and Application.get_env? For example, what are best practices to use one versus another.
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New

We're in Beta

About us Mission Statement