# 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 alias Styler.Style alias Styler.Zipper @directives ~w(alias import require use)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, _, children}, _} = zipper, ctx) do [{:__aliases__, _, aliases}, [{{:__block__, do_meta, [:do]}, _module_body}]] = children if do_meta[:format] == :keyword do {:skip, zipper, ctx} else name = aliases |> List.last() |> to_string() add_moduledoc? = not String.ends_with?(name, @dont_moduledoc) # Move the zipper's focus to the module's body body_zipper = zipper |> Zipper.down() |> Zipper.right() |> Zipper.down() |> Zipper.down() |> Zipper.right() case Zipper.node(body_zipper) do {:__block__, _, _} -> {:skip, organize_directives(body_zipper, add_moduledoc?), ctx} {:@, _, [{:moduledoc, _, _}]} -> # a module whose only child is a moduledoc. nothing to do here! # seems weird at first blush but lots of projects/libraries do this with their root namespace module {:skip, zipper, ctx} only_child -> # There's only one child, and it's not a moduledoc. Conditionally add a moduledoc, then style the only_child if add_moduledoc? do body_zipper |> Zipper.replace({:__block__, [], [@moduledoc_false, only_child]}) |> Zipper.down() |> Zipper.right() |> run(ctx) else run(body_zipper, ctx) end end end end def run({{directive, _, _}, _} = zipper, ctx) when directive in @directives do parent = zipper |> Style.ensure_block_parent() |> Zipper.up() {:skip, organize_directives(parent), ctx} end def run(zipper, ctx), do: {:cont, zipper, ctx} defp organize_directives(parent, add_moduledoc? \\ false) do {directives, nondirectives} = parent |> Zipper.node() |> Zipper.children() |> Enum.split_with(fn {:@, _, [{attr, _, _}]} -> attr in @attr_directives {directive, _, _} -> directive in @directives _ -> false end) directives = Enum.group_by(directives, fn {:@, _, [{attr_name, _, _}]} -> :"@#{attr_name}" {directive, _, _} -> directive end) shortdocs = directives[:"@shortdoc"] || [] moduledocs = directives[:"@moduledoc"] || if add_moduledoc?, do: [@moduledoc_false], else: [] behaviours = expand_and_sort(directives[:"@behaviour"] || []) uses = (directives[:use] || []) |> Enum.flat_map(&expand_directive/1) |> reset_newlines() imports = expand_and_sort(directives[:import] || []) aliases = expand_and_sort(directives[:alias] || []) requires = expand_and_sort(directives[:require] || []) directives = Enum.concat([ shortdocs, moduledocs, behaviours, uses, imports, aliases, requires ]) cond do Enum.empty?(directives) -> parent Enum.empty?(nondirectives) -> Zipper.update(parent, &Zipper.replace_children(&1, directives)) true -> {last_directive, meta} = parent |> Zipper.update(&Zipper.replace_children(&1, directives)) |> Zipper.down() |> Zipper.rightmost() {last_directive, %{meta | r: nondirectives}} end end defp expand_and_sort(directives) do # sorting is done with `downcase` to match Credo directives |> Enum.flat_map(&expand_directive/1) |> Enum.map(&{&1, &1 |> Macro.to_string() |> String.downcase()}) |> Enum.uniq_by(&elem(&1, 1)) |> List.keysort(1) |> Enum.map(&elem(&1, 0)) |> reset_newlines() end # 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) defp expand_directive(other), do: [other] defp reset_newlines([]), do: [] defp reset_newlines(directives), do: reset_newlines(directives, []) defp reset_newlines([directive], acc), do: Enum.reverse([set_newlines(directive, 2) | acc]) defp reset_newlines([directive | rest], acc), do: reset_newlines(rest, [set_newlines(directive, 1) | acc]) 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