lib/credo_naming/check/warning/avoid_specific_terms_in_module_names.ex

defmodule CredoNaming.Check.Warning.AvoidSpecificTermsInModuleNames do
  use Credo.Check,
    base_priority: :low,
    tags: [:naming],
    explanations: [
      check: """
      In an effort to encourage more accurate module naming practices, it is
      sometimes useful to maintain a list of terms to avoid in module names.

      For example, if the list of terms to avoid is ["Manager", "Fetcher"]:

          # preferred

          defmodule Accounts do
          end

          defmodule App.Networking do
          end

          # NOT preferred

          defmodule AccountManager do
          end

          defmodule App.DataFetcher do
          end
      """,
      params: [
        terms: "A list of terms to avoid"
      ]
    ],
    param_defaults: [terms: []]

  alias Credo.Code
  alias Credo.Code.Name

  @doc false
  def run(source_file, params \\ []) do
    terms = Params.get(params, :terms, __MODULE__)

    issue_meta = IssueMeta.for(source_file, params)

    Code.prewalk(source_file, &traverse(&1, &2, terms, issue_meta))
  end

  def traverse({:defmodule, _, [{:__aliases__, opts, mod} | _]} = ast, issues, terms, issue_meta) do
    issues =
      mod
      |> Enum.flat_map(&Name.split_pascal_case(Atom.to_string(&1)))
      |> Enum.reduce(issues, fn term, acc ->
        if term_to_avoid?(term, terms) do
          acc ++ [issue_for(issue_meta, Keyword.get(opts, :line), term)]
        else
          acc
        end
      end)

    {ast, issues}
  end

  def traverse(ast, issues, _, _), do: {ast, issues}

  defp term_to_avoid?(term, terms) do
    Enum.any?(terms, fn
      term_to_avoid when is_binary(term_to_avoid) -> String.downcase(term_to_avoid) == String.downcase(term)
      %Regex{} = term_to_avoid -> Regex.match?(term_to_avoid, term)
      term -> raise(~s(The "terms" config expected each term to be a String or Regex, got: #{inspect(term)}))
    end)
  end

  defp issue_for(issue_meta, line_no, trigger) do
    format_issue(
      issue_meta,
      message: "`#{trigger}` is included in the list of terms to avoid in module names. Consider replacing it with a more accurate one.",
      trigger: trigger,
      line_no: line_no
    )
  end
end