Repository: adobe/elixir-styler
Pull request #6 (2023-04-03 20:13:56)

`Styler.Style.ModuleDirectives`
## New Credo Checks covered

    * `Credo.Check.Consistency.MultiAliasImportRequireUse` (force expansion)
    * `Credo.Check.Readability.ModuleDoc` (adds `@moduledoc false` if missing. includes `*.exs` files)
    * `Credo.Check.Readability.StrictModuleLayout` (see section below for details)

## Desc

Ideally the README + moduledoc of `Styler.Style.ModuleDirectives` tell you everything you need to know. If not, let's get there!

You probably didn't even know `use` could be multi-aliased! Pretty wild eh. (I sure didn't)

I did this by redesigning the `aliases` module and then taking that basis to work with each directive.

This is our first style to reach into the zipper's meta in order to work efficiently. I'm fine with that, but if we repeat this pattern we might want to extract that into a new zipper function like `Zipper.siblings(zipper, :right)`




Patch:
diff --git a/README.md b/README.md
index 7747b02..9d44f17 100644
--- a/README.md
+++ b/README.md
@@ -31,7 +31,7 @@ Run `mix help style` for more details on arguments and flags.
 
 As stated above, `Styler` takes a cue from Elixir's Formatter and offers no configuration. Instead, it harnesses the same `.formatter.exs` file as Formatter to know which files within your project it should style.
 
-Styler wraps up its work by running its rewrites through the Formatter - in fact, it's meant to be a complete stand-in for  `mix format`. You can alias it as `format` to quickly standardize its use across your project and save yourself the work of having to update existing formatter-related CI scripts and documentation.
+`Styler` wraps up its work by running its rewrites through the Formatter - in fact, it's meant to be a complete stand-in for  `mix format`. You can alias it as `format` to quickly standardize its use across your project and save yourself the work of having to update existing formatter-related CI scripts and documentation.
 
 ```elixir
 def aliases do
@@ -47,30 +47,35 @@ end
 
 You can find the currently-enabled styles in the `Mix.Tasks.Style` module, inside of its `@styles` module attribute. Each Style's moduledoc will tell you more about what it rewrites.
 
-### Credo Rules Styler Replaces
+## Credo Rules Styler Replaces
 
-| credo rule                            | Styler Style                         |
-|---------------------------------------|--------------------------------------|
-| `Credo.Check.Readability.AliasOrder`  | `Styler.Style.Aliases`               |
-| `Credo.Check.Readability.BlockPipe`   | `Styler.Style.Pipes`                 |
-| `Credo.Check.Readability.LargeNumbers`| `Styler.Style.Simple`                |
-| `Credo.Check.Readability.MultiAlias`  | `Styler.Style.Aliases`               |
-| `Credo.Check.Readability.SinglePipe`  | `Styler.Style.Pipes`                 |
-| `Credo.Check.Readability.UnnecessaryAliasExpansion` | `Styler.Style.Aliases` |
-| `Credo.Check.Refactor.PipeChainStart` | `Styler.Style.Pipes`                 |
+| `Credo.Check`                                        | `Styler.Style`                       | Style notes              |
+|------------------------------------------------------|--------------------------------------|--------------------------|
+| `Credo.Check.Consistency.MultiAliasImportRequireUse` | `Styler.Style.ModuleDirectives`      | always expands `A.{B, C}`
+| `Credo.Check.Readability.AliasOrder`                 | `Styler.Style.ModuleDirectives`      |
+| `Credo.Check.Readability.BlockPipe`                  | `Styler.Style.Pipes`                 |
+| `Credo.Check.Readability.LargeNumbers`               | `Styler.Style.Simple`                | fixes bad underscores, ie: `100_00`
+| `Credo.Check.Readability.ModuleDoc`                  | `Styler.Style.ModuleDirectives`      | adds `@moduledoc false`
+| `Credo.Check.Readability.MultiAlias`                 | `Styler.Style.ModuleDirectives`      |
+| `Credo.Check.Readability.SinglePipe`                 | `Styler.Style.Pipes`                 |
+| `Credo.Check.Readability.StrictModuleLayout`         | `Styler.Style.ModuleDirectives`      | potentially destructive! (see moduledoc)
+| `Credo.Check.Readability.UnnecessaryAliasExpansion`  | `Styler.Style.ModuleDirectives`      |
+| `Credo.Check.Refactor.PipeChainStart`                | `Styler.Style.Pipes`                 |
 
+If you're using Credo and Styler, we recommend disabling these rules in Credo to save on unnecessary checks in CI.
 
 ### Styler and Comments...
 
-Styler is currently unaware of comments, so you may find that it puts them in really odd spots after a rewrite.
+Styler is currently unaware of comments, so you may find that they end up in really odd spots after a rewrite.
 
 If you find that a comment was put somewhere weird after using Styler, you'll just have to manually put it back where you want it after.
 Feel free to grumble about it in an Issue so that we can properly prioritize making this work better in the future.
+
 ## Thanks & Inspiration
 
-### Sourceror
+### [Sourceror](https://github.com/doorgan/sourceror/)
 
-This work was inspired by earlier large-scale rewrites of an internal codebase that used the fantastic tool [`Sourceror`](https://github.com/doorgan/sourceror/).
+This work was inspired by earlier large-scale rewrites of an internal codebase that used the fantastic tool `Sourceror`.
 
 The initial implementation of Styler used Sourceror, but Sourceror's AST-embedding comment algorithm slows Styler down to
 the point that it's no longer an appropriate drop-in for `mix format`.
@@ -79,9 +84,13 @@ Still, we're grateful for the inspiration Sourceror provided and the changes to
 
 The AST-Zipper implementation in this project was forked from Sourceror's implementation.
 
-### Credo
+### [Credo](https://github.com/rrrene/credo/)
 
 Similarly, this project originated from one-off scripts doing large scale rewrites of an enormous codebase as part of an
 effort to enable particular Credo rules for that codebase. Credo's tests and implementations were referenced for implementing
 Styles that took the work the rest of the way. Thanks to Credo & the Elixir community at large for coalescing around
 many of these Elixir style credos.
+
+### Elixir's Formatter
+
+The low-hassle, (almost) no-config design of `mix format` greatly influenced the implementation of `mix style`.
diff --git a/lib/mix/tasks/style.ex b/lib/mix/tasks/style.ex
index 30a28a5..f13a012 100644
--- a/lib/mix/tasks/style.ex
+++ b/lib/mix/tasks/style.ex
@@ -35,7 +35,7 @@ defmodule Mix.Tasks.Style do
   alias Styler.Zipper
 
   @styles [
-    Styler.Style.Aliases,
+    Styler.Style.ModuleDirectives,
     Styler.Style.Pipes,
     Styler.Style.Simple
   ]
diff --git a/lib/style/aliases.ex b/lib/style/aliases.ex
deleted file mode 100644
index 4db0b38..0000000
--- a/lib/style/aliases.ex
+++ /dev/null
@@ -1,98 +0,0 @@
-# Copyright 2023 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.Aliases do
-  @moduledoc """
-  Styles up aliases!
-
-  This Style will expand multi-aliases and sort aliases within their groups.
-  It also adds a newline after all alias groups.
-
-  Rewrites for the following Credo rules:
-
-    * `Credo.Check.Readability.AliasOrder`
-    * `Credo.Check.Readability.MultiAlias`
-    * `Credo.Check.Readability.UnnecessaryAliasExpansion`
-
-  This module is more particular than credo for sorting. Notably, it sorts `alias __MODULE__`, whereas Credo allowed
-  that alias intermixed anywhere in a group.
-  """
-
-  @behaviour Styler.Style
-
-  alias Styler.Zipper
-
-  def run({{:alias, _, _}, _} = zipper) do
-    {zipper, aliases} =
-      zipper
-      |> Zipper.insert_left(:placeholder)
-      |> consume_alias_group([])
-
-    [first | rest] =
-      aliases
-      # Credo does case-agnostic sorting, so we have to match that here
-      |> Enum.map(&{&1, &1 |> Macro.to_string() |> String.downcase()})
-      # a splash of deduping for happiness
-      |> Enum.uniq_by(&elem(&1, 1))
-      |> Enum.sort_by(&elem(&1, 1))
-      |> Enum.map(&(&1 |> elem(0) |> set_newlines(1)))
-
-    zipper =
-      zipper
-      |> Zipper.find(:prev, &(&1 == :placeholder))
-      |> Zipper.replace(first)
-
-    rest
-    |> Enum.reduce(zipper, &(&2 |> Zipper.insert_right(&1) |> Zipper.right()))
-    |> Zipper.update(&set_newlines(&1, 2))
-  end
-
-  def run(zipper), do: zipper
-
-  defp set_newlines({node, meta, children}, newline) do
-    meta = Keyword.update(meta, :end_of_expression, [newlines: newline], &Keyword.put(&1, :newlines, newline))
-    {node, meta, children}
-  end
-
-  defp consume_alias_group({{:alias, meta, _} = alias, _} = zipper, aliases) do
-    aliases = expand_alias(alias, aliases)
-    zipper = Zipper.remove(zipper)
-
-    # multiple newlines means this isn't a group. missing newline means EOF.
-    # thus only when there's one newline do we continue accumulating for our alias group
-    if meta[:end_of_expression][:newlines] == 1 do
-      consume_alias_group(zipper, aliases)
-    else
-      {zipper, aliases}
-    end
-  end
-
-  defp consume_alias_group(zipper, aliases) do
-    # aliases in groups are always siblings, so we're using `Zipper.right` to save time from going down subtrees
-    case Zipper.right(zipper) do
-      nil -> {zipper, aliases}
-      zipper -> consume_alias_group(zipper, aliases)
-    end
-  end
-
-  # This is where multi alias expansion happens
-  #
-  # alias Foo.{Bar, Baz}
-  # =>
-  # alias Foo.Bar
-  # alias Foo.Baz
-  defp expand_alias({:alias, _, [{{:., _, [{_, _, module}, :{}]}, _, right}]}, aliases) do
-    right
-    |> Enum.map(fn {_, meta, segments} -> {:alias, meta, [{:__aliases__, [], module ++ segments}]} end)
-    |> Enum.concat(aliases)
-  end
-
-  defp expand_alias({:alias, _, _} = alias, aliases), do: [alias | aliases]
-end
diff --git a/lib/style/module_directives.ex b/lib/style/module_directives.ex
new file mode 100644
index 0000000..8997156
--- /dev/null
+++ b/lib/style/module_directives.ex
@@ -0,0 +1,192 @@
+# Copyright 2023 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.ModuleDirectives do
+  @moduledoc """
+  Styles up module directives!
+
+  This Style will expand multi-aliases/requires/imports/use and sort the directive within its groups (except `use`s, which cannot be sorted)
+  It also adds a blank line after each directive group.
+
+  ## Credo rules
+
+  Rewrites for the following Credo rules:
+
+    * `Credo.Check.Consistency.MultiAliasImportRequireUse` (force expansion)
+    * `Credo.Check.Readability.AliasOrder` (we sort `__MODULE__`, which credo doesn't)
+    * `Credo.Check.Readability.ModuleDoc` (adds `@moduledoc false` if missing. includes `*.exs` files)
+    * `Credo.Check.Readability.MultiAlias`
+    * `Credo.Check.Readability.StrictModuleLayout` (see section below for details)
+    * `Credo.Check.Readability.UnnecessaryAliasExpansion`
+
+  ## Strict Layout
+
+  **This can break your code.**
+
+  Modules directives are sorted into the following order:
+
+    * `@shortdoc`
+    * `@moduledoc`
+    * `@behaviour`
+    * `use`
+    * `import`
+    * `alias`
+    * `require`
+    * everything else (unchanged)
+
+  If any of the sorted directives had a dependency on code that is now below it, your code will fail to compile after being styled.
+
+  For instance, the following will be broken because the module attribute definition will
+  be moved below the `use` clause, meaning `@pi` is undefined when invoked.
+
+    ```elixir
+    # before `mix style`
+    defmodule Approximation do
+      @pi 3.14
+      use Math, pi: @pi
+    end
+
+    # after `mix style`
+    defmodule Approximation do
+      @moduledoc false
+      use Math, pi: @pi
+      @pi 3.14
+    end
+    ```
+
+  For now, it's up to you to come up with a fix for this issue. Sorry!
+  """
+  @behaviour Styler.Style
+
+  @directives ~w(alias import require)a
+  @attr_directives ~w(moduledoc shortdoc behaviour)a
+
+  # module names ending with these suffixes will not have a default moduledoc appended
+  @dont_moduledoc ~w(Test Mixfile MixProject Controller Endpoint Repo Router Socket View HTML JSON)
+  @moduledoc_false {:@, [], [{:moduledoc, [], [{:__block__, [], [false]}]}]}
+
+  def run({{:defmodule, def_meta, [name, [{mod_do, {:__block__, children_meta, children}}]]}, zipper_meta}) do
+    {directives, other} =
+      Enum.split_with(children, fn
+        {:@, _, [{attr, _, _}]} -> attr in @attr_directives
+        {directive, _, _} -> directive in [:use | @directives]
+        _ -> false
+      end)
+
+    directives =
+      Enum.group_by(directives, fn
+        {:@, _, [{attr_name, _, _}]} -> :"@#{attr_name}"
+        {directive, _, _} -> directive
+      end)
+
+    # TODO: (optimization)
+    # now that we have use/import/alias/require, we might as well run
+    # them through the sort/expand/dedupe functionality and skip them in the traversal
+    shortdocs = directives[:"@shortdoc"] || []
+    moduledocs = directives[:"@moduledoc"] || if needs_moduledoc?(name), do: [@moduledoc_false], else: []
+    # TODO sort behaviours?
+    behaviours = directives[:"@behaviour"] || []
+    behaviours = List.update_at(behaviours, -1, &set_newlines(&1, 2))
+
+    uses = directives[:use] || []
+    imports = directives[:import] || []
+    aliases = directives[:alias] || []
+    requires = directives[:require] || []
+
+    children =
+      Enum.concat([
+        shortdocs,
+        moduledocs,
+        behaviours,
+        uses,
+        imports,
+        aliases,
+        requires,
+        other
+      ])
+
+    {{:defmodule, def_meta, [name, [{mod_do, {:__block__, children_meta, children}}]]}, zipper_meta}
+  end
+
+  # a module whose only child is a moduledoc. pass it on through
+  def run({{:defmodule, _, [_, [{_, {:@, _, [{:moduledoc, _, _}]}}]]}, _} = zipper), do: zipper
+
+  def run({{:defmodule, def_meta, [name, [{mod_do, mod_children}]]}, zipper_meta} = zipper) do
+    # a module with a single child. lets add moduledoc false
+    # ... unless it's a `defmodule Foo, do: ...`, that is
+    if needs_moduledoc?(name, mod_do) do
+      mod_children = {:__block__, [], [@moduledoc_false, mod_children]}
+      {{:defmodule, def_meta, [name, [{mod_do, mod_children}]]}, zipper_meta}
+    else
+      zipper
+    end
+  end
+
+  def run({{:use, _, _} = directive, meta}) do
+    [last | rest] = directive |> expand_directive() |> Enum.reverse()
+    meta = %{meta | l: rest ++ meta.l}
+
+    case meta.r do
+      [{:use, _, _} | _] -> {last, meta}
+      _ -> {set_newlines(last, 2), meta}
+    end
+  end
+
+  def run({{d, _, _} = directive, %{l: left, r: right} = meta}) when d in @directives do
+    {right, directives} = consume_directive_group(d, [directive | right], [])
+
+    [last | rest] =
+      directives
+      # Credo does case-agnostic sorting, so we have to match that here
+      |> Enum.map(&{&1, &1 |> Macro.to_string() |> String.downcase()})
+      # a splash of deduping for happiness
+      |> Enum.uniq_by(&elem(&1, 1))
+      |> List.keysort(1, :desc)
+      |> Enum.map(&(&1 |> elem(0) |> set_newlines(1)))
+
+    {set_newlines(last, 2), %{meta | r: right, l: rest ++ left}}
+  end
+
+  def run(zipper), do: zipper
+
+  def needs_moduledoc?({_, _, aliases}) do
+    name = aliases |> List.last() |> to_string()
+    not String.ends_with?(name, @dont_moduledoc)
+  end
+
+  def needs_moduledoc?(name, {_, do_meta, _}) do
+    needs_moduledoc?(name) and do_meta[:format] != :keyword
+  end
+
+  defp consume_directive_group(d, [{d, meta, _} = directive | siblings], directives) do
+    directives = expand_directive(directive) ++ directives
+
+    if meta[:end_of_expression][:newlines] == 1,
+      do: consume_directive_group(d, siblings, directives),
+      else: {siblings, directives}
+  end
+
+  defp consume_directive_group(_, siblings, directives), do: {siblings, directives}
+
+  # alias Foo.{Bar, Baz}
+  # =>
+  # alias Foo.Bar
+  # alias Foo.Baz
+  defp expand_directive({directive, _, [{{:., _, [{_, _, module}, :{}]}, _, right}]}) do
+    Enum.map(right, fn {_, meta, segments} -> {directive, meta, [{:__aliases__, [], module ++ segments}]} end)
+  end
+
+  defp expand_directive(alias), do: [alias]
+
+  defp set_newlines({directive, meta, children}, newline) do
+    updated_meta = Keyword.update(meta, :end_of_expression, [newlines: newline], &Keyword.put(&1, :newlines, newline))
+    {directive, updated_meta, children}
+  end
+end
diff --git a/lib/zipper.ex b/lib/zipper.ex
index b1f22a9..0b44f41 100644
--- a/lib/zipper.ex
+++ b/lib/zipper.ex
@@ -8,11 +8,12 @@
 # OF ANY KIND, either express or implied. See the License for the specific language
 # governing permissions and limitations under the License.
 
+# Branched from https://github.com/doorgan/sourceror/blob/main/lib/sourceror/zipper.ex,
+# see this issue for context on branching: https://github.com/doorgan/sourceror/issues/67
+
+# Initial implementation Copyright (c) 2021 dorgandash@gmail.com, licenced under Apache 2.0
 defmodule Styler.Zipper do
   @moduledoc """
-  Branched from https://github.com/doorgan/sourceror/blob/main/lib/sourceror/zipper.ex,
-    see this issue for context on branching: https://github.com/doorgan/sourceror/issues/67
-
   Implements a Zipper for the Elixir AST based on Gérard Huet [Functional pearl: the
   zipper](https://www.st.cs.uni-saarland.de/edu/seminare/2005/advanced-fp/docs/huet-zipper.pdf) paper and
   Clojure's `clojure.zip` API.


Test patch:
diff --git a/test/style/aliases_test.exs b/test/style/aliases_test.exs
deleted file mode 100644
index 889f593..0000000
--- a/test/style/aliases_test.exs
+++ /dev/null
@@ -1,51 +0,0 @@
-# Copyright 2023 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.AliasesTest do
-  use Styler.StyleCase, style: Styler.Style.Aliases, async: true
-
-  describe "run/1" do
-    test "sorts, dedupes & expands aliases while respecting groups" do
-      assert_style(
-        """
-        alias D
-        alias A.{B}
-        alias A.{
-          A,
-          B,
-          C
-        }
-        alias A.B
-
-        alias B
-        alias A
-        """,
-        """
-        alias A.A
-        alias A.B
-        alias A.C
-        alias D
-
-        alias A
-        alias B
-        """
-      )
-    end
-
-    test "respects as" do
-      assert_style("""
-      alias Foo.Asset
-      alias Foo.Project.Loaders, as: ProjectLoaders
-      alias Foo.ProjectDevice.Loaders, as: ProjectDeviceLoaders
-      alias Foo.User.Loaders
-      """)
-    end
-  end
-end
diff --git a/test/style/module_directives_test.exs b/test/style/module_directives_test.exs
new file mode 100644
index 0000000..fce7f25
--- /dev/null
+++ b/test/style/module_directives_test.exs
@@ -0,0 +1,219 @@
+# Copyright 2023 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.ModuleDirectivesTest do
+  @moduledoc false
+  use Styler.StyleCase, style: Styler.Style.ModuleDirectives, async: true
+
+  describe "module directive sorting" do
+    test "adds moduledoc" do
+      assert_style(
+        """
+        defmodule A do
+        end
+
+        defmodule Bar do
+          alias Bop
+
+          :ok
+        end
+
+        defmodule DocsOnly do
+          @moduledoc "woohoo"
+        end
+
+        defmodule Foo do
+          use Bar
+        end
+
+        def Foo, do: :ok
+        """,
+        """
+        defmodule A do
+          @moduledoc false
+        end
+
+        defmodule Bar do
+          @moduledoc false
+          alias Bop
+
+          :ok
+        end
+
+        defmodule DocsOnly do
+          @moduledoc "woohoo"
+        end
+
+        defmodule Foo do
+          @moduledoc false
+          use Bar
+        end
+
+        def Foo, do: :ok
+        """
+      )
+    end
+
+    test "doesn't add moduledoc to modules of specific names" do
+      for verboten <- ~w(Test Mixfile Controller Endpoint Repo Router Socket View HTML JSON) do
+        assert_style("""
+        defmodule A.B.C#{verboten} do
+          @shortdoc "Don't change me!"
+        end
+        """)
+      end
+    end
+
+    test "groups directives in order" do
+      assert_style(
+        """
+        defmodule Foo do
+          require A
+          alias A
+
+          def c(x), do: y
+          @behaviour Chaotic
+          def d do
+            alias X
+            alias H
+            import Ecto.Query
+            X.foo()
+          end
+          @shortdoc "it's pretty short"
+          import A
+          require B
+          use A
+          @moduledoc "README.md"
+                     |> File.read!()
+                     |> String.split("<!-- MDOC !-->")
+                     |> Enum.fetch!(1)
+        end
+        """,
+        """
+        defmodule Foo do
+          @shortdoc "it's pretty short"
+          @moduledoc "README.md"
+                     |> File.read!()
+                     |> String.split("<!-- MDOC !-->")
+                     |> Enum.fetch!(1)
+          @behaviour Chaotic
+
+          use A
+
+          import A
+
+          alias A
+
+          require A
+          require B
+
+          def c(x), do: y
+
+          def d do
+            alias H
+            alias X
+
+            import Ecto.Query
+
+            X.foo()
+          end
+        end
+        """
+      )
+    end
+  end
+
+  describe "directive sort/dedupe/expansion" do
+    test "sorts, dedupes & expands alias/require/import while respecting groups" do
+      for d <- ~w(alias require import) do
+        assert_style(
+          """
+          #{d} D
+          #{d} A.{B}
+          #{d} A.{
+            A,
+            B,
+            C
+          }
+          #{d} A.B
+
+          #{d} B
+          #{d} A
+          """,
+          """
+          #{d} A.A
+          #{d} A.B
+          #{d} A.C
+          #{d} D
+
+          #{d} A
+          #{d} B
+          """
+        )
+      end
+    end
+
+    test "expands use but does not sort it" do
+      assert_style(
+        """
+        use A
+        use D
+        use A.{
+          C,
+          B
+        }
+        import F
+        """,
+        """
+        use A
+        use D
+        use A.C
+        use A.B
+
+        import F
+        """
+      )
+    end
+
+    test "interwoven directives w/o the context of a module" do
+      assert_style(
+        """
+        alias D
+        alias A.{B}
+        require A.{
+          A,
+          C
+        }
+        alias B
+        alias A
+        """,
+        """
+        alias A.B
+        alias D
+
+        require A.A
+        require A.C
+
+        alias A
+        alias B
+        """
+      )
+    end
+
+    test "respects as" do
+      assert_style("""
+      alias Foo.Asset
+      alias Foo.Project.Loaders, as: ProjectLoaders
+      alias Foo.ProjectDevice.Loaders, as: ProjectDeviceLoaders
+      alias Foo.User.Loaders
+      """)
+    end
+  end
+end
diff --git a/test/support/style_case.ex b/test/support/style_case.ex
index 0b8380f..635f1b3 100644
--- a/test/support/style_case.ex
+++ b/test/support/style_case.ex
@@ -30,9 +30,10 @@ defmodule Styler.StyleCase do
   end
 
   defmacro assert_style(before, expected \\ nil) do
-    expected = String.trim(expected || before)
+    expected = expected || before
 
     quote bind_quoted: [before: before, expected: expected] do
+      expected = String.trim(expected)
       {styled_ast, styled} = style(before)
 
       if styled != expected and ExUnit.configuration()[:trace] do