lib/dangers_detector.ex

defmodule ExcellentMigrations.DangersDetector do
  @moduledoc """
  This module finds potentially dangerous or destructive database operations in a given
  migration AST.
  """

  alias ExcellentMigrations.{
    AstParser,
    AstParserFullDetections,
    ConfigCommentsParser,
    DangersFilter
  }

  @type ast :: list | tuple | atom | String.t()

  @type danger_type ::
          :check_constraint_added
          | :column_added_with_default
          | :column_reference_added
          | :column_removed
          | :column_renamed
          | :column_type_changed
          | :column_volatile_default
          | :index_concurrently_without_disable_ddl_transaction
          | :index_concurrently_without_disable_migration_lock
          | :index_not_concurrently
          | :json_column_added
          | :many_columns_index
          | :not_null_added
          | :operation_delete
          | :operation_insert
          | :operation_update
          | :raw_sql_executed
          | :table_dropped
          | :table_renamed

  @type line :: integer

  @doc """
  Traverses `ast` and finds potentially dangerous database operations. Returns keyword list
  containing danger types and lines where they were detected.
  ## Parameters
    * `ast` is a structure that represents AST of database migration.
      It can be obtained e.g. via `Code.string_to_quoted!/1`.
  ## Examples
          iex> source_code = \"""
          ...>   alter table("dumplings") do
          ...>     remove(:taste, :string)
          ...>     remove(:stuffing, :string)
          ...>   end
          ...> \"""
          iex> ast = Code.string_to_quoted!(source_code)
          {:ok,
          {:alter, [line: 1],
          [
            {:table, [line: 1], ["dumplings"]},
            [
              do: {:__block__, [],
               [
                 {:remove, [line: 2], [:taste, :string]},
                 {:remove, [line: 3], [:stuffing, :string]}
               ]}
            ]
          ]}}

          iex> ExcellentMigrations.DangersDetector.detect_dangers(ast, source_code)
          [column_removed: 2, column_removed: 3]
  """
  @spec detect_dangers(ast, String.t()) :: [{danger_type, line}]
  def detect_dangers(ast, source_code) do
    parsed_dangers =
      [
        AstParser.parse(ast),
        AstParserFullDetections.parse(ast)
      ]
      |> Enum.concat()

    parsed_safety_assured = ConfigCommentsParser.parse(source_code)
    skipped_types = Application.get_env(:excellent_migrations, :skip_checks, [])
    DangersFilter.filter_dangers(parsed_dangers, parsed_safety_assured, skipped_types)
  end
end