# This module was vendored from the Elixir source code to backport new features. defmodule Sourceror.Code.Formatter do @moduledoc false import Inspect.Algebra, except: [format: 2, surround: 3, surround: 4] @double_quote "\"" @double_heredoc "\"\"\"" @single_quote "'" @single_heredoc "'''" @newlines 2 @min_line 0 @max_line 9_999_999 @empty empty() @ampersand_prec Code.Identifier.unary_op(:&) |> elem(1) # Operators that are composed of multiple binary operators @multi_binary_operators [:"..//"] # Operators that do not have space between operands @no_space_binary_operators [:.., :"//"] # Operators that do not have newline between operands (as well as => and keywords) @no_newline_binary_operators [:\\, :in] # Left associative operators that start on the next line in case of breaks (always pipes) @pipeline_operators [:|>, :~>>, :<<~, :~>, :<~, :<~>, :<|>] # Right associative operators that start on the next line in case of breaks @right_new_line_before_binary_operators [:|, :when] # Operators that are logical cannot be mixed without parens @required_parens_logical_binary_operands [:||, :|||, :or, :&&, :&&&, :and] # Operators with next break fits. = and :: do not consider new lines though @next_break_fits_operators [:<-, :==, :!=, :=~, :===, :!==, :<, :>, :<=, :>=, :=, :"::"] # Operators that always require parens on operands when they are the parent base_operators = [ :|>, :<<<, :>>>, :<~, :~>, :<<~, :~>>, :<~>, :<|>, :^^^, :in, :++, :--, :.., :<> ] newer_operators = [ "+++", "---" ] required_parens_on_binary_operands = if Version.match?(System.version(), "~> 1.12") do base_operators ++ (newer_operators |> Enum.map(&String.to_existing_atom/1)) else base_operators end @required_parens_on_binary_operands required_parens_on_binary_operands @locals_without_parens [ # Special forms alias: 1, alias: 2, case: 2, cond: 1, for: :*, import: 1, import: 2, quote: 1, quote: 2, receive: 1, require: 1, require: 2, try: 1, with: :*, # Kernel def: 1, def: 2, defp: 1, defp: 2, defguard: 1, defguardp: 1, defmacro: 1, defmacro: 2, defmacrop: 1, defmacrop: 2, defmodule: 2, defdelegate: 2, defexception: 1, defoverridable: 1, defstruct: 1, destructure: 2, raise: 1, raise: 2, reraise: 2, reraise: 3, if: 2, unless: 2, use: 1, use: 2, # Stdlib, defrecord: 2, defrecord: 3, defrecordp: 2, defrecordp: 3, # Testing assert: 1, assert: 2, assert_in_delta: 3, assert_in_delta: 4, assert_raise: 2, assert_raise: 3, assert_receive: 1, assert_receive: 2, assert_receive: 3, assert_received: 1, assert_received: 2, doctest: 1, doctest: 2, refute: 1, refute: 2, refute_in_delta: 3, refute_in_delta: 4, refute_receive: 1, refute_receive: 2, refute_receive: 3, refute_received: 1, refute_received: 2, setup: 1, setup: 2, setup_all: 1, setup_all: 2, test: 1, test: 2, # Mix config config: 2, config: 3, import_config: 1 ] @do_end_keywords [:rescue, :catch, :else, :after] @doc """ Checks if two strings are equivalent. """ def equivalent(string1, string2) when is_binary(string1) and is_binary(string2) do quoted1 = :sourceror_elixir.string_to_quoted!(to_charlist(string1), 1, 1, "nofile", []) quoted2 = :sourceror_elixir.string_to_quoted!(to_charlist(string2), 1, 1, "nofile", []) case not_equivalent(quoted1, quoted2) do {left, right} -> {:error, left, right} nil -> :ok end end defp not_equivalent({:__block__, _, [left]}, right) do not_equivalent(left, right) end defp not_equivalent(left, {:__block__, _, [right]}) do not_equivalent(left, right) end defp not_equivalent({:__block__, _, []}, nil) do nil end defp not_equivalent(nil, {:__block__, _, []}) do nil end defp not_equivalent([left | lefties], [right | righties]) do not_equivalent(left, right) || not_equivalent(lefties, righties) end defp not_equivalent({left_name, _, left_args}, {right_name, _, right_args}) do not_equivalent(left_name, right_name) || not_equivalent(left_args, right_args) end defp not_equivalent({left1, left2}, {right1, right2}) do not_equivalent(left1, right1) || not_equivalent(left2, right2) end defp not_equivalent(side, side) do nil end defp not_equivalent(left, right) do {left, right} end @doc """ Converts the quoted expression into an algebra document. """ def to_algebra(quoted, opts \\ []) do comments = Keyword.get(opts, :comments, []) state = comments |> Enum.map(&format_comment/1) |> gather_comments() |> state(opts) {doc, _} = block_to_algebra(quoted, @min_line, @max_line, state) doc end @doc """ Lists all default locals without parens. """ def locals_without_parens do @locals_without_parens end @doc """ Checks if a function is a local without parens. """ def local_without_parens?(fun, arity, locals_without_parens) do arity > 0 and Enum.any?(locals_without_parens, fn {key, val} -> key == fun and (val == :* or val == arity) end) end defp state(comments, opts) do force_do_end_blocks = Keyword.get(opts, :force_do_end_blocks, false) locals_without_parens = Keyword.get(opts, :locals_without_parens, []) %{ force_do_end_blocks: force_do_end_blocks, locals_without_parens: locals_without_parens ++ locals_without_parens(), operand_nesting: 2, skip_eol: false, comments: comments } end defp format_comment(%{text: text} = comment) do %{comment | text: format_comment_text(text, "")} end defp format_comment_text("##" <> rest, acc), do: format_comment_text("#" <> rest, "#" <> acc) defp format_comment_text("#!", acc), do: reverse_with_prefix(acc, "#!") defp format_comment_text("#! " <> _ = rest, acc), do: reverse_with_prefix(acc, rest) defp format_comment_text("#!" <> rest, acc), do: reverse_with_prefix(acc, "#! " <> rest) defp format_comment_text("#", acc), do: reverse_with_prefix(acc, "#") defp format_comment_text("# " <> _ = rest, acc), do: reverse_with_prefix(acc, rest) defp format_comment_text("#" <> rest, acc), do: reverse_with_prefix(acc, "# " <> rest) defp reverse_with_prefix(acc, prefix) do String.reverse(acc) <> prefix end # If there is a no new line before, we can't gather all followup comments. defp gather_comments([%{previous_eol_count: 0} = comment | comments]) do comment = %{comment | previous_eol_count: @newlines} [comment | gather_comments(comments)] end defp gather_comments([comment | comments]) do %{line: line, next_eol_count: next_eol_count, text: doc} = comment {next_eol_count, comments, doc} = gather_followup_comments(line + 1, next_eol_count, comments, doc) comment = %{comment | next_eol_count: next_eol_count, text: doc} [comment | gather_comments(comments)] end defp gather_comments([]) do [] end defp gather_followup_comments( line, _, [%{line: line, previous_eol_count: previous_eol_count} = comment | comments], doc ) when previous_eol_count != 0 do %{next_eol_count: next_eol_count, text: text} = comment gather_followup_comments(line + 1, next_eol_count, comments, line(doc, text)) end defp gather_followup_comments(_line, next_eol_count, comments, doc) do {next_eol_count, comments, doc} end # Special AST nodes from compiler feedback defp quoted_to_algebra({{:special, :clause_args}, _meta, [args]}, _context, state) do {doc, state} = clause_args_to_algebra(args, state) {group(doc), state} end defp quoted_to_algebra({{:special, :bitstring_segment}, _meta, [arg, last]}, _context, state) do bitstring_segment_to_algebra({arg, -1}, state, last) end defp quoted_to_algebra({var, _meta, var_context}, _context, state) when is_atom(var_context) do {var |> Atom.to_string() |> string(), state} end defp quoted_to_algebra({:<<>>, meta, entries}, _context, state) do cond do entries == [] -> {"<<>>", state} not interpolated?(entries) -> bitstring_to_algebra(meta, entries, state) meta[:delimiter] == ~s["""] -> {doc, state} = entries |> prepend_heredoc_line() |> interpolation_to_algebra(:heredoc, state, @double_heredoc, @double_heredoc) {force_unfit(doc), state} true -> interpolation_to_algebra(entries, @double_quote, state, @double_quote, @double_quote) end end defp quoted_to_algebra( {{:., _, [List, :to_charlist]}, meta, [entries]} = quoted, context, state ) do cond do not list_interpolated?(entries) -> remote_to_algebra(quoted, context, state) meta[:delimiter] == ~s['''] -> {doc, state} = entries |> prepend_heredoc_line() |> list_interpolation_to_algebra(:heredoc, state, @single_heredoc, @single_heredoc) {force_unfit(doc), state} true -> list_interpolation_to_algebra(entries, @single_quote, state, @single_quote, @single_quote) end end defp quoted_to_algebra( {{:., _, [:erlang, :binary_to_atom]}, _, [{:<<>>, _, entries}, :utf8]} = quoted, context, state ) do if interpolated?(entries) do interpolation_to_algebra(entries, @double_quote, state, ":\"", @double_quote) else remote_to_algebra(quoted, context, state) end end # foo[bar] defp quoted_to_algebra({{:., _, [Access, :get]}, meta, [target, arg]}, _context, state) do {target_doc, state} = remote_target_to_algebra(target, state) {access_doc, state} = if keyword?(arg) do list_to_algebra(meta, arg, state) else list_to_algebra(meta, [arg], state) end {concat(target_doc, access_doc), state} end # %Foo{} # %name{foo: 1} # %name{bar | foo: 1} defp quoted_to_algebra({:%, _, [name, {:%{}, meta, args}]}, _context, state) do {name_doc, state} = quoted_to_algebra(name, :parens_arg, state) map_to_algebra(meta, name_doc, args, state) end # %{foo: 1} # %{foo => bar} # %{name | foo => bar} defp quoted_to_algebra({:%{}, meta, args}, _context, state) do map_to_algebra(meta, @empty, args, state) end # {} # {1, 2} defp quoted_to_algebra({:{}, meta, args}, _context, state) do tuple_to_algebra(meta, args, :flex_break, state) end defp quoted_to_algebra({:__block__, meta, [{left, right}]}, _context, state) do tuple_to_algebra(meta, [left, right], :flex_break, state) end defp quoted_to_algebra({:__block__, meta, [list]}, _context, state) when is_list(list) do case meta[:delimiter] do ~s['''] -> string = list |> List.to_string() |> escape_heredoc() {@single_heredoc |> concat(string) |> concat(@single_heredoc) |> force_unfit(), state} ~s['] -> string = list |> List.to_string() |> escape_string(@single_quote) {@single_quote |> concat(string) |> concat(@single_quote), state} _other -> list_to_algebra(meta, list, state) end end defp quoted_to_algebra({:__block__, meta, [string]}, _context, state) when is_binary(string) do if meta[:delimiter] == ~s["""] do string = escape_heredoc(string) {@double_heredoc |> concat(string) |> concat(@double_heredoc) |> force_unfit(), state} else string = escape_string(string, @double_quote) {@double_quote |> concat(string) |> concat(@double_quote), state} end end defp quoted_to_algebra({:__block__, _, [atom]}, _context, state) when is_atom(atom) do {atom_to_algebra(atom), state} end defp quoted_to_algebra({:__block__, meta, [integer]}, _context, state) when is_integer(integer) do {integer_to_algebra(Keyword.fetch!(meta, :token)), state} end defp quoted_to_algebra({:__block__, meta, [float]}, _context, state) when is_float(float) do {float_to_algebra(Keyword.fetch!(meta, :token)), state} end defp quoted_to_algebra( {:__block__, _meta, [{:unquote_splicing, meta, [_] = args}]}, context, state ) do {doc, state} = local_to_algebra(:unquote_splicing, meta, args, context, state) {wrap_in_parens(doc), state} end defp quoted_to_algebra({:__block__, _meta, [arg]}, context, state) do quoted_to_algebra(arg, context, state) end defp quoted_to_algebra({:__block__, _meta, []}, _context, state) do {"nil", state} end defp quoted_to_algebra({:__block__, meta, _} = block, _context, state) do {block, state} = block_to_algebra(block, line(meta), closing_line(meta), state) {surround("(", block, ")"), state} end defp quoted_to_algebra({:__aliases__, _meta, [head | tail]}, context, state) do {doc, state} = if is_atom(head) do {Atom.to_string(head), state} else quoted_to_algebra_with_parens_if_operator(head, context, state) end {Enum.reduce(tail, doc, &concat(&2, "." <> Atom.to_string(&1))), state} end # &1 # &local(&1) # &local/1 # &Mod.remote/1 # & &1 # & &1 + &2 defp quoted_to_algebra({:&, _, [arg]}, context, state) do capture_to_algebra(arg, context, state) end defp quoted_to_algebra({:@, meta, [arg]}, context, state) do module_attribute_to_algebra(meta, arg, context, state) end # not(left in right) # left not in right defp quoted_to_algebra({:not, meta, [{:in, _, [left, right]}]}, context, state) do binary_op_to_algebra(:in, "not in", meta, left, right, context, state) end # 1..2//3 defp quoted_to_algebra({:"..//", meta, [left, middle, right]}, context, state) do quoted_to_algebra({:"//", meta, [{:.., meta, [left, middle]}, right]}, context, state) end defp quoted_to_algebra({:fn, meta, [_ | _] = clauses}, _context, state) do anon_fun_to_algebra(clauses, line(meta), closing_line(meta), state, eol?(meta, state)) end defp quoted_to_algebra({fun, meta, args}, context, state) when is_atom(fun) and is_list(args) do with :error <- maybe_sigil_to_algebra(fun, meta, args, state), :error <- maybe_unary_op_to_algebra(fun, meta, args, context, state), :error <- maybe_binary_op_to_algebra(fun, meta, args, context, state), do: local_to_algebra(fun, meta, args, context, state) end defp quoted_to_algebra({_, _, args} = quoted, context, state) when is_list(args) do remote_to_algebra(quoted, context, state) end # (left -> right) defp quoted_to_algebra([{:->, _, _} | _] = clauses, _context, state) do type_fun_to_algebra(clauses, @max_line, @min_line, state) end # [keyword: :list] (inner part) # %{:foo => :bar} (inner part) defp quoted_to_algebra(list, context, state) when is_list(list) do many_args_to_algebra(list, state, "ed_to_algebra(&1, context, &2)) end # keyword: :list # key => value defp quoted_to_algebra({left_arg, right_arg}, context, state) do {left, op, right, state} = if keyword_key?(left_arg) do {left, state} = case left_arg do # TODO: Remove this clause in v1.16 when we no longer quote operator :..// {:__block__, _, [:"..//"]} -> {string(~S{"..//":}), state} {:__block__, _, [atom]} when is_atom(atom) -> key = case Sourceror.Code.classify_atom(atom) do type when type in [ :callable_local, :callable_operator, :not_callable, :identifier, :unquoted ] -> IO.iodata_to_binary([Atom.to_string(atom), ?:]) _ -> IO.iodata_to_binary([?", Atom.to_string(atom), ?", ?:]) end {string(key), state} {{:., _, [:erlang, :binary_to_atom]}, _, [{:<<>>, _, entries}, :utf8]} -> interpolation_to_algebra(entries, @double_quote, state, "\"", "\":") end {right, state} = quoted_to_algebra(right_arg, context, state) {left, "", right, state} else {left, state} = quoted_to_algebra(left_arg, context, state) {right, state} = quoted_to_algebra(right_arg, context, state) left = wrap_in_parens_if_binary_operator(left, left_arg) {left, " =>", right, state} end doc = with_next_break_fits(next_break_fits?(right_arg, state), right, fn right -> concat(group(left), group(nest(glue(op, group(right)), 2, :break))) end) {doc, state} end # #PID's and #Ref's may appear on regular AST defp quoted_to_algebra(unknown, _context, state) do {inspect(unknown), state} end ## Blocks defp block_to_algebra([{:->, _, _} | _] = type_fun, min_line, max_line, state) do type_fun_to_algebra(type_fun, min_line, max_line, state) end defp block_to_algebra({:__block__, _, []}, min_line, max_line, state) do block_args_to_algebra([], min_line, max_line, state) end defp block_to_algebra({:__block__, _, [_, _ | _] = args}, min_line, max_line, state) do block_args_to_algebra(args, min_line, max_line, state) end defp block_to_algebra(block, min_line, max_line, state) do block_args_to_algebra([block], min_line, max_line, state) end defp block_args_to_algebra(args, min_line, max_line, state) do quoted_to_algebra = fn {kind, meta, _} = arg, _args, state -> newlines = meta[:end_of_expression][:newlines] || 1 {doc, state} = quoted_to_algebra(arg, :block, state) {{doc, block_next_line(kind), newlines}, state} end {args_docs, _comments?, state} = quoted_to_algebra_with_comments(args, [], min_line, max_line, state, quoted_to_algebra) case args_docs do [] -> {@empty, state} [line] -> {line, state} lines -> {lines |> Enum.reduce(&line(&2, &1)) |> force_unfit(), state} end end defp block_next_line(:@), do: @empty defp block_next_line(_), do: break("") ## Operators defp maybe_unary_op_to_algebra(fun, meta, args, context, state) do with [arg] <- args, {_, _} <- Code.Identifier.unary_op(fun) do unary_op_to_algebra(fun, meta, arg, context, state) else _ -> :error end end defp unary_op_to_algebra(op, _meta, arg, context, state) do {doc, state} = quoted_to_algebra(arg, force_many_args_or_operand(context, :operand), state) # not and ! are nestable, all others are not. doc = case arg do {^op, _, [_]} when op in [:!, :not] -> doc _ -> wrap_in_parens_if_operator(doc, arg) end # not requires a space unless the doc was wrapped in parens. op_string = if op == :not do "not " else Atom.to_string(op) end {concat(op_string, doc), state} end defp maybe_binary_op_to_algebra(fun, meta, args, context, state) do with [left, right] <- args, {_, _} <- Code.Identifier.binary_op(fun) do binary_op_to_algebra(fun, Atom.to_string(fun), meta, left, right, context, state) else _ -> :error end end # There are five kinds of operators. # # 1. no space binary operators, for example, 1..2 # 2. no newline binary operators, for example, left in right # 3. strict newlines before a left precedent operator, for example, foo |> bar |> baz # 4. strict newlines before a right precedent operator, for example, foo when bar when baz # 5. flex newlines after the operator, for example, foo ++ bar ++ baz # # Cases 1, 2 and 5 are handled fairly easily by relying on the # operator precedence and making sure nesting is applied only once. # # Cases 3 and 4 are the complex ones, as it requires passing the # strict or flex mode around. defp binary_op_to_algebra(op, op_string, meta, left_arg, right_arg, context, state) do %{operand_nesting: nesting} = state binary_op_to_algebra(op, op_string, meta, left_arg, right_arg, context, state, nesting) end defp binary_op_to_algebra(op, op_string, meta, left_arg, right_arg, context, state, _nesting) when op in @right_new_line_before_binary_operators do op_info = Code.Identifier.binary_op(op) op_string = op_string <> " " left_context = left_op_context(context) right_context = right_op_context(context) min_line = case left_arg do {_, left_meta, _} -> line(left_meta) _ -> line(meta) end {operands, max_line} = unwrap_right(right_arg, op, meta, right_context, [{{:root, left_context}, left_arg}]) fun = fn {{:root, context}, arg}, _args, state -> {doc, state} = binary_operand_to_algebra(arg, context, state, op, op_info, :left, 2) {{doc, @empty, 1}, state} {{kind, context}, arg}, _args, state -> {doc, state} = binary_operand_to_algebra(arg, context, state, op, op_info, kind, 0) doc = doc |> nest_by_length(op_string) |> force_keyword(arg) {{concat(op_string, doc), @empty, 1}, state} end {doc, state} = operand_to_algebra_with_comments(operands, meta, min_line, max_line, context, state, fun) if keyword?(right_arg) and context in [:parens_arg, :no_parens_arg] do {wrap_in_parens(doc), state} else {doc, state} end end defp binary_op_to_algebra(op, _, meta, left_arg, right_arg, context, state, _nesting) when op in @pipeline_operators do op_info = Code.Identifier.binary_op(op) left_context = left_op_context(context) right_context = right_op_context(context) max_line = line(meta) {pipes, min_line} = unwrap_pipes(left_arg, meta, left_context, [{{op, right_context}, right_arg}]) fun = fn {{:root, context}, arg}, _args, state -> {doc, state} = binary_operand_to_algebra(arg, context, state, op, op_info, :left, 2) {{doc, @empty, 1}, state} {{op, context}, arg}, _args, state -> op_info = Code.Identifier.binary_op(op) op_string = Atom.to_string(op) <> " " {doc, state} = binary_operand_to_algebra(arg, context, state, op, op_info, :right, 0) {{concat(op_string, doc), @empty, 1}, state} end operand_to_algebra_with_comments(pipes, meta, min_line, max_line, context, state, fun) end defp binary_op_to_algebra(op, op_string, meta, left_arg, right_arg, context, state, nesting) do op_info = Code.Identifier.binary_op(op) left_context = left_op_context(context) right_context = right_op_context(context) {left, state} = binary_operand_to_algebra(left_arg, left_context, state, op, op_info, :left, 2) {right, state} = binary_operand_to_algebra(right_arg, right_context, state, op, op_info, :right, 0) doc = cond do op in @no_space_binary_operators -> concat(concat(group(left), op_string), group(right)) op in @no_newline_binary_operators -> op_string = " " <> op_string <> " " concat(concat(group(left), op_string), group(right)) true -> eol? = eol?(meta, state) next_break_fits? = op in @next_break_fits_operators and next_break_fits?(right_arg, state) and not eol? with_next_break_fits(next_break_fits?, right, fn right -> op_string = " " <> op_string right = nest(glue(op_string, group(right)), nesting, :break) right = if eol?, do: force_unfit(right), else: right concat(group(left), group(right)) end) end {doc, state} end # TODO: We can remove this workaround once we remove # ?rearrange_uop from the parser on v2.0. # (! left) in right # (not left) in right defp binary_operand_to_algebra( {:__block__, _, [{op, meta, [arg]}]}, context, state, :in, _parent_info, :left, _nesting ) when op in [:not, :!] do {doc, state} = unary_op_to_algebra(op, meta, arg, context, state) {wrap_in_parens(doc), state} end defp binary_operand_to_algebra(operand, context, state, parent_op, parent_info, side, nesting) do {parent_assoc, parent_prec} = parent_info with {op, meta, [left, right]} <- operand, op_info = Code.Identifier.binary_op(op), {_assoc, prec} <- op_info do op_string = Atom.to_string(op) cond do # If the operator has the same precedence as the parent and is on # the correct side, we respect the nesting rule to avoid multiple # nestings. This only applies for left associativity or same operator. parent_prec == prec and parent_assoc == side and (side == :left or op == parent_op) -> binary_op_to_algebra(op, op_string, meta, left, right, context, state, nesting) # If the parent requires parens or the precedence is inverted or # it is in the wrong side, then we *need* parenthesis. (parent_op in @required_parens_on_binary_operands and op not in @no_space_binary_operators) or (op in @required_parens_logical_binary_operands and parent_op in @required_parens_logical_binary_operands) or parent_prec > prec or (parent_prec == prec and parent_assoc != side) -> {operand, state} = binary_op_to_algebra(op, op_string, meta, left, right, context, state, 2) {wrap_in_parens(operand), state} # Otherwise, we rely on precedence but also nest. true -> binary_op_to_algebra(op, op_string, meta, left, right, context, state, 2) end else {:&, _, [arg]} when not is_integer(arg) and side == :left when not is_integer(arg) and parent_assoc == :left and parent_prec > @ampersand_prec -> {doc, state} = quoted_to_algebra(operand, context, state) {wrap_in_parens(doc), state} _ -> quoted_to_algebra(operand, context, state) end end defp unwrap_pipes({op, meta, [left, right]}, _meta, context, acc) when op in @pipeline_operators do left_context = left_op_context(context) right_context = right_op_context(context) unwrap_pipes(left, meta, left_context, [{{op, right_context}, right} | acc]) end defp unwrap_pipes(left, meta, context, acc) do min_line = case left do {_, meta, _} -> line(meta) _ -> line(meta) end {[{{:root, context}, left} | acc], min_line} end defp unwrap_right({op, meta, [left, right]}, op, _meta, context, acc) do left_context = left_op_context(context) right_context = right_op_context(context) unwrap_right(right, op, meta, right_context, [{{:left, left_context}, left} | acc]) end defp unwrap_right(right, _op, meta, context, acc) do acc = [{{:right, context}, right} | acc] {Enum.reverse(acc), line(meta)} end defp operand_to_algebra_with_comments(operands, meta, min_line, max_line, context, state, fun) do # If we are in a no_parens_one_arg expression, we actually cannot # extract comments from the first operand, because it would rewrite: # # @spec function(x) :: # # Comment # any # when x: any # # to: # # @spec # Comment # function(x) :: # any # when x: any # # Instead we get: # # @spec function(x) :: # any # # Comment # when x: any # # Which may look counter-intuitive but it actually makes sense, # as the closest possible location for the comment is the when # operator. {operands, acc, state} = if context == :no_parens_one_arg do [operand | operands] = operands {doc_triplet, state} = fun.(operand, :unused, state) {operands, [doc_triplet], state} else {operands, [], state} end {docs, comments?, state} = quoted_to_algebra_with_comments(operands, acc, min_line, max_line, state, fun) if comments? or eol?(meta, state) do {docs |> Enum.reduce(&line(&2, &1)) |> force_unfit(), state} else {docs |> Enum.reduce(&glue(&2, &1)), state} end end ## Module attributes # @Foo # @Foo.Bar defp module_attribute_to_algebra(_meta, {:__aliases__, _, [_, _ | _]} = quoted, _context, state) do {doc, state} = quoted_to_algebra(quoted, :parens_arg, state) {concat(concat("@(", doc), ")"), state} end # @foo bar # @foo(bar) defp module_attribute_to_algebra(meta, {name, call_meta, [_] = args} = expr, context, state) when is_atom(name) and name not in [:__block__, :__aliases__] do if Sourceror.Code.classify_atom(name) in [:callable_local, :identifier, :unquoted] do {{call_doc, state}, wrap_in_parens?} = call_args_to_algebra(args, call_meta, context, :skip_unless_many_args, false, state) doc = "@#{name}" |> string() |> concat(call_doc) doc = if wrap_in_parens?, do: wrap_in_parens(doc), else: doc {doc, state} else unary_op_to_algebra(:@, meta, expr, context, state) end end # @foo # @(foo.bar()) defp module_attribute_to_algebra(meta, quoted, context, state) do unary_op_to_algebra(:@, meta, quoted, context, state) end ## Capture operator defp capture_to_algebra(integer, _context, state) when is_integer(integer) do {"&" <> Integer.to_string(integer), state} end defp capture_to_algebra(arg, context, state) do {doc, state} = capture_target_to_algebra(arg, context, state) if doc |> format_to_string() |> String.starts_with?("&") do {concat("& ", doc), state} else {concat("&", doc), state} end end defp capture_target_to_algebra( {:/, _, [{{:., _, [target, fun]}, _, []}, {:__block__, _, [arity]}]}, _context, state ) when is_atom(fun) and is_integer(arity) do {target_doc, state} = remote_target_to_algebra(target, state) fun = Sourceror.Code.inspect_atom(:remote_call, fun) {target_doc |> nest(1) |> concat(string(".#{fun}/#{arity}")), state} end defp capture_target_to_algebra( {:/, _, [{name, _, var_context}, {:__block__, _, [arity]}]}, _context, state ) when is_atom(name) and is_atom(var_context) and is_integer(arity) do {string("#{name}/#{arity}"), state} end defp capture_target_to_algebra(arg, context, state) do {doc, state} = quoted_to_algebra(arg, context, state) {wrap_in_parens_if_operator(doc, arg), state} end ## Calls (local, remote and anonymous) # expression.{arguments} defp remote_to_algebra({{:., _, [target, :{}]}, meta, args}, _context, state) do {target_doc, state} = remote_target_to_algebra(target, state) {call_doc, state} = tuple_to_algebra(meta, args, :break, state) {concat(concat(target_doc, "."), call_doc), state} end # expression.(arguments) defp remote_to_algebra({{:., _, [target]}, meta, args}, context, state) do {target_doc, state} = remote_target_to_algebra(target, state) {{call_doc, state}, wrap_in_parens?} = call_args_to_algebra(args, meta, context, :skip_if_do_end, true, state) doc = concat(concat(target_doc, "."), call_doc) doc = if wrap_in_parens?, do: wrap_in_parens(doc), else: doc {doc, state} end # Mod.function() # var.function # expression.function(arguments) defp remote_to_algebra({{:., _, [target, fun]}, meta, args}, context, state) when is_atom(fun) do {target_doc, state} = remote_target_to_algebra(target, state) fun = Sourceror.Code.inspect_atom(:remote_call, fun) remote_doc = target_doc |> concat(".") |> concat(string(fun)) if args == [] and not remote_target_is_a_module?(target) and not meta?(meta, :closing) do {remote_doc, state} else {{call_doc, state}, wrap_in_parens?} = call_args_to_algebra(args, meta, context, :skip_if_do_end, true, state) doc = concat(remote_doc, call_doc) doc = if wrap_in_parens?, do: wrap_in_parens(doc), else: doc {doc, state} end end # call(call)(arguments) defp remote_to_algebra({target, meta, args}, context, state) do {target_doc, state} = quoted_to_algebra(target, :no_parens_arg, state) {{call_doc, state}, wrap_in_parens?} = call_args_to_algebra(args, meta, context, :required, true, state) doc = concat(target_doc, call_doc) doc = if wrap_in_parens?, do: wrap_in_parens(doc), else: doc {doc, state} end defp remote_target_is_a_module?(target) do case target do {:__MODULE__, _, context} when is_atom(context) -> true {:__block__, _, [atom]} when is_atom(atom) -> true {:__aliases__, _, _} -> true _ -> false end end defp remote_target_to_algebra({:fn, _, [_ | _]} = quoted, state) do # This change is not semantically required but for beautification. {doc, state} = quoted_to_algebra(quoted, :no_parens_arg, state) {wrap_in_parens(doc), state} end defp remote_target_to_algebra(quoted, state) do quoted_to_algebra_with_parens_if_operator(quoted, :no_parens_arg, state) end # function(arguments) defp local_to_algebra(fun, meta, args, context, state) when is_atom(fun) do skip_parens = cond do meta?(meta, :closing) -> :skip_if_only_do_end local_without_parens?(fun, length(args), state.locals_without_parens) -> :skip_unless_many_args true -> :skip_if_do_end end {{call_doc, state}, wrap_in_parens?} = call_args_to_algebra(args, meta, context, skip_parens, true, state) doc = fun |> Atom.to_string() |> string() |> concat(call_doc) doc = if wrap_in_parens?, do: wrap_in_parens(doc), else: doc {doc, state} end # parens may be one of: # # * :skip_unless_many_args - skips parens unless we are the argument context # * :skip_if_only_do_end - skip parens if we are do-end and the only arg # * :skip_if_do_end - skip parens if we are do-end # * :required - never skip parens # defp call_args_to_algebra([], meta, _context, _parens, _list_to_keyword?, state) do {args_doc, _join, state} = args_to_algebra_with_comments([], meta, false, :none, :break, state, &{&1, &2}) {{surround("(", args_doc, ")"), state}, false} end defp call_args_to_algebra(args, meta, context, parens, list_to_keyword?, state) do {rest, last} = split_last(args) if blocks = do_end_blocks(meta, last, state) do {call_doc, state} = case rest do [] when parens == :required -> {"() do", state} [] -> {" do", state} _ -> no_parens? = parens not in [:required, :skip_if_only_do_end] call_args_to_algebra_no_blocks(meta, rest, no_parens?, list_to_keyword?, " do", state) end {blocks_doc, state} = do_end_blocks_to_algebra(blocks, state) call_doc = call_doc |> concat(blocks_doc) |> line("end") |> force_unfit() {{call_doc, state}, context in [:no_parens_arg, :no_parens_one_arg]} else no_parens? = parens == :skip_unless_many_args and context in [:block, :operand, :no_parens_one_arg, :parens_one_arg] res = call_args_to_algebra_no_blocks(meta, args, no_parens?, list_to_keyword?, @empty, state) {res, false} end end defp call_args_to_algebra_no_blocks(meta, args, skip_parens?, list_to_keyword?, extra, state) do {left, right} = split_last(args) {keyword?, right} = last_arg_to_keyword(right, list_to_keyword?, skip_parens?, state.comments) context = if left == [] and not keyword? do if skip_parens?, do: :no_parens_one_arg, else: :parens_one_arg else if skip_parens?, do: :no_parens_arg, else: :parens_arg end args = if keyword?, do: left ++ right, else: left ++ [right] many_eol? = match?([_, _ | _], args) and eol?(meta, state) no_generators? = no_generators?(args) to_algebra_fun = "ed_to_algebra(&1, context, &2) {args_doc, next_break_fits?, state} = if left != [] and keyword? and no_generators? do join = if force_args?(left) or many_eol?, do: :line, else: :break {left_doc, _join, state} = args_to_algebra_with_comments( left, Keyword.delete(meta, :closing), skip_parens?, :force_comma, join, state, to_algebra_fun ) join = if force_args?(right) or force_args?(args) or many_eol?, do: :line, else: :break {right_doc, _join, state} = args_to_algebra_with_comments(right, meta, false, :none, join, state, to_algebra_fun) right_doc = apply(Inspect.Algebra, join, []) |> concat(right_doc) args_doc = if skip_parens? do left_doc |> concat(next_break_fits(group(right_doc, :inherit), :enabled)) |> nest(:cursor, :break) else right_doc = right_doc |> nest(2, :break) |> concat(break("")) |> group(:inherit) |> next_break_fits(:enabled) concat(nest(left_doc, 2, :break), right_doc) end {args_doc, true, state} else join = if force_args?(args) or many_eol?, do: :line, else: :break next_break_fits? = join == :break and next_break_fits?(right, state) last_arg_mode = if next_break_fits?, do: :next_break_fits, else: :none {args_doc, _join, state} = args_to_algebra_with_comments( args, meta, skip_parens?, last_arg_mode, join, state, to_algebra_fun ) # If we have a single argument, then we won't have an option to break # before the "extra" part, so we ungroup it and build it later. args_doc = ungroup_if_group(args_doc) args_doc = if skip_parens? do nest(args_doc, :cursor, :break) else nest(args_doc, 2, :break) |> concat(break("")) end {args_doc, next_break_fits?, state} end doc = cond do left != [] and keyword? and skip_parens? and no_generators? -> " " |> concat(args_doc) |> nest(2) |> concat(extra) |> group() skip_parens? -> " " |> concat(args_doc) |> concat(extra) |> group() true -> "(" |> concat(break("")) |> nest(2, :break) |> concat(args_doc) |> concat(")") |> concat(extra) |> group() end if next_break_fits? do {next_break_fits(doc, :disabled), state} else {doc, state} end end defp no_generators?(args) do not Enum.any?(args, &match?({:<-, _, [_, _]}, &1)) end defp do_end_blocks(meta, [{{:__block__, _, [:do]}, _} | rest] = blocks, state) do if meta?(meta, :do) or can_force_do_end_blocks?(rest, state) do blocks |> Enum.map(fn {{:__block__, meta, [key]}, value} -> {key, line(meta), value} end) |> do_end_blocks_with_range(end_line(meta)) end end defp do_end_blocks(_, _, _), do: nil defp can_force_do_end_blocks?(rest, state) do state.force_do_end_blocks and Enum.all?(rest, fn {{:__block__, _, [key]}, _} -> key in @do_end_keywords end) end defp do_end_blocks_with_range([{key1, line1, value1}, {_, line2, _} = h | t], end_line) do [{key1, line1, line2, value1} | do_end_blocks_with_range([h | t], end_line)] end defp do_end_blocks_with_range([{key, line, value}], end_line) do [{key, line, end_line, value}] end defp do_end_blocks_to_algebra([{:do, line, end_line, value} | blocks], state) do {acc, state} = do_end_block_to_algebra(@empty, line, end_line, value, state) Enum.reduce(blocks, {acc, state}, fn {key, line, end_line, value}, {acc, state} -> {doc, state} = do_end_block_to_algebra(Atom.to_string(key), line, end_line, value, state) {line(acc, doc), state} end) end defp do_end_block_to_algebra(key_doc, line, end_line, value, state) do case clauses_to_algebra(value, line, end_line, state) do {@empty, state} -> {key_doc, state} {value_doc, state} -> {key_doc |> line(value_doc) |> nest(2), state} end end ## Interpolation defp list_interpolated?(entries) do Enum.all?(entries, fn {{:., _, [Kernel, :to_string]}, _, [_]} -> true entry when is_binary(entry) -> true _ -> false end) end defp interpolated?(entries) do Enum.all?(entries, fn {:"::", _, [{{:., _, [Kernel, :to_string]}, _, [_]}, {:binary, _, _}]} -> true entry when is_binary(entry) -> true _ -> false end) end defp prepend_heredoc_line([entry | entries]) when is_binary(entry) do ["\n" <> entry | entries] end defp prepend_heredoc_line(entries) do ["\n" | entries] end defp list_interpolation_to_algebra([entry | entries], escape, state, acc, last) when is_binary(entry) do acc = concat(acc, escape_string(entry, escape)) list_interpolation_to_algebra(entries, escape, state, acc, last) end defp list_interpolation_to_algebra([entry | entries], escape, state, acc, last) do {{:., _, [Kernel, :to_string]}, _meta, [quoted]} = entry {doc, state} = interpolation_to_string(quoted, state) list_interpolation_to_algebra(entries, escape, state, concat(acc, doc), last) end defp list_interpolation_to_algebra([], _escape, state, acc, last) do {concat(acc, last), state} end defp interpolation_to_algebra([entry | entries], escape, state, acc, last) when is_binary(entry) do acc = concat(acc, escape_string(entry, escape)) interpolation_to_algebra(entries, escape, state, acc, last) end defp interpolation_to_algebra([entry | entries], escape, state, acc, last) do {:"::", _, [{{:., _, [Kernel, :to_string]}, _meta, [quoted]}, {:binary, _, _}]} = entry {doc, state} = interpolation_to_string(quoted, state) interpolation_to_algebra(entries, escape, state, concat(acc, doc), last) end defp interpolation_to_algebra([], _escape, state, acc, last) do {concat(acc, last), state} end defp interpolation_to_string(quoted, %{skip_eol: skip_eol} = state) do {doc, state} = block_to_algebra(quoted, @max_line, @min_line, %{state | skip_eol: true}) doc = interpolation_to_string(surround("\#{", doc, "}")) {doc, %{state | skip_eol: skip_eol}} end defp interpolation_to_string(doc) do [head | tail] = doc |> format_to_string() |> String.split("\n") Enum.reduce(tail, string(head), fn line, acc -> concat([acc, line(), string(line)]) end) end ## Sigils defp maybe_sigil_to_algebra(fun, meta, args, state) do with <<"sigil_", name>> <- Atom.to_string(fun), [{:<<>>, _, entries}, modifiers] when is_list(modifiers) <- args, opening_delimiter when not is_nil(opening_delimiter) <- meta[:delimiter] do doc = <> if opening_delimiter in [@double_heredoc, @single_heredoc] do closing_delimiter = concat(opening_delimiter, List.to_string(modifiers)) {doc, state} = entries |> prepend_heredoc_line() |> interpolation_to_algebra(:heredoc, state, doc, closing_delimiter) {force_unfit(doc), state} else escape = closing_sigil_delimiter(opening_delimiter) closing_delimiter = concat(escape, List.to_string(modifiers)) interpolation_to_algebra(entries, escape, state, doc, closing_delimiter) end else _ -> :error end end defp closing_sigil_delimiter("("), do: ")" defp closing_sigil_delimiter("["), do: "]" defp closing_sigil_delimiter("{"), do: "}" defp closing_sigil_delimiter("<"), do: ">" defp closing_sigil_delimiter(other) when other in ["\"", "'", "|", "/"], do: other ## Bitstrings defp bitstring_to_algebra(meta, args, state) do last = length(args) - 1 join = if eol?(meta, state), do: :line, else: :flex_break to_algebra_fun = &bitstring_segment_to_algebra(&1, &2, last) {args_doc, join, state} = args |> Enum.with_index() |> args_to_algebra_with_comments(meta, false, :none, join, state, to_algebra_fun) if join == :flex_break do {"<<" |> concat(args_doc) |> nest(2) |> concat(">>") |> group(), state} else {surround("<<", args_doc, ">>"), state} end end defp bitstring_segment_to_algebra({{:<-, meta, [left, right]}, i}, state, last) do left = {{:special, :bitstring_segment}, meta, [left, last]} {doc, state} = quoted_to_algebra({:<-, meta, [left, right]}, :parens_arg, state) {bitstring_wrap_parens(doc, i, last), state} end defp bitstring_segment_to_algebra({{:"::", _, [segment, spec]}, i}, state, last) do {doc, state} = quoted_to_algebra(segment, :parens_arg, state) {spec, state} = bitstring_spec_to_algebra(spec, state) spec = wrap_in_parens_if_inspected_atom(spec) spec = if i == last, do: bitstring_wrap_parens(spec, i, last), else: spec doc = doc |> bitstring_wrap_parens(i, -1) |> concat("::") |> concat(spec) {doc, state} end defp bitstring_segment_to_algebra({segment, i}, state, last) do {doc, state} = quoted_to_algebra(segment, :parens_arg, state) {bitstring_wrap_parens(doc, i, last), state} end defp bitstring_spec_to_algebra({op, _, [left, right]}, state) when op in [:-, :*] do {left, state} = bitstring_spec_to_algebra(left, state) {right, state} = quoted_to_algebra_with_parens_if_operator(right, :parens_arg, state) {concat(concat(left, Atom.to_string(op)), right), state} end defp bitstring_spec_to_algebra(spec, state) do quoted_to_algebra_with_parens_if_operator(spec, :parens_arg, state) end defp bitstring_wrap_parens(doc, i, last) when i == 0 or i == last do string = format_to_string(doc) if (i == 0 and String.starts_with?(string, ["~", "<<"])) or (i == last and String.ends_with?(string, [">>"])) do wrap_in_parens(doc) else doc end end defp bitstring_wrap_parens(doc, _, _), do: doc ## Literals defp list_to_algebra(meta, args, state) do join = if eol?(meta, state), do: :line, else: :break fun = "ed_to_algebra(&1, :parens_arg, &2) {args_doc, _join, state} = args_to_algebra_with_comments(args, meta, false, :none, join, state, fun) {surround("[", args_doc, "]"), state} end defp map_to_algebra(meta, name_doc, [{:|, _, [left, right]}], state) do join = if eol?(meta, state), do: :line, else: :break fun = "ed_to_algebra(&1, :parens_arg, &2) {left_doc, state} = fun.(left, state) {right_doc, _join, state} = args_to_algebra_with_comments(right, meta, false, :none, join, state, fun) args_doc = left_doc |> wrap_in_parens_if_binary_operator(left) |> glue(concat("| ", nest(right_doc, 2))) name_doc = "%" |> concat(name_doc) |> concat("{") {surround(name_doc, args_doc, "}"), state} end defp map_to_algebra(meta, name_doc, args, state) do join = if eol?(meta, state), do: :line, else: :break fun = "ed_to_algebra(&1, :parens_arg, &2) {args_doc, _join, state} = args_to_algebra_with_comments(args, meta, false, :none, join, state, fun) name_doc = "%" |> concat(name_doc) |> concat("{") {surround(name_doc, args_doc, "}"), state} end defp tuple_to_algebra(meta, args, join, state) do join = if eol?(meta, state), do: :line, else: join fun = "ed_to_algebra(&1, :parens_arg, &2) {args_doc, join, state} = args_to_algebra_with_comments(args, meta, false, :none, join, state, fun) if join == :flex_break do {"{" |> concat(args_doc) |> nest(1) |> concat("}") |> group(), state} else {surround("{", args_doc, "}"), state} end end defp atom_to_algebra(atom) when atom in [nil, true, false] do Atom.to_string(atom) end # TODO: Remove this clause in v1.16 when we no longer quote operator :..// defp atom_to_algebra(:"..//") do string(":\"..//\"") end defp atom_to_algebra(atom) do string = Atom.to_string(atom) iodata = case Sourceror.Code.classify_atom(atom) do type when type in [:callable_local, :callable_operator, :not_callable, :identifier, :unquoted] -> [?:, string] _ -> [?:, ?", String.replace(string, "\"", "\\\""), ?"] end iodata |> IO.iodata_to_binary() |> string() end defp integer_to_algebra(text) do case text do <> -> "0x" <> String.upcase(rest) <> = digits when base in [?b, ?o] -> digits <> = char -> char decimal -> insert_underscores(decimal) end end defp float_to_algebra(text) do [int_part, decimal_part] = :binary.split(text, ".") decimal_part = String.downcase(decimal_part) insert_underscores(int_part) <> "." <> decimal_part end defp insert_underscores(digits) do cond do digits =~ "_" -> digits byte_size(digits) >= 6 -> digits |> String.to_charlist() |> Enum.reverse() |> Enum.chunk_every(3) |> Enum.intersperse('_') |> List.flatten() |> Enum.reverse() |> List.to_string() true -> digits end end defp escape_heredoc(string) do heredoc_to_algebra(["" | String.split(string, "\n")]) end defp escape_string(string, :heredoc) do heredoc_to_algebra(String.split(string, "\n")) end defp escape_string(string, escape) when is_binary(escape) do string |> String.replace(escape, "\\" <> escape) |> String.split("\n") |> Enum.reverse() |> Enum.map(&string/1) |> Enum.reduce(&concat(&1, concat(nest(line(), :reset), &2))) end defp heredoc_to_algebra([string]) do string(string) end defp heredoc_to_algebra(["" | rest]) do rest |> heredoc_line() |> concat(heredoc_to_algebra(rest)) end defp heredoc_to_algebra([string | rest]) do string |> string() |> concat(heredoc_line(rest)) |> concat(heredoc_to_algebra(rest)) end defp heredoc_line(["", _ | _]), do: nest(line(), :reset) defp heredoc_line(_), do: line() defp args_to_algebra_with_comments(args, meta, skip_parens?, last_arg_mode, join, state, fun) do min_line = line(meta) max_line = closing_line(meta) arg_to_algebra = fn arg, args, state -> {doc, state} = fun.(arg, state) doc = case args do [_ | _] -> concat_to_last_group(doc, ",") [] when last_arg_mode == :force_comma -> concat_to_last_group(doc, ",") [] when last_arg_mode == :next_break_fits -> next_break_fits(doc, :enabled) [] when last_arg_mode == :none -> doc end {{doc, @empty, 1}, state} end # If skipping parens, we cannot extract the comments of the first # argument as there is no place to move them to, so we handle it now. {args, acc, state} = case args do [head | tail] when skip_parens? -> {doc_triplet, state} = arg_to_algebra.(head, tail, state) {tail, [doc_triplet], state} _ -> {args, [], state} end {args_docs, comments?, state} = quoted_to_algebra_with_comments(args, acc, min_line, max_line, state, arg_to_algebra) cond do args_docs == [] -> {@empty, :empty, state} join == :line or comments? -> {args_docs |> Enum.reduce(&line(&2, &1)) |> force_unfit(), :line, state} join == :break -> {args_docs |> Enum.reduce(&glue(&2, &1)), :break, state} join == :flex_break -> {args_docs |> Enum.reduce(&flex_glue(&2, &1)), :flex_break, state} end end ## Anonymous functions # fn -> block end defp anon_fun_to_algebra( [{:->, meta, [[], body]}] = clauses, _min_line, max_line, state, _multi_clauses_style ) do min_line = line(meta) {body_doc, state} = block_to_algebra(body, min_line, max_line, state) doc = "fn ->" |> glue(body_doc) |> nest(2) |> glue("end") |> maybe_force_clauses(clauses, state) |> group() {doc, state} end # fn x -> y end # fn x -> # y # end defp anon_fun_to_algebra( [{:->, meta, [args, body]}] = clauses, _min_line, max_line, state, false = _multi_clauses_style ) do min_line = line(meta) {args_doc, state} = clause_args_to_algebra(args, min_line, state) {body_doc, state} = block_to_algebra(body, min_line, max_line, state) head = args_doc |> ungroup_if_group() |> concat(" ->") |> nest(:cursor) |> group() doc = "fn " |> concat(head) |> glue(body_doc) |> nest(2) |> glue("end") |> maybe_force_clauses(clauses, state) |> group() {doc, state} end # fn # args1 -> # block1 # args2 -> # block2 # end defp anon_fun_to_algebra(clauses, min_line, max_line, state, _multi_clauses_style) do {clauses_doc, state} = clauses_to_algebra(clauses, min_line, max_line, state) {"fn" |> line(clauses_doc) |> nest(2) |> line("end") |> force_unfit(), state} end ## Type functions # (() -> block) defp type_fun_to_algebra([{:->, meta, [[], body]}] = clauses, _min_line, max_line, state) do min_line = line(meta) {body_doc, state} = block_to_algebra(body, min_line, max_line, state) doc = "(() -> " |> concat(nest(body_doc, :cursor)) |> concat(")") |> maybe_force_clauses(clauses, state) |> group() {doc, state} end # (x -> y) # (x -> # y) defp type_fun_to_algebra([{:->, meta, [args, body]}] = clauses, _min_line, max_line, state) do min_line = line(meta) {args_doc, state} = clause_args_to_algebra(args, min_line, state) {body_doc, state} = block_to_algebra(body, min_line, max_line, state) doc = args_doc |> ungroup_if_group() |> concat(" ->") |> group() |> concat(break() |> concat(body_doc) |> nest(2)) |> wrap_in_parens() |> maybe_force_clauses(clauses, state) |> group() {doc, state} end # ( # args1 -> # block1 # args2 -> # block2 # ) defp type_fun_to_algebra(clauses, min_line, max_line, state) do {clauses_doc, state} = clauses_to_algebra(clauses, min_line, max_line, state) {"(" |> line(clauses_doc) |> nest(2) |> line(")") |> force_unfit(), state} end ## Clauses defp maybe_force_clauses(doc, clauses, state) do if Enum.any?(clauses, fn {:->, meta, _} -> eol?(meta, state) end) do force_unfit(doc) else doc end end defp clauses_to_algebra([{:->, _, _} | _] = clauses, min_line, max_line, state) do [clause | clauses] = add_max_line_to_last_clause(clauses, max_line) {clause_doc, state} = clause_to_algebra(clause, min_line, state) {clauses_doc, state} = Enum.reduce(clauses, {clause_doc, state}, fn clause, {doc_acc, state_acc} -> {clause_doc, state_acc} = clause_to_algebra(clause, min_line, state_acc) doc_acc = doc_acc |> concat(maybe_empty_line()) |> line(clause_doc) {doc_acc, state_acc} end) {clauses_doc |> maybe_force_clauses([clause | clauses], state) |> group(), state} end defp clauses_to_algebra(other, min_line, max_line, state) do case block_to_algebra(other, min_line, max_line, state) do {@empty, state} -> {@empty, state} {doc, state} -> {group(doc), state} end end defp clause_to_algebra({:->, meta, [[], body]}, _min_line, state) do {body_doc, state} = block_to_algebra(body, line(meta), closing_line(meta), state) {"() ->" |> glue(body_doc) |> nest(2), state} end defp clause_to_algebra({:->, meta, [args, body]}, min_line, state) do %{operand_nesting: nesting} = state state = %{state | operand_nesting: nesting + 2} {args_doc, state} = clause_args_to_algebra(args, min_line, state) state = %{state | operand_nesting: nesting} {body_doc, state} = block_to_algebra(body, min_line, closing_line(meta), state) doc = args_doc |> ungroup_if_group() |> concat(" ->") |> group() |> concat(break() |> concat(body_doc) |> nest(2)) {doc, state} end defp add_max_line_to_last_clause([{op, meta, args}], max_line) do [{op, [closing: [line: max_line]] ++ meta, args}] end defp add_max_line_to_last_clause([clause | clauses], max_line) do [clause | add_max_line_to_last_clause(clauses, max_line)] end defp clause_args_to_algebra(args, min_line, state) do arg_to_algebra = fn arg, _args, state -> {doc, state} = clause_args_to_algebra(arg, state) {{doc, @empty, 1}, state} end {args_docs, comments?, state} = quoted_to_algebra_with_comments([args], [], min_line, @min_line, state, arg_to_algebra) if comments? do {Enum.reduce(args_docs, &line(&2, &1)), state} else {Enum.reduce(args_docs, &glue(&2, &1)), state} end end # fn a, b, c when d -> e end defp clause_args_to_algebra([{:when, meta, args}], state) do {args, right} = split_last(args) left = {{:special, :clause_args}, meta, [args]} binary_op_to_algebra(:when, "when", meta, left, right, :no_parens_arg, state) end # fn () -> e end defp clause_args_to_algebra([], state) do {"()", state} end # fn a, b, c -> e end defp clause_args_to_algebra(args, state) do many_args_to_algebra(args, state, "ed_to_algebra(&1, :no_parens_arg, &2)) end ## Quoted helpers for comments defp quoted_to_algebra_with_comments(args, acc, min_line, max_line, state, fun) do {pre_comments, state} = get_and_update_in(state.comments, fn comments -> Enum.split_while(comments, fn %{line: line} -> line <= min_line end) end) {docs, comments?, state} = each_quoted_to_algebra_with_comments(args, acc, max_line, state, false, fun) {docs, comments?, update_in(state.comments, &(pre_comments ++ &1))} end defp each_quoted_to_algebra_with_comments([], acc, max_line, state, comments?, _fun) do {acc, comments, comments?} = extract_comments_before(max_line, acc, state.comments, comments?) args_docs = merge_algebra_with_comments(Enum.reverse(acc), @empty) {args_docs, comments?, %{state | comments: comments}} end defp each_quoted_to_algebra_with_comments([arg | args], acc, max_line, state, comments?, fun) do {doc_start, doc_end} = traverse_line(arg, {@max_line, @min_line}) {acc, comments, comments?} = extract_comments_before(doc_start, acc, state.comments, comments?) {doc_triplet, state} = fun.(arg, args, %{state | comments: comments}) {acc, comments, comments?} = extract_comments_trailing(doc_start, doc_end, acc, state.comments, comments?) acc = [adjust_trailing_newlines(doc_triplet, doc_end, comments) | acc] state = %{state | comments: comments} each_quoted_to_algebra_with_comments(args, acc, max_line, state, comments?, fun) end defp extract_comments_before(max, acc, [%{line: line} = comment | rest], _) when line < max do %{previous_eol_count: previous, next_eol_count: next, text: doc} = comment acc = [{doc, @empty, next} | add_previous_to_acc(acc, previous)] extract_comments_before(max, acc, rest, true) end defp extract_comments_before(_max, acc, rest, comments?) do {acc, rest, comments?} end defp add_previous_to_acc([{doc, next_line, newlines} | acc], previous) when newlines < previous, do: [{doc, next_line, previous} | acc] defp add_previous_to_acc(acc, _previous), do: acc defp extract_comments_trailing(min, max, acc, [%{line: line, text: doc_comment} | rest], _) when line >= min and line <= max do acc = [{doc_comment, @empty, 1} | acc] extract_comments_trailing(min, max, acc, rest, true) end defp extract_comments_trailing(_min, _max, acc, rest, comments?) do {acc, rest, comments?} end # If the document is immediately followed by comment which is followed by newlines, # its newlines wouldn't have considered the comment, so we need to adjust it. defp adjust_trailing_newlines({doc, next_line, newlines}, doc_end, [%{line: line} | _]) when newlines > 1 and line == doc_end + 1 do {doc, next_line, 1} end defp adjust_trailing_newlines(doc_triplet, _, _), do: doc_triplet defp traverse_line({expr, meta, args}, {min, max}) do acc = case Keyword.fetch(meta, :line) do {:ok, line} -> {min(line, min), max(line, max)} :error -> {min, max} end traverse_line(args, traverse_line(expr, acc)) end defp traverse_line({left, right}, acc) do traverse_line(right, traverse_line(left, acc)) end defp traverse_line(args, acc) when is_list(args) do Enum.reduce(args, acc, &traverse_line/2) end defp traverse_line(_, acc) do acc end # Below are the rules for line rendering in the formatter: # # 1. respect the user's choice # 2. and add empty lines around expressions that take multiple lines # (except for module attributes) # 3. empty lines are collapsed as to not exceed more than one # defp merge_algebra_with_comments([{doc, next_line, newlines} | docs], left) do right = if newlines >= @newlines, do: line(), else: next_line doc = if left != @empty do concat(left, doc) else doc end doc = if docs != [] and right != @empty do concat(doc, concat(collapse_lines(2), right)) else doc end [group(doc) | merge_algebra_with_comments(docs, right)] end defp merge_algebra_with_comments([], _) do [] end ## Quoted helpers defp left_op_context(context), do: force_many_args_or_operand(context, :parens_arg) defp right_op_context(context), do: force_many_args_or_operand(context, :operand) defp force_many_args_or_operand(:no_parens_one_arg, _choice), do: :no_parens_arg defp force_many_args_or_operand(:parens_one_arg, _choice), do: :parens_arg defp force_many_args_or_operand(:no_parens_arg, _choice), do: :no_parens_arg defp force_many_args_or_operand(:parens_arg, _choice), do: :parens_arg defp force_many_args_or_operand(:operand, choice), do: choice defp force_many_args_or_operand(:block, choice), do: choice defp quoted_to_algebra_with_parens_if_operator(ast, context, state) do {doc, state} = quoted_to_algebra(ast, context, state) {wrap_in_parens_if_operator(doc, ast), state} end defp wrap_in_parens_if_operator(doc, {:__block__, _, [expr]}) do wrap_in_parens_if_operator(doc, expr) end defp wrap_in_parens_if_operator(doc, quoted) do if operator?(quoted) and not module_attribute_read?(quoted) and not integer_capture?(quoted) do wrap_in_parens(doc) else doc end end defp wrap_in_parens_if_binary_operator(doc, quoted) do if binary_operator?(quoted) do wrap_in_parens(doc) else doc end end defp wrap_in_parens_if_inspected_atom(":" <> _ = doc) do "(" <> doc <> ")" end defp wrap_in_parens_if_inspected_atom(doc) do doc end defp wrap_in_parens(doc) do concat(concat("(", nest(doc, :cursor)), ")") end defp many_args_to_algebra([arg | args], state, fun) do Enum.reduce(args, fun.(arg, state), fn arg, {doc_acc, state_acc} -> {arg_doc, state_acc} = fun.(arg, state_acc) {glue(concat(doc_acc, ","), arg_doc), state_acc} end) end defp module_attribute_read?({:@, _, [{var, _, var_context}]}) when is_atom(var) and is_atom(var_context) do Sourceror.Code.classify_atom(var) in [:callable_local, :identifier, :unquoted] end defp module_attribute_read?(_), do: false defp integer_capture?({:&, _, [integer]}) when is_integer(integer), do: true defp integer_capture?(_), do: false defp operator?(quoted) do unary_operator?(quoted) or binary_operator?(quoted) end defp binary_operator?(quoted) do case quoted do {op, _, [_, _, _]} when op in @multi_binary_operators -> true {op, _, [_, _]} when is_atom(op) -> Code.Identifier.binary_op(op) != :error _ -> false end end defp unary_operator?(quoted) do case quoted do {op, _, [_]} when is_atom(op) -> Code.Identifier.unary_op(op) != :error _ -> false end end defp with_next_break_fits(condition, doc, fun) do if condition do doc |> next_break_fits(:enabled) |> fun.() |> next_break_fits(:disabled) else fun.(doc) end end defp next_break_fits?({:{}, meta, _args}, state) do eol_or_comments?(meta, state) end defp next_break_fits?({:__block__, meta, [{_, _}]}, state) do eol_or_comments?(meta, state) end defp next_break_fits?({:<<>>, meta, [_ | _] = entries}, state) do meta[:delimiter] == ~s["""] or (not interpolated?(entries) and eol_or_comments?(meta, state)) end defp next_break_fits?({{:., _, [List, :to_charlist]}, meta, [[_ | _]]}, _state) do meta[:delimiter] == ~s['''] end defp next_break_fits?({{:., _, [_left, :{}]}, _, _}, _state) do true end defp next_break_fits?({:__block__, meta, [string]}, _state) when is_binary(string) do meta[:delimiter] == ~s["""] end defp next_break_fits?({:__block__, meta, [list]}, _state) when is_list(list) do meta[:delimiter] != ~s['] end defp next_break_fits?({form, _, [_ | _]}, _state) when form in [:fn, :%{}, :%] do true end defp next_break_fits?({fun, meta, args}, _state) when is_atom(fun) and is_list(args) do meta[:delimiter] in [@double_heredoc, @single_heredoc] and fun |> Atom.to_string() |> String.starts_with?("sigil_") end defp next_break_fits?({{:__block__, _, [atom]}, expr}, state) when is_atom(atom) do next_break_fits?(expr, state) end defp next_break_fits?(_, _state) do false end defp eol_or_comments?(meta, %{comments: comments} = state) do eol?(meta, state) or ( min_line = line(meta) max_line = closing_line(meta) Enum.any?(comments, fn %{line: line} -> line > min_line and line < max_line end) ) end # A literal list is a keyword or (... -> ...) defp last_arg_to_keyword([_ | _] = arg, _list_to_keyword?, _skip_parens?, _comments) do {keyword?(arg), arg} end # This is a list of tuples, it can be converted to keywords. defp last_arg_to_keyword( {:__block__, meta, [[_ | _] = arg]} = block, true, skip_parens?, comments ) do cond do not keyword?(arg) -> {false, block} skip_parens? -> block_line = line(meta) {{_, arg_meta, _}, _} = hd(arg) first_line = line(arg_meta) case Enum.drop_while(comments, fn %{line: line} -> line <= block_line end) do [%{line: line} | _] when line <= first_line -> {false, block} _ -> {true, arg} end true -> {true, arg} end end # Otherwise we don't have a keyword. defp last_arg_to_keyword(arg, _list_to_keyword?, _skip_parens?, _comments) do {false, arg} end defp force_args?(args) do match?([_ | _], args) and force_args?(args, %{}) end defp force_args?([[arg | _] | args], lines) do force_args?([arg | args], lines) end defp force_args?([arg | args], lines) do line = case arg do {{_, meta, _}, _} -> meta[:line] {_, meta, _} -> meta[:line] end cond do # Line may be missing from non-formatter AST is_nil(line) -> force_args?(args, lines) Map.has_key?(lines, line) -> false true -> force_args?(args, Map.put(lines, line, true)) end end defp force_args?([], lines), do: map_size(lines) >= 2 defp force_keyword(doc, arg) do if force_args?(arg), do: force_unfit(doc), else: doc end defp keyword?([{_, _} | list]), do: keyword?(list) defp keyword?(rest), do: rest == [] defp keyword_key?({:__block__, meta, [atom]}) when is_atom(atom), do: meta[:format] == :keyword defp keyword_key?({{:., _, [:erlang, :binary_to_atom]}, meta, [{:<<>>, _, _}, :utf8]}), do: meta[:format] == :keyword defp keyword_key?(_), do: false defp eol?(_meta, %{skip_eol: true}), do: false defp eol?(meta, _state), do: Keyword.get(meta, :newlines, 0) > 0 defp meta?(meta, key) do is_list(meta[key]) end defp line(meta) do meta[:line] || @max_line end defp end_line(meta) do meta[:end][:line] || @min_line end defp closing_line(meta) do meta[:closing][:line] || @min_line end ## Algebra helpers # Relying on the inner document is brittle and error prone. # It would be best if we had a mechanism to apply this. defp concat_to_last_group({:doc_cons, left, right}, concat) do {:doc_cons, left, concat_to_last_group(right, concat)} end defp concat_to_last_group({:doc_group, group, mode}, concat) do {:doc_group, {:doc_cons, group, concat}, mode} end defp concat_to_last_group(other, concat) do {:doc_cons, other, concat} end defp ungroup_if_group({:doc_group, group, _mode}), do: group defp ungroup_if_group(other), do: other defp format_to_string(doc) do doc |> Inspect.Algebra.format(:infinity) |> IO.iodata_to_binary() end defp maybe_empty_line() do nest(break(""), :reset) end defp surround(left, doc, right) do if doc == @empty do concat(left, right) else group(glue(nest(glue(left, "", doc), 2, :break), "", right)) end end defp nest_by_length(doc, string) do nest(doc, String.length(string)) end defp split_last(list) do {left, [right]} = Enum.split(list, -1) {left, right} end end