Packages

Component-based HTML templates for Elixir/Phoenix, inspired by Vue.

Current section

Files

Jump to
x_component lib mix tasks x.format.ex
Raw

lib/mix/tasks/x.format.ex

defmodule Mix.Tasks.X.Format do
@shortdoc "Formats the given files/patterns"
@moduledoc """
Formatter task uses settings from `.formatter.exs` by default.
All project files can be formatted with:
mix x.format
Also, formatter task can be used to format a specific file:
mix x.format path/to/file.ex
"""
use Mix.Task
# https://github.com/elixir-lang/elixir/blob/master/lib/mix/lib/mix/tasks/format.ex
@switches [
dot_formatter: :string
]
@impl true
def run(args) do
{opts, args} = OptionParser.parse!(args, strict: @switches)
{dot_formatter, formatter_opts} = eval_dot_formatter(opts)
args
|> expand_args(dot_formatter, {formatter_opts, []})
|> Task.async_stream(&format_file(&1), ordered: false, timeout: 30000)
|> Enum.reduce([], &collect_status/2)
|> check!()
end
defp eval_dot_formatter(opts) do
cond do
dot_formatter = opts[:dot_formatter] ->
{dot_formatter, eval_file_with_keyword_list(dot_formatter)}
File.regular?(".formatter.exs") ->
{".formatter.exs", eval_file_with_keyword_list(".formatter.exs")}
true ->
{".formatter.exs", []}
end
end
defp eval_file_with_keyword_list(path) do
{opts, _} = Code.eval_file(path)
unless Keyword.keyword?(opts) do
Mix.raise("Expected #{inspect(path)} to return a keyword list, got: #{inspect(opts)}")
end
opts
end
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 or :subdirectories key"
)
end
dot_formatter
|> expand_dot_inputs([], formatter_opts_and_subs, %{})
|> Enum.map(fn {file, {_dot_formatter, formatter_opts}} -> {file, formatter_opts} end)
end
defp expand_args(files_and_patterns, _dot_formatter, {formatter_opts, subs}) do
files =
for file_or_pattern <- files_and_patterns,
file <- stdin_or_wildcard(file_or_pattern),
uniq: true,
do: file
if files == [] do
Mix.raise(
"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
for file <- files do
if file == :stdin do
{file, formatter_opts}
else
split = file |> Path.relative_to_cwd() |> Path.split()
{file, find_formatter_opts_for_file(split, {formatter_opts, 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]), match_dot: true),
do: {expand_relative_to_cwd(file), {dot_formatter, formatter_opts}},
into: %{}
acc =
Map.merge(acc, map, fn file, {dot_formatter1, _}, {dot_formatter2, formatter_opts} ->
Mix.shell().error(
"Both #{dot_formatter1} and #{dot_formatter2} specify the file " <>
"#{Path.relative_to_cwd(file)} in their :inputs option. To resolve the " <>
"conflict, the configuration in #{dot_formatter1} will be ignored. " <>
"Please change the list of :inputs in one of the formatter files so only " <>
"one of them matches #{Path.relative_to_cwd(file)}"
)
{dot_formatter2, formatter_opts}
end)
Enum.reduce(subs, acc, 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 expand_relative_to_cwd(path) do
case File.cwd() do
{:ok, cwd} -> Path.expand(path, cwd)
_ -> path
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]
defp stdin_or_wildcard(path), do: path |> Path.expand() |> Path.wildcard(match_dot: true)
defp read_file(file) do
{File.read!(file), file: file}
end
defp format_file({file, _}) do
{input, _} = read_file(file)
output = X.format_file!(input)
write_file(file, input, output)
rescue
exception ->
{:exit, file, exception, __STACKTRACE__}
end
defp write_file(file, input, output) do
cond do
input == output -> :ok
true -> File.write!(file, output)
end
:ok
end
defp collect_status({:ok, :ok}, acc), do: acc
defp collect_status({:ok, {:exit, _, _, _} = exit}, exits) do
[exit | exits]
end
defp check!([]) do
:ok
end
defp check!([{:exit, :stdin, exception, stacktrace} | _]) do
Mix.shell().error("mix format failed for stdin")
reraise exception, stacktrace
end
defp check!([{:exit, file, exception, stacktrace} | _]) do
Mix.shell().error("mix format failed for file: #{Path.relative_to_cwd(file)}")
reraise exception, stacktrace
end
end