Repository: surface-ui/surface_formatter
Pull request #57 (2021-12-23 19:18:57)

keep single-line and empty inputs intact
Currently, the formatter unconditionally adds a final newline to the formatted nodes, which breaks empty and single-line inputs.

Empty example:

```elixir
# input
  def render(assigns) do
    ~F"""
    """
  end

# expected output -- no change from input

# actual output (extra newline inserted)
  def render(assigns) do
    ~F"""

    """
  end
```

Single-line example:

```elixir
# input
  def render(assigns) do
    ~F{<CheckmarkIcon />}
  end

# expected output -- no change from input

# actual output (extra newline inserted)
  def render(assigns) do
    ~F{<CheckmarkIcon />
}
  end
```

This PR fixes both of the above cases.


Patch:
diff --git a/lib/surface/formatter.ex b/lib/surface/formatter.ex
index fc08781..43fc5de 100644
--- a/lib/surface/formatter.ex
+++ b/lib/surface/formatter.ex
@@ -376,13 +376,22 @@ defmodule Surface.Formatter do
   """
   @spec format_string!(String.t(), list(option)) :: String.t()
   def format_string!(string, opts \\ []) do
+    trailing_newline =
+      case Regex.run(~r/\n+\s*$/, string) do
+        [match] -> match
+        nil -> nil
+      end
+
     parsed =
       string
       |> String.trim()
       |> Surface.Compiler.Parser.parse!(translator: Surface.Formatter.NodeTranslator)
 
-    # Ensure the :indent option is set
-    opts = Keyword.put_new(opts, :indent, 0)
+    # Ensure the :indent and :trailing_newline options are set
+    opts =
+      opts
+      |> Keyword.put_new(:indent, 0)
+      |> Keyword.put(:trailing_newline, !is_nil(trailing_newline))
 
     [
       Phases.TagWhitespace,
diff --git a/lib/surface/formatter/phases/final_newline.ex b/lib/surface/formatter/phases/final_newline.ex
index c03c319..0b501a0 100644
--- a/lib/surface/formatter/phases/final_newline.ex
+++ b/lib/surface/formatter/phases/final_newline.ex
@@ -1,9 +1,16 @@
 defmodule Surface.Formatter.Phases.FinalNewline do
-  @moduledoc "Add a newline after all of the nodes"
+  @moduledoc "Add a newline after all of the nodes if one was present on the original input"
 
   @behaviour Surface.Formatter.Phase
 
-  def run(nodes, _opts) do
-    nodes ++ [:newline]
+  # special case for empty heredocs
+  def run([:indent], _opts), do: []
+
+  def run(nodes, opts) do
+    if Keyword.get(opts, :trailing_newline, false) do
+      nodes ++ [:newline]
+    else
+      nodes
+    end
   end
 end


Test patch:
diff --git a/test/surface/formatter_test.exs b/test/surface/formatter_test.exs
index bfb30b0..de5002a 100644
--- a/test/surface/formatter_test.exs
+++ b/test/surface/formatter_test.exs
@@ -42,6 +42,25 @@ defmodule Surface.FormatterTest do
       )
     end
 
+    test "empty inputs are not changed" do
+      assert_formatter_doesnt_change("")
+
+      assert_formatter_doesnt_change("""
+      """)
+    end
+
+    test "single line inputs are not changed" do
+      assert_formatter_doesnt_change("<div />")
+
+      assert_formatter_doesnt_change("""
+      <Component with="attribute" />
+      """)
+    end
+
+    test "trailing whitespace is trimmed on single line inputs" do
+      assert_formatter_outputs(~s{<div/>    }, ~s{<div />})
+    end
+
     test "Contents of macro components are preserved" do
       assert_formatter_doesnt_change("""
       <#MacroComponent>
@@ -678,7 +697,7 @@ defmodule Surface.FormatterTest do
     test "shorthand surface syntax (invisible []) is formatted by Elixir code formatter" do
       assert_formatter_outputs(
         "<div class={ foo:        bar }></div>",
-        "<div class={foo: bar} />\n"
+        "<div class={foo: bar} />"
       )
     end