Current section

Files

Jump to
nicene lib alias_import_grouping.ex
Raw

lib/alias_import_grouping.ex

defmodule Nicene.AliasImportGrouping do
@moduledoc """
Module parts should appear in the following order:
1. @shortdoc
2. @moduledoc
3. @behaviour
4. use
5. import
6. alias
7. require
"""
@checkdoc @moduledoc
@explanation [check: @checkdoc]
use Credo.Check, base_priority: :high, category: :readability
@expected_order Map.new(Enum.with_index(~w/
shortdoc
moduledoc
behaviour
use
import
alias
require
/a))
@doc false
def run(source_file, params \\ []) do
source_file
|> Credo.Code.ast()
|> analyze()
|> all_errors(IssueMeta.for(source_file, params))
|> Enum.sort_by(&{&1.line_no, &1.column})
end
defp all_errors(modules_and_parts, issue_meta) do
Enum.reduce(
modules_and_parts,
[],
fn {module, parts}, errors -> module_errors(module, parts, issue_meta) ++ errors end
)
end
defp module_errors(module, parts, issue_meta) do
Enum.reduce(
parts,
%{module: module, current_part: nil, errors: []},
&check_part_location(&2, &1, issue_meta)
).errors
end
defp check_part_location(state, {part, file_pos}, issue_meta) do
state
|> validate_order(part, file_pos, issue_meta)
|> store_current_part(part)
end
defp store_current_part(state, part) do
if is_nil(order(part)), do: state, else: Map.put(state, :current_part, part)
end
defp validate_order(state, part, file_pos, issue_meta) do
current_part = state.current_part
if is_nil(current_part) or is_nil(order(part)) or order(state.current_part) <= order(part),
do: state,
else: add_error(state, part, file_pos, issue_meta)
end
defp order(part), do: Map.get(@expected_order, part)
defp add_error(state, part, file_pos, issue_meta) do
update_in(
state.errors,
&[error(issue_meta, part, state.current_part, state.module, file_pos) | &1]
)
end
defp error(issue_meta, part, current_part, module, file_pos) do
format_issue(
issue_meta,
message: "#{part_to_string(part)} must appear before #{part_to_string(current_part)}",
trigger: inspect(module),
line_no: Keyword.get(file_pos, :line),
column: Keyword.get(file_pos, :column)
)
end
defp part_to_string(:module_attribute), do: "module attribute"
defp part_to_string(:public_guard), do: "public guard"
defp part_to_string(:public_macro), do: "public macro"
defp part_to_string(:public_fun), do: "public function"
defp part_to_string(:private_fun), do: "private function"
defp part_to_string(:impl), do: "callback implementation"
defp part_to_string(part), do: "#{part}"
defp analyze(ast) do
{_ast, state} = Macro.prewalk(ast, initial_state(), &traverse_file/2)
module_parts(state)
end
defp traverse_file({:defmodule, meta, args}, state) do
[{:__aliases__, _, name_parts}, [do: module_def]] = args
state = start_module(state, Module.concat(name_parts), meta)
{_ast, state} = Macro.prewalk(module_def, state, &traverse_module/2)
{[], state}
end
defp traverse_file(ast, state),
do: {ast, state}
defp traverse_module(ast, state) do
case analyze(state, ast) do
nil -> traverse_deeper(ast, state)
state -> traverse_sibling(state)
end
end
defp traverse_deeper(ast, state), do: {ast, state}
defp traverse_sibling(state), do: {[], state}
# Part extractors
defp analyze(state, {:@, _meta, [{:doc, _, [value]}]}),
do: set_next_fun_modifier(state, if(value == false, do: :private, else: nil))
defp analyze(state, {:@, _meta, [{:impl, _, [value]}]}),
do: set_next_fun_modifier(state, if(value == false, do: nil, else: :impl))
defp analyze(state, {:@, meta, [{attribute, _, _}]})
when attribute in ~w/moduledoc shortdoc behaviour type typep opaque callback macrocallback optional_callbacks/a,
do: add_module_element(state, attribute, meta)
defp analyze(state, {:@, _meta, [{ignore_attribute, _, _}]})
when ignore_attribute in ~w/after_compile before_compile compile impl deprecated doc
typedoc dialyzer external_resource file on_definition on_load vsn spec/a,
do: state
defp analyze(state, {:@, meta, _}),
do: add_module_element(state, :module_attribute, meta)
defp analyze(state, {clause, meta, _})
when clause in ~w/use import alias require defstruct/a,
do: add_module_element(state, clause, meta)
defp analyze(state, {clause, meta, _})
when clause in ~w/def defmacro defguard defp defmacrop defguard/a do
state
|> add_module_element(code_type(clause, state.next_fun_modifier), meta)
|> clear_next_fun_modifier()
end
defp analyze(state, {:do, _code}) do
# Not entering a do block, since this is possibly a custom macro invocation we can't
# understand.
state
end
defp analyze(_state, _ast), do: nil
defp code_type(:def, nil), do: :public_fun
defp code_type(:def, :impl), do: :impl
defp code_type(:def, :private), do: :private_fun
defp code_type(:defp, _), do: :private_fun
defp code_type(:defmacro, nil), do: :public_macro
defp code_type(:defmacro, :impl), do: :impl
defp code_type(macro, _) when macro in ~w/defmacro defmacrop/a, do: :private_macro
defp code_type(:defguard, nil), do: :public_guard
defp code_type(guard, _) when guard in ~w/defguard defguardp/a, do: :private_guard
# Internal state
defp initial_state, do: %{modules: %{}, current_module: nil, next_fun_modifier: nil}
defp set_next_fun_modifier(state, value), do: %{state | next_fun_modifier: value}
defp clear_next_fun_modifier(state), do: set_next_fun_modifier(state, nil)
defp module_parts(state) do
state.modules
|> Enum.sort_by(fn {_name, module} -> module.location end)
|> Enum.map(fn {name, module} -> {name, Enum.reverse(module.parts)} end)
end
defp start_module(state, module, meta) do
state = %{state | current_module: module}
put_in(state.modules[module], %{parts: [], location: Keyword.take(meta, ~w/line column/a)})
end
defp add_module_element(state, element, meta) do
location = Keyword.take(meta, ~w/line column/a)
update_in(state.modules[state.current_module].parts, &[{element, location} | &1])
end
end