diff --git a/lib/mix/lib/mix/tasks/format.ex b/lib/mix/lib/mix/tasks/format.ex index b19e82ed316..2951ceac1ac 100644 --- a/lib/mix/lib/mix/tasks/format.ex +++ b/lib/mix/lib/mix/tasks/format.ex @@ -349,24 +349,17 @@ defmodule Mix.Tasks.Format do end end - sigils = + flatten_sigils = for plugin <- plugins, sigil <- find_sigils_from_plugins(plugin, formatter_opts), do: {sigil, plugin} - sigils = - sigils + formatter_opts = + flatten_sigils |> Enum.group_by(&elem(&1, 0), &elem(&1, 1)) - |> Enum.map(fn {sigil, plugins} -> - {sigil, - fn input, opts -> - Enum.reduce(plugins, input, fn plugin, input -> - plugin.format(input, opts ++ formatter_opts) - end) - end} - end) + |> prepend_sigils(Keyword.delete(formatter_opts, :sigils)) - {Keyword.put(formatter_opts, :sigils, sigils), + {formatter_opts, Enum.map(subs, fn {path, formatter_opts_and_subs} -> {path, load_plugins(formatter_opts_and_subs, opts)} end)} @@ -384,6 +377,24 @@ defmodule Mix.Tasks.Format do plugins end + # A sigil formatter must receive :sigils itself, so a plugin can format sigils + # nested inside the sigil it was given. The list is rebuilt on each invocation. + defp prepend_sigils(grouped_sigils, formatter_opts_without_sigils) do + sigils = + Enum.map(grouped_sigils, fn {sigil, plugins} -> + {sigil, + fn input, opts -> + formatter_opts = prepend_sigils(grouped_sigils, formatter_opts_without_sigils) + + Enum.reduce(plugins, input, fn plugin, input -> + plugin.format(input, opts ++ formatter_opts) + end) + end} + end) + + [sigils: sigils] ++ formatter_opts_without_sigils + end + @typedoc """ Options for `formatter_for_file/2`. """ diff --git a/lib/mix/test/mix/tasks/format_test.exs b/lib/mix/test/mix/tasks/format_test.exs index 1498d2a162f..f828e5c4790 100644 --- a/lib/mix/test/mix/tasks/format_test.exs +++ b/lib/mix/test/mix/tasks/format_test.exs @@ -572,6 +572,66 @@ defmodule Mix.Tasks.FormatTest do end) end + defmodule Elixir.SigilOuterPlugin do + @behaviour Mix.Tasks.Format + + def features(opts) do + assert opts[:from_formatter_exs] == :yes + [sigils: [:O]] + end + + def format(contents, opts) do + assert opts[:from_formatter_exs] == :yes + assert opts[:sigil] == :O + [Code.format_string!(contents, opts), ?\n] + end + end + + defmodule Elixir.SigilInnerPlugin do + @behaviour Mix.Tasks.Format + + def features(opts) do + assert opts[:from_formatter_exs] == :yes + [sigils: [:I]] + end + + def format(contents, opts) do + assert opts[:from_formatter_exs] == :yes + assert opts[:sigil] == :I + String.upcase(contents) + end + end + + test "uses sigil plugins for sigils nested inside sigils", context do + in_tmp(context.test, fn -> + File.write!(".formatter.exs", """ + [ + inputs: ["a.ex"], + plugins: [SigilOuterPlugin, SigilInnerPlugin], + from_formatter_exs: :yes + ] + """) + + File.write!("a.ex", """ + def nested_sigil_test do + ~O''' + inner(~I"foo bar") + ''' + end + """) + + Mix.Tasks.Format.run([]) + + assert File.read!("a.ex") == """ + def nested_sigil_test do + ~O''' + inner(~I"FOO BAR") + ''' + end + """ + end) + end + test "customizes plugin loading", context do in_tmp(context.test, fn -> File.write!(".formatter.exs", """