Repository: adobe/elixir-styler (elixir)

Issue:
feat: comment directive "sort"
Often there's a series of ast siblings that codebase authors want to keep sorted. We know this to be the case for things like aliases, imports, etc, but what about arbitrary lists within the code? The author just has to put a comment like "please keep this sorted :(" and hope their coworkers play along.

But the whole point of styler is saving you from having to think about your coworkers preferences! (this is a joke. sort of.)

So maybe it could maintain this for us?

## Example Input

```elixir
# given
@my_list [
  # styler:sort
  :a,
  :quick,
  :brown,
  :fox
]
# get
@my_list [
  # styler:sort
  :a,
  :brown,
  :fox,
  :quick
]
```

could be neat!


Pull request description:
introduce `# styler:sort` comment directive
closes #167

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e3cb4bc..e40e0c6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,72 @@
 they can and will change without that change being reflected in Styler's semantic version.
 ## main
 
+### Improvements
+
+#### `# styler:sort` Styler's first comment directive
+
+Styler will now keep a user-designated list or wordlist (`~w` sigil) sorted as part of formatting via the use of comments.
+
+The intention is to remove comments to humans, like `# Please keep this list sorted!`, in favor of comments to robots: `# styler:sort`. Personally speaking, Styler is much better at alphabetical-order than I ever will be.
+
+To use the new directive, put it on the line before a list or wordlist.
+
+This example:
+
+```elixir
+# styler:sort
+[:c, :a, :b]
+
+# styler:sort
+~w(a list of words)
+
+# styler:sort
+@country_codes ~w(
+  en_US
+  po_PO
+  fr_CA
+  ja_JP
+)
+
+# styler:sort
+a_var =
+  [
+    Modules,
+    In,
+    A,
+    List
+  ]
+```
+
+Would yield:
+
+```elixir
+# styler:sort
+[:a, :b, :c]
+
+# styler:sort
+~w(a list of words)
+
+# styler:sort
+@country_codes ~w(
+  en_US
+  fr_CA
+  ja_JP
+  po_PO
+)
+
+# styler:sort
+a_var =
+  [
+    A,
+    In,
+    List,
+    Modules
+  ]
+```
+
+Sorting is done according to erlang term ordering, so lists with elements of multiple types will work just fine.
+
 ## 1.2.1
 
 ### Fixes
diff --git a/lib/style/comment_directives.ex b/lib/style/comment_directives.ex
new file mode 100644
index 0000000..d41e088
--- /dev/null
+++ b/lib/style/comment_directives.ex
@@ -0,0 +1,67 @@
+# Copyright 2024 Adobe. All rights reserved.
+# This file is licensed to you under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License. You may obtain a copy
+# of the License at http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software distributed under
+# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
+# OF ANY KIND, either express or implied. See the License for the specific language
+# governing permissions and limitations under the License.
+
+defmodule Styler.Style.CommentDirectives do
+  @moduledoc "TODO"
+
+  @behaviour Styler.Style
+
+  alias Styler.Zipper
+
+  def run(zipper, ctx) do
+    zipper =
+      ctx.comments
+      |> Enum.filter(&(&1.text == "# styler:sort"))
+      |> Enum.map(& &1.line)
+      |> Enum.reduce(zipper, fn line, zipper ->
+        found =
+          Zipper.find(zipper, fn
+            {_, meta, _} -> Keyword.get(meta, :line, -1) >= line
+            _ -> false
+          end)
+
+        if found do
+          Zipper.update(found, &sort/1)
+        else
+          zipper
+        end
+      end)
+
+    {:halt, zipper, ctx}
+  end
+
+  defp sort({:__block__, meta, [list]}) when is_list(list) do
+    list = Enum.sort_by(list, fn {f, _, a} -> {f, a} end)
+    {:__block__, meta, [list]}
+  end
+
+  defp sort({:sigil_w, sm, [{:<<>>, bm, [string]}, modifiers]}) do
+    # ew. gotta be a better way.
+    # this keeps indentation for the sigil via joiner, while prepend and append are the bookending whitespace
+    {prepend, joiner, append} =
+      case Regex.run(~r|^\s+|, string) do
+        # oneliner like `~w|c a b|`
+        nil -> {"", " ", ""}
+        # multline like
+        # `"\n  a\n  list\n  long\n  of\n  static\n  values\n"`
+        #   ^^^^ `prepend`       ^^^^ `joiner`             ^^ `append`
+        # note that joiner and prepend are the same in a multiline (unsure if this is always true)
+        # @TODO: get all 3 in one pass of a regex. probably have to turn off greedy or something...
+        [joiner] -> {joiner, joiner, ~r|\s+$| |> Regex.run(string) |> hd()}
+      end
+
+    string = string |> String.split() |> Enum.sort() |> Enum.join(joiner)
+    {:sigil_w, sm, [{:<<>>, bm, [prepend, string, append]}, modifiers]}
+  end
+
+  defp sort({:=, m, [lhs, rhs]}), do: {:=, m, [lhs, sort(rhs)]}
+  defp sort({:@, m, [{a, am, [assignment]}]}), do: {:@, m, [{a, am, [sort(assignment)]}]}
+  defp sort(x), do: x
+end
diff --git a/lib/styler.ex b/lib/styler.ex
index 8179c41..50e0d20 100644
--- a/lib/styler.ex
+++ b/lib/styler.ex
@@ -25,7 +25,8 @@ defmodule Styler do
     Styler.Style.Defs,
     Styler.Style.Blocks,
     Styler.Style.Deprecations,
-    Styler.Style.Configs
+    Styler.Style.Configs,
+    Styler.Style.CommentDirectives
   ]
 
   @doc false


Test patch:
diff --git a/test/style/comment_directives_test.exs b/test/style/comment_directives_test.exs
new file mode 100644
index 0000000..16c8621
--- /dev/null
+++ b/test/style/comment_directives_test.exs
@@ -0,0 +1,157 @@
+# Copyright 2024 Adobe. All rights reserved.
+# This file is licensed to you under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License. You may obtain a copy
+# of the License at http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software distributed under
+# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
+# OF ANY KIND, either express or implied. See the License for the specific language
+# governing permissions and limitations under the License.
+
+defmodule Styler.Style.CommentDirectivesTest do
+  @moduledoc false
+  use Styler.StyleCase, async: true
+
+  describe "sort" do
+    test "we dont just sort by accident" do
+      assert_style "[:c, :b, :a]"
+    end
+
+    test "sorts lists of atoms" do
+      assert_style(
+        """
+        # styler:sort
+        [
+          :c,
+          :b,
+          :a
+        ]
+        """,
+        """
+        # styler:sort
+        [
+          :a,
+          :b,
+          :c
+        ]
+        """
+      )
+    end
+
+    test "sorts sigils" do
+      assert_style("# styler:sort\n~w|c a b|", "# styler:sort\n~w|a b c|")
+
+      assert_style(
+        """
+        # styler:sort
+        ~w(
+          a
+          long
+          list
+          of
+          static
+          values
+        )
+        """,
+        """
+        # styler:sort
+        ~w(
+          a
+          list
+          long
+          of
+          static
+          values
+        )
+        """
+      )
+    end
+
+    test "assignments" do
+      assert_style(
+        """
+        # styler:sort
+        my_var =
+          ~w(
+            a
+            long
+            list
+            of
+            static
+            values
+          )
+        """,
+        """
+        # styler:sort
+        my_var =
+          ~w(
+            a
+            list
+            long
+            of
+            static
+            values
+          )
+        """
+      )
+
+      assert_style(
+        """
+        defmodule M do
+          @moduledoc false
+          # styler:sort
+          @attr ~w(
+              a
+              long
+              list
+              of
+              static
+              values
+            )
+        end
+        """,
+        """
+        defmodule M do
+          @moduledoc false
+          # styler:sort
+          @attr ~w(
+              a
+              list
+              long
+              of
+              static
+              values
+            )
+        end
+        """
+      )
+    end
+
+    test "doesnt affect downstream nodes" do
+      assert_style(
+        """
+        # styler:sort
+        [:c, :a, :b]
+
+        @country_codes ~w(
+          po_PO
+          en_US
+          fr_CA
+          ja_JP
+        )
+        """,
+        """
+        # styler:sort
+        [:a, :b, :c]
+
+        @country_codes ~w(
+          po_PO
+          en_US
+          fr_CA
+          ja_JP
+        )
+        """
+      )
+    end
+  end
+end