Repository: adobe/elixir-styler (elixir)

Issue:
edge case: compile callback module attribute ordering
## Versions

* Elixir: `Elixir 1.16.0 (compiled with Erlang/OTP 26)`
* Styler: `locked at 0.11.5 (styler) ed924524`

## Example Input

```elixir
defmacro __using__(opts \\ []) do
  quote do
    import Phoenix.LiveView
    @behaviour Phoenix.LiveComponent
    @before_compile Phoenix.LiveView.Renderer

    # Phoenix.Component must come last so its @before_compile runs last
    use Phoenix.Component, Keyword.take(unquote(opts), [:global_prefixes])

    @doc false
    def __live__, do: %{kind: :component, layout: false}
  end
end
```

## Stacktrace / Current Behaviour

```elixir
defmacro __using__(opts \\ []) do
  quote do
    @behaviour Phoenix.LiveComponent

    # Phoenix.Component must come last so its @before_compile runs last
    use Phoenix.Component, Keyword.take(unquote(opts), [:global_prefixes])

    import Phoenix.LiveView

    @before_compile Phoenix.LiveView.Renderer # <-- Problem line

    @doc false
    def __live__, do: %{kind: :component, layout: false}
  end
end
```

It's important to be able to place the [compile callback module attributes](https://hexdocs.pm/elixir/1.15/Module.html#module-compile-callbacks) in very specific places. Is there a workaround for this, or does there need to be a special case to handle `@before_compile`, `@after_compile` and `@after_verify`?

Pull request description:
keep module callback ordering relative to `use` directives.
closes #120

Patch:
diff --git a/lib/style/module_directives.ex b/lib/style/module_directives.ex
index 1c973d0..175d8bc 100644
--- a/lib/style/module_directives.ex
+++ b/lib/style/module_directives.ex
@@ -69,7 +69,8 @@ defmodule Styler.Style.ModuleDirectives do
   alias Styler.Zipper
 
   @directives ~w(alias import require use)a
-  @attr_directives ~w(moduledoc shortdoc behaviour)a
+  @callback_attrs ~w(before_compile after_compile after_verify)a
+  @attr_directives ~w(moduledoc shortdoc behaviour)a ++ @callback_attrs
 
   @moduledoc_false {:@, [line: nil], [{:moduledoc, [line: nil], [{:__block__, [line: nil], [false]}]}]}
 
@@ -142,6 +143,9 @@ defmodule Styler.Style.ModuleDirectives do
 
     directives =
       Enum.group_by(directives, fn
+        # callbacks are essentially the same as `use` -- they invoke macros.
+        # so, we need to group / order them with `use` statements to make sure we don't break things!
+        {:@, _, [{callback, _, _}]} when callback in @callback_attrs -> :use
         {:@, _, [{attr_name, _, _}]} -> :"@#{attr_name}"
         {directive, _, _} -> directive
       end)


Test patch:
diff --git a/test/style/module_directives_test.exs b/test/style/module_directives_test.exs
index d1d68f8..408a533 100644
--- a/test/style/module_directives_test.exs
+++ b/test/style/module_directives_test.exs
@@ -343,6 +343,17 @@ defmodule Styler.Style.ModuleDirectivesTest do
       )
     end
 
+    test "groups module callbacks with uses, keeping ordering" do
+      assert_style """
+      defmacro __using__(_) do
+        quote do
+          @after_compile Foo
+          use Bar
+        end
+      end
+      """
+    end
+
     test "interwoven directives w/o the context of a module" do
       assert_style(
         """