Skip to content

Commit 0ae9ac3

Browse files
committed
Add new options to control deps and plugin loading in formatter
1 parent aa108eb commit 0ae9ac3

File tree

2 files changed

+87
-25
lines changed

2 files changed

+87
-25
lines changed

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

Lines changed: 55 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -244,9 +244,9 @@ defmodule Mix.Tasks.Format do
244244
end
245245

246246
{formatter_opts_and_subs, _sources} =
247-
eval_deps_and_subdirectories(cwd, dot_formatter, formatter_opts, [dot_formatter])
247+
eval_deps_and_subdirectories(cwd, dot_formatter, formatter_opts, [dot_formatter], opts)
248248

249-
formatter_opts_and_subs = load_plugins(formatter_opts_and_subs)
249+
formatter_opts_and_subs = load_plugins(formatter_opts_and_subs, opts)
250250
files = expand_args(args, cwd, dot_formatter, formatter_opts_and_subs, opts)
251251

252252
maybe_cache_timestamps(all_args, files, fn files ->
@@ -290,20 +290,19 @@ defmodule Mix.Tasks.Format do
290290

291291
defp maybe_cache_timestamps([_ | _], files, fun), do: fun.(files)
292292

293-
defp load_plugins({formatter_opts, subs}) do
293+
defp load_plugins({formatter_opts, subs}, opts) do
294294
plugins = Keyword.get(formatter_opts, :plugins, [])
295295

296296
if not is_list(plugins) do
297297
Mix.raise("Expected :plugins to return a list of modules, got: #{inspect(plugins)}")
298298
end
299299

300-
if plugins != [] do
301-
Mix.Task.run("loadpaths", [])
302-
end
303-
304-
if not Enum.all?(plugins, &Code.ensure_loaded?/1) do
305-
Mix.Task.run("compile", [])
306-
end
300+
plugins =
301+
if plugins != [] do
302+
Keyword.get(opts, :plugin_loader, &plugin_loader/1).(plugins)
303+
else
304+
[]
305+
end
307306

308307
for plugin <- plugins do
309308
cond do
@@ -336,7 +335,21 @@ defmodule Mix.Tasks.Format do
336335
end)
337336

338337
{Keyword.put(formatter_opts, :sigils, sigils),
339-
Enum.map(subs, fn {path, opts} -> {path, load_plugins(opts)} end)}
338+
Enum.map(subs, fn {path, formatter_opts_and_subs} ->
339+
{path, load_plugins(formatter_opts_and_subs, opts)}
340+
end)}
341+
end
342+
343+
defp plugin_loader(plugins) do
344+
if plugins != [] do
345+
Mix.Task.run("loadpaths", [])
346+
end
347+
348+
if not Enum.all?(plugins, &Code.ensure_loaded?/1) do
349+
Mix.Task.run("compile", [])
350+
end
351+
352+
plugins
340353
end
341354

342355
@doc """
@@ -346,24 +359,41 @@ defmodule Mix.Tasks.Format do
346359
The function must be called with the contents of the file
347360
to be formatted. The options are returned for reflection
348361
purposes.
362+
363+
## Options
364+
365+
* `:deps_paths` (since v1.18.0) - the dependencies path to be used to resolve
366+
`import_deps`. It defaults to `Mix.Project.deps_paths`.
367+
368+
* `:dot_formatter` - use the given file as the `dot_formatter`
369+
root. If this option is specified, it uses the default one.
370+
The default one is cached, so use this option only if necessary.
371+
372+
* `:plugin_loader` (since v1.18.0) - a function that receives a list of plugins,
373+
which may or may not yet be loaded, and ensures all of them are
374+
loaded. It must return a list of plugins, which is recommended
375+
to be the exact same list given as argument. You may choose to
376+
skip plugins, but then it means the code will be partially
377+
formatted (as in the plugins will be skipped). By default,
378+
this function calls `mix loadpaths` and then, if not enough,
379+
`mix compile`.
380+
381+
* `:root` - use the given root as the current working directory.
349382
"""
350383
@doc since: "1.13.0"
351384
def formatter_for_file(file, opts \\ []) do
352385
cwd = Keyword.get_lazy(opts, :root, &File.cwd!/0)
353386
{dot_formatter, formatter_opts} = eval_dot_formatter(cwd, opts)
354387

355388
{formatter_opts_and_subs, _sources} =
356-
eval_deps_and_subdirectories(cwd, dot_formatter, formatter_opts, [dot_formatter])
389+
eval_deps_and_subdirectories(cwd, dot_formatter, formatter_opts, [dot_formatter], opts)
357390

358-
formatter_opts_and_subs = load_plugins(formatter_opts_and_subs)
391+
formatter_opts_and_subs = load_plugins(formatter_opts_and_subs, opts)
359392

360393
find_formatter_and_opts_for_file(Path.expand(file, cwd), formatter_opts_and_subs)
361394
end
362395

363-
@doc """
364-
Returns formatter options to be used for the given file.
365-
"""
366-
# TODO: Remove me Elixir v1.17
396+
@doc false
367397
@deprecated "Use formatter_for_file/2 instead"
368398
def formatter_opts_for_file(file, opts \\ []) do
369399
{_, formatter_opts} = formatter_for_file(file, opts)
@@ -391,7 +421,7 @@ defmodule Mix.Tasks.Format do
391421
# This function reads exported configuration from the imported
392422
# dependencies and subdirectories and deals with caching the result
393423
# of reading such configuration in a manifest file.
394-
defp eval_deps_and_subdirectories(cwd, dot_formatter, formatter_opts, sources) do
424+
defp eval_deps_and_subdirectories(cwd, dot_formatter, formatter_opts, sources, opts) do
395425
deps = Keyword.get(formatter_opts, :import_deps, [])
396426
subs = Keyword.get(formatter_opts, :subdirectories, [])
397427

@@ -410,8 +440,8 @@ defmodule Mix.Tasks.Format do
410440

411441
{{locals_without_parens, subdirectories}, sources} =
412442
maybe_cache_in_manifest(dot_formatter, manifest, fn ->
413-
{subdirectories, sources} = eval_subs_opts(subs, cwd, sources)
414-
{{eval_deps_opts(deps), subdirectories}, sources}
443+
{subdirectories, sources} = eval_subs_opts(subs, cwd, sources, opts)
444+
{{eval_deps_opts(deps, opts), subdirectories}, sources}
415445
end)
416446

417447
formatter_opts =
@@ -457,12 +487,12 @@ defmodule Mix.Tasks.Format do
457487
{entry, sources}
458488
end
459489

460-
defp eval_deps_opts([]) do
490+
defp eval_deps_opts([], _opts) do
461491
[]
462492
end
463493

464-
defp eval_deps_opts(deps) do
465-
deps_paths = Mix.Project.deps_paths()
494+
defp eval_deps_opts(deps, opts) do
495+
deps_paths = opts[:deps_paths] || Mix.Project.deps_paths()
466496

467497
for dep <- deps,
468498
dep_path = assert_valid_dep_and_fetch_path(dep, deps_paths),
@@ -474,7 +504,7 @@ defmodule Mix.Tasks.Format do
474504
do: parenless_call
475505
end
476506

477-
defp eval_subs_opts(subs, cwd, sources) do
507+
defp eval_subs_opts(subs, cwd, sources, opts) do
478508
{subs, sources} =
479509
Enum.flat_map_reduce(subs, sources, fn sub, sources ->
480510
cwd = Path.expand(sub, cwd)
@@ -488,7 +518,7 @@ defmodule Mix.Tasks.Format do
488518
formatter_opts = eval_file_with_keyword_list(sub_formatter)
489519

490520
{formatter_opts_and_subs, sources} =
491-
eval_deps_and_subdirectories(sub, :in_memory, formatter_opts, sources)
521+
eval_deps_and_subdirectories(sub, :in_memory, formatter_opts, sources, opts)
492522

493523
{[{sub, formatter_opts_and_subs}], sources}
494524
else

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,31 @@ defmodule Mix.Tasks.FormatTest do
510510
end)
511511
end
512512

513+
test "customizes plugin loading", context do
514+
in_tmp(context.test, fn ->
515+
File.write!(".formatter.exs", """
516+
[
517+
inputs: ["a.ex"],
518+
plugins: [UnknownPlugin],
519+
]
520+
""")
521+
522+
File.write!("a.ex", """
523+
foo bar baz
524+
""")
525+
526+
assert_raise Mix.NoProjectError, fn ->
527+
Mix.Tasks.Format.formatter_for_file("a.ex")
528+
end
529+
530+
assert_raise Mix.Error, "Formatter plugin UnknownPlugin cannot be found", fn ->
531+
Mix.Tasks.Format.formatter_for_file("a.ex", plugin_loader: fn plugins -> plugins end)
532+
end
533+
534+
assert Mix.Tasks.Format.formatter_for_file("a.ex", plugin_loader: fn _plugins -> [] end)
535+
end)
536+
end
537+
513538
test "uses extension plugins with --stdin-filename", context do
514539
in_tmp(context.test, fn ->
515540
File.write!(".formatter.exs", """
@@ -695,6 +720,13 @@ defmodule Mix.Tasks.FormatTest do
695720
{_, formatter_opts} = Mix.Tasks.Format.formatter_for_file("a.ex")
696721
assert [my_fun: 2] = Keyword.get(formatter_opts, :locals_without_parens)
697722

723+
# Check the deps_path option is respected
724+
assert_raise Mix.Error, ~r"Unknown dependency :my_dep given to :import_deps", fn ->
725+
# Let's check that the manifest gets updated if it's stale.
726+
File.touch!(manifest_path, {{2010, 1, 1}, {0, 0, 0}})
727+
Mix.Tasks.Format.formatter_for_file("a.ex", deps_paths: %{})
728+
end
729+
698730
Mix.Tasks.Format.run(["a.ex"])
699731
assert File.stat!(manifest_path).mtime > {{2010, 1, 1}, {0, 0, 0}}
700732
end)

0 commit comments

Comments
 (0)