From 5ebda57ed4afdc68dff43b24f62c1d1e72776478 Mon Sep 17 00:00:00 2001 From: Maarten van Vliet Date: Tue, 26 Oct 2021 09:42:34 +0200 Subject: [PATCH 1/6] Pass sigil/modifiers through to formatter This allows the formatter plugins to format content based on whether it's a file or a sigil being formatted. The modifiers could be used for additional options. --- lib/elixir/lib/code/formatter.ex | 6 ++--- .../elixir/code_formatter/general_test.exs | 24 +++++++++++++++---- lib/mix/lib/mix/tasks/format.ex | 2 +- lib/mix/test/mix/tasks/format_test.exs | 19 +++++++++++++-- 4 files changed, 41 insertions(+), 10 deletions(-) diff --git a/lib/elixir/lib/code/formatter.ex b/lib/elixir/lib/code/formatter.ex index 635d41f8968..277371466a9 100644 --- a/lib/elixir/lib/code/formatter.ex +++ b/lib/elixir/lib/code/formatter.ex @@ -181,7 +181,7 @@ defmodule Code.Formatter do sigils = Map.new(sigils, fn {key, value} -> - with true <- is_atom(key) and is_function(value, 1), + with true <- is_atom(key) and is_function(value, 2), [char] <- Atom.to_charlist(key), true <- char in ?A..?Z do {char, value} @@ -189,7 +189,7 @@ defmodule Code.Formatter do _ -> raise ArgumentError, ":sigils must be a keyword list with a single uppercased letter as key and an " <> - "anonymous function expecting a single argument as value, got: #{inspect(sigils)}" + "anonymous function expecting two arguments as value, got: #{inspect(sigils)}" end end) @@ -1320,7 +1320,7 @@ defmodule Code.Formatter do entries = case state.sigils do %{^name => callback} -> - case callback.(hd(entries)) do + case callback.(hd(entries), sigil: {List.to_atom([name]), List.to_string(modifiers)}) do binary when is_binary(binary) -> [binary] diff --git a/lib/elixir/test/elixir/code_formatter/general_test.exs b/lib/elixir/test/elixir/code_formatter/general_test.exs index 9559087fbac..76149b64725 100644 --- a/lib/elixir/test/elixir/code_formatter/general_test.exs +++ b/lib/elixir/test/elixir/code_formatter/general_test.exs @@ -125,7 +125,11 @@ defmodule Code.Formatter.GeneralTest do ~W/foo bar baz/ """ - formatter = &(&1 |> String.split(~r/ +/) |> Enum.join(" ")) + formatter = fn content, opts -> + assert opts == [sigil: {:W, ""}] + content |> String.split(~r/ +/) |> Enum.join(" ") + end + assert_format bad, good, sigils: [W: formatter] bad = """ @@ -136,7 +140,11 @@ defmodule Code.Formatter.GeneralTest do var = ~W/foo bar baz/abc """ - formatter = &(&1 |> String.split(~r/ +/) |> Enum.join(" ")) + formatter = fn content, opts -> + assert opts == [sigil: {:W, "abc"}] + content |> String.split(~r/ +/) |> Enum.join(" ") + end + assert_format bad, good, sigils: [W: formatter] end @@ -153,7 +161,11 @@ defmodule Code.Formatter.GeneralTest do ''' """ - formatter = &(&1 |> String.split(~r/ +/) |> Enum.join(" ")) + formatter = fn content, opts -> + assert opts == [sigil: {:W, ""}] + content |> String.split(~r/ +/) |> Enum.join(" ") + end + assert_format bad, good, sigils: [W: formatter] bad = """ @@ -176,7 +188,11 @@ defmodule Code.Formatter.GeneralTest do end """ - formatter = &(&1 |> String.split(~r/ +/) |> Enum.join("\n")) + formatter = fn content, opts -> + assert opts == [sigil: {:W, "abc"}] + content |> String.split(~r/ +/) |> Enum.join("\n") + end + assert_format bad, good, sigils: [W: formatter] end end diff --git a/lib/mix/lib/mix/tasks/format.ex b/lib/mix/lib/mix/tasks/format.ex index 1fd5722ccae..bb065f3cd13 100644 --- a/lib/mix/lib/mix/tasks/format.ex +++ b/lib/mix/lib/mix/tasks/format.ex @@ -531,7 +531,7 @@ defmodule Mix.Tasks.Format do sigils = for plugin <- Keyword.fetch!(formatter_opts, :plugins), sigil <- find_sigils_from_plugins(plugin, formatter_opts), - do: {sigil, &plugin.format(&1, formatter_opts)} + do: {sigil, &plugin.format(&1, &2 ++ formatter_opts)} IO.iodata_to_binary([Code.format_string!(content, [sigils: sigils] ++ formatter_opts), ?\n]) end diff --git a/lib/mix/test/mix/tasks/format_test.exs b/lib/mix/test/mix/tasks/format_test.exs index aacb19f128f..d7912d05c5d 100644 --- a/lib/mix/test/mix/tasks/format_test.exs +++ b/lib/mix/test/mix/tasks/format_test.exs @@ -199,11 +199,12 @@ defmodule Mix.Tasks.FormatTest do def features(opts) do assert opts[:from_formatter_exs] == :yes - [sigils: [:W], extensions: ~w(.w)] + [sigils: [:W]] end def format(contents, opts) do assert opts[:from_formatter_exs] == :yes + assert opts[:sigil] == {:W, "abc"} contents |> String.split(~r/\s/) |> Enum.join("\n") end end @@ -240,12 +241,26 @@ defmodule Mix.Tasks.FormatTest do end) end + defmodule Elixir.ExtensionWPlugin do + @behaviour Mix.Tasks.Format + + def features(opts) do + assert opts[:from_formatter_exs] == :yes + [extensions: ~w(.w)] + end + + def format(contents, opts) do + assert opts[:from_formatter_exs] == :yes + contents |> String.split(~r/\s/) |> Enum.join("\n") + end + end + test "uses extension plugins from .formatter.exs", context do in_tmp(context.test, fn -> File.write!(".formatter.exs", """ [ inputs: ["a.w"], - plugins: [SigilWPlugin], + plugins: [ExtensionWPlugin], from_formatter_exs: :yes ] """) From f01ff068d59eb8c04a61556d0175b87dae28d5c3 Mon Sep 17 00:00:00 2001 From: Maarten van Vliet Date: Tue, 26 Oct 2021 11:21:44 +0200 Subject: [PATCH 2/6] Pass sigil modifiers as charlist --- lib/elixir/lib/code/formatter.ex | 2 +- lib/elixir/test/elixir/code_formatter/general_test.exs | 8 ++++---- lib/mix/test/mix/tasks/format_test.exs | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/elixir/lib/code/formatter.ex b/lib/elixir/lib/code/formatter.ex index 277371466a9..43ea84ffda1 100644 --- a/lib/elixir/lib/code/formatter.ex +++ b/lib/elixir/lib/code/formatter.ex @@ -1320,7 +1320,7 @@ defmodule Code.Formatter do entries = case state.sigils do %{^name => callback} -> - case callback.(hd(entries), sigil: {List.to_atom([name]), List.to_string(modifiers)}) do + case callback.(hd(entries), sigil: {List.to_atom([name]), modifiers}) do binary when is_binary(binary) -> [binary] diff --git a/lib/elixir/test/elixir/code_formatter/general_test.exs b/lib/elixir/test/elixir/code_formatter/general_test.exs index 76149b64725..ce84923cc37 100644 --- a/lib/elixir/test/elixir/code_formatter/general_test.exs +++ b/lib/elixir/test/elixir/code_formatter/general_test.exs @@ -126,7 +126,7 @@ defmodule Code.Formatter.GeneralTest do """ formatter = fn content, opts -> - assert opts == [sigil: {:W, ""}] + assert opts == [sigil: {:W, []}] content |> String.split(~r/ +/) |> Enum.join(" ") end @@ -141,7 +141,7 @@ defmodule Code.Formatter.GeneralTest do """ formatter = fn content, opts -> - assert opts == [sigil: {:W, "abc"}] + assert opts == [sigil: {:W, 'abc'}] content |> String.split(~r/ +/) |> Enum.join(" ") end @@ -162,7 +162,7 @@ defmodule Code.Formatter.GeneralTest do """ formatter = fn content, opts -> - assert opts == [sigil: {:W, ""}] + assert opts == [sigil: {:W, []}] content |> String.split(~r/ +/) |> Enum.join(" ") end @@ -189,7 +189,7 @@ defmodule Code.Formatter.GeneralTest do """ formatter = fn content, opts -> - assert opts == [sigil: {:W, "abc"}] + assert opts == [sigil: {:W, 'abc'}] content |> String.split(~r/ +/) |> Enum.join("\n") end diff --git a/lib/mix/test/mix/tasks/format_test.exs b/lib/mix/test/mix/tasks/format_test.exs index d7912d05c5d..43a8de8c97b 100644 --- a/lib/mix/test/mix/tasks/format_test.exs +++ b/lib/mix/test/mix/tasks/format_test.exs @@ -204,7 +204,7 @@ defmodule Mix.Tasks.FormatTest do def format(contents, opts) do assert opts[:from_formatter_exs] == :yes - assert opts[:sigil] == {:W, "abc"} + assert opts[:sigil] == {:W, 'abc'} contents |> String.split(~r/\s/) |> Enum.join("\n") end end From 96c4f9bf416920e8e20cb5e86d447bab70a1f629 Mon Sep 17 00:00:00 2001 From: Maarten van Vliet Date: Tue, 26 Oct 2021 11:45:56 +0200 Subject: [PATCH 3/6] Pass through file extension to formatter plugins --- lib/mix/lib/mix/tasks/format.ex | 2 +- lib/mix/test/mix/tasks/format_test.exs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/mix/lib/mix/tasks/format.ex b/lib/mix/lib/mix/tasks/format.ex index bb065f3cd13..c3bd5ed803b 100644 --- a/lib/mix/lib/mix/tasks/format.ex +++ b/lib/mix/lib/mix/tasks/format.ex @@ -490,7 +490,7 @@ defmodule Mix.Tasks.Format do &elixir_format(&1, [file: file] ++ formatter_opts) plugin = find_plugin_for_extension(formatter_opts, ext) -> - &plugin.format(&1, formatter_opts) + &plugin.format(&1, [extension: Path.extname(file)] ++ formatter_opts) true -> & &1 diff --git a/lib/mix/test/mix/tasks/format_test.exs b/lib/mix/test/mix/tasks/format_test.exs index 43a8de8c97b..17450c1f63a 100644 --- a/lib/mix/test/mix/tasks/format_test.exs +++ b/lib/mix/test/mix/tasks/format_test.exs @@ -251,6 +251,7 @@ defmodule Mix.Tasks.FormatTest do def format(contents, opts) do assert opts[:from_formatter_exs] == :yes + assert opts[:extension] == ".w" contents |> String.split(~r/\s/) |> Enum.join("\n") end end From b2cfddec830985be0216ef3c3ede9e4abcaf2bca Mon Sep 17 00:00:00 2001 From: Maarten van Vliet Date: Tue, 26 Oct 2021 12:56:54 +0200 Subject: [PATCH 4/6] Add documentation sigil/extensions in format plugin --- lib/elixir/lib/code.ex | 6 ++++++ lib/mix/lib/mix/tasks/format.ex | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/lib/elixir/lib/code.ex b/lib/elixir/lib/code.ex index 98eabf23fb4..1c118498817 100644 --- a/lib/elixir/lib/code.ex +++ b/lib/elixir/lib/code.ex @@ -437,6 +437,12 @@ defmodule Code do If you set it to `false` later on, `do`-`end` blocks won't be converted back to keywords. + * `:extension` (since v1.13.0) - extension of the file. Used by + formatter plugins. + + * `:sigil` (since v1.13.0) - a tuple of the sigil and its modifiers. + Used by formatter plugins. + ## Design principles The formatter was designed under three principles. diff --git a/lib/mix/lib/mix/tasks/format.ex b/lib/mix/lib/mix/tasks/format.ex index c3bd5ed803b..244769596f9 100644 --- a/lib/mix/lib/mix/tasks/format.ex +++ b/lib/mix/lib/mix/tasks/format.ex @@ -102,6 +102,13 @@ defmodule Mix.Tasks.Format do end end + The `opts` passed to `format/2` contains all the formatting options and either: + + * `:sigil` (tuple of sigil and its modifiers) - the sigil being formatted, + e.g. `{:M, []}`. + + * `:extension` (string) - the extension of the file being formatted, e.g. `".md"`. + Now any application can use your formatter as follows: # .formatters.exs From ab815861664be401241fe6c907c4f7c5ca993e3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 26 Oct 2021 13:36:00 +0200 Subject: [PATCH 5/6] Apply suggestions from code review --- lib/elixir/lib/code.ex | 6 ------ lib/mix/lib/mix/tasks/format.ex | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/lib/elixir/lib/code.ex b/lib/elixir/lib/code.ex index 1c118498817..98eabf23fb4 100644 --- a/lib/elixir/lib/code.ex +++ b/lib/elixir/lib/code.ex @@ -437,12 +437,6 @@ defmodule Code do If you set it to `false` later on, `do`-`end` blocks won't be converted back to keywords. - * `:extension` (since v1.13.0) - extension of the file. Used by - formatter plugins. - - * `:sigil` (since v1.13.0) - a tuple of the sigil and its modifiers. - Used by formatter plugins. - ## Design principles The formatter was designed under three principles. diff --git a/lib/mix/lib/mix/tasks/format.ex b/lib/mix/lib/mix/tasks/format.ex index 244769596f9..cda8f880182 100644 --- a/lib/mix/lib/mix/tasks/format.ex +++ b/lib/mix/lib/mix/tasks/format.ex @@ -497,7 +497,7 @@ defmodule Mix.Tasks.Format do &elixir_format(&1, [file: file] ++ formatter_opts) plugin = find_plugin_for_extension(formatter_opts, ext) -> - &plugin.format(&1, [extension: Path.extname(file)] ++ formatter_opts) + &plugin.format(&1, [extension: ext] ++ formatter_opts) true -> & &1 From 6766172a39532e42a4715df6e7b7b0e071c5459d Mon Sep 17 00:00:00 2001 From: Maarten van Vliet Date: Tue, 26 Oct 2021 13:56:12 +0200 Subject: [PATCH 6/6] Pass modifiers separately --- lib/elixir/lib/code/formatter.ex | 2 +- lib/elixir/test/elixir/code_formatter/general_test.exs | 8 ++++---- lib/mix/lib/mix/tasks/format.ex | 5 +++-- lib/mix/test/mix/tasks/format_test.exs | 3 ++- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/lib/elixir/lib/code/formatter.ex b/lib/elixir/lib/code/formatter.ex index 43ea84ffda1..7c0dd8bda31 100644 --- a/lib/elixir/lib/code/formatter.ex +++ b/lib/elixir/lib/code/formatter.ex @@ -1320,7 +1320,7 @@ defmodule Code.Formatter do entries = case state.sigils do %{^name => callback} -> - case callback.(hd(entries), sigil: {List.to_atom([name]), modifiers}) do + case callback.(hd(entries), sigil: List.to_atom([name]), modifiers: modifiers) do binary when is_binary(binary) -> [binary] diff --git a/lib/elixir/test/elixir/code_formatter/general_test.exs b/lib/elixir/test/elixir/code_formatter/general_test.exs index ce84923cc37..5aed5d17cec 100644 --- a/lib/elixir/test/elixir/code_formatter/general_test.exs +++ b/lib/elixir/test/elixir/code_formatter/general_test.exs @@ -126,7 +126,7 @@ defmodule Code.Formatter.GeneralTest do """ formatter = fn content, opts -> - assert opts == [sigil: {:W, []}] + assert opts == [sigil: :W, modifiers: []] content |> String.split(~r/ +/) |> Enum.join(" ") end @@ -141,7 +141,7 @@ defmodule Code.Formatter.GeneralTest do """ formatter = fn content, opts -> - assert opts == [sigil: {:W, 'abc'}] + assert opts == [sigil: :W, modifiers: 'abc'] content |> String.split(~r/ +/) |> Enum.join(" ") end @@ -162,7 +162,7 @@ defmodule Code.Formatter.GeneralTest do """ formatter = fn content, opts -> - assert opts == [sigil: {:W, []}] + assert opts == [sigil: :W, modifiers: []] content |> String.split(~r/ +/) |> Enum.join(" ") end @@ -189,7 +189,7 @@ defmodule Code.Formatter.GeneralTest do """ formatter = fn content, opts -> - assert opts == [sigil: {:W, 'abc'}] + assert opts == [sigil: :W, modifiers: 'abc'] content |> String.split(~r/ +/) |> Enum.join("\n") end diff --git a/lib/mix/lib/mix/tasks/format.ex b/lib/mix/lib/mix/tasks/format.ex index cda8f880182..b9e7f6c9a8b 100644 --- a/lib/mix/lib/mix/tasks/format.ex +++ b/lib/mix/lib/mix/tasks/format.ex @@ -104,8 +104,9 @@ defmodule Mix.Tasks.Format do The `opts` passed to `format/2` contains all the formatting options and either: - * `:sigil` (tuple of sigil and its modifiers) - the sigil being formatted, - e.g. `{:M, []}`. + * `:sigil` (atom) - the sigil being formatted, e.g. `:M`. + + * `:modifiers` (charlist) - list of sigil modifiers. * `:extension` (string) - the extension of the file being formatted, e.g. `".md"`. diff --git a/lib/mix/test/mix/tasks/format_test.exs b/lib/mix/test/mix/tasks/format_test.exs index 17450c1f63a..6473cfd4a71 100644 --- a/lib/mix/test/mix/tasks/format_test.exs +++ b/lib/mix/test/mix/tasks/format_test.exs @@ -204,7 +204,8 @@ defmodule Mix.Tasks.FormatTest do def format(contents, opts) do assert opts[:from_formatter_exs] == :yes - assert opts[:sigil] == {:W, 'abc'} + assert opts[:sigil] == :W + assert opts[:modifiers] == 'abc' contents |> String.split(~r/\s/) |> Enum.join("\n") end end