Skip to content

Commit 44ea7a6

Browse files
authored
Pass :sigils to mix format plugins invoked for a sigil (#15672)
mix format passes the :sigils option to a plugin invoked for a file extension, but not to a plugin invoked for a sigil, because the sigil formatter closure captures the options before :sigils is put into them. A plugin that formats embedded Elixir code therefore cannot dispatch a sigil nested inside the sigil it is formatting by passing on the options it received, so the same snippet formats differently in a .heex file and in an ~H sigil. The sigil formatters are part of the options they receive, so a private function rebuilds the list whenever a sigil is formatted.
1 parent a8d9921 commit 44ea7a6

2 files changed

Lines changed: 83 additions & 12 deletions

File tree

lib/mix/lib/mix/tasks/format.ex

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -349,24 +349,17 @@ defmodule Mix.Tasks.Format do
349349
end
350350
end
351351

352-
sigils =
352+
flatten_sigils =
353353
for plugin <- plugins,
354354
sigil <- find_sigils_from_plugins(plugin, formatter_opts),
355355
do: {sigil, plugin}
356356

357-
sigils =
358-
sigils
357+
formatter_opts =
358+
flatten_sigils
359359
|> Enum.group_by(&elem(&1, 0), &elem(&1, 1))
360-
|> Enum.map(fn {sigil, plugins} ->
361-
{sigil,
362-
fn input, opts ->
363-
Enum.reduce(plugins, input, fn plugin, input ->
364-
plugin.format(input, opts ++ formatter_opts)
365-
end)
366-
end}
367-
end)
360+
|> prepend_sigils(Keyword.delete(formatter_opts, :sigils))
368361

369-
{Keyword.put(formatter_opts, :sigils, sigils),
362+
{formatter_opts,
370363
Enum.map(subs, fn {path, formatter_opts_and_subs} ->
371364
{path, load_plugins(formatter_opts_and_subs, opts)}
372365
end)}
@@ -384,6 +377,24 @@ defmodule Mix.Tasks.Format do
384377
plugins
385378
end
386379

380+
# A sigil formatter must receive :sigils itself, so a plugin can format sigils
381+
# nested inside the sigil it was given. The list is rebuilt on each invocation.
382+
defp prepend_sigils(grouped_sigils, formatter_opts_without_sigils) do
383+
sigils =
384+
Enum.map(grouped_sigils, fn {sigil, plugins} ->
385+
{sigil,
386+
fn input, opts ->
387+
formatter_opts = prepend_sigils(grouped_sigils, formatter_opts_without_sigils)
388+
389+
Enum.reduce(plugins, input, fn plugin, input ->
390+
plugin.format(input, opts ++ formatter_opts)
391+
end)
392+
end}
393+
end)
394+
395+
[sigils: sigils] ++ formatter_opts_without_sigils
396+
end
397+
387398
@typedoc """
388399
Options for `formatter_for_file/2`.
389400
"""

lib/mix/test/mix/tasks/format_test.exs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,66 @@ defmodule Mix.Tasks.FormatTest do
572572
end)
573573
end
574574

575+
defmodule Elixir.SigilOuterPlugin do
576+
@behaviour Mix.Tasks.Format
577+
578+
def features(opts) do
579+
assert opts[:from_formatter_exs] == :yes
580+
[sigils: [:O]]
581+
end
582+
583+
def format(contents, opts) do
584+
assert opts[:from_formatter_exs] == :yes
585+
assert opts[:sigil] == :O
586+
[Code.format_string!(contents, opts), ?\n]
587+
end
588+
end
589+
590+
defmodule Elixir.SigilInnerPlugin do
591+
@behaviour Mix.Tasks.Format
592+
593+
def features(opts) do
594+
assert opts[:from_formatter_exs] == :yes
595+
[sigils: [:I]]
596+
end
597+
598+
def format(contents, opts) do
599+
assert opts[:from_formatter_exs] == :yes
600+
assert opts[:sigil] == :I
601+
String.upcase(contents)
602+
end
603+
end
604+
605+
test "uses sigil plugins for sigils nested inside sigils", context do
606+
in_tmp(context.test, fn ->
607+
File.write!(".formatter.exs", """
608+
[
609+
inputs: ["a.ex"],
610+
plugins: [SigilOuterPlugin, SigilInnerPlugin],
611+
from_formatter_exs: :yes
612+
]
613+
""")
614+
615+
File.write!("a.ex", """
616+
def nested_sigil_test do
617+
~O'''
618+
inner(~I"foo bar")
619+
'''
620+
end
621+
""")
622+
623+
Mix.Tasks.Format.run([])
624+
625+
assert File.read!("a.ex") == """
626+
def nested_sigil_test do
627+
~O'''
628+
inner(~I"FOO BAR")
629+
'''
630+
end
631+
"""
632+
end)
633+
end
634+
575635
test "customizes plugin loading", context do
576636
in_tmp(context.test, fn ->
577637
File.write!(".formatter.exs", """

0 commit comments

Comments
 (0)