Skip to content

Make formatter plugins which target .ex/.exs run alongside (not "instead of") standard formatting and other plugins which target sigils. - #15742

Open
evnp wants to merge 2 commits into
elixir-lang:mainfrom
evnp:format-multi-plugin-ex-exs-and-sigils
Open

Make formatter plugins which target .ex/.exs run alongside (not "instead of") standard formatting and other plugins which target sigils.#15742
evnp wants to merge 2 commits into
elixir-lang:mainfrom
evnp:format-multi-plugin-ex-exs-and-sigils

Conversation

@evnp

@evnp evnp commented Aug 13, 2026

Copy link
Copy Markdown

Expected behavior

  • When a mix format plugin is used which specifies .ex/.exs file extensions, plugin formatting is run after standard mix format formatting for those files.
  • When mix format plugin A is used which specifies .ex/.exs file extensions, alongside plugin B which specifies sigils within those files,
    • formatting from A is applied to the whole file
    • formatting from both A and B are applied to sigils specified by B (in order of plugins list in .formatter.exs)

Observed behavior

  • When a mix format plugin is used which targets .ex/.exs file extensions, standard mix format formatting is not applied to those files.
  • When mix format plugin A is used which targets .ex/.exs file extensions, alongside plugin B which targets sigils within those files,
    • formatting from A is applied to the whole file
    • formatting from B is not applied at all (to sigils within .ex/.exs files)

Working with some mix format plugins, I noticed that a plugin targeting .ex files would cause a second plugin targeting ~H"""...""" sigils to not format them at all. After looking into format.ex, it seems like this is because of the current implementation of find_formatter_for_file which has a cond with three branches:

    cond do
      plugins = find_plugins_for_extension(formatter_opts, ext) ->
        fn input ->
          Enum.reduce(plugins, input, fn plugin, input ->
            plugin.format(input, [extension: ext, file: file] ++ formatter_opts)
          end)
        end

      ext in ~w(.ex .exs) ->
        &elixir_format(&1, [file: file] ++ formatter_opts)

      true ->
        & &1
    end

It looks like sigil formatting happens during elixir_format, which iiuc won't run at all if a plugin is found for an .ex or .exs file matching the first case of this cond. Because of this, it seems that sigil-targeting plugins won't run at all for files where another plugin was found targeting .ex or .exs. I think it also means standard elixir formatting won't run on .ex/.exs files if a plugin targets them. These interpretations seem to be verified by the three tests cases in 12a14ef which fail prior to the fix.

Perhaps this is all intentional behavior? I found it a bit surprising, and didn't see mention of it at https://mix.hexdocs.pm/main/Mix.Tasks.Format.html#plugins, but happy to simply be informed here as well!

Using this command to run the three test cases:

bin/elixirc lib/mix/lib/mix/tasks/format.ex -o lib/elixir/ebin && LINE=607 bin/elixir lib/mix/test/mix/tasks/format_test.exs; LINE=634 bin/elixir lib/mix/test/mix/tasks/format_test.exs; LINE=664 bin/elixir lib/mix/test/mix/tasks/format_test.exs

Now, I can't help but add something you've likely heard many times before: Elixir has been (and continues to be) a joy to work with, thank you for all your labors making it the wonderful set of tools it is today.

evnp added 2 commits August 13, 2026 13:55
…em should all format, after standard .ex/.exs formatting.
… should all format, after standard .ex/.exs formatting.
@josevalim

Copy link
Copy Markdown
Member

This is definitely a breaking change. What we could try to do is to either have an option that allows you to call the original formatter or pass a function with the original formatter that you could call.

@josevalim

Copy link
Copy Markdown
Member

It is probably better as an option because of you have multiple .ex plugins, we only need to format it once. But then there is the question if we run it before or after.

@novaugust

Copy link
Copy Markdown
Contributor

fwiw, styler does the equivalent of mix formatas part of turning ast back into text code, and even relies on the formatter to clean up any mess styler made along the way.
https://github.com/adobe/elixir-styler/blob/4674cdb3728cb83e93ec185b59e014609db5f059/lib/styler.ex#L92-L102

so anything non-optional would be redundant cpu in styler's case.

perhaps the plugins themselves can flag something in their features callback specifying whether they want the formatter to still run after they do (or not?). i think after makes more sense, as plugins expect to receive unformatted code and finish with formatted code. but that would just introduce the problem of multiple .ex plugins arguing over whether formatter should be run or not - probably if any want it, it's worth doing after running all?

you could get the same behaviour by having it be user configured in .formatter.exs, but i'd guess that in practice the user is only going to do that when a plugin's README tells them to, at which point the plugin could've just told the formatter itself.

OR if the status quo is preferable, maybe the Formatter ### Plugins docs just need updating to surface how plugin authors can call code similar to styler's above to run the formatter themselves when their plugin runs on .ex/.exs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants