# Copyright 2023 Adobe. All rights reserved.
# This file is licensed to you under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. You may obtain a copy
# of the License at http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software distributed under
# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
# OF ANY KIND, either express or implied. See the License for the specific language
# governing permissions and limitations under the License.
defmodule Mix.Tasks.Style do
@shortdoc "Rewrites (styles!) and formats your code as a drop in replacement for `mix format`"
@moduledoc """
Formats and rewrites the given files and patterns.
mix style mix.exs "lib/**/*.{ex,exs}" "test/**/*.{ex,exs}"
If `-` is one of the files, input is read from stdin and written to stdout.
`mix style` uses the same options as `mix format` specified in `.formatter.exs` to
format the code, and to determine which files to style if you don't pass any as arguments
## Task-specific options
* `--check-formatted` - an alias for `--check-styled`, included for compatibility with `mix format`
* `--check-styled` - checks that the file is already styled rather than styling it.
useful for CI.
"""
use Mix.Task
@impl Mix.Task
def run(args) do
# we take `check_formatted` so we can easily replace `mix format`
{opts, files} = OptionParser.parse!(args, strict: [check_styled: :boolean, check_formatted: :boolean])
check_styled? = opts[:check_styled] || opts[:check_formatted] || false
{_, formatter_opts} = Mix.Tasks.Format.formatter_for_file("mix.exs")
files =
if Enum.empty?(files) do
case Keyword.fetch(formatter_opts, :inputs) do
:error -> Mix.raise("you must pass file arguments or run `mix style` from the project's root directory")
{:ok, inputs} -> inputs
end
else
files
end
files
|> Stream.flat_map(fn
"-" -> [:stdin]
path -> path |> Path.expand() |> Path.wildcard(match_dot: true)
end)
|> Task.async_stream(&style_file(&1, formatter_opts, check_styled?),
ordered: false,
timeout: :timer.seconds(30)
)
|> Enum.reduce({[], []}, fn
{:ok, :ok}, acc -> acc
{:ok, {:exit, exit}}, {exits, not_styled} -> {[exit | exits], not_styled}
{:ok, {:not_styled, file}}, {exits, not_styled} -> {exits, [file | not_styled]}
end)
|> check!()
end
defp check!({[], []}) do
:ok
end
defp check!({[{:stdin, exception, stacktrace} | _], _not_styled}) do
Mix.shell().error("mix style failed for stdin:")
reraise exception, stacktrace
end
defp check!({[{file, exception, stacktrace} | _], _not_styled}) do
Mix.shell().error("mix style failed for file: #{Path.relative_to_cwd(file)}")
reraise exception, stacktrace
end
defp check!({_exits, [_ | _] = not_styled}) do
Mix.raise("""
mix style failed due to --check-styled.
The following files are not styled:
#{Enum.join(not_styled, "\n")}
""")
end
defp style_file(file, formatter_opts, check_styled?) do
input =
if file == :stdin,
do: IO.stream() |> Enum.to_list() |> IO.iodata_to_binary(),
else: file |> File.read!() |> String.trim()
styled = Styler.format(input, formatter_opts)
changed? = input != styled
cond do
check_styled? and changed? -> {:not_styled, file}
file == :stdin -> IO.write(styled)
changed? -> File.write!(file, styled)
true -> :ok
end
rescue
exception -> {:exit, {file, exception, __STACKTRACE__}}
end
end