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

srinivasu
How to handle excepions in elixir? Suppose i have A, B, C ,D, E modules. and each module has get() function. A.get() method will call th...
New
pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
New
yawaramin
In the Dialyzer docs ( http://erlang.org/doc/man/dialyzer.html#requesting-or-suppressing-warnings-in-source-files ), there is a way to tu...
New
Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New
logicmason
Hi there, I'm working through my first release with elixir/phoenix. I've built a release with distillery and found that it crashes when I...
New
lk-geimfari
What is most correct way to open, read and parse JSON file with poison? For example if we have example.json file in root of some projec...
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
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
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
Mooodi
Given a string, how can I get access to its character by index? Enum.at("my_string", 2) doesn't work. Or rather, not char, but a substr...
New

Other popular topics Top

chrismccord
Phoenix 1.4.0 released Phoenix 1.4 is out! This release ships with exciting new features, most notably with HTTP2 support, improved deve...
688 30048 115
New
yurko
Here are few pieces of (common) Linux knowledge that we use for reasonably small one server apps. We use Ubuntu but this should work for ...
New
_russellb
I want to try my hand at web scraping. What tools/libraries do I need to use. I’m hoping to turn this into something professional so don’...
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
vac
Hi, I'm quite new in Elixir and I'm trying to format a string to a PEM format. I have the certificate value like MIIDBTCCAe2...... and ...
New
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
shahryarjb
Hello, I have map which I want to convert it to string like this: the map: %{last_name: "tavakkoli", name: "shahryar"} the string I ne...
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
9mm
I am constructing a JSON object (map) and I need to conditionally set a field. I’m trying to write proper elixir-way code… and I’m at a l...
New

We're in Beta

About us Mission Statement