defmodule Exfmt do @moduledoc """ Exfmt, an opinionated Elixir source code formatter. 🌸 aka The everyone's favourite Elixir-to-Elixir compiler. ## API Functions in this module can be considered part of Exfmt's public interface, and can be presumed stable. Functions exposed by other modules may change at any time without warning, especially before v1.0.0. """ alias Exfmt.{Ast, Algebra, Comment, Context, SyntaxError, SemanticsError} @max_width 80 @doc ~S""" Format a string of Elixir source code. iex> format("[1,2,3]") {:ok, "[1, 2, 3]\n"} This function performs a check to ensure the input and output are semantically equivalent. """ @spec format(String.t, integer) :: {:ok, String.t} | SyntaxError.t | SemanticsError.t def format(source, max_width \\ @max_width) do with {:ok, formatted} <- unsafe_format(source, max_width) do original_ast = Code.string_to_quoted!(source) formatted_ast = Code.string_to_quoted!(formatted) if Ast.eq?(original_ast, formatted_ast) do {:ok, formatted} else SemanticsError.exception() end end rescue _ in Elixir.SyntaxError -> SemanticsError.exception() end @doc ~S""" Format a string of Elixir source code, throwing an exception in the event of failure. iex> format!("[1,2,3]") "[1, 2, 3]\n" This function performs a check to ensure the input and output are semantically equivalent. """ @spec format!(String.t, integer) :: String.t def format!(source, max_width \\ @max_width) do case format(source, max_width) do {:ok, formatted} -> formatted error -> raise error end end @doc ~S""" Format a string of Elixir source code. iex> unsafe_format("[1,2,3]") {:ok, "[1, 2, 3]\n"} Unlike `format/2` and `format!/2` this code does not compare the semantics of the input and the output, so if there is a bug in `exfmt` it may semantically alter your code when reformatting it. """ @spec unsafe_format(String.t, integer) :: {:ok, String.t} | SyntaxError.t def unsafe_format(source, max_width \\ @max_width) def unsafe_format("", _) do {:ok, "\n"} end def unsafe_format("\n" <> rest, max_width) do unsafe_format(rest, max_width) end def unsafe_format(source, max_width) do parse_opts = [wrap_literals_in_blocks: true] with {:ok, tree} <- Code.string_to_quoted(source, parse_opts), {:ok, comments} <- Comment.extract_comments(source) do {:ok, do_format(tree, comments, max_width)} else {:error, error} -> SyntaxError.exception(error) end end @doc """ Check that a string of source code conforms to the exfmt style. If formatting the source code would not result in the source code changing this function will return `:ok`. """ @spec check(String.t, integer) :: :ok | {:format_error, String.t} | SyntaxError.t def check(source, max_width \\ @max_width) do with {:ok, formatted} <- format(source, max_width) do if source == formatted do :ok else {:format_error, formatted} end end end # # Private # defp do_format(tree, comments, max_width) do new_tree = Ast.insert_empty_lines(tree) comments |> Comment.merge(new_tree) |> Ast.compact_wrapped_literals() |> Ast.to_algebra(Context.new) |> Algebra.format(max_width) |> IO.chardata_to_string() |> trim_whitespace() |> append_newline() end # # FIXME: We shouldn't need to trim whitespace like this. # Prevent the printer from inserting indentation with no # content afterwards. # @trim_pattern ~r/\n +\n/ defp trim_whitespace(source) do source |> String.replace(@trim_pattern, "\n\n") |> String.replace(@trim_pattern, "\n\n") end defp append_newline(source) do source <> "\n" end end