lib/credo/check/refactor/long_quote_blocks.ex

defmodule Credo.Check.Refactor.LongQuoteBlocks do
  use Credo.Check,
    id: "EX4012",
    base_priority: :high,
    param_defaults: [max_line_count: 150, ignore_comments: false],
    explanations: [
      check: """
      Long `quote` blocks are generally an indication that too much is done inside
      them.

      Let's look at why this is problematic:

          defmodule MetaCommand do
            def __using__(opts \\\\ []) do
              modes = opts[:modes]
              command_name = opts[:command_name]

              quote do
                def run(filename) do
                  contents =
                    if File.exists?(filename) do
                      {:ok, file} = File.open(filename, unquote(modes))
                      {:ok, contents} = IO.read(file, :line)
                      File.close(file)
                      contents
                    else
                      ""
                    end

                  case contents do
                    "" ->
                      # ...
                    unquote(command_name) <> rest ->
                      # ...
                  end
                end

                # ...
              end
            end
          end

      A cleaner solution would be to call "regular" functions outside the
      `quote` block to perform the actual work.

          defmodule MyMetaCommand do
            def __using__(opts \\\\ []) do
              modes = opts[:modes]
              command_name = opts[:command_name]

              quote do
                def run(filename) do
                  MyMetaCommand.run_on_file(filename, unquote(modes), unquote(command_name))
                end

                # ...
              end
            end

            def run_on_file(filename, modes, command_name) do
              contents =
                # actual implementation
            end
          end

      This way it is easier to reason about what is actually happening. And to debug
      it.
      """,
      params: [
        max_line_count: "The maximum number of lines a quote block should be allowed to have.",
        ignore_comments: "Ignores comments when counting the lines of a `quote` block."
      ]
    ]

  alias Credo.IssueMeta

  @doc false
  @impl true
  def run(%SourceFile{} = source_file, params) do
    issue_meta = IssueMeta.for(source_file, params)
    max_line_count = Params.get(params, :max_line_count, __MODULE__)
    ignore_comments = Params.get(params, :ignore_comments, __MODULE__)

    Credo.Code.prewalk(
      source_file,
      &traverse(&1, &2, issue_meta, max_line_count, ignore_comments)
    )
  end

  defp traverse(
         {:quote, meta, arguments} = ast,
         issues,
         issue_meta,
         max_line_count,
         ignore_comments
       ) do
    max_line_no = Credo.Code.prewalk(arguments, &find_max_line_no(&1, &2), 0)
    line_count = max_line_no - meta[:line]

    issue =
      if line_count > max_line_count do
        source_file = IssueMeta.source_file(issue_meta)

        lines =
          source_file
          |> Credo.Code.to_lines()
          |> Enum.slice(meta[:line] - 1, line_count)

        lines =
          if ignore_comments do
            Enum.reject(lines, fn {_line_no, line} ->
              Regex.run(~r/^\s*#/, line)
            end)
          else
            lines
          end

        if Enum.count(lines) > max_line_count do
          issue_for(issue_meta, meta[:line])
        end
      end

    {ast, issues ++ List.wrap(issue)}
  end

  defp traverse(ast, issues, _issue_meta, _max_line_count, _ignore_comments) do
    {ast, issues}
  end

  defp find_max_line_no({_, meta, _} = ast, max_line_no) do
    line_no = meta[:line] || 0

    if line_no > max_line_no do
      {ast, line_no}
    else
      {ast, max_line_no}
    end
  end

  defp find_max_line_no(ast, max_line_no) do
    {ast, max_line_no}
  end

  defp issue_for(issue_meta, line_no) do
    format_issue(
      issue_meta,
      message: "Avoid long quote blocks.",
      trigger: "quote",
      line_no: line_no
    )
  end
end