From d655704031d225e5508035e117f525a811e7fb93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 26 Feb 2018 20:27:27 +0100 Subject: [PATCH 1/3] Allow subdirectories in .formatter.exs --- lib/mix/lib/mix/tasks/format.ex | 210 +++++++++++++++++-------- lib/mix/lib/mix/tasks/new.ex | 3 +- lib/mix/test/mix/tasks/format_test.exs | 206 ++++++++++++++++++++---- 3 files changed, 329 insertions(+), 90 deletions(-) diff --git a/lib/mix/lib/mix/tasks/format.ex b/lib/mix/lib/mix/tasks/format.ex index 5966a225755..7323002d120 100644 --- a/lib/mix/lib/mix/tasks/format.ex +++ b/lib/mix/lib/mix/tasks/format.ex @@ -22,6 +22,16 @@ defmodule Mix.Tasks.Format do * `:inputs` (a list of paths and patterns) - specifies the default inputs to be used by this task. For example, `["mix.exs", "{config,lib,test}/**/*.{ex,exs}"]`. + * `:subdirectories` (a list of paths and patterns) - specifies subdirectories + that have their own formatting rules. Each subdirectory should have a + `.formatter.exs` that configures how entries in that subdirectory should be + formatted as. Configuration between `.formatter.exs` are not shared nor + inherited. If a `.formatter.exs` lists "lib/app" as a subdirectory, the rules + in `.formatter.exs` won't be available in the inner `lib/app/.formatter.exs`. + It is also important that the parent `.formatter.exs` does not specify files + inside the "lib/app" subdirectory in its `:inputs` configuration. If this + happens, the behaviour of which formatter will be picked is unspecified. + * `:import_deps` (a list of dependencies as atoms) - specifies a list of dependencies whose formatter configuration will be imported. When specified, the formatter should run in the same directory as @@ -115,16 +125,19 @@ defmodule Mix.Tasks.Format do dry_run: :boolean ] - @deps_manifest "cached_formatter_deps" + @manifest "cached_dot_formatter" + @manifest_vsn 1 def run(args) do {opts, args} = OptionParser.parse!(args, strict: @switches) {dot_formatter, formatter_opts} = eval_dot_formatter(opts) - formatter_opts = fetch_deps_opts(dot_formatter, formatter_opts) + + {formatter_opts_and_subs, _sources} = + eval_deps_and_subdirectories(dot_formatter, [], formatter_opts, [dot_formatter]) args - |> expand_args(formatter_opts) - |> Task.async_stream(&format_file(&1, opts, formatter_opts), ordered: false, timeout: 30000) + |> expand_args(dot_formatter, formatter_opts_and_subs) + |> Task.async_stream(&format_file(&1, opts), ordered: false, timeout: 30000) |> Enum.reduce({[], [], []}, &collect_status/2) |> check!() end @@ -142,71 +155,110 @@ defmodule Mix.Tasks.Format do end end - # This function reads exported configuration from the imported dependencies and deals with - # caching the result of reading such configuration in a manifest file. - defp fetch_deps_opts(dot_formatter, formatter_opts) do + # This function reads exported configuration from the imported + # dependencies and subdirectories and deals with caching the result + # of reading such configuration in a manifest file. + defp eval_deps_and_subdirectories(dot_formatter, prefix, formatter_opts, sources) do deps = Keyword.get(formatter_opts, :import_deps, []) + subs = Keyword.get(formatter_opts, :subdirectories, []) - cond do - deps == [] -> - formatter_opts - - is_list(deps) -> - # Since we have dependencies listed, we write the manifest even if those - # dependencies don't export anything so that we avoid lookups everytime. - deps_manifest = Path.join(Mix.Project.manifest_path(), @deps_manifest) - dep_parenless_calls = maybe_cache_eval_deps_opts(dot_formatter, deps_manifest, deps) - - Keyword.update( - formatter_opts, - :locals_without_parens, - dep_parenless_calls, - &(&1 ++ dep_parenless_calls) - ) + if not is_list(deps) do + Mix.raise("Expected :import_deps to return a list of dependencies, got: #{inspect(deps)}") + end - true -> - Mix.raise("Expected :import_deps to return a list of dependencies, got: #{inspect(deps)}") + if not is_list(subs) do + Mix.raise("Expected :subdirectories to return a list of directories, got: #{inspect(subs)}") + end + + if deps == [] and subs == [] do + {{formatter_opts, []}, sources} + else + manifest = Path.join(Mix.Project.manifest_path(), @manifest) + + maybe_cache_in_manifest(dot_formatter, manifest, fn -> + {subdirectories, sources} = eval_subs_opts(subs, prefix, sources) + {{eval_deps_opts(formatter_opts, deps), subdirectories}, sources} + end) end end - defp maybe_cache_eval_deps_opts(dot_formatter, deps_manifest, deps) do + defp maybe_cache_in_manifest(dot_formatter, manifest, fun) do cond do - dot_formatter != ".formatter.exs" -> - eval_deps_opts(deps) - - deps_dot_formatters_stale?(dot_formatter, deps_manifest) -> - write_deps_manifest(deps_manifest, eval_deps_opts(deps)) + is_nil(Mix.Project.get()) or dot_formatter != ".formatter.exs" -> fun.() + entry = read_manifest(manifest) -> entry + true -> write_manifest!(manifest, fun.()) + end + end - true -> - read_deps_manifest(deps_manifest) + def read_manifest(manifest) do + with {:ok, binary} <- File.read(manifest), + {:ok, {@manifest_vsn, entry, sources}} <- safe_binary_to_term(binary), + expanded_sources = Enum.flat_map(sources, &Path.wildcard(&1, match_dot: true)), + false <- Mix.Utils.stale?(Mix.Project.config_files() ++ expanded_sources, [manifest]) do + {entry, sources} + else + _ -> nil end end - defp deps_dot_formatters_stale?(dot_formatter, deps_manifest) do - Mix.Utils.stale?([dot_formatter | Mix.Project.config_files()], [deps_manifest]) + defp safe_binary_to_term(binary) do + {:ok, :erlang.binary_to_term(binary)} + rescue + _ -> :error end - defp read_deps_manifest(deps_manifest) do - deps_manifest |> File.read!() |> :erlang.binary_to_term() + defp write_manifest!(manifest, {entry, sources}) do + File.mkdir_p!(Path.dirname(manifest)) + File.write!(manifest, :erlang.term_to_binary({@manifest_vsn, entry, sources})) + {entry, sources} end - defp write_deps_manifest(deps_manifest, parenless_calls) do - File.mkdir_p!(Path.dirname(deps_manifest)) - File.write!(deps_manifest, :erlang.term_to_binary(parenless_calls)) - parenless_calls + defp eval_deps_opts(formatter_opts, []) do + formatter_opts end - defp eval_deps_opts(deps) do + defp eval_deps_opts(formatter_opts, deps) do deps_paths = Mix.Project.deps_paths() - for dep <- deps, - dep_path = assert_valid_dep_and_fetch_path(dep, deps_paths), - dep_dot_formatter = Path.join(dep_path, ".formatter.exs"), - File.regular?(dep_dot_formatter), - dep_opts = eval_file_with_keyword_list(dep_dot_formatter), - parenless_call <- dep_opts[:export][:locals_without_parens] || [], - uniq: true, - do: parenless_call + parenless_calls = + for dep <- deps, + dep_path = assert_valid_dep_and_fetch_path(dep, deps_paths), + dep_dot_formatter = Path.join(dep_path, ".formatter.exs"), + File.regular?(dep_dot_formatter), + dep_opts = eval_file_with_keyword_list(dep_dot_formatter), + parenless_call <- dep_opts[:export][:locals_without_parens] || [], + uniq: true, + do: parenless_call + + Keyword.update( + formatter_opts, + :locals_without_parens, + parenless_calls, + &(&1 ++ parenless_calls) + ) + end + + defp eval_subs_opts(subs, prefix, sources) do + {subs, sources} = + Enum.flat_map_reduce(subs, sources, fn sub, sources -> + prefix = Path.join(prefix ++ [sub]) + {Path.wildcard(prefix), [Path.join(prefix, ".formatter.exs") | sources]} + end) + + Enum.flat_map_reduce(subs, sources, fn sub, sources -> + sub_formatter = Path.join(sub, ".formatter.exs") + + if File.exists?(sub_formatter) do + formatter_opts = eval_file_with_keyword_list(sub_formatter) + + {formatter_opts_and_subs, sources} = + eval_deps_and_subdirectories(:in_memory, [sub], formatter_opts, sources) + + {[{sub, formatter_opts_and_subs}], sources} + else + {[], sources} + end + end) end defp assert_valid_dep_and_fetch_path(dep, deps_paths) when is_atom(dep) do @@ -243,22 +295,20 @@ defmodule Mix.Tasks.Format do opts end - defp expand_args([], formatter_opts) do - if inputs = formatter_opts[:inputs] do - expand_files_and_patterns(List.wrap(inputs), ".formatter.exs") - else + defp expand_args([], dot_formatter, formatter_opts_and_subs) do + if no_entries_in_formatter_opts?(formatter_opts_and_subs) do Mix.raise( "Expected one or more files/patterns to be given to mix format " <> - "or for a .formatter.exs to exist with an :inputs key" + "or for a .formatter.exs to exist with an :inputs or :subdirectories key" ) end - end - defp expand_args(files_and_patterns, _formatter_opts) do - expand_files_and_patterns(files_and_patterns, "command line") + dot_formatter + |> expand_dot_inputs([], formatter_opts_and_subs, %{}) + |> Enum.uniq() end - defp expand_files_and_patterns(files_and_patterns, context) do + defp expand_args(files_and_patterns, _dot_formatter, formatter_opts_and_subs) do files = for file_or_pattern <- files_and_patterns, file <- stdin_or_wildcard(file_or_pattern), @@ -267,12 +317,48 @@ defmodule Mix.Tasks.Format do if files == [] do Mix.raise( - "Could not find a file to format. The files/patterns from #{context} " <> + "Could not find a file to format. The files/patterns given to command line " <> "did not point to any existing file. Got: #{inspect(files_and_patterns)}" ) end - files + for file <- files do + if file == :stdin do + {file, []} + else + split = file |> Path.relative_to_cwd() |> Path.split() + {file, find_formatter_opts_for_file(split, formatter_opts_and_subs)} + end + end + end + + defp expand_dot_inputs(dot_formatter, prefix, {formatter_opts, subs}, acc) do + if no_entries_in_formatter_opts?({formatter_opts, subs}) do + Mix.raise("Expected :inputs or :subdirectories key in #{dot_formatter}") + end + + map = + for input <- List.wrap(formatter_opts[:inputs]), + file <- Path.wildcard(Path.join(prefix ++ [input])), + do: {file, formatter_opts}, + into: %{} + + Enum.reduce(subs, Map.merge(acc, map), fn {sub, formatter_opts_and_subs}, acc -> + sub_formatter = Path.join(sub, ".formatter.exs") + expand_dot_inputs(sub_formatter, [sub], formatter_opts_and_subs, acc) + end) + end + + defp find_formatter_opts_for_file(split, {formatter_opts, subs}) do + Enum.find_value(subs, formatter_opts, fn {sub, formatter_opts_and_subs} -> + if List.starts_with?(split, Path.split(sub)) do + find_formatter_opts_for_file(split, formatter_opts_and_subs) + end + end) + end + + defp no_entries_in_formatter_opts?({formatter_opts, subs}) do + is_nil(formatter_opts[:inputs]) and subs == [] end defp stdin_or_wildcard("-"), do: [:stdin] @@ -286,7 +372,7 @@ defmodule Mix.Tasks.Format do {File.read!(file), file: file} end - defp format_file(file, task_opts, formatter_opts) do + defp format_file({file, formatter_opts}, task_opts) do {input, extra_opts} = read_file(file) output = IO.iodata_to_binary([Code.format_string!(input, extra_opts ++ formatter_opts), ?\n]) diff --git a/lib/mix/lib/mix/tasks/new.ex b/lib/mix/lib/mix/tasks/new.ex index 8730b13e085..fb1243447e9 100644 --- a/lib/mix/lib/mix/tasks/new.ex +++ b/lib/mix/lib/mix/tasks/new.ex @@ -255,7 +255,8 @@ defmodule Mix.Tasks.New do embed_template(:formatter_umbrella, """ # Used by "mix format" [ - inputs: ["mix.exs", "apps/*/mix.exs", "apps/*/{config,lib,test}/**/*.{ex,exs}"] + inputs: ["mix.exs", "config/*.exs"], + subdirectories: ["apps/*"] ] """) diff --git a/lib/mix/test/mix/tasks/format_test.exs b/lib/mix/test/mix/tasks/format_test.exs index e7afb18c602..252f7960827 100644 --- a/lib/mix/test/mix/tasks/format_test.exs +++ b/lib/mix/test/mix/tasks/format_test.exs @@ -16,7 +16,7 @@ defmodule Mix.Tasks.FormatTest do end test "formats the given files", context do - in_tmp context.test, fn -> + in_tmp(context.test, fn -> File.write!("a.ex", """ foo bar """) @@ -26,11 +26,11 @@ defmodule Mix.Tasks.FormatTest do assert File.read!("a.ex") == """ foo(bar) """ - end + end) end test "formats the given pattern", context do - in_tmp context.test, fn -> + in_tmp(context.test, fn -> File.write!("a.ex", """ foo bar """) @@ -40,11 +40,11 @@ defmodule Mix.Tasks.FormatTest do assert File.read!("a.ex") == """ foo(bar) """ - end + end) end test "is a no-op if the file is already formatted", context do - in_tmp context.test, fn -> + in_tmp(context.test, fn -> File.write!("a.ex", """ foo(bar) """) @@ -52,11 +52,11 @@ defmodule Mix.Tasks.FormatTest do File.touch!("a.ex", {{2000, 1, 1}, {0, 0, 0}}) Mix.Tasks.Format.run(["a.ex"]) assert File.stat!("a.ex").mtime == {{2000, 1, 1}, {0, 0, 0}} - end + end) end test "does not write file to disk on dry-run", context do - in_tmp context.test, fn -> + in_tmp(context.test, fn -> File.write!("a.ex", """ foo bar """) @@ -66,11 +66,11 @@ defmodule Mix.Tasks.FormatTest do assert File.read!("a.ex") == """ foo bar """ - end + end) end test "reads file from stdin and prints to stdout", context do - in_tmp context.test, fn -> + in_tmp(context.test, fn -> File.write!("a.ex", """ foo bar """) @@ -87,11 +87,11 @@ defmodule Mix.Tasks.FormatTest do assert File.read!("a.ex") == """ foo(bar) """ - end + end) end test "checks if file is formatted with --check-formatted", context do - in_tmp context.test, fn -> + in_tmp(context.test, fn -> File.write!("a.ex", """ foo bar """) @@ -110,7 +110,7 @@ defmodule Mix.Tasks.FormatTest do assert File.read!("a.ex") == """ foo(bar) """ - end + end) end test "checks if stdin is formatted with --check-formatted" do @@ -129,7 +129,7 @@ defmodule Mix.Tasks.FormatTest do end test "checks if file is equivalent with --check-equivalent", context do - in_tmp context.test, fn -> + in_tmp(context.test, fn -> File.write!("a.ex", """ foo bar """) @@ -139,11 +139,11 @@ defmodule Mix.Tasks.FormatTest do assert File.read!("a.ex") == """ foo(bar) """ - end + end) end test "uses inputs and configuration from .formatter.exs", context do - in_tmp context.test, fn -> + in_tmp(context.test, fn -> File.write!(".formatter.exs", """ [ inputs: ["a.ex"], @@ -160,11 +160,11 @@ defmodule Mix.Tasks.FormatTest do assert File.read!("a.ex") == """ foo bar(baz) """ - end + end) end test "uses inputs and configuration from --dot-formatter", context do - in_tmp context.test, fn -> + in_tmp(context.test, fn -> File.write!("custom_formatter.exs", """ [ inputs: ["a.ex"], @@ -181,13 +181,62 @@ defmodule Mix.Tasks.FormatTest do assert File.read!("a.ex") == """ foo bar(baz) """ - end + end) + end + + test "can read exported configuration from subdirectories", context do + in_tmp(context.test, fn -> + File.write!(".formatter.exs", """ + [subdirectories: ["lib"]] + """) + + File.mkdir_p!("lib") + + File.write!("lib/.formatter.exs", """ + [inputs: "a.ex", locals_without_parens: [my_fun: 2]] + """) + + File.write!("lib/a.ex", """ + my_fun :foo, :bar + other_fun :baz + """) + + Mix.Tasks.Format.run([]) + + assert File.read!("lib/a.ex") == """ + my_fun :foo, :bar + other_fun(:baz) + """ + + Mix.Tasks.Format.run(["lib/a.ex"]) + + assert File.read!("lib/a.ex") == """ + my_fun :foo, :bar + other_fun(:baz) + """ + + # No caching without a project + manifest_path = Path.join(Mix.Project.manifest_path(), "cached_dot_formatter") + refute File.regular?(manifest_path) + + # Caching with a project + Mix.Project.push(__MODULE__.FormatWithDepsApp) + Mix.Tasks.Format.run(["lib/a.ex"]) + manifest_path = Path.join(Mix.Project.manifest_path(), "cached_dot_formatter") + assert File.regular?(manifest_path) + + # Let's check that the manifest gets updated if it's stale. + File.touch!(manifest_path, {{1970, 1, 1}, {0, 0, 0}}) + + Mix.Tasks.Format.run(["lib/a.ex"]) + assert File.stat!(manifest_path).mtime > {{1970, 1, 1}, {0, 0, 0}} + end) end test "can read exported configuration from dependencies", context do Mix.Project.push(__MODULE__.FormatWithDepsApp) - in_tmp context.test, fn -> + in_tmp(context.test, fn -> File.write!(".formatter.exs", """ [import_deps: [:my_dep]] """) @@ -208,7 +257,7 @@ defmodule Mix.Tasks.FormatTest do my_fun :foo, :bar """ - manifest_path = Path.join(Mix.Project.manifest_path(), "cached_formatter_deps") + manifest_path = Path.join(Mix.Project.manifest_path(), "cached_dot_formatter") assert File.regular?(manifest_path) # Let's check that the manifest gets updated if it's stale. @@ -216,13 +265,116 @@ defmodule Mix.Tasks.FormatTest do Mix.Tasks.Format.run(["a.ex"]) assert File.stat!(manifest_path).mtime > {{1970, 1, 1}, {0, 0, 0}} - end + end) + end + + test "can read exported configuration from dependencies and subdirectories", context do + Mix.Project.push(__MODULE__.FormatWithDepsApp) + + in_tmp(context.test, fn -> + File.mkdir_p!("deps/my_dep/") + + File.write!("deps/my_dep/.formatter.exs", """ + [export: [locals_without_parens: [my_fun: 2]]] + """) + + File.mkdir_p!("lib/sub") + File.mkdir_p!("lib/not_used_and_wont_raise") + + File.write!(".formatter.exs", """ + [subdirectories: ["lib"]] + """) + + File.write!("lib/.formatter.exs", """ + [subdirectories: ["*"]] + """) + + File.write!("lib/sub/.formatter.exs", """ + [inputs: "a.ex", import_deps: [:my_dep]] + """) + + File.write!("lib/sub/a.ex", """ + my_fun :foo, :bar + other_fun :baz + """) + + Mix.Tasks.Format.run([]) + + assert File.read!("lib/sub/a.ex") == """ + my_fun :foo, :bar + other_fun(:baz) + """ + + Mix.Tasks.Format.run(["lib/sub/a.ex"]) + + assert File.read!("lib/sub/a.ex") == """ + my_fun :foo, :bar + other_fun(:baz) + """ + + # Update .formatter.exs, check that file is updated + File.write!("lib/sub/.formatter.exs", """ + [inputs: "a.ex"] + """) + + File.touch!("lib/sub/.formatter.exs", {{2030, 1, 1}, {0, 0, 0}}) + Mix.Tasks.Format.run([]) + + assert File.read!("lib/sub/a.ex") == """ + my_fun(:foo, :bar) + other_fun(:baz) + """ + + # Add a new entry to "lib" and it also gets picked. + File.mkdir_p!("lib/extra") + + File.write!("lib/extra/.formatter.exs", """ + [inputs: "a.ex", locals_without_parens: [other_fun: 1]] + """) + + File.write!("lib/extra/a.ex", """ + my_fun :foo, :bar + other_fun :baz + """) + + File.touch!("lib/extra/.formatter.exs", {{2030, 1, 1}, {0, 0, 0}}) + Mix.Tasks.Format.run([]) + + assert File.read!("lib/extra/a.ex") == """ + my_fun(:foo, :bar) + other_fun :baz + """ + end) + end + + test "validates subdirectories in :subdirectories", context do + in_tmp(context.test, fn -> + File.write!(".formatter.exs", """ + [subdirectories: "oops"] + """) + + message = "Expected :subdirectories to return a list of directories, got: \"oops\"" + assert_raise Mix.Error, message, fn -> Mix.Tasks.Format.run([]) end + + File.write!(".formatter.exs", """ + [subdirectories: ["lib"]] + """) + + File.mkdir_p!("lib") + + File.write!("lib/.formatter.exs", """ + [] + """) + + message = "Expected :inputs or :subdirectories key in lib/.formatter.exs" + assert_raise Mix.Error, message, fn -> Mix.Tasks.Format.run([]) end + end) end test "validates dependencies in :import_deps", context do Mix.Project.push(__MODULE__.FormatWithDepsApp) - in_tmp context.test, fn -> + in_tmp(context.test, fn -> File.write!(".formatter.exs", """ [import_deps: [:my_dep]] """) @@ -242,11 +394,11 @@ defmodule Mix.Tasks.FormatTest do "The dependency is not listed in your mix.exs for environment :dev" assert_raise Mix.Error, message, fn -> Mix.Tasks.Format.run([]) end - end + end) end test "raises on invalid arguments", context do - in_tmp context.test, fn -> + in_tmp(context.test, fn -> assert_raise Mix.Error, ~r"Expected one or more files\/patterns to be given", fn -> Mix.Tasks.Format.run([]) end @@ -254,11 +406,11 @@ defmodule Mix.Tasks.FormatTest do assert_raise Mix.Error, ~r"Could not find a file to format", fn -> Mix.Tasks.Format.run(["unknown.whatever"]) end - end + end) end test "raises SyntaxError when parsing invalid source file", context do - in_tmp context.test, fn -> + in_tmp(context.test, fn -> File.write!("a.ex", """ defmodule <%= module %>.Bar do end """) @@ -268,6 +420,6 @@ defmodule Mix.Tasks.FormatTest do end assert_received {:mix_shell, :error, ["mix format failed for file: a.ex"]} - end + end) end end From 03c5a7805b6a6796b2e2dd404030dd905125ba27 Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Wed, 28 Feb 2018 01:42:00 +0100 Subject: [PATCH 2/3] Update format.ex --- lib/mix/lib/mix/tasks/format.ex | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/mix/lib/mix/tasks/format.ex b/lib/mix/lib/mix/tasks/format.ex index 7323002d120..d0a49a3cd16 100644 --- a/lib/mix/lib/mix/tasks/format.ex +++ b/lib/mix/lib/mix/tasks/format.ex @@ -26,11 +26,11 @@ defmodule Mix.Tasks.Format do that have their own formatting rules. Each subdirectory should have a `.formatter.exs` that configures how entries in that subdirectory should be formatted as. Configuration between `.formatter.exs` are not shared nor - inherited. If a `.formatter.exs` lists "lib/app" as a subdirectory, the rules - in `.formatter.exs` won't be available in the inner `lib/app/.formatter.exs`. - It is also important that the parent `.formatter.exs` does not specify files - inside the "lib/app" subdirectory in its `:inputs` configuration. If this - happens, the behaviour of which formatter will be picked is unspecified. + inherited. If a `.formatter.exs` lists `"lib/app"` as a subdirectory, the rules + in `.formatter.exs` won't be available in `lib/app/.formatter.exs`. + Note that the parent `.formatter.exs` must not specify files inside the "lib/app" + subdirectory in its `:inputs` configuration. If this happens, the behaviour of + which formatter configuration will be picked is unspecified. * `:import_deps` (a list of dependencies as atoms) - specifies a list of dependencies whose formatter configuration will be imported. From 8216c7c1cafd11d3049bb70ebd3a4e3f2a0bfa41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 28 Feb 2018 10:22:37 +0100 Subject: [PATCH 3/3] Update format.ex --- lib/mix/lib/mix/tasks/format.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mix/lib/mix/tasks/format.ex b/lib/mix/lib/mix/tasks/format.ex index d0a49a3cd16..7bb4e48a066 100644 --- a/lib/mix/lib/mix/tasks/format.ex +++ b/lib/mix/lib/mix/tasks/format.ex @@ -26,7 +26,7 @@ defmodule Mix.Tasks.Format do that have their own formatting rules. Each subdirectory should have a `.formatter.exs` that configures how entries in that subdirectory should be formatted as. Configuration between `.formatter.exs` are not shared nor - inherited. If a `.formatter.exs` lists `"lib/app"` as a subdirectory, the rules + inherited. If a `.formatter.exs` lists "lib/app" as a subdirectory, the rules in `.formatter.exs` won't be available in `lib/app/.formatter.exs`. Note that the parent `.formatter.exs` must not specify files inside the "lib/app" subdirectory in its `:inputs` configuration. If this happens, the behaviour of