lib/credo/check/readability/redundant_blank_lines.ex

defmodule Credo.Check.Readability.RedundantBlankLines do
  use Credo.Check,
    id: "EX3019",
    base_priority: :low,
    tags: [:formatter],
    param_defaults: [max_blank_lines: 1],
    explanations: [
      check: """
      Files should not have two or more consecutive blank lines.

      Like all `Readability` issues, this one is not a technical concern.
      But you can improve the odds of others reading and liking your code by making
      it easier to follow.
      """,
      params: [
        max_blank_lines: "The maximum number of tolerated consecutive blank lines."
      ]
    ]

  alias Credo.Code.Charlists
  alias Credo.Code.Heredocs
  alias Credo.Code.Sigils
  alias Credo.Code.Strings

  @doc false
  def run(%SourceFile{} = source_file, params) do
    issue_meta = IssueMeta.for(source_file, params)

    max_blank_lines = Params.get(params, :max_blank_lines, __MODULE__)

    source_file
    |> Charlists.replace_with_spaces("=")
    |> Sigils.replace_with_spaces("=", "=", source_file.filename)
    |> Strings.replace_with_spaces("=", "=", source_file.filename, "=")
    |> Heredocs.replace_with_spaces("=", "=", "=", source_file.filename)
    |> Credo.Code.to_lines()
    |> blank_lines()
    |> consecutive_lines(max_blank_lines)
    |> Enum.map(fn line -> issue_for(issue_meta, line, max_blank_lines) end)
  end

  defp issue_for(issue_meta, line, max_blank_lines) do
    format_issue(
      issue_meta,
      message: "There should be no more than #{max_blank_lines} consecutive blank lines.",
      line_no: line,
      trigger: Issue.no_trigger()
    )
  end

  defp blank_lines(lines) do
    lines
    |> Enum.filter(fn {_, content} -> content == "" end)
    |> Enum.map(fn {pos, _} -> pos end)
  end

  defp consecutive_lines([], _), do: []

  defp consecutive_lines([first_line | other_lines], max_blank_lines) do
    reducer = consecutive_lines_reducer(max_blank_lines)

    other_lines
    |> Enum.reduce({first_line, 0, []}, reducer)
    |> elem(2)
  end

  defp consecutive_lines_reducer(max_blank_lines) do
    fn line, {last, consecutive, lines} ->
      consecutive =
        if last && line == last + 1 do
          consecutive + 1
        else
          0
        end

      lines =
        if consecutive >= max_blank_lines do
          lines ++ [line]
        else
          lines
        end

      {line, consecutive, lines}
    end
  end
end