lib/runner.ex

defmodule ExcellentMigrations.Runner do
  @moduledoc """
  This module finds migration files in a project and detects potentially dangerous database
  operations in them.
  """

  alias ExcellentMigrations.{
    DangersDetector,
    FilesFinder
  }

  @type danger_type ::
          :column_added_with_default
          | :column_removed
          | :column_renamed
          | :column_type_changed
          | :index_not_concurrently
          | :many_columns_index
          | :not_null_added
          | :operation_delete
          | :operation_insert
          | :operation_update
          | :raw_sql_executed
          | :table_dropped
          | :table_renamed

  @type danger :: %{
          type: danger_type,
          path: String.t(),
          line: integer
        }

  @doc """
  Detects potentially dangerous database operations in database migration files.
  ## Options
    * `:migrations_paths` - optional list of file paths to be checked.
  ## Scope of analysis
    * If `migrations_paths` are specified, the analysis will be narrowed down to these files only.
    * If not and application env `:excellent_migrations, :start_after` is set, only migrations with
      timestamp older than the provided one will be chosen.
    * If none of the above, all migration files in a project will be analyzed.

  """
  @spec check_migrations(migrations_paths: [String.t()]) :: :safe | {:dangerous, [danger]}
  def check_migrations(opts \\ []) do
    opts
    |> get_migrations_paths()
    |> Task.async_stream(fn path ->
      source_code = File.read!(path)
      ast = Code.string_to_quoted!(source_code)
      dangers = DangersDetector.detect_dangers(ast, source_code)
      build_result(dangers, path)
    end)
    |> Stream.flat_map(fn {:ok, items} -> items end)
    |> Enum.to_list()
    |> close()
  end

  defp get_migrations_paths(opts) do
    opts
    |> Keyword.get_lazy(:migrations_paths, &FilesFinder.get_migrations_paths/0)
    |> Enum.sort()
  end

  defp build_result(dangers, path) do
    Enum.map(dangers, fn {type, line} ->
      %{
        type: type,
        path: path,
        line: line
      }
    end)
  end

  defp close([]), do: :safe
  defp close(dangers), do: {:dangerous, dangers}
end