lib/mix/tasks/spark.formatter.ex

defmodule Mix.Tasks.Spark.Formatter do
  @shortdoc "Manages a variable called `spark_locals_without_parens` in the .formatter.exs from a list of DSL extensions."
  @moduledoc @shortdoc
  use Mix.Task

  @spec run(term) :: no_return
  def run(opts) do
    Mix.Task.run("compile")
    {opts, []} = OptionParser.parse!(opts, strict: [check: :boolean, extensions: :string])

    unless opts[:extensions] do
      raise "Must supply a comma separated list of extensions to generate a .formatter.exs for"
    end

    extensions =
      opts[:extensions]
      |> String.split(",")
      |> Enum.map(&Module.concat([&1]))

    locals_without_parens =
      Enum.flat_map(extensions, fn extension_mod ->
        case Code.ensure_compiled(extension_mod) do
          {:module, _module} -> :ok
          other -> raise "Error ensuring extension compiled #{inspect(other)}"
        end

        all_entity_builders_everywhere(extension_mod.sections(), extensions)
      end)
      |> Enum.uniq()
      |> Enum.sort()

    contents = File.read!(".formatter.exs")

    {_ast, spark_locals_without_parens} =
      contents
      |> Sourceror.parse_string!()
      |> Macro.prewalk(
        nil,
        fn
          {:=, _,
           [
             {:spark_locals_without_parens, _, _},
             right
           ]} = ast,
          _acc ->
            {ast, right}

          ast, acc ->
            {ast, acc}
        end
      )

    if !spark_locals_without_parens do
      raise "Add `spark_locals_without_parens = []` to your .formatter.exs and run this again to populate the list."
    end

    new_contents =
      contents
      |> Sourceror.patch_string([
        %{
          range: Sourceror.get_range(spark_locals_without_parens, include_comments: true),
          change: Sourceror.to_string(locals_without_parens, opts)
        }
      ])
      |> Code.format_string!()

    contents_with_newline = [new_contents, "\n"]

    if opts[:check] do
      if contents != IO.iodata_to_binary(contents_with_newline) do
        raise """
        .formatter.exs is not up to date!

        Run the following command and commit the result:

        mix spark.formatter --extensions #{opts[:extensions]}
        """
      else
        IO.puts("The current .formatter.exs is correct")
      end
    else
      File.write!(".formatter.exs", contents_with_newline)
    end
  end

  def all_entity_builders_everywhere(sections, extensions, path \\ []) do
    sections
    |> Enum.flat_map(fn section ->
      all_entity_builders_everywhere(
        section.sections,
        extensions,
        path ++ [section.name]
      )
    end)
    |> Enum.concat(Spark.Formatter.all_entity_builders(sections, extensions, path))
  end
end