lib/credo/check/config_comment.ex

defmodule Credo.Check.ConfigComment do
  @moduledoc """
  `ConfigComment` structs represent comments which follow control Credo's behaviour.

  The following comments are supported:

      # credo:disable-for-this-file
      # credo:disable-for-next-line
      # credo:disable-for-previous-line
      # credo:disable-for-lines:<number>

  """

  @instruction_disable_file "disable-for-this-file"
  @instruction_disable_next_line "disable-for-next-line"
  @instruction_disable_previous_line "disable-for-previous-line"
  @instruction_disable_lines "disable-for-lines"

  alias Credo.Issue

  defstruct line_no: nil,
            line_no_end: nil,
            instruction: nil,
            params: nil

  @doc "Returns a `ConfigComment` struct based on the given parameters."
  def new(instruction, param_string, line_no)

  def new("#{@instruction_disable_lines}:" <> line_count, param_string, line_no) do
    line_count = String.to_integer(line_count)

    params =
      param_string
      |> value_for()
      |> List.wrap()

    if line_count >= 0 do
      %__MODULE__{
        line_no: line_no,
        line_no_end: line_no + line_count,
        instruction: @instruction_disable_lines,
        params: params
      }
    else
      %__MODULE__{
        line_no: line_no + line_count,
        line_no_end: line_no,
        instruction: @instruction_disable_lines,
        params: params
      }
    end
  end

  def new(instruction, param_string, line_no) do
    %__MODULE__{
      line_no: line_no,
      instruction: instruction,
      params: param_string |> value_for() |> List.wrap()
    }
  end

  @doc "Returns `true` if the given `issue` should be ignored based on the given `config_comment`"
  def ignores_issue?(config_comment, issue)

  def ignores_issue?(
        %__MODULE__{instruction: @instruction_disable_file, params: params},
        %Issue{} = issue
      ) do
    params_ignore_issue?(params, issue)
  end

  def ignores_issue?(
        %__MODULE__{
          instruction: @instruction_disable_next_line,
          line_no: line_no,
          params: params
        },
        %Issue{line_no: line_no_issue} = issue
      )
      when line_no_issue == line_no + 1 do
    params_ignore_issue?(params, issue)
  end

  def ignores_issue?(
        %__MODULE__{
          instruction: @instruction_disable_previous_line,
          line_no: line_no,
          params: params
        },
        %Issue{line_no: line_no_issue} = issue
      )
      when line_no_issue == line_no - 1 do
    params_ignore_issue?(params, issue)
  end

  def ignores_issue?(
        %__MODULE__{
          instruction: @instruction_disable_lines,
          line_no: line_no_start,
          line_no_end: line_no_end,
          params: params
        },
        %Issue{line_no: line_no_issue} = issue
      )
      when line_no_issue >= line_no_start and line_no_issue <= line_no_end do
    params_ignore_issue?(params, issue)
  end

  def ignores_issue?(_, _) do
    false
  end

  #

  defp params_ignore_issue?([], _issue) do
    true
  end

  defp params_ignore_issue?(params, issue) when is_list(params) do
    Enum.any?(params, &check_tuple_ignores_issue?(&1, issue))
  end

  defp check_tuple_ignores_issue?(check_or_regex, issue) do
    if Regex.regex?(check_or_regex) do
      issue.check
      |> to_string
      |> String.match?(check_or_regex)
    else
      issue.check == check_or_regex
    end
  end

  defp value_for(""), do: nil

  defp value_for(param_string) do
    if regex_value?(param_string) do
      param_string
      |> String.slice(1..-2)
      |> Regex.compile!("i")
    else
      String.to_atom("Elixir.#{param_string}")
    end
  end

  defp regex_value?(param_string), do: param_string =~ ~r'^/.+/$'
end