diff --git a/lib/elixir/lib/code.ex b/lib/elixir/lib/code.ex index 0999eebf1aa..b39731ed2a1 100644 --- a/lib/elixir/lib/code.ex +++ b/lib/elixir/lib/code.ex @@ -177,6 +177,150 @@ defmodule Code do {value, binding} end + @doc """ + Formats the given code `string`. + + The formatter receives a string representing Elixir code and + returns iodata representing the formatted code according to + pre-defined rules. + + ## Options + + * `:file` - the file which contains the string, used for error + reporting + + * `:line` - the line the string starts, used for error reporting + + * `:line_length` - the line length to aim for when formatting + the document. Defaults to 98. + + * `:locals_without_parens` - a keyword list of name and arity + pairs that should be kept without parens whenever possible. + The arity may be the atom `:*`, which implies all arities of + that name. The formatter already includes a list of functions + and this option augments this list. + + * `:rename_deprecated_at` - rename all known deprecated functions + at the given version to their non-deprecated equivalent. It + expects a valid `Version` which is usually the minimum Elixir + version supported by the project. + + ## Design principles + + The formatter was designed under three principles. + + First, the formatter never changes the semantics of the code by + default. This means the input AST and the output AST are equivalent. + Optional behaviour, such as `:rename_deprecated_at`, is allowed to + break this guarantee. + + The second principle is to provide as little configuration as possible. + This eases the formatter adoption by removing contention points while + making sure a single style is followed consistently by the community as + a whole. + + The formatter does not hard code names. The formatter will not behave + specially because a function is named `defmodule`, `def`, etc. This + principle mirrors Elixir's goal of being an extensible language where + developers can extend the language with new constructs as if they were + part of the language. When it is absolutely necessary to change behaviour + based on the name, this behaviour should be configurable, such as the + `:locals_without_parens` option. + + ## Keeping input formatting + + The formatter respects the input format in some cases. Those are + listed below: + + * Insignificant digits in numbers are kept as is. The formatter + however always inserts underscores for decimal numbers with more + than 5 digits and converts hexadecimal digits to uppercase + + * Strings, charlists, atoms and sigils are kept as is. No character + is automatically escaped or unescaped. The choice of delimiter is + also respected from the input + + * Newlines inside blocks are kept as in the input except for: + 1) expressions that take multiple lines will always have an empty + line before and after and 2) empty lines are always squeezed + together into a single empty line + + * The choice between `:do` keyword and `do/end` blocks is left + to the user + + * Lists, tuples, bitstrings, maps, and structs will be broken into + multiple lines if they are followed by a newline in the opening + bracker and preceeded by a new lie in the closing bracker. For + example with a newline after `[` and before `]` for lists + + * Pipeline operators, like `|>` and others with the same precedence, + will span multiple lines if they spanned multiple lines in the input + + The behaviours above are not guaranteed. We may remove or add new + rules in the future. The goal of documenting them is to provide better + understanding on what to expect from the formatter. + + ## Code comments + + The formatter also formats code comments in a way to guarantee a space + is always added between the start of the comments (#) and the next + character. + + The formatter also extracts all trailing comments to their previous line. + For example, the code below + + hello # world + + will be rewritten to + + # world + hello + + Because code comments are handled apart from the code representation (AST), + there are some situations where code comments are seen as ambiguous by the + code formatter. For example, the comment in the anonymous function below + + fn + arg1 -> + body1 + # comment + + arg2 -> + body2 + end + + and in this one + + fn + arg1 -> + body1 + + # comment + arg2 -> + body2 + end + + are considered equivalent because the formatter strips all original formatting. + In such cases, the code formatter will always format to the latter. + """ + def format_string!(string, opts \\ []) when is_binary(string) and is_list(opts) do + line_length = Keyword.get(opts, :line_length, 98) + algebra = Code.Formatter.to_algebra!(string, opts) + Inspect.Algebra.format(algebra, line_length) + end + + @doc """ + Formats a file. + + See `format_string!/2` for more information on code formatting and + available options. + """ + def format_file!(file, opts \\ []) when is_binary(file) and is_list(opts) do + string = File.read!(file) + formatted = format_string!(string, [file: file, line: 1] ++ opts) + [formatted, ?\n] + end + @doc """ Evaluates the quoted contents. @@ -306,7 +450,7 @@ defmodule Code do byte code, `eval_file` simply evaluates the file contents and returns the evaluation result and its bindings. """ - def eval_file(file, relative_to \\ nil) do + def eval_file(file, relative_to \\ nil) when is_binary(file) do file = find_file(file, relative_to) eval_string File.read!(file), [], [file: file, line: 1] end @@ -466,7 +610,9 @@ defmodule Code do Compiles the given string. Returns a list of tuples where the first element is the module name - and the second one is its byte code (as a binary). + and the second one is its byte code (as a binary). A `file` can be + given as second argument which will be used for reporting warnings + and errors. For compiling many files at once, check `Kernel.ParallelCompiler.compile/2`. """ @@ -478,7 +624,9 @@ defmodule Code do Compiles the quoted expression. Returns a list of tuples where the first element is the module name and - the second one is its byte code (as a binary). + the second one is its byte code (as a binary). A `file` can be + given as second argument which will be used for reporting warnings + and errors. """ def compile_quoted(quoted, file \\ "nofile") when is_binary(file) do :elixir_compiler.quoted quoted, file diff --git a/lib/elixir/lib/code/formatter.ex b/lib/elixir/lib/code/formatter.ex new file mode 100644 index 00000000000..4db03c4211c --- /dev/null +++ b/lib/elixir/lib/code/formatter.ex @@ -0,0 +1,1830 @@ +defmodule 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() + + # 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 + @left_new_line_before_binary_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 that always require parens on operands when they are the parent + @required_parens_on_binary_operands [ + :|>, + :<<<, + :>>>, + :<~, + :~>, + :<<~, + :~>>, + :<~>, + :<|>, + :^^^, + :in, + :++, + :--, + :.., + :<> + ] + + locals_without_parens = [ + # Special forms + alias: 1, + alias: 2, + import: 1, + import: 2, + require: 1, + require: 2, + for: :*, + with: :*, + + # Kernel + def: 1, + def: 2, + defp: 1, + defp: 2, + defmacro: 1, + defmacro: 2, + defmacrop: 1, + defmacrop: 2, + defdelegate: 1, + defexception: 1, + defoverridable: 1, + defstruct: 1, + raise: 1, + raise: 2, + if: 2, + unless: 2, + use: 1, + use: 2, + + # Testing + all: :*, + assert: 1, + assert: 2 + ] + + @locals_without_parens MapSet.new(locals_without_parens) + + @doc """ + Checks if two strings are equivalent. + """ + def equivalent(string1, string2) when is_binary(string1) and is_binary(string2) do + quoted1 = :elixir.string_to_quoted!(to_charlist(string1), "nofile", 1, []) + quoted2 = :elixir.string_to_quoted!(to_charlist(string2), "nofile", 1, []) + + 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 `string` to an algebra document. + + Returns `{:ok, doc}` or `{:error, parser_error}`. + + See `format!/2` for the list of options. + """ + def to_algebra(string, opts \\ []) when is_binary(string) and is_list(opts) do + file = Keyword.get(opts, :file, "nofile") + line = Keyword.get(opts, :line, 1) + charlist = String.to_charlist(string) + + Process.put(:code_formatter_comments, []) + tokenizer_options = [unescape: false, preserve_comments: &preserve_comments/5] + + with {:ok, tokens} <- :elixir.string_to_tokens(charlist, line, file, tokenizer_options), + {:ok, forms} <- :elixir.tokens_to_quoted(tokens, file, formatter_metadata: true) do + state = + Process.get(:code_formatter_comments) + |> Enum.reverse() + |> gather_comments() + |> state(opts) + + {doc, _} = block_to_algebra(forms, @min_line, @max_line, state) + {:ok, doc} + end + after + Process.delete(:code_formatter_comments) + end + + @doc """ + Converts `string` to an algebra document. + + Raises if the `string` cannot be parsed. + + See `format!/2` for the list of options. + """ + def to_algebra!(string, opts \\ []) do + case to_algebra(string, opts) do + {:ok, doc} -> + doc + + {:error, {line, error, token}} -> + :elixir_errors.parse_error(line, Keyword.get(opts, :file, "nofile"), error, token) + end + end + + defp state(comments, opts) do + rename_deprecated_at = + if version = opts[:rename_deprecated_at] do + case Version.parse(version) do + {:ok, parsed} -> + parsed + + :error -> + raise ArgumentError, + "invalid version #{inspect(version)} given to :rename_deprecated_at" + end + end + + locals_without_parens = + opts + |> Keyword.get(:locals_without_parens, []) + |> MapSet.new() + |> MapSet.union(@locals_without_parens) + + %{ + locals_without_parens: locals_without_parens, + operand_nesting: 2, + rename_deprecated_at: rename_deprecated_at, + comments: comments + } + end + + # Code comment handling + + defp preserve_comments(line, _column, tokens, comment, rest) do + comments = Process.get(:code_formatter_comments) + comment = {line, {previous_eol(tokens), next_eol(rest, 0)}, format_comment(comment, [])} + Process.put(:code_formatter_comments, [comment | comments]) + end + + defp next_eol('\s' ++ rest, count), do: next_eol(rest, count) + defp next_eol('\t' ++ rest, count), do: next_eol(rest, count) + defp next_eol('\n' ++ rest, count), do: next_eol(rest, count + 1) + defp next_eol('\r\n' ++ rest, count), do: next_eol(rest, count + 1) + defp next_eol(_, count), do: count + + defp previous_eol([{token, {_, _, count}} | _]) when token in [:eol, :",", :";"] and count > 0 do + count + end + + defp previous_eol([]), do: 1 + defp previous_eol(_), do: nil + + defp format_comment('##' ++ rest, acc), do: format_comment([?# | rest], [?# | acc]) + defp format_comment('#', acc), do: List.to_string(Enum.reverse(acc, '#')) + defp format_comment('# ' ++ _ = rest, acc), do: List.to_string(Enum.reverse(acc, rest)) + defp format_comment('#' ++ rest, acc), do: List.to_string(Enum.reverse(acc, [?#, ?\s, rest])) + + # If there is a no new line before, we can't gather all followup comments. + defp gather_comments([{line, {nil, next_eol}, doc} | comments]) do + comment = {line, {@newlines, next_eol}, doc} + [comment | gather_comments(comments)] + end + + defp gather_comments([{line, {previous_eol, next_eol}, doc} | comments]) do + {next_eol, comments, doc} = gather_followup_comments(line + 1, next_eol, comments, doc) + comment = {line, {previous_eol, next_eol}, doc} + [comment | gather_comments(comments)] + end + + defp gather_comments([]) do + [] + end + + defp gather_followup_comments(line, _, [{line, {previous_eol, next_eol}, text} | comments], doc) + when previous_eol != nil do + gather_followup_comments(line + 1, next_eol, comments, line(doc, text)) + end + + defp gather_followup_comments(_line, next_eol, comments, doc) do + {next_eol, comments, doc} + end + + # Special AST nodes from compiler feedback. + + defp quoted_to_algebra({:special, :clause_args, [args, min_line]}, _context, state) do + {doc, state} = clause_args_to_algebra(args, min_line, state) + {group(doc), state} + 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[:format] == :bin_heredoc -> + initial = @double_heredoc |> concat(line()) |> force_break() + interpolation_to_algebra(entries, :heredoc, state, initial, @double_heredoc) + + true -> + interpolation_to_algebra(entries, @double_quote, state, @double_quote, @double_quote) + end + end + + defp quoted_to_algebra( + {{:., _, [String, :to_charlist]}, _, [{:<<>>, meta, entries}]} = quoted, + context, + state + ) do + cond do + not interpolated?(entries) -> + remote_to_algebra(quoted, context, state) + + meta[:format] == :list_heredoc -> + initial = @single_heredoc |> concat(line()) |> force_break() + interpolation_to_algebra(entries, :heredoc, state, initial, @single_heredoc) + + true -> + 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 | args]}, _context, state) do + {target_doc, state} = remote_target_to_algebra(target, state) + {call_doc, state} = list_to_algebra(meta, args, state) + {concat(target_doc, call_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, :argument, 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, state) + end + + defp quoted_to_algebra({:__block__, meta, [{left, right}]}, _context, state) do + tuple_to_algebra(meta, [left, right], state) + end + + defp quoted_to_algebra({:__block__, meta, [list]}, _context, state) when is_list(list) do + case meta[:format] do + :list_heredoc -> + string = list |> List.to_string() |> escape_string(:heredoc) + {@single_heredoc |> line(string) |> concat(@single_heredoc) |> force_break(), state} + + :charlist -> + 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[:format] == :bin_heredoc do + string = escape_string(string, :heredoc) + {@double_heredoc |> line(string) |> concat(@double_heredoc) |> force_break(), 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, :original)), state} + end + + defp quoted_to_algebra({:__block__, meta, [float]}, _context, state) when is_float(float) do + {float_to_algebra(Keyword.fetch!(meta, :original)), state} + end + + defp quoted_to_algebra( + {:__block__, _meta, [{:unquote_splicing, _, [_] = args}]}, + context, + state + ) do + {doc, state} = local_to_algebra(:unquote_splicing, 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, _} = block, _context, state) do + {block, state} = block_to_algebra(block, line(meta), end_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_necessary(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 + + defp quoted_to_algebra({:fn, meta, [_ | _] = clauses}, _context, state) do + anon_fun_to_algebra(clauses, line(meta), end_line(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, 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 + args_to_algebra(list, state, "ed_to_algebra(&1, context, &2)) + end + + # keyword: :list + # key => value + defp quoted_to_algebra({left, right}, context, state) do + if keyword_key?(left) do + {left, state} = + case left do + {:__block__, _, [atom]} when is_atom(atom) -> + {atom |> Code.Identifier.inspect_as_key() |> string(), state} + + {{:., _, [:erlang, :binary_to_atom]}, _, [{:<<>>, _, entries}, :utf8]} -> + interpolation_to_algebra(entries, @double_quote, state, "\"", "\": ") + end + + {right, state} = quoted_to_algebra(right, context, state) + {concat(left, right), state} + else + {left, state} = quoted_to_algebra(left, context, state) + {right, state} = quoted_to_algebra(right, context, state) + {left |> concat(" => ") |> concat(right), state} + end + 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, doc_newlines, state -> + doc_newlines = Keyword.get(meta, :newlines, doc_newlines) + {doc, state} = quoted_to_algebra(arg, :block, state) + {doc, block_next_line(kind), doc_newlines, state} + end + + {args_docs, state} = + quoted_to_algebra_with_comments(args, min_line, max_line, 2, state, quoted_to_algebra) + + case args_docs do + [] -> {@empty, state} + [line] -> {line, state} + lines -> {lines |> Enum.reduce(&line(&2, &1)) |> force_break(), 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, if_operand_or_block(context, :operand), state) + + # not and ! are nestable, all others are not. + wrapped_doc = + case arg do + {^op, _, [_]} when op in [:!, :not] -> doc + _ -> wrap_in_parens_if_necessary(arg, doc) + end + + # not requires a space unless the doc was wrapped in parens. + op_string = + if op == :not and wrapped_doc == doc do + "not " + else + Atom.to_string(op) + end + + {concat(op_string, wrapped_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, e.g. 1..2 + # 2. no newline binary operators, e.g. left in right + # 3. strict newlines before a left precedent operator, e.g. foo |> bar |> baz + # 4. strict newlines before a right precedent operator, e.g. foo when bar when baz + # 5. flex newlines after the operator, e.g. 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, nil, nesting) + end + + defp binary_op_to_algebra( + op, + op_string, + meta, + left_arg, + right_arg, + context, + state, + parent_info, + nesting + ) do + op_info = Code.Identifier.binary_op(op) + left_context = if_operand_or_block(context, :argument) + right_context = if_operand_or_block(context, :operand) + + {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(left, op_string), right) + + op in @no_newline_binary_operators -> + op_string = " " <> op_string <> " " + concat(concat(left, op_string), right) + + op in @left_new_line_before_binary_operators -> + op_string = op_string <> " " + doc = glue(left, concat(op_string, nest_by_length(right, op_string))) + doc = if Keyword.get(meta, :eol, false), do: force_break(doc), else: doc + if op_info == parent_info, do: doc, else: group(doc) + + op in @right_new_line_before_binary_operators -> + op_string = op_string <> " " + + # If the parent is of the same type (computed via same precedence), + # we need to nest the left side because of the associativity. + left = + if op_info == parent_info do + nest_by_length(left, op_string) + else + left + end + + # If the right side is of the same type, we do the nesting above + # on the left side later on. + right = + case right_arg do + {^op, _, [_, _]} -> right + _ -> nest_by_length(right, op_string) + end + + doc = glue(left, concat(op_string, right)) + if is_nil(parent_info) or op_info == parent_info, do: doc, else: group(doc) + + true -> + with_next_break_fits(next_break_fits?(right_arg), right, fn right -> + op_string = " " <> op_string + concat(left, group(nest(glue(op_string, group(right)), nesting, :break))) + end) + end + + {doc, state} + end + + # TODO: We can remove this workaround once we remove + # ?rearrange_uop from the parser in Elixir 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 + with {op, meta, [left, right]} <- operand, + op_info = Code.Identifier.binary_op(op), + {_assoc, prec} <- op_info do + {parent_assoc, parent_prec} = parent_info + 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. + parent_prec == prec and parent_assoc == side -> + binary_op_to_algebra(op, op_string, meta, left, right, context, state, op_info, 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, op_info, 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, op_info, 2) + end + else + {:&, _, [arg]} when not is_integer(arg) -> + {doc, state} = quoted_to_algebra(operand, context, state) + {_, prec} = Code.Identifier.unary_op(:&) + {_, parent_prec} = parent_info + + if parent_prec < prec do + {doc, state} + else + {wrap_in_parens(doc), state} + end + + _ -> + quoted_to_algebra(operand, context, 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, :argument, state) + {concat(concat("@(", doc), ")"), state} + end + + # @foo bar + # @foo(bar) + defp module_attribute_to_algebra(meta, {name, _, [_] = args} = expr, context, state) + when is_atom(name) and name not in [:__block__, :__aliases__] do + if Code.Identifier.classify(name) == :callable_local do + {{call_doc, state}, wrap_in_parens?} = + call_args_to_algebra(args, context, :skip_unless_argument, 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 = remote_fun_to_algebra(target, fun, arity, state) + {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({op, _, [_, _]} = arg, context, state) when is_atom(op) do + {doc, state} = quoted_to_algebra(arg, context, state) + + case Code.Identifier.binary_op(op) do + {_, _} -> {wrap_in_parens(doc), state} + _ -> {doc, state} + end + end + + defp capture_target_to_algebra(arg, context, state) do + quoted_to_algebra(arg, context, state) + end + + ## Calls (local, remote and anonymous) + + # expression.{arguments} + defp remote_to_algebra({{:., _, [target, :{}]}, _, args}, _context, state) do + {target_doc, state} = remote_target_to_algebra(target, state) + {call_doc, state} = tuple_to_algebra([], args, state) + {concat(concat(target_doc, "."), call_doc), state} + end + + # expression.(arguments) + defp remote_to_algebra({{:., _, [target]}, _, args}, context, state) do + {target_doc, state} = remote_target_to_algebra(target, state) + + {{call_doc, state}, wrap_in_parens?} = + call_args_to_algebra(args, context, :skip_if_do_end, 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 + defp remote_to_algebra({{:., _, [target, fun]}, _, []}, _context, state) when is_atom(fun) do + {target_doc, state} = remote_target_to_algebra(target, state) + fun = remote_fun_to_algebra(target, fun, 0, state) + + if remote_target_is_a_module?(target) do + {target_doc |> concat(".") |> concat(string(fun)) |> concat("()"), state} + else + {target_doc |> concat(".") |> concat(string(fun)), state} + end + end + + # expression.function(arguments) + defp remote_to_algebra({{:., _, [target, fun]}, _, args}, context, state) when is_atom(fun) do + {target_doc, state} = remote_target_to_algebra(target, state) + fun = remote_fun_to_algebra(target, fun, length(args), state) + + {{call_doc, state}, wrap_in_parens?} = + call_args_to_algebra(args, context, :skip_if_do_end, state) + + doc = concat(concat(target_doc, "."), concat(string(fun), call_doc)) + doc = if wrap_in_parens?, do: wrap_in_parens(doc), else: doc + {doc, state} + end + + # call(call)(arguments) + defp remote_to_algebra({target, _, args}, context, state) do + {target_doc, state} = quoted_to_algebra(target, :no_parens, state) + {{call_doc, state}, wrap_in_parens?} = call_args_to_algebra(args, context, :required, 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_fun_to_algebra(target, fun, arity, state) do + %{rename_deprecated_at: since} = state + + atom_target = + case since && target do + {:__aliases__, _, [alias | _] = aliases} when is_atom(alias) -> + Module.concat(aliases) + + {:__block__, _, [atom]} when is_atom(atom) -> + atom + + _ -> + nil + end + + with {fun, requirement} <- deprecated(atom_target, fun, arity), + true <- Version.match?(since, requirement) do + fun + else + _ -> Code.Identifier.inspect_as_function(fun) + end + end + + # We can only rename functions in the same module because + # introducing a new module may wrong due to aliases. + defp deprecated(Enum, :partition, 2), do: {"split_with", "~> 1.4"} + defp deprecated(_, _, _), do: :error + + 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, state) + {wrap_in_parens(doc), state} + end + + defp remote_target_to_algebra(quoted, state) do + quoted_to_algebra_with_parens_if_necessary(quoted, :no_parens, state) + end + + # function(arguments) + defp local_to_algebra(fun, args, context, state) when is_atom(fun) do + skip_parens = + if skip_parens?(fun, args, state), do: :skip_unless_argument, else: :skip_if_do_end + + {{call_doc, state}, wrap_in_parens?} = call_args_to_algebra(args, context, skip_parens, 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 one of: + # + # * :skip_if_block - skips parens if we are inside the block context + # * :skip_if_do_end - skip parens if we are do-end + # * :required - never skip parens + # + defp call_args_to_algebra([], _context, _parens, state) do + {{"()", state}, false} + end + + defp call_args_to_algebra(args, context, parens, state) do + {args, last} = split_last(args) + + if blocks = do_end_blocks(last) do + {call_doc, state} = + case args do + [] -> + {@empty, state} + + _ -> + {args, last} = split_last(args) + call_args_to_algebra_without_do_end_blocks(args, last, parens != :required, state) + end + + {blocks_doc, state} = do_end_blocks_to_algebra(blocks, state) + call_doc = call_doc |> space(blocks_doc) |> line("end") |> force_break() + {{call_doc, state}, context == :no_parens} + else + skip_parens? = parens == :skip_unless_argument and context in [:block, :operand] + {call_args_to_algebra_without_do_end_blocks(args, last, skip_parens?, state), false} + end + end + + defp call_args_to_algebra_without_do_end_blocks(left, right, skip_parens?, state) do + context = if skip_parens?, do: :no_parens, else: :argument + multiple_generators? = multiple_generators?([right | left]) + {keyword?, right} = last_arg_to_keyword(right) + + if left != [] and keyword? and skip_parens? and not multiple_generators? do + call_args_to_algebra_with_no_parens_keywords(left, right, context, state) + else + {left, right} = + if keyword? do + {keyword_left, keyword_right} = split_last(right) + {left ++ keyword_left, keyword_right} + else + {left, right} + end + + {left_doc, state} = args_to_algebra(left, state, "ed_to_algebra(&1, context, &2)) + {right_doc, state} = quoted_to_algebra(right, context, state) + + doc = + with_next_break_fits(next_break_fits?(right), right_doc, fn right_doc -> + args_doc = + if left == [] do + right_doc + else + glue(concat(left_doc, ","), right_doc) + end + + args_doc = + if multiple_generators? do + force_break(args_doc) + else + args_doc + end + + if skip_parens? do + " " + |> concat(nest(args_doc, :cursor, :break)) + |> group() + else + surround("(", args_doc, ")", :break) + end + end) + + {doc, state} + end + end + + defp call_args_to_algebra_with_no_parens_keywords(left, right, context, state) do + {left_doc, state} = args_to_algebra(left, state, "ed_to_algebra(&1, context, &2)) + {right_doc, state} = quoted_to_algebra(right, context, state) + right_doc = break(" ") |> concat(right_doc) |> group() + + doc = + with_next_break_fits(true, right_doc, fn right_doc -> + args_doc = concat(concat(left_doc, ","), right_doc) + + " " + |> concat(nest(args_doc, :cursor, :break)) + |> nest(2) + |> group() + end) + + {doc, state} + end + + defp skip_parens?(fun, args, %{locals_without_parens: locals_without_parens}) do + length = length(args) + + length > 0 and + Enum.any?(locals_without_parens, fn {key, val} -> + key == fun and (val == :* or val == length) + end) + end + + defp multiple_generators?(args) do + Enum.count(args, &match?({:<-, _, [_, _]}, &1)) >= 2 + end + + defp do_end_blocks([{{:__block__, meta, [:do]}, _} | _] = blocks) do + if meta[:format] == :block 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 + 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(:do, 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(key, line, end_line, value, state) + {line(acc, doc), state} + end) + end + + defp do_end_block_to_algebra(key, line, end_line, value, state) do + key_doc = Atom.to_string(key) + + 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 interpolated?(entries) do + Enum.all?(entries, fn + {:::, _, [{{:., _, [Kernel, :to_string]}, _, [_]}, {:binary, _, _}]} -> true + entry when is_binary(entry) -> true + _ -> false + end) + 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} = block_to_algebra(quoted, line(meta), end_line(meta), state) + doc = surround("\#{", doc, "}") + 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 + + ## Sigils + + defp maybe_sigil_to_algebra(fun, meta, args, state) do + case {Atom.to_string(fun), args} do + {<<"sigil_", name>>, [{:<<>>, _, entries}, modifiers]} -> + opening_terminator = Keyword.fetch!(meta, :terminator) + acc = <> + + if opening_terminator in [@double_heredoc, @single_heredoc] do + acc = force_break(concat(acc, line())) + closing_terminator = concat(opening_terminator, List.to_string(modifiers)) + interpolation_to_algebra(entries, :heredoc, state, acc, closing_terminator) + else + escape = closing_sigil_terminator(opening_terminator) + closing_terminator = concat(escape, List.to_string(modifiers)) + interpolation_to_algebra(entries, escape, state, acc, closing_terminator) + end + + _ -> + :error + end + end + + defp closing_sigil_terminator("("), do: ")" + defp closing_sigil_terminator("["), do: "]" + defp closing_sigil_terminator("{"), do: "}" + defp closing_sigil_terminator("<"), do: ">" + defp closing_sigil_terminator(other) when other in ["\"", "'", "|", "/"], do: other + + ## Bitstrings + + defp bitstring_to_algebra(meta, args, state) do + last = length(args) - 1 + + {args_doc, state} = + args + |> Enum.with_index() + |> args_to_algebra_with_comments(meta, state, &bitstring_segment_to_algebra(&1, &2, last)) + + {surround("<<", args_doc, ">>"), state} + end + + defp bitstring_segment_to_algebra({{:::, _, [segment, spec]}, i}, state, last) do + {doc, state} = quoted_to_algebra(segment, :argument, state) + {spec, state} = bitstring_spec_to_algebra(spec, state) + doc = concat(concat(doc, "::"), wrap_in_parens_if_inspected_atom(spec)) + {bitstring_wrap_parens(doc, i, last), state} + end + + defp bitstring_segment_to_algebra({segment, i}, state, last) do + {doc, state} = quoted_to_algebra(segment, :argument, 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_necessary(right, :argument, 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_necessary(spec, :argument, state) + end + + defp bitstring_wrap_parens(doc, i, last) do + if 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 + else + doc + end + end + + ## Literals + + defp list_to_algebra(meta, args, state) do + {args_doc, state} = + args_to_algebra_with_comments(args, meta, state, "ed_to_algebra(&1, :argument, &2)) + + {surround("[", args_doc, "]"), state} + end + + defp map_to_algebra(meta, name_doc, [{:|, _, [left, right]}], state) do + {left_doc, state} = quoted_to_algebra(left, :argument, state) + + {right_doc, state} = + args_to_algebra_with_comments(right, meta, state, "ed_to_algebra(&1, :argument, &2)) + + args_doc = group(glue(left_doc, 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 + {args_doc, state} = + args_to_algebra_with_comments(args, meta, state, "ed_to_algebra(&1, :argument, &2)) + + name_doc = "%" |> concat(name_doc) |> concat("{") + {surround(name_doc, args_doc, "}"), state} + end + + defp tuple_to_algebra(meta, args, state) do + {args_doc, state} = + args_to_algebra_with_comments(args, meta, state, "ed_to_algebra(&1, :argument, &2)) + + {surround("{", args_doc, "}"), state} + end + + defp atom_to_algebra(atom) when atom in [nil, true, false] do + Atom.to_string(atom) + end + + defp atom_to_algebra(atom) do + string = Atom.to_string(atom) + + iodata = + case Code.Identifier.classify(atom) do + type when type in [:callable_local, :callable_operator, :not_callable] -> + [?:, string] + + _ -> + [?:, ?", String.replace(string, "\"", "\\\""), ?"] + end + + iodata |> IO.iodata_to_binary() |> string() + end + + defp integer_to_algebra(text) do + case text do + [?0, ?x | rest] -> + "0x" <> String.upcase(List.to_string(rest)) + + [?0, base | _rest] = digits when base in [?b, ?o] -> + List.to_string(digits) + + [?? | _rest] = char -> + List.to_string(char) + + decimal -> + List.to_string(insert_underscores(decimal)) + end + end + + defp float_to_algebra(text) do + {int_part, [?. |decimal_part]} = Enum.split_while(text, &(&1 != ?.)) + + decimal_part = + decimal_part + |> List.to_string() + |> String.downcase() + + List.to_string(insert_underscores(int_part)) <> "." <> decimal_part + end + + defp insert_underscores(digits) do + if length(digits) >= 6 do + digits + |> Enum.reverse() + |> Enum.chunk_every(3) + |> Enum.intersperse('_') + |> List.flatten() + |> Enum.reverse() + else + digits + end + 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([string, ""]) do + string + |> string() + |> concat(line()) + end + + defp heredoc_to_algebra([string, "" | rest]) do + string + |> string() + |> concat(nest(line(), :reset)) + |> line(heredoc_to_algebra(rest)) + end + + defp heredoc_to_algebra([string | rest]) do + line(string(string), heredoc_to_algebra(rest)) + end + + defp args_to_algebra_with_comments(args, meta, state, fun) do + min_line = line(meta) + max_line = end_line(meta) + + arg_to_algebra = fn arg, args, newlines, state -> + {doc, state} = fun.(arg, state) + doc = if args == [], do: doc, else: concat(doc, ",") + {doc, @empty, newlines, state} + end + + {args_docs, new_state} = + quoted_to_algebra_with_comments(args, min_line, max_line, 1, state, arg_to_algebra) + + cond do + args_docs == [] -> + {@empty, new_state} + + Keyword.get(meta, :eol, false) or force_container_break?(state, new_state) -> + {args_docs |> Enum.reduce(&line(&2, &1)) |> force_break(), new_state} + + true -> + {args_docs |> Enum.reduce(&glue(&2, &1)), new_state} + end + end + + defp force_container_break?(%{comments: comments}, %{comments: comments}), do: false + defp force_container_break?(_, _), do: true + + ## Anonymous functions + + # fn -> block end + defp anon_fun_to_algebra([{:"->", meta, [[], body]}], _min_line, max_line, state) 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") + |> group() + + {doc, state} + end + + # fn x -> y end + # fn x -> + # y + # end + defp anon_fun_to_algebra([{:"->", meta, [args, body]}], _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 = + "fn " + |> concat(group(args_doc)) + |> concat(" ->") + |> nest(1) + |> glue(body_doc) + |> nest(2) + |> glue("end") + |> group() + + {doc, state} + end + + # fn + # args1 -> + # block1 + # args2 -> + # block2 + # end + defp anon_fun_to_algebra(clauses, min_line, max_line, state) do + {clauses_doc, state} = clauses_to_algebra(clauses, min_line, max_line, state) + {"fn" |> line(clauses_doc) |> nest(2) |> line("end") |> force_break(), state} + end + + ## Type functions + + # (-> block) + defp type_fun_to_algebra([{:"->", meta, [[], body]}], _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(")") + |> group() + + {doc, state} + end + + # (x -> y) + # (x -> + # y) + defp type_fun_to_algebra([{:"->", meta, [args, body]}], _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) + + clause_doc = + " ->" + |> glue(body_doc) + |> nest(2) + + doc = + args_doc + |> group() + |> concat(clause_doc) + |> wrap_in_parens() + |> 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_break(), state} + end + + ## Clauses + + defp maybe_force_clauses(doc, clauses) do + if Enum.any?(clauses, fn {:"->", meta, _} -> Keyword.get(meta, :eol, false) end) do + force_break(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]) |> 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), end_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, end_line(meta), state) + {concat(group(args_doc), " ->" |> glue(body_doc) |> nest(2)), state} + end + + defp add_max_line_to_last_clause([{op, meta, args}], max_line) do + [{op, [end_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 + + # fn a, b, c when d -> e end + defp clause_args_to_algebra([{:when, meta, args}], min_line, state) do + {args, right} = split_last(args) + left = {:special, :clause_args, [args, min_line]} + binary_op_to_algebra(:when, "when", meta, left, right, :no_parens, state) + end + + # fn a, b, c -> e end + defp clause_args_to_algebra([], _min_line, state) do + {"()", state} + end + + defp clause_args_to_algebra(args, min_line, state) do + arg_to_algebra = "ed_to_algebra(&1, :no_parens, &2) + args_to_algebra_with_comments(args, [line: min_line], state, arg_to_algebra) + end + + ## Quoted helpers for comments + + defp quoted_to_algebra_with_comments(args, min_line, max_line, newlines, state, fun) do + {pre_comments, state} = + get_and_update_in(state.comments, fn comments -> + Enum.split_while(comments, fn {line, _, _} -> line <= min_line end) + end) + + {docs, state} = each_quoted_to_algebra_with_comments(args, [], max_line, newlines, state, fun) + {docs, update_in(state.comments, &(pre_comments ++ &1))} + end + + defp each_quoted_to_algebra_with_comments([arg | args], acc, max_line, newlines, state, fun) do + %{comments: comments} = state + {doc_start, doc_end} = traverse_line(arg, {@max_line, @min_line}) + + {doc_newlines, acc, comments} = extract_comments_before(doc_start, newlines, acc, comments) + + {doc, next_line, doc_newlines, state} = + fun.(arg, args, doc_newlines, %{state | comments: comments}) + + %{comments: comments} = state + + {doc_newlines, acc, comments} = + extract_comments_trailing(doc_start, doc_end, doc_newlines, acc, comments) + + acc = [{doc, next_line, doc_newlines} | acc] + state = %{state | comments: comments} + each_quoted_to_algebra_with_comments(args, acc, max_line, newlines, state, fun) + end + + defp each_quoted_to_algebra_with_comments([], acc, max_line, _newlines, state, _fun) do + %{comments: comments} = state + + {current, comments} = Enum.split_with(comments, fn {line, _, _} -> line < max_line end) + + extra = for {_, {previous, _}, doc} <- current, do: {doc, @empty, previous} + args_docs = merge_algebra_with_comments(Enum.reverse(acc, extra), @empty) + {args_docs, %{state | comments: comments}} + end + + defp extract_comments_before(max, _, acc, [{line, _, _} = comment | comments]) when line < max do + {_, {previous, next}, doc} = comment + acc = [{doc, @empty, previous} | acc] + extract_comments_before(max, next, acc, comments) + end + + defp extract_comments_before(_max, newlines, acc, comments) do + {newlines, acc, comments} + end + + defp extract_comments_trailing(min, max, newlines, acc, [{line, _, doc_comment} | comments]) + when line >= min and line <= max do + acc = [{doc_comment, @empty, newlines} | acc] + extract_comments_trailing(min, max, 1, acc, comments) + end + + defp extract_comments_trailing(_min, _max, newlines, acc, comments) do + {newlines, acc, comments} + end + + 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 = next_line_separator(docs, 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 if_operand_or_block(:operand, choice), do: choice + defp if_operand_or_block(:block, choice), do: choice + defp if_operand_or_block(other, _choice), do: other + + defp quoted_to_algebra_with_parens_if_necessary(ast, context, state) do + {doc, state} = quoted_to_algebra(ast, context, state) + {wrap_in_parens_if_necessary(ast, doc), state} + end + + # TODO: We can remove this workaround once we remove + # ?rearrange_uop from the parser in Elixir v2.0. + defp wrap_in_parens_if_necessary({:__block__, _, [expr]}, doc) do + wrap_in_parens_if_necessary(expr, doc) + end + + defp wrap_in_parens_if_necessary(quoted, doc) 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_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 args_to_algebra([], state, _fun) do + {@empty, state} + end + + defp 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 next_line_separator([{_doc, _next_line, newlines} | _], next_line) do + if newlines >= @newlines, do: line(), else: next_line + end + + defp next_line_separator([], _) do + line() + end + + defp module_attribute_read?({:@, _, [{var, _, var_context}]}) + when is_atom(var) and is_atom(var_context) do + Code.Identifier.classify(var) == :callable_local + 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 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, [_ | _] = entries}) do + meta[:format] == :bin_heredoc or not interpolated?(entries) + end + + defp next_break_fits?({{:., _, [String, :to_charlist]}, _, [{:<<>>, meta, [_ | _]}]}) do + meta[:format] == :list_heredoc + end + + defp next_break_fits?({:{}, _, _}) do + true + end + + defp next_break_fits?({:__block__, _meta, [{_, _}]}) do + true + end + + defp next_break_fits?({:__block__, meta, [string]}) when is_binary(string) do + meta[:format] == :bin_heredoc + end + + defp next_break_fits?({:__block__, meta, [list]}) when is_list(list) do + meta[:format] != :charlist + end + + defp next_break_fits?({form, _, [_ | _]}) when form in [:fn, :%{}, :%] do + true + end + + defp next_break_fits?({fun, meta, args}) when is_atom(fun) and is_list(args) do + meta[:terminator] in [@double_heredoc, @single_heredoc] and + fun |> Atom.to_string() |> String.starts_with?("sigil_") + end + + defp next_break_fits?({{:__block__, _, [atom]}, expr}) when is_atom(atom) do + next_break_fits?(expr) + end + + defp next_break_fits?(_) do + false + end + + defp last_arg_to_keyword([_ | _] = arg) do + {keyword?(arg), arg} + end + + defp last_arg_to_keyword({:__block__, _, [[_ | _] = arg]} = block) do + if keyword?(arg), do: {true, arg}, else: {false, block} + end + + defp last_arg_to_keyword(arg) do + {false, arg} + end + + defp keyword?([{key, _} | list]) do + keyword_key?(key) and keyword?(list) + end + + defp keyword?(rest) do + rest == [] + end + + defp keyword_key?({:__block__, meta, [_]}) do + meta[:format] == :keyword + end + + defp keyword_key?({{:., _, [:erlang, :binary_to_atom]}, _, [{:<<>>, meta, _}, :utf8]}) do + meta[:format] == :keyword + end + + defp keyword_key?(_) do + false + end + + defp line(meta) do + Keyword.get(meta, :line, @max_line) + end + + defp end_line(meta) do + Keyword.get(meta, :end_line, @min_line) + end + + ## Algebra helpers + + 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, nest \\ :always) do + if doc == @empty do + concat(left, right) + else + group(glue(nest(glue(left, "", doc), 2, nest), "", 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 diff --git a/lib/elixir/test/elixir/code/identifier_test.exs b/lib/elixir/test/elixir/code/identifier_test.exs deleted file mode 100644 index c34852a0f87..00000000000 --- a/lib/elixir/test/elixir/code/identifier_test.exs +++ /dev/null @@ -1,5 +0,0 @@ -Code.require_file "../test_helper.exs", __DIR__ - -defmodule Code.IdentifierTest do - use ExUnit.Case, async: true -end diff --git a/lib/elixir/test/elixir/code_formatter/calls_test.exs b/lib/elixir/test/elixir/code_formatter/calls_test.exs new file mode 100644 index 00000000000..fdbbbfa0c63 --- /dev/null +++ b/lib/elixir/test/elixir/code_formatter/calls_test.exs @@ -0,0 +1,887 @@ +Code.require_file "../test_helper.exs", __DIR__ + +defmodule Code.Formatter.CallsTest do + use ExUnit.Case, async: true + + import CodeFormatterHelpers + + @short_length [line_length: 10] + @medium_length [line_length: 20] + + describe "next break fits" do + test "does not apply to function calls" do + bad = "foo(very_long_call(bar))" + good = """ + foo( + very_long_call( + bar + ) + ) + """ + assert_format bad, good, @short_length + end + + test "does not apply to strings" do + bad = "foo(\"very long string\")" + good = """ + foo( + "very long string" + ) + """ + assert_format bad, good, @short_length + end + + test "for functions" do + assert_same """ + foo(fn x -> y end) + """ + + assert_same """ + foo(fn + a1 -> :ok + b2 -> :error + end) + """ + + assert_same """ + foo(bar, fn + a1 -> :ok + b2 -> :error + end) + """ + + assert_same """ + foo(fn x -> + :really_long_atom + end) + """, @medium_length + + assert_same """ + foo(bar, fn + a1 -> + :ok + + b2 -> + :really_long_error + end) + """, @medium_length + end + + test "for heredocs" do + assert_same """ + foo(''' + bar + ''') + """ + + assert_same to_string(''' + foo(""" + bar + """) + ''') + + assert_same """ + foo(~S''' + bar + ''') + """ + + assert_same """ + foo(~S''' + very long line does trigger another break + ''') + """, @short_length + end + + test "for binaries" do + bad = "foo(<<1, 2, 3, 4>>)" + good = """ + foo(<< + 1, + 2, + 3, + 4 + >>) + """ + assert_format bad, good, @short_length + end + + test "for lists" do + bad = "foo([1, 2, 3, 4])" + good = """ + foo([ + 1, + 2, + 3, + 4 + ]) + """ + assert_format bad, good, @short_length + end + + test "for tuples" do + bad = "long_call({1, 2})" + good = """ + long_call( + {1, 2} + ) + """ + assert_format bad, good, @short_length + + bad = "foo({1, 2, 3, 4})" + good = """ + foo({ + 1, + 2, + 3, + 4 + }) + """ + assert_format bad, good, @short_length + end + + + test "with keyword lists" do + assert_same """ + foo(:hello, foo: foo, bar: ''' + baz + ''') + """ + + assert_same """ + foo(do: fn + 1 -> 2 + 3 -> 4 + end) + """ + end + end + + describe "local calls" do + test "without arguments" do + assert_format "foo( )", "foo()" + end + + test "without arguments doesn't split on line limit" do + assert_same "very_long_function_name()", @short_length + end + + test "removes outer parens except for unquote_splicing/1" do + assert_format "(foo())", "foo()" + assert_same "(unquote_splicing(123))" + end + + test "with arguments" do + assert_format "foo( :one ,:two,\n :three)", "foo(:one, :two, :three)" + end + + test "with arguments splits on line limit" do + bad = """ + fun(x, y, z) + """ + good = """ + fun( + x, + y, + z + ) + """ + assert_format bad, good, @short_length + end + + test "with keyword lists" do + assert_same "foo(foo: 1, bar: 2)" + + assert_same "foo(:hello, foo: 1, bar: 2)" + + assert_same """ + foo( + :hello, + foo: 1, + bar: 2 + ) + """, @short_length + end + + test "with lists maybe rewritten as keyword lists" do + assert_format "foo([foo: 1, bar: 2])", + "foo(foo: 1, bar: 2)" + + assert_format "foo(:arg, [foo: 1, bar: 2])", + "foo(:arg, foo: 1, bar: 2)" + + assert_same "foo(:arg, [:elem, foo: 1, bar: 2])" + end + + test "without parens" do + assert_same "import :foo, :bar" + assert_same "bar = if foo, do: bar, else: baz" + + assert_same """ + for :one, + :two, + :three, + fn -> + :ok + end + """, @short_length + + assert_same """ + for :one, fn -> + :ok + end + """, @medium_length + end + + test "without parens on line limit" do + bad = "import :long_atom, :other_arg" + good = """ + import :long_atom, + :other_arg + """ + assert_format bad, good, @short_length + end + + test "without parens and with keyword lists on line limit" do + assert_same "import :atom, opts: [foo: :bar]" + + bad = "import :atom, opts: [foo: :bar]" + good = """ + import :atom, + opts: [foo: :bar] + """ + assert_format bad, good, @medium_length + + bad = "import :atom, really_long_key: [foo: :bar]" + good = """ + import :atom, + really_long_key: [ + foo: :bar + ] + """ + assert_format bad, good, @medium_length + + assert_same """ + import :foo, + one: two, + three: four, + five: [6, 7, 8, 9] + """, @medium_length + + assert_same """ + import :really_long_atom1, + one: two, + three: four + """, @medium_length + + bad = "with :really_long_atom1, :really_long_atom2, opts: [foo: :bar]" + good = """ + with :really_long_atom1, + :really_long_atom2, + opts: [ + foo: :bar + ] + """ + assert_format bad, good, @medium_length + end + + test "without parens from option" do + assert_format "foo bar", "foo(bar)" + assert_same "foo bar", locals_without_parens: [foo: 1] + assert_same "foo bar", locals_without_parens: [foo: :*] + end + + test "call on call" do + assert_same "unquote(call)()" + assert_same "unquote(call)(one, two)" + assert_same """ + unquote(call)(one, two) do + :ok + end + """ + end + + test "call on call on line limit" do + bad = "foo(bar)(one, two, three)" + good = """ + foo(bar)( + one, + two, + three + ) + """ + assert_format bad, good, @short_length + end + + test "with generators" do + assert_same "foo(bar <- baz, is_bat(bar))" + assert_same "for bar <- baz, is_bat(bar)" + + assert_same """ + foo( + bar <- baz, + is_bat(bar), + bat <- bar + ) + """ + + assert_same """ + for bar <- baz, + is_bat(bar), + bat <- bar + """ + + assert_same """ + for bar <- baz, + is_bat(bar), + bat <- bar do + :ok + end + """ + + assert_same """ + for bar <- baz, + is_bat(bar), + bat <- bar, + into: %{} + """ + end + end + + describe "remote calls" do + test "with no arguments" do + assert_format "Foo . Bar . baz", "Foo.Bar.baz()" + assert_format ":erlang.\nget_stacktrace", ":erlang.get_stacktrace()" + assert_format "@foo.bar()", "@foo.bar" + assert_format "(@foo).bar()", "@foo.bar" + assert_format "__MODULE__.start_link", "__MODULE__.start_link()" + assert_format "Foo.bar.baz.bong", "Foo.bar().baz.bong" + assert_format "(1 + 2).foo()", "(1 + 2).foo" + end + + test "with arguments" do + assert_format "Foo . Bar. baz(1, 2, 3)", "Foo.Bar.baz(1, 2, 3)" + assert_format ":erlang.\nget(\n:some_key\n)", ":erlang.get(:some_key)" + assert_same "@foo.bar(1, 2, 3)" + assert_same "__MODULE__.start_link(1, 2, 3)" + assert_same "foo.bar(1).baz(2, 3)" + end + + test "inspects function names correctly" do + assert_same ~S[MyModule."my function"(1, 2)] + assert_same ~S[MyModule."Foo.Bar"(1, 2)] + assert_same ~S[Kernel.+(1, 2)] + assert_same ~S[:erlang.+(1, 2)] + assert_same ~S[foo."bar baz"(1, 2)] + end + + test "splits on arguments and dot on line limit" do + bad = """ + MyModule.Foo.bar(:one, :two, :three) + """ + good = """ + MyModule.Foo.bar( + :one, + :two, + :three + ) + """ + assert_format bad, good, @medium_length + + bad = """ + My_function.foo().bar(2, 3).baz(4, 5) + """ + good = """ + My_function.foo().bar( + 2, + 3 + ).baz(4, 5) + """ + assert_format bad, good, @medium_length + end + + test "doesn't split on parens on empty arguments" do + assert_same "Mod.func()", @short_length + end + + test "with keyword lists" do + assert_same "mod.foo(foo: 1, bar: 2)" + + assert_same "mod.foo(:hello, foo: 1, bar: 2)" + + assert_same """ + mod.really_long_function_name( + :hello, + foo: 1, + bar: 2 + ) + """, @short_length + + assert_same """ + really_long_module_name.foo( + :hello, + foo: 1, + bar: 2 + ) + """, @short_length + end + + test "wraps left side in parens if it is an anonymous function" do + assert_same "(fn -> :ok end).foo" + end + + test "wraps left side in parens if it is a do-end block" do + assert_same """ + (if true do + :ok + end).foo + """ + end + + test "wraps left side in parens if it is a do-end block as an argument" do + assert_same """ + import (if true do + :ok + end).foo + """ + end + + test "call on call" do + assert_same "foo.bar(call)()" + assert_same "foo.bar(call)(one, two)" + assert_same """ + foo.bar(call)(one, two) do + :ok + end + """ + end + + test "call on call on line limit" do + bad = "a.b(foo)(one, two, three)" + good = """ + a.b(foo)( + one, + two, + three + ) + """ + assert_format bad, good, @short_length + end + + test "on vars" do + assert_format "foo.bar()", "foo.bar" + end + end + + describe "anonymous function calls" do + test "without arguments" do + assert_format "foo . ()", "foo.()" + assert_format "(foo.()).().()", "foo.().().()" + assert_same "@foo.()" + assert_same "(1 + 1).()" + assert_same ":foo.()" + end + + test "with arguments" do + assert_format "foo . (1, 2 , 3 )", "foo.(1, 2, 3)" + assert_format "foo . (1, 2 ).(3,4)", "foo.(1, 2).(3, 4)" + assert_same "@foo.(:one, :two)" + assert_same "foo.(1 + 1).(hello)" + end + + test "does not split on dot on line limit" do + assert_same "my_function.()", @short_length + end + + test "splits on arguments on line limit" do + bad = """ + my_function.(1, 2, 3) + """ + good = """ + my_function.( + 1, + 2, + 3 + ) + """ + assert_format bad, good, @short_length + + bad = """ + my_function.(1, 2).f(3, 4).(5, 6) + """ + good = """ + my_function.( + 1, + 2 + ).f(3, 4).( + 5, + 6 + ) + """ + assert_format bad, good, @short_length + end + + test "with keyword lists" do + assert_same "foo.(foo: 1, bar: 2)" + + assert_same "foo.(:hello, foo: 1, bar: 2)" + + assert_same """ + foo.( + :hello, + foo: 1, + bar: 2 + ) + """, @short_length + end + + test "wraps left side in parens if it is an anonymous function" do + assert_same "(fn -> :ok end).()" + end + + test "wraps left side in parens if it is a do-end block" do + assert_same """ + (if true do + :ok + end).() + """ + end + + test "wraps left side in parens if it is a do-end block as an argument" do + assert_same """ + import (if true do + :ok + end).() + """ + end + end + + describe "do-end blocks" do + test "with non-block keywords" do + assert_same "foo(do: nil)" + end + + test "with multiple keywords" do + assert_same """ + foo do + :do + rescue + :rescue + catch + :catch + else + :else + after + :after + end + """ + end + + test "with multiple keywords and arrows" do + assert_same """ + foo do + a1 -> a2 + b1 -> b2 + rescue + a1 -> a2 + b1 -> b2 + catch + a1 -> a2 + b1 -> b2 + else + a1 -> a2 + b1 -> b2 + after + a1 -> a2 + b1 -> b2 + end + """ + end + + test "with no extra arguments" do + assert_same """ + foo do + :ok + end + """ + end + + test "with no extra arguments and line breaks" do + assert_same """ + foo do + a1 -> + really_long_line + + b1 -> + b2 + rescue + c1 + catch + d1 -> d1 + e1 -> e1 + else + f2 + after + g1 -> + really_long_line + + h1 -> + h2 + end + """, @medium_length + end + + test "with extra arguments" do + assert_same """ + foo bar, baz do + :ok + end + """ + end + + test "with extra arguments and line breaks" do + assert_same """ + foo bar, baz do + a1 -> + really_long_line + + b1 -> + b2 + rescue + c1 + catch + d1 -> d1 + e1 -> e1 + else + f2 + after + g1 -> + really_long_line + + h1 -> + h2 + end + """, @medium_length + + assert_same """ + foo really, + long, + list, + of, + arguments do + a1 -> + really_long_line + + b1 -> + b2 + rescue + c1 + catch + d1 -> d1 + e1 -> e1 + else + f2 + after + g1 -> + really_long_line + + h1 -> + h2 + end + """, @medium_length + end + + test "when empty" do + assert_same """ + foo do + end + """ + + assert_same """ + foo do + rescue + catch + else + after + end + """ + end + + test "inside call" do + bad = "foo (bar do :ok end)" + good = """ + foo( + bar do + :ok + end + ) + """ + assert_format bad, good + + bad = "import (bar do :ok end)" + good = """ + import (bar do + :ok + end) + """ + assert_format bad, good + end + + test "inside operator" do + bad = "foo + bar do :ok end" + good = """ + foo + + bar do + :ok + end + """ + assert_format bad, good + end + + test "inside operator inside argument" do + bad = "fun foo + (bar do :ok end)" + good = """ + fun( + foo + + bar do + :ok + end + ) + """ + assert_format bad, good + + bad = "if foo + (bar do :ok end) do :ok end" + good = """ + if foo + + (bar do + :ok + end) do + :ok + end + """ + assert_format bad, good + end + + test "inside operator inside argument with remote call" do + bad = "if foo + (Bar.baz do :ok end) do :ok end" + good = """ + if foo + + (Bar.baz do + :ok + end) do + :ok + end + """ + assert_format bad, good + end + + test "keeps repeated keys" do + assert_same """ + receive do + :ok + after + 0 -> 1 + after + 2 -> 3 + end + """ + end + + test "preserves user choice even when it fits" do + assert_same """ + case do + 1 -> + :ok + + 2 -> + :ok + end + """ + + assert_same """ + case do + 1 -> + :ok + + 2 -> + :ok + + 3 -> + :ok + end + """ + end + end + + describe "tuple calls" do + test "without arguments" do + assert_format "foo . {}", "foo.{}" + end + + test "with arguments" do + assert_format "foo.{bar,baz,bat,}", "foo.{bar, baz, bat}" + end + + test "with arguments on line limit" do + bad = "foo.{bar,baz,bat,}" + good = """ + foo.{ + bar, + baz, + bat + } + """ + assert_format bad, good, @short_length + + bad = "really_long_expression.{bar,baz,bat,}" + good = """ + really_long_expression.{ + bar, + baz, + bat + } + """ + assert_format bad, good, @short_length + end + + test "with keywords" do + assert_same "expr.{:hello, foo: bar, baz: bat}" + end + end + + describe "access" do + test "with one argument" do + assert_format "foo[ bar ]", "foo[bar]" + end + + test "with arguments on line limit" do + bad = "foo[really_long_argument()]" + good = """ + foo[ + really_long_argument() + ] + """ + assert_format bad, good, @short_length + + bad = "really_long_expression[really_long_argument()]" + good = """ + really_long_expression[ + really_long_argument() + ] + """ + assert_format bad, good, @short_length + end + + test "with do-end blocks" do + assert_same """ + (if true do + false + end)[key] + """ + end + + test "with keywords" do + assert_format "expr[foo: bar, baz: bat]", "expr[[foo: bar, baz: bat]]" + end + end +end diff --git a/lib/elixir/test/elixir/code_formatter/comments_test.exs b/lib/elixir/test/elixir/code_formatter/comments_test.exs new file mode 100644 index 00000000000..6cddb7011e6 --- /dev/null +++ b/lib/elixir/test/elixir/code_formatter/comments_test.exs @@ -0,0 +1,989 @@ +Code.require_file "../test_helper.exs", __DIR__ + +defmodule Code.Formatter.CommentsTest do + use ExUnit.Case, async: true + + import CodeFormatterHelpers + + @short_length [line_length: 10] + + describe "at the root" do + test "for empty documents" do + assert_same "# hello world" + end + + test "are reformatted" do + assert_format "#oops", "# oops" + assert_format "##oops", "## oops" + assert_same "# ## oops" + end + + test "before and after expressions" do + assert_same """ + # before comment + :hello + """ + + assert_same """ + :hello + # after comment + """ + + assert_same """ + # before comment + :hello + # after comment + """ + end + + test "on expressions" do + bad = """ + :hello # this is hello + :world # this is world + """ + + good = """ + # this is hello + :hello + # this is world + :world + """ + + assert_format bad, good + + bad = """ + foo # this is foo + |> bar # this is bar + |> baz # this is baz + """ + + good = """ + # this is foo + # this is bar + # this is baz + foo + |> bar + |> baz + """ + + assert_format bad, good, @short_length + + bad = """ + foo # this is foo + | bar # this is bar + | baz # this is baz + """ + + good = """ + # this is foo + # this is bar + # this is baz + foo + | bar + | baz + """ + + assert_format bad, good, @short_length + end + + test "empty comment" do + assert_same """ + # + :foo + """ + end + + test "before and after expressions with newlines" do + assert_same """ + # before comment + # second line + + :hello + + # middle comment 1 + + # + + # middle comment 2 + + :world + + # after comment + # second line + """ + end + end + + describe "interpolation" do + test "with comment outside before, during and after" do + assert_same ~S""" + # comment + IO.puts("Hello #{world}") + """ + + assert_same ~S""" + IO.puts("Hello #{world}") + # comment + """ + end + + test "with trailing comments" do + # This is trailing so we move the comment out + trailing = ~S""" + IO.puts("Hello #{world}") # comment + """ + + assert_format trailing, ~S""" + # comment + IO.puts("Hello #{world}") + """ + + # This is ambiguous so we move the comment out + ambiguous = ~S""" + IO.puts("Hello #{world # comment + }") + """ + + assert_format ambiguous, ~S""" + # comment + IO.puts("Hello #{world}") + """ + end + + test "with comment inside before and after" do + assert_same ~S""" + IO.puts( + "Hello #{ + # comment + world + }" + ) + """ + + assert_same ~S""" + IO.puts( + "Hello #{ + world + # comment + }" + ) + """ + end + end + + describe "parens blocks" do + test "with comment outside before and after" do + assert_same ~S""" + # comment + assert ( + hello + world + ) + """ + + assert_same ~S""" + assert ( + hello + world + ) + + # comment + """ + end + + test "with trailing comments" do + # This is ambiguous so we move the comment out + ambiguous = ~S""" + assert ( # comment + hello + world + ) + """ + + assert_format ambiguous, ~S""" + # comment + assert ( + hello + world + ) + """ + + # This is ambiguous so we move the comment out + ambiguous = ~S""" + assert ( + hello + world + ) # comment + """ + + assert_format ambiguous, ~S""" + assert ( + hello + world + ) + + # comment + """ + end + + test "with comment inside before and after" do + assert_same ~S""" + assert ( + # comment + hello + world + ) + """ + + assert_same ~S""" + assert ( + hello + world + # comment + ) + """ + end + end + + describe "anonymous functions" do + test "with one clause and no args" do + assert_same ~S""" + fn -> + # comment + hello + world + end + """ + + assert_same ~S""" + fn -> + hello + world + # comment + end + """ + end + + test "with one clause and no args and trailing comments" do + bad = ~S""" + fn # comment + -> + hello + world + end + """ + + assert_format bad, ~S""" + # comment + fn -> + hello + world + end + """ + + bad = ~S""" + fn + # comment + -> + hello + world + end + """ + + assert_format bad, ~S""" + # comment + fn -> + hello + world + end + """ + end + + test "with one clause and args" do + assert_same ~S""" + fn hello -> + # before + hello + # middle + world + # after + end + """ + end + + test "with one clause and args and trailing comments" do + bad = ~S""" + fn # fn + # before head + hello # middle head + # after head + -> + # before body + world # middle body + # after body + end + """ + + assert_format bad, ~S""" + # fn + # before head + # middle head + # after head + fn hello -> + # before body + # middle body + world + # after body + end + """ + end + + test "with multiple clauses and args" do + bad = ~S""" + fn # fn + # before one + one, # middle one + # after one / before two + two # middle two + # after two + -> + # before hello + hello # middle hello + # after hello + + # before three + three # middle three + # after three + -> + # before world + world # middle world + # after world + end + """ + + assert_format bad, ~S""" + # fn + fn + # before one + # middle one + one, + # after one / before two + # middle two + two -> + # after two + # before hello + # middle hello + hello + + # after hello + + # before three + # middle three + three -> + # after three + # before world + # middle world + world + # after world + end + """ + end + + test "with commented out clause" do + assert_same """ + fn + arg1 -> + body1 + + # arg2 -> + # body 2 + + arg3 -> + body3 + end + """ + end + end + + describe "do-end blocks" do + test "with comment outside before and after" do + assert_same ~S""" + # comment + assert do + hello + world + end + """ + + assert_same ~S""" + assert do + hello + world + end + + # comment + """ + end + + test "with trailing comments" do + # This is ambiguous so we move the comment out + ambiguous = ~S""" + assert do # comment + hello + world + end + """ + + assert_format ambiguous, ~S""" + # comment + assert do + hello + world + end + """ + + # This is ambiguous so we move the comment out + ambiguous = ~S""" + assert do + hello + world + end # comment + """ + + assert_format ambiguous, ~S""" + assert do + hello + world + end + + # comment + """ + end + + test "with comment inside before and after" do + assert_same ~S""" + assert do + # comment + hello + world + end + """ + + assert_same ~S""" + assert do + hello + world + # comment + end + """ + end + + test "with comment inside before and after and multiple keywords" do + assert_same ~S""" + assert do + # before + hello + world + # after + rescue + # before + hello + world + # after + after + # before + hello + world + # after + catch + # before + hello + world + # after + else + # before + hello + world + # after + end + """ + end + + test "when empty" do + assert_same ~S""" + assert do + # comment + end + """ + + assert_same ~S""" + assert do + # comment + rescue + # comment + after + # comment + catch + # comment + else + # comment + end + """ + end + + test "with multiple clauses and args" do + bad = ~S""" + assert do # do + # before one + one, # middle one + # after one / before two + two # middle two + # after two + -> + # before hello + hello # middle hello + # after hello + + # before three + three # middle three + # after three + -> + # before world + world # middle world + # after world + end + """ + + assert_format bad, ~S""" + # do + assert do + # before one + # middle one + one, + # after one / before two + # middle two + two -> + # after two + # before hello + # middle hello + hello + + # after hello + + # before three + # middle three + three -> + # after three + # before world + # middle world + world + # after world + end + """ + end + end + + describe "containers" do + test "with comment outside before, during and after" do + assert_same ~S""" + # comment + [one, two, three] + """ + + assert_same ~S""" + [one, two, three] + # comment + """ + end + + test "with trailing comments" do + # This is trailing so we move the comment out + trailing = ~S""" + [one, two, three] # comment + """ + + assert_format trailing, ~S""" + # comment + [one, two, three] + """ + + # This is ambiguous so we move the comment out + ambiguous = ~S""" + [# comment + one, two, three] + """ + + assert_format ambiguous, ~S""" + # comment + [one, two, three] + """ + end + + test "when empty" do + assert_same ~S""" + [ + # comment + ] + """ + end + + test "with block" do + assert_same ~S""" + [ + ( + # before + multi + line + # after + ) + ] + """ + end + + test "with comments inside lists before and after" do + bad = ~S""" + [ + # 1. one + + # 1. two + # 1. three + one, + after_one, + after_one do + :ok + end, + + # 2. one + + # 2. two + # 2. three + # two, + + # 3. one + + # 3. two + # 3. three + three # final + + # 4. one + + # 4. two + # 4. three + # four + ] + """ + + good = ~S""" + [ + # 1. one + + # 1. two + # 1. three + one, + after_one, + after_one do + :ok + end, + + # 2. one + + # 2. two + # 2. three + # two, + + # 3. one + + # 3. two + # 3. three + # final + three + + # 4. one + + # 4. two + # 4. three + # four + ] + """ + + assert_format bad, good + end + + test "with comments inside tuples before and after" do + bad = ~S""" + { + # 1. one + + # 1. two + # 1. three + one, + after_one, + after_one do + :ok + end, + + # 2. one + + # 2. two + # 2. three + # two, + + # 3. one + + # 3. two + # 3. three + three # final + + # 4. one + + # 4. two + # 4. three + # four + } + """ + + good = ~S""" + { + # 1. one + + # 1. two + # 1. three + one, + after_one, + after_one do + :ok + end, + + # 2. one + + # 2. two + # 2. three + # two, + + # 3. one + + # 3. two + # 3. three + # final + three + + # 4. one + + # 4. two + # 4. three + # four + } + """ + + assert_format bad, good + end + + test "with comments inside bitstrings before and after" do + bad = ~S""" + << + # 1. one + + # 1. two + # 1. three + one, + after_one, + after_one do + :ok + end, + + # 2. one + + # 2. two + # 2. three + # two, + + # 3. one + + # 3. two + # 3. three + three # final + + # 4. one + + # 4. two + # 4. three + # four + >> + """ + + good = ~S""" + << + # 1. one + + # 1. two + # 1. three + one, + after_one, + after_one do + :ok + end, + + # 2. one + + # 2. two + # 2. three + # two, + + # 3. one + + # 3. two + # 3. three + # final + three + + # 4. one + + # 4. two + # 4. three + # four + >> + """ + + assert_format bad, good + end + + test "with comments inside maps before and after" do + bad = ~S""" + %{ + # 1. one + + # 1. two + # 1. three + one: one, + one: after_one, + one: after_one do + :ok + end, + + # 2. one + + # 2. two + # 2. three + # two, + + # 3. one + + # 3. two + # 3. three + three: three # final + + # 4. one + + # 4. two + # 4. three + # four + } + """ + + good = ~S""" + %{ + # 1. one + + # 1. two + # 1. three + one: one, + one: after_one, + one: after_one do + :ok + end, + + # 2. one + + # 2. two + # 2. three + # two, + + # 3. one + + # 3. two + # 3. three + # final + three: three + + # 4. one + + # 4. two + # 4. three + # four + } + """ + + assert_format bad, good + end + + test "with comments inside structs before and after" do + bad = ~S""" + %Foo{bar | + # 1. one + + # 1. two + # 1. three + one: one, + one: after_one, + one: after_one do + :ok + end, + + # 2. one + + # 2. two + # 2. three + # two, + + # 3. one + + # 3. two + # 3. three + three: three # final + + # 4. one + + # 4. two + # 4. three + # four + } + """ + + good = ~S""" + %Foo{ + bar + | # 1. one + + # 1. two + # 1. three + one: one, + one: after_one, + one: after_one do + :ok + end, + + # 2. one + + # 2. two + # 2. three + # two, + + # 3. one + + # 3. two + # 3. three + # final + three: three + + # 4. one + + # 4. two + # 4. three + # four + } + """ + + assert_format bad, good + end + end +end diff --git a/lib/elixir/test/elixir/code_formatter/containers_test.exs b/lib/elixir/test/elixir/code_formatter/containers_test.exs new file mode 100644 index 00000000000..87baabbf29e --- /dev/null +++ b/lib/elixir/test/elixir/code_formatter/containers_test.exs @@ -0,0 +1,449 @@ +Code.require_file "../test_helper.exs", __DIR__ + +defmodule Code.Formatter.ContainersTest do + use ExUnit.Case, async: true + + import CodeFormatterHelpers + + @short_length [line_length: 10] + + describe "tuples" do + test "without arguments" do + assert_format "{ }", "{}" + end + + test "with arguments" do + assert_format "{1,2}", "{1, 2}" + assert_format "{1,2,3}", "{1, 2, 3}" + end + + test "is strict on line limits" do + bad = "{1, 2, 3, 4}" + good = """ + { + 1, + 2, + 3, + 4 + } + """ + assert_format bad, good, @short_length + end + + test "removes trailing comma" do + assert_format "{1,}", "{1}" + assert_format "{1, 2, 3,}", "{1, 2, 3}" + end + + test "with keyword lists" do + # The one below is not valid syntax + # assert_same "{foo: 1, bar: 2}" + + assert_same "{:hello, foo: 1, bar: 2}" + + assert_same """ + { + :hello, + foo: 1, + bar: 2 + } + """, @short_length + end + + test "preserves user choice even when it fits" do + assert_same """ + { + :hello, + :foo, + :bar + } + """ + + # Doesn't preserve this because only the beginning has a newline + assert_format "{\nfoo, bar, baz}", "{foo, bar, baz}" + end + end + + describe "lists" do + test "empty" do + assert_format "[ ]", "[]" + assert_format "[\n]", "[]" + end + + test "with elements" do + assert_format "[ 1 , 2,3, 4 ]", "[1, 2, 3, 4]" + end + + test "with tail" do + assert_format "[1,2,3|4]", "[1, 2, 3 | 4]" + end + + test "are strict on line limit" do + bad = """ + [11, 22, 33, 44] + """ + good = """ + [ + 11, + 22, + 33, + 44 + ] + """ + assert_format bad, good, @short_length + + bad = """ + [11, 22, 33 | 44] + """ + good = """ + [ + 11, + 22, + 33 | 44 + ] + """ + assert_format bad, good, @short_length + + bad = """ + [1, 2, 3 | 4] + """ + good = """ + [ + 1, + 2, + 3 | 4 + ] + """ + assert_format bad, good, @short_length + + bad = """ + [1, 2, 3 | really_long_expression()] + """ + good = """ + [ + 1, + 2, + 3 + | really_long_expression() + ] + """ + assert_format bad, good, @short_length + end + + test "removes trailing comma" do + assert_format "[1,]", "[1]" + assert_format "[1, 2, 3,]", "[1, 2, 3]" + end + + test "with keyword lists" do + assert_same "[foo: 1, bar: 2]" + assert_same "[:hello, foo: 1, bar: 2]" + + # Pseudo keyword lists are kept as is + assert_same "[{:foo, 1}, {:bar, 2}]" + + assert_same """ + [ + foo: 1, + bar: 2 + ] + """, @short_length + end + + test "with quoted keyword lists" do + assert_same ~S(["with spaces": 1]) + assert_same ~S(["one #{two} three": 1]) + assert_format ~S(["Foo": 1, "Bar": 2]), ~S([Foo: 1, Bar: 2]) + end + + test "preserves user choice even when it fits" do + assert_same """ + [ + :hello, + :foo, + :bar + ] + """ + + # Doesn't preserve this because only the beginning has a newline + assert_format "[\nfoo, bar, baz]", "[foo, bar, baz]" + end + end + + describe "bitstrings" do + test "without arguments" do + assert_format "<< >>", "<<>>" + assert_format "<<\n>>", "<<>>" + end + + test "with arguments" do + assert_format "<<1,2,3>>", "<<1, 2, 3>>" + end + + test "add parens on first and last in case of ambiguity" do + assert_format "<< <<>> >>", "<<(<<>>)>>" + assert_format "<< <<>> + <<>> >>", "<<(<<>> + <<>>)>>" + assert_format "<< 1 + <<>> >>", "<<(1 + <<>>)>>" + assert_format "<< <<>> + 1 >>", "<<(<<>> + 1)>>" + assert_format "<< <<>>, <<>>, <<>> >>", "<<(<<>>), <<>>, (<<>>)>>" + assert_format "<< <<>>::1, <<>>::2, <<>>::3 >>", "<<(<<>>::1), <<>>::2, <<>>::3>>" + end + + test "with modifiers" do + assert_format "<< 1 :: 1 >>", "<<1::1>>" + assert_format "<< 1 :: 2 + 3 >>", "<<1::(2 + 3)>>" + assert_format "<< 1 :: 2 - integer >>", "<<1::2-integer>>" + assert_format "<< 1 :: 2 - unit(3) >>", "<<1::2-unit(3)>>" + assert_format "<< 1 :: 2 * 3 - unit(4) >>", "<<1::2*3-unit(4)>>" + assert_format "<< 1 :: 2 - unit(3) - 4 / 5 >>", "<<1::2-unit(3)-(4 / 5)>>" + end + + test "is strict on line limits" do + bad = "<<1, 2, 3, 4>>" + good = """ + << + 1, + 2, + 3, + 4 + >> + """ + assert_format bad, good, @short_length + end + + test "preserves user choice even when it fits" do + assert_same """ + << + :hello, + :foo, + :bar + >> + """ + + # Doesn't preserve this because only the beginning has a newline + assert_format "<<\nfoo, bar, baz>>", "<>" + end + end + + describe "maps" do + test "without arguments" do + assert_format "%{ }", "%{}" + end + + test "with arguments" do + assert_format "%{1 => 2,3 => 4}", "%{1 => 2, 3 => 4}" + end + + test "is strict on line limits" do + bad = "%{1 => 2, 3 => 4}" + good = """ + %{ + 1 => 2, + 3 => 4 + } + """ + assert_format bad, good, @short_length + + assert_same """ + %{ + a(1, 2) => b, + c(3, 4) => d + } + """, @short_length + + assert_same """ + %{ + a => fn x -> + y + end, + b => fn y -> + z + end + } + """, @short_length + + assert_same """ + %{ + a => for( + y <- x, + z <- y, + do: 123 + ) + } + """, @short_length + + assert_same """ + %{ + a => for do + :ok + end + } + """, @short_length + end + + test "removes trailing comma" do + assert_format "%{1 => 2,}", "%{1 => 2}" + end + + test "with keyword lists" do + assert_same "%{:foo => :bar, baz: :bat}" + + assert_same """ + %{ + :foo => :bar, + baz: :bat + } + """, @short_length + end + + test "preserves user choice even when it fits" do + assert_same """ + %{ + :hello => 1, + :foo => 2, + :bar => 3 + } + """ + + # Doesn't preserve this because only the beginning has a newline + assert_format "%{\nfoo: 1, bar: 2}", "%{foo: 1, bar: 2}" + end + end + + describe "maps with update" do + test "with arguments" do + assert_format "%{foo | 1 => 2,3 => 4}", "%{foo | 1 => 2, 3 => 4}" + end + + test "is strict on line limits" do + bad = "%{foo | 1 => 2, 3 => 4}" + good = """ + %{ + foo + | 1 => 2, + 3 => 4 + } + """ + assert_format bad, good, @short_length + end + + test "removes trailing comma" do + assert_format "%{foo | 1 => 2,}", "%{foo | 1 => 2}" + end + + test "with keyword lists" do + assert_same "%{foo | :foo => :bar, baz: :bat}" + + assert_same """ + %{ + foo + | :foo => :bar, + baz: :bat + } + """, @short_length + end + + test "preserves user choice even when it fits" do + assert_same """ + %{ + foo + | :hello => 1, + :foo => 2, + :bar => 3 + } + """ + end + end + + + describe "structs" do + test "without arguments" do + assert_format "%struct{ }", "%struct{}" + end + + test "with arguments" do + assert_format "%struct{1 => 2,3 => 4}", "%struct{1 => 2, 3 => 4}" + end + + test "is strict on line limits" do + bad = "%struct{1 => 2, 3 => 4}" + good = """ + %struct{ + 1 => 2, + 3 => 4 + } + """ + assert_format bad, good, @short_length + end + + test "removes trailing comma" do + assert_format "%struct{1 => 2,}", "%struct{1 => 2}" + end + + test "with keyword lists" do + assert_same "%struct{:foo => :bar, baz: :bat}" + + assert_same """ + %struct{ + :foo => :bar, + baz: :bat + } + """, @short_length + end + + test "preserves user choice even when it fits" do + assert_same """ + %Foo{ + :hello => 1, + :foo => 2, + :bar => 3 + } + """ + end + end + + describe "struct with update" do + test "with arguments" do + assert_format "%struct{foo | 1 => 2,3 => 4}", "%struct{foo | 1 => 2, 3 => 4}" + end + + test "is strict on line limits" do + bad = "%struct{foo | 1 => 2, 3 => 4}" + good = """ + %struct{ + foo + | 1 => 2, + 3 => 4 + } + """ + assert_format bad, good, @short_length + end + + test "removes trailing comma" do + assert_format "%struct{foo | 1 => 2,}", "%struct{foo | 1 => 2}" + end + + test "with keyword lists" do + assert_same "%struct{foo | :foo => :bar, baz: :bat}" + + assert_same """ + %struct{ + foo + | :foo => :bar, + baz: :bat + } + """, @short_length + end + + test "preserves user choice even when it fits" do + assert_same """ + %Foo{ + foo + | :hello => 1, + :foo => 2, + :bar => 3 + } + """ + end + end +end diff --git a/lib/elixir/test/elixir/code_formatter/general_test.exs b/lib/elixir/test/elixir/code_formatter/general_test.exs new file mode 100644 index 00000000000..55821cc4fa7 --- /dev/null +++ b/lib/elixir/test/elixir/code_formatter/general_test.exs @@ -0,0 +1,731 @@ +Code.require_file "../test_helper.exs", __DIR__ + +defmodule Code.Formatter.GeneralTest do + use ExUnit.Case, async: true + + import CodeFormatterHelpers + + @short_length [line_length: 10] + + describe "aliases" do + test "with atom-only parts" do + assert_same "Elixir" + assert_same "Elixir.Foo" + assert_same "Foo.Bar.Baz" + end + + test "removes spaces between aliases" do + assert_format "Foo . Bar . Baz", "Foo.Bar.Baz" + end + + test "starting with expression" do + assert_same "__MODULE__.Foo.Bar" + assert_same "'Foo'.Bar.Baz" # Syntatically valid, semantically invalid + end + + test "wraps the head in parens if it has an operator" do + assert_format "+(Foo . Bar . Baz)", "+Foo.Bar.Baz" + assert_format "(+Foo) . Bar . Baz", "(+Foo).Bar.Baz" + end + end + + describe "sigils" do + test "without interpolation" do + assert_same ~S[~s(foo)] + assert_same ~S[~s{foo bar}] + assert_same ~S[~r/Bar Baz/] + assert_same ~S[~w<>] + assert_same ~S[~W()] + end + + test "with escapes" do + assert_same ~S[~s(foo \) bar)] + assert_same ~S[~s(f\a\b\ro)] + assert_same ~S""" + ~S(foo\ +bar) + """ + end + + test "with nested new lines" do + assert_same ~S""" + foo do + ~S(foo\ + bar) + end + """ + + assert_same ~S""" + foo do + ~s(#{bar} +) + end + """ + end + + test "with interpolation" do + assert_same ~S[~s(one #{2} three)] + end + + test "with modifiers" do + assert_same ~S[~w(one two three)a] + assert_same ~S[~z(one two three)foo] + end + + test "with interpolation on line limit" do + bad = ~S""" + ~s(one #{"two"} three) + """ + good = ~S""" + ~s(one #{ + "two" + } three) + """ + assert_format bad, good, @short_length + end + + test "with heredoc syntax" do + assert_same ~S""" + ~s''' + one\a + #{:two}\r + three\0 + ''' + """ + + assert_same ~S''' + ~s""" + one\a + #{:two}\r + three\0 + """ + ''' + end + + test "with heredoc syntax and modifier" do + assert_same ~S""" + ~s''' + foo + '''rsa + """ + end + + test "with heredoc syntax and interpolation on line limit" do + bad = ~S""" + ~s''' + one #{"two two"} three + ''' + """ + + good = ~S""" + ~s''' + one #{ + "two two" + } three + ''' + """ + + assert_format bad, good, @short_length + end + end + + describe "anonymous functions" do + test "with a single clause and no arguments" do + assert_format "fn ->:ok end", "fn -> :ok end" + + bad = "fn -> :foo end" + good = """ + fn -> + :foo + end + """ + assert_format bad, good, @short_length + + assert_same "fn () when node() == :nonode@nohost -> true end" + end + + test "with a single clause and arguments" do + assert_format "fn x ,y-> x + y end", "fn x, y -> x + y end" + + bad = "fn x -> foo(x) end" + good = """ + fn x -> + foo(x) + end + """ + assert_format bad, good, @short_length + + bad = "fn one, two, three -> foo(x) end" + good = """ + fn one, + two, + three -> + foo(x) + end + """ + assert_format bad, good, @short_length + end + + test "with a single clause and when" do + assert_same """ + fn arg + when guard -> + :ok + end + """, @short_length + end + + test "with multiple clauses" do + assert_same """ + fn + 1 -> :ok + 2 -> :ok + end + """, @short_length + + assert_same """ + fn + 1 -> + :ok + + 2 -> + :error + end + """, @short_length + + assert_same """ + fn + arg11, + arg12 -> + body1 + + arg21, + arg22 -> + body2 + end + """, @short_length + + assert_same """ + fn + arg11, + arg12 -> + body1 + + arg21, + arg22 -> + body2 + + arg31, + arg32 -> + body3 + end + """, @short_length + end + + test "with heredocs" do + assert_same """ + fn + arg1 -> + ''' + foo + ''' + + arg2 -> + ''' + bar + ''' + end + """ + end + + test "with multiple empty clauses" do + assert_same """ + fn + () -> :ok1 + () -> :ok2 + end + """ + end + + test "with when in clauses" do + assert_same """ + fn + a1 when a + b -> :ok + b1 when c + d -> :ok + end + """ + + long = """ + fn + a1, a2 when a + b -> :ok + b1, b2 when c + d -> :ok + end + """ + assert_same long + + good = """ + fn + a1, a2 + when a + + b -> + :ok + + b1, b2 + when c + + d -> + :ok + end + """ + assert_format long, good, @short_length + end + + test "uses block context for the body of each clause" do + assert_same "fn -> @foo bar end" + end + + test "preserves user choice even when it fits" do + assert_same """ + fn + 1 -> + :ok + + 2 -> + :ok + end + """ + + assert_same """ + fn + 1 -> + :ok + + 2 -> + :ok + + 3 -> + :ok + end + """ + end + end + + describe "anonymous functions types" do + test "with a single clause and no arguments" do + assert_format "(->:ok)", "(-> :ok)" + assert_same "(-> :really_long_atom)", @short_length + assert_same "(() when node() == :nonode@nohost -> true)" + end + + test "with a single clause and arguments" do + assert_format "( x ,y-> x + y )", "(x, y -> x + y)" + + bad = "(x -> :really_long_atom)" + good = """ + (x -> + :really_long_atom) + """ + assert_format bad, good, @short_length + + bad = "(one, two, three -> foo(x))" + good = """ + (one, + two, + three -> + foo(x)) + """ + assert_format bad, good, @short_length + end + + test "with multiple clauses" do + assert_same """ + ( + 1 -> :ok + 2 -> :ok + ) + """, @short_length + + assert_same """ + ( + 1 -> + :ok + + 2 -> + :error + ) + """, @short_length + + assert_same """ + ( + arg11, + arg12 -> + body1 + + arg21, + arg22 -> + body2 + ) + """, @short_length + + assert_same """ + ( + arg11, + arg12 -> + body1 + + arg21, + arg22 -> + body2 + + arg31, + arg32 -> + body2 + ) + """, @short_length + end + + test "with heredocs" do + assert_same """ + ( + arg1 -> + ''' + foo + ''' + + arg2 -> + ''' + bar + ''' + ) + """ + end + + test "with multiple empty clauses" do + assert_same """ + ( + () -> :ok1 + () -> :ok2 + ) + """ + end + + test "preserves user choice even when it fits" do + assert_same """ + ( + 1 -> + :ok + + 2 -> + :ok + ) + """ + + assert_same """ + ( + 1 -> + :ok + + 2 -> + :ok + + 3 -> + :ok + ) + """ + end + end + + describe "blocks" do + test "with multiple lines" do + assert_same """ + foo = bar + baz = bat + """ + end + + test "with multiple lines with line limit" do + assert_same """ + foo = + bar(one) + + baz = + bat(two) + + a(b) + """, @short_length + + assert_same """ + foo = + bar(one) + + a(b) + + baz = + bat(two) + """, @short_length + + assert_same """ + a(b) + + foo = + bar(one) + + baz = + bat(two) + """, @short_length + + assert_same """ + foo = + bar(one) + + one = + two(ghi) + + baz = + bat(two) + """, @short_length + end + + test "with multiple lines with line limit inside block" do + assert_same """ + block do + a = + b(foo) + + c = + d(bar) + + e = + f(baz) + end + """, @short_length + end + + test "with multiple lines with cancel expressions" do + assert_same """ + foo(%{ + long_key: 1 + }) + + bar(%{ + long_key: 1 + }) + + baz(%{ + long_key: 1 + }) + """, @short_length + end + + test "with heredoc" do + assert_same """ + block do + ''' + a + + b + + c + ''' + end + """ + end + + test "keeps user newlines" do + assert_same """ + defmodule Mod do + field(:foo) + field(:bar) + field(:baz) + belongs_to(:one) + belongs_to(:two) + timestamp() + lock() + has_many(:three) + has_many(:four) + :ok + has_one(:five) + has_one(:six) + foo = 1 + bar = 2 + :before + baz = 3 + :after + end + """ + + bad = """ + defmodule Mod do + field(:foo) + + field(:bar) + + field(:baz) + + + belongs_to(:one) + belongs_to(:two) + + + timestamp() + + lock() + + + has_many(:three) + has_many(:four) + + + :ok + + + has_one(:five) + has_one(:six) + + + foo = 1 + bar = 2 + + + :before + baz = 3 + :after + end + """ + + good = """ + defmodule Mod do + field(:foo) + + field(:bar) + + field(:baz) + + belongs_to(:one) + belongs_to(:two) + + timestamp() + + lock() + + has_many(:three) + has_many(:four) + + :ok + + has_one(:five) + has_one(:six) + + foo = 1 + bar = 2 + + :before + baz = 3 + :after + end + """ + + assert_format bad, good + end + + test "with multiple defs" do + assert_same """ + def foo(:one), do: 1 + def foo(:two), do: 2 + def foo(:three), do: 3 + """ + end + + test "with module attributes" do + assert_same """ + defmodule Foo do + @constant 1 + @constant 2 + + @doc ''' + foo + ''' + def foo do + :ok + end + + @spec bar :: 1 + @spec bar :: 2 + def bar do + :ok + end + + @other_constant 3 + + @spec baz :: 4 + @doc ''' + baz + ''' + def baz do + :ok + end + + @another_constant 5 + @another_constant 5 + + @doc ''' + baz + ''' + @spec baz :: 6 + def baz do + :ok + end + end + """ + end + + test "as funciton arguments" do + assert_same """ + fun( + ( + foo + bar + ) + ) + """ + + assert_same """ + assert true, + do: ( + foo + bar + ) + """ + end + end + + describe "renames deprecated calls" do + test "without deprecation option" do + assert_same "Enum.partition(foo, bar)" + assert_same "&Enum.partition/2" + end + + test "with matching deprecation option" do + assert_format "Enum.partition(foo, bar)", + "Enum.split_with(foo, bar)", + rename_deprecated_at: "1.4.0" + + assert_format "Enum.partition(foo, bar)", + "Enum.split_with(foo, bar)", + rename_deprecated_at: "1.4.0" + end + + test "without matching deprecation option" do + assert_same "Enum.partition(foo, bar)", + rename_deprecated_at: "1.3.0" + + assert_same "Enum.partition(foo, bar)", + rename_deprecated_at: "1.3.0" + end + + test "raises on invalid version" do + assert_raise ArgumentError, ~r"invalid version", fn -> + assert_same "Enum.partition(foo, bar)", rename_deprecated_at: "1.3" + end + end + end +end diff --git a/lib/elixir/test/elixir/code_formatter/integration_test.exs b/lib/elixir/test/elixir/code_formatter/integration_test.exs new file mode 100644 index 00000000000..dc470472182 --- /dev/null +++ b/lib/elixir/test/elixir/code_formatter/integration_test.exs @@ -0,0 +1,175 @@ +Code.require_file "../test_helper.exs", __DIR__ + +defmodule Code.Formatter.IntegrationTest do + use ExUnit.Case, async: true + + import CodeFormatterHelpers + + test "empty documents" do + assert_format " ", "" + assert_format "\n", "" + assert_format ";", "" + end + + test "function with multiple calls and case" do + assert_same """ + def equivalent(string1, string2) when is_binary(string1) and is_binary(string2) do + quoted1 = Code.string_to_quoted!(string1) + quoted2 = Code.string_to_quoted!(string2) + + case not_equivalent(quoted1, quoted2) do + {left, right} -> {:error, left, right} + nil -> :ok + end + end + """ + end + + test "function with long pipeline" do + assert_same ~S""" + def to_algebra!(string, opts \\ []) when is_binary(string) and is_list(opts) do + string + |> Code.string_to_quoted!(wrap_literals_in_blocks: true, unescape: false) + |> block_to_algebra(state(opts)) + |> elem(0) + end + """ + end + + test "case with multiple multi-line arrows" do + assert_same ~S""" + case meta[:format] do + :list_heredoc -> + string = list |> List.to_string() |> escape_string(:heredoc) + {@single_heredoc |> line(string) |> concat(@single_heredoc) |> force_break(), state} + + :charlist -> + string = list |> List.to_string() |> escape_string(@single_quote) + {@single_quote |> concat(string) |> concat(@single_quote), state} + + _other -> + list_to_algebra(list, state) + end + """ + end + + test "function with long guards" do + assert_same """ + defp module_attribute_read?({:@, _, [{var, _, var_context}]}) + when is_atom(var) and is_atom(var_context) do + Code.Identifier.classify(var) == :callable_local + end + """ + end + + test "anonymous function with single clause and blocks" do + assert_same """ + {args_doc, state} = + Enum.reduce(args, {[], state}, fn quoted, {acc, state} -> + {doc, state} = quoted_to_algebra(quoted, :block, state) + doc = doc |> concat(nest(break(""), :reset)) |> group() + {[doc | acc], state} + end) + """ + end + + test "cond with long clause args" do + assert_same """ + cond do + parent_prec == prec and parent_assoc == side -> + binary_op_to_algebra(op, op_string, left, right, context, state, op_info, nesting) + + parent_op in @required_parens_on_binary_operands or parent_prec > prec or + (parent_prec == prec and parent_assoc != side) -> + {operand, state} = + binary_op_to_algebra(op, op_string, left, right, context, state, op_info, 2) + + {concat(concat("(", nest(operand, 1)), ")"), state} + + true -> + binary_op_to_algebra(op, op_string, left, right, context, state, op_info, 2) + end + """ + end + + test "type with multiple |" do + assert_same """ + @type t :: + binary + | :doc_nil + | :doc_line + | doc_string + | doc_cons + | doc_nest + | doc_break + | doc_group + | doc_color + | doc_force + | doc_cancel + """ + end + + test "function with operator and pipeline" do + assert_same """ + defp apply_next_break_fits?({fun, meta, args}) when is_atom(fun) and is_list(args) do + meta[:terminator] in [@double_heredoc, @single_heredoc] and + fun |> Atom.to_string() |> String.starts_with?("sigil_") + end + """ + end + + test "mixed parens and no parens calls with anonymous function" do + assert_same ~S""" + node interface do + resolve_type(fn + %{__struct__: str}, _ -> + str |> Model.Node.model_to_node_type() + + value, _ -> + Logger.warn("Could not extract node type from value: #{inspect(value)}") + nil + end) + end + """ + end + + test "long defstruct definition" do + assert_same """ + defstruct name: nil, + module: nil, + schema: nil, + alias: nil, + base_module: nil, + web_module: nil, + basename: nil, + file: nil, + test_file: nil + """ + end + + test "mix of operators and arguments" do + assert_same """ + def count(%{path: path, line_or_bytes: bytes}) do + case File.stat(path) do + {:ok, %{size: 0}} -> {:error, __MODULE__} + {:ok, %{size: size}} -> {:ok, div(size, bytes) + if(rem(size, bytes) == 0, do: 0, else: 1)} + {:error, reason} -> raise File.Error, reason: reason, action: "stream", path: path + end + end + """ + end + + test "mix of left and right operands" do + assert_same """ + defp server_get_modules(handlers) do + for(handler(module: module) <- handlers, do: module) + |> :ordsets.from_list() + |> :ordsets.to_list() + end + """ + + assert_same """ + neighbours = for({_, _} = t <- neighbours, do: t) |> :sets.from_list() + """ + end +end diff --git a/lib/elixir/test/elixir/code_formatter/literals_test.exs b/lib/elixir/test/elixir/code_formatter/literals_test.exs new file mode 100644 index 00000000000..9e6a5b20cf8 --- /dev/null +++ b/lib/elixir/test/elixir/code_formatter/literals_test.exs @@ -0,0 +1,393 @@ +Code.require_file "../test_helper.exs", __DIR__ + +defmodule Code.Formatter.LiteralsTest do + use ExUnit.Case, async: true + + import CodeFormatterHelpers + + @short_length [line_length: 10] + + describe "integers" do + test "in decimal base" do + assert_same "0" + assert_same "100" + assert_same "007" + assert_same "10000" + assert_format "100000", "100_000" + assert_format "1000000", "1_000_000" + end + + test "in binary base" do + assert_same "0b0" + assert_same "0b1" + assert_same "0b101" + assert_same "0b01" + assert_format "0b111_111", "0b111111" + end + + test "in octal base" do + assert_same "0o77" + assert_same "0o0" + assert_same "0o01" + assert_format "0o777_777", "0o777777" + end + + test "in hex base" do + assert_same "0x1" + assert_format "0xabcdef", "0xABCDEF" + assert_same "0x01" + assert_format "0xFFF_FFF", "0xFFFFFF" + end + + test "as chars" do + assert_same "?a" + assert_same "?1" + assert_same "?è" + assert_same "??" + assert_same "?\\\\" + assert_same "?\\s" + assert_same "?🎾" + end + end + + describe "floats" do + test "with normal notation" do + assert_same "0.0" + assert_same "1.0" + assert_same "123.456" + assert_same "0.0000001" + assert_same "001.100" + assert_format "0_10000_0.000_000", "0_100_000.000000" + end + + test "with scientific notation" do + assert_same "1.0e1" + assert_same "1.0e-1" + assert_same "1.0e01" + assert_same "1.0e-01" + assert_same "001.100e-010" + assert_format "0_1_00_0_000.100e-010", "01_000_000.100e-010" + + assert_format "1.0E01", "1.0e01" + assert_format "1.0E-01", "1.0e-01" + end + end + + describe "atoms" do + test "true, false, nil" do + assert_same "nil" + assert_same "true" + assert_same "false" + end + + test "without escapes" do + assert_same ~S[:foo] + end + + test "with escapes" do + assert_same ~S[:"f\a\b\ro"] + assert_format ~S[:'f\a\b\ro'], ~S[:"f\a\b\ro"] + assert_format ~S[:'single \' quote'], ~S[:"single ' quote"] + assert_format ~S[:"double \" quote"], ~S[:"double \" quote"] + end + + # TODO: Remove this check once we depend only on 20 + if :erlang.system_info(:otp_release) >= '20' do + test "with unicode" do + assert_same ~S[:ólá] + end + end + + test "does not reformat aliases" do + assert_same ~S[:"Elixir.String"] + end + + test "removes quotes when they are not necessary" do + assert_format ~S[:"foo"], ~S[:foo] + assert_format ~S[:"++"], ~S[:++] + end + + test "uses double quotes even when single quotes are used" do + assert_format ~S[:'foo bar'], ~S[:"foo bar"] + end + + test "with interpolation" do + assert_same ~S[:"one #{2} three"] + end + + test "with escapes and interpolation" do + assert_same ~S[:"one\n\"#{2}\"\nthree"] + end + + test "with interpolation on line limit" do + bad = ~S""" + :"one #{"two"} three" + """ + + good = ~S""" + :"one #{ + "two" + } three" + """ + + assert_format bad, good, @short_length + end + end + + describe "strings" do + test "without escapes" do + assert_same ~S["foo"] + end + + test "with escapes" do + assert_same ~S["f\a\b\ro"] + assert_same ~S["double \" quote"] + end + + test "keeps literal new lines" do + assert_same """ + "fo + o" + """ + end + + test "with interpolation" do + assert_same ~S["one #{} three"] + assert_same ~S["one #{2} three"] + end + + test "with interpolation uses block content" do + assert_format ~S["one #{@two(three)}"], ~S["one #{@two three}"] + end + + test "with interpolation on line limit" do + bad = ~S""" + "one #{"two"} three" + """ + + good = ~S""" + "one #{ + "two" + } three" + """ + + assert_format bad, good, @short_length + end + + test "with escaped interpolation" do + assert_same ~S["one\#{two}three"] + end + + test "with escapes and interpolation" do + assert_same ~S["one\n\"#{2}\"\nthree"] + end + + test "is measured in graphemes" do + assert_same ~S""" + "áá#{0}áá" + """, @short_length + end + + test "literal new lines don't count towards line limit" do + assert_same ~S""" + "one + #{"two"} + three" + """, @short_length + end + end + + describe "charlists" do + test "without escapes" do + assert_same ~S[''] + assert_same ~S[' '] + assert_same ~S['foo'] + end + + test "with escapes" do + assert_same ~S['f\a\b\ro'] + assert_same ~S['single \' quote'] + end + + test "keeps literal new lines" do + assert_same """ + 'fo + o' + """ + end + + test "with interpolation" do + assert_same ~S['one #{2} three'] + end + + test "with escape and interpolation" do + assert_same ~S['one\n\'#{2}\'\nthree'] + end + + test "with interpolation on line limit" do + bad = ~S""" + 'one #{"two"} three' + """ + + good = ~S""" + 'one #{ + "two" + } three' + """ + + assert_format bad, good, @short_length + end + + test "literal new lines don't count towards line limit" do + assert_same ~S""" + 'one + #{"two"} + three' + """, @short_length + end + end + + describe "string heredocs" do + test "without escapes" do + assert_same to_string(~S''' + """ + hello + """ + ''') + end + + test "with escapes" do + assert_same to_string(~S''' + """ + f\a\b\ro + """ + ''') + + assert_same to_string(~S''' + """ + multiple "\"" quotes + """ + ''') + end + + test "with interpolation" do + assert_same to_string(~S''' + """ + one + #{2} + three + """ + ''') + + assert_same to_string(~S''' + """ + one + " + #{2} + " + three + """ + ''') + end + + test "with interpolation on line limit" do + bad = to_string(~S''' + """ + one #{"two two"} three + """ + ''') + + good = to_string(~S''' + """ + one #{ + "two two" + } three + """ + ''') + + assert_format bad, good, @short_length + end + + test "literal new lines don't count towards line limit" do + assert_same to_string(~S''' + """ + one + #{"two"} + three + """ + '''), @short_length + end + end + + describe "charlist heredocs" do + test "without escapes" do + assert_same ~S""" + ''' + hello + ''' + """ + end + + test "with escapes" do + assert_same ~S""" + ''' + f\a\b\ro + ''' + """ + + assert_same ~S""" + ''' + multiple "\"" quotes + ''' + """ + end + + test "with interpolation" do + assert_same ~S""" + ''' + one + #{2} + three + ''' + """ + + assert_same ~S""" + ''' + one + " + #{2} + " + three + ''' + """ + end + + test "with interpolation on line limit" do + bad = ~S""" + ''' + one #{"two two"} three + ''' + """ + + good = ~S""" + ''' + one #{ + "two two" + } three + ''' + """ + + assert_format bad, good, @short_length + end + + test "literal new lines don't count towards line limit" do + assert_same ~S""" + ''' + one + #{"two"} + three + ''' + """, @short_length + end + end +end diff --git a/lib/elixir/test/elixir/code_formatter/operators_test.exs b/lib/elixir/test/elixir/code_formatter/operators_test.exs new file mode 100644 index 00000000000..83f16fb8b80 --- /dev/null +++ b/lib/elixir/test/elixir/code_formatter/operators_test.exs @@ -0,0 +1,747 @@ +Code.require_file "../test_helper.exs", __DIR__ + +defmodule Code.Formatter.OperatorsTest do + use ExUnit.Case, async: true + + import CodeFormatterHelpers + + @short_length [line_length: 10] + @medium_length [line_length: 20] + + describe "unary" do + test "formats symbol operators without spaces" do + assert_format "+ 1", "+1" + assert_format "- 1", "-1" + assert_format "! 1", "!1" + assert_format "^ 1", "^1" + assert_format "~~~ 1", "~~~1" + end + + test "formats word operators with spaces" do + assert_same "not 1" + assert_same "not true" + end + + test "wraps operand if it is a unary or binary operator" do + assert_format "!+1", "!(+1)" + assert_format "+ +1", "+(+1)" + assert_format "not +1", "not(+1)" + assert_format "!not 1", "!(not 1)" + assert_format "not !1", "not(!1)" + assert_format "not (!1)", "not(!1)" + assert_format "not (1 + 1)", "not(1 + 1)" + end + + test "does not wrap operand if it is a nestable operator" do + assert_format "! ! var", "!!var" + assert_same "not not var" + end + + test "nests operand" do + bad = "+foo(bar, baz, bat)" + good = """ + +foo( + bar, + baz, + bat + ) + """ + assert_format bad, good, @short_length + + assert_same """ + +assert foo, + bar + """, @short_length + end + + test "does not nest operand" do + bad = "not foo(bar, baz, bat)" + good = """ + not foo( + bar, + baz, + bat + ) + """ + assert_format bad, good, @short_length + + bad = "~~~ foo(bar, baz, bat)" + good = """ + ~~~foo( + bar, + baz, + bat + ) + """ + assert_format bad, good, @short_length + + assert_same """ + not assert foo, + bar + """, @short_length + end + + test "inside do-end block" do + assert_same """ + if +value do + true + end + """ + end + end + + describe "binary without space" do + test "formats without spaces" do + assert_format "1 .. 2", "1..2" + end + + test "never breaks" do + assert_same "123_456_789..987_654_321", @short_length + end + end + + describe "binary without newline" do + test "formats without spaces" do + assert_same "1 in 2" + assert_format "1\\\\2", "1 \\\\ 2" + end + + test "never breaks" do + assert_same "123_456_789 in 987_654_321", @short_length + end + + test "not in" do + assert_format "not(foo in bar)", "foo not in bar" + assert_same "(not foo) in bar" + assert_same "(!foo) in bar" + end + end + + describe "binary operators with preceding new line" do + test "formats with spaces" do + assert_format "1|>2", "1 |> 2" + end + + test "breaks into new line" do + bad = "123_456_789 |> 987_654_321" + good = """ + 123_456_789 + |> 987_654_321 + """ + assert_format bad, good, @short_length + + bad = "123 |> foo(bar, baz)" + good = """ + 123 + |> foo( + bar, + baz + ) + """ + assert_format bad, good, @short_length + + bad = "123 |> foo(bar) |> bar(bat)" + good = """ + 123 + |> foo( + bar + ) + |> bar( + bat + ) + """ + assert_format bad, good, @short_length + + bad = "foo(bar, 123 |> bar(baz))" + good = """ + foo( + bar, + 123 + |> bar( + baz + ) + ) + """ + assert_format bad, good, @short_length + + bad = "foo(bar, baz) |> 123" + good = """ + foo( + bar, + baz + ) + |> 123 + """ + assert_format bad, good, @short_length + + bad = "foo(bar, baz) |> 123 |> 456" + good = """ + foo( + bar, + baz + ) + |> 123 + |> 456 + """ + assert_format bad, good, @short_length + + bad = "123 |> foo(bar, baz) |> 456" + good = """ + 123 + |> foo( + bar, + baz + ) + |> 456 + """ + assert_format bad, good, @short_length + end + + test "with multiple of the different entry and same precedence" do + assert_same "foo <|> bar ~> baz" + + bad = "foo <|> bar ~> baz" + good = """ + foo + <|> bar + ~> baz + """ + assert_format bad, good, @short_length + end + + test "preserves user choice even when it fits" do + assert_same """ + foo + |> bar + """ + + assert_same """ + foo = + one + |> two() + |> three() + """ + + bad = """ + foo |> + bar + """ + + good = """ + foo + |> bar + """ + + assert_format bad, good + end + end + + describe "binary with following new line" do + test "formats with spaces" do + assert_format "1++2", "1 ++ 2" + end + + test "breaks into new line" do + bad = "123_456_789 ++ 987_654_321" + good = """ + 123_456_789 ++ + 987_654_321 + """ + assert_format bad, good, @short_length + + bad = "123 ++ foo(bar)" + good = """ + 123 ++ + foo(bar) + """ + assert_format bad, good, @short_length + + bad = "123 ++ foo(bar, baz)" + good = """ + 123 ++ + foo( + bar, + baz + ) + """ + assert_format bad, good, @short_length + + bad = "foo(bar, 123 ++ bar(baz))" + good = """ + foo( + bar, + 123 ++ + bar( + baz + ) + ) + """ + assert_format bad, good, @short_length + + bad = "foo(bar, baz) ++ 123" + good = """ + foo( + bar, + baz + ) ++ 123 + """ + assert_format bad, good, @short_length + end + + test "with multiple of the same entry and left associative" do + assert_same "foo == bar == baz" + + bad = "a == b == c" + good = """ + a == b == + c + """ + assert_format bad, good, @short_length + + bad = "(a == (b == c))" + good = """ + a == + (b == c) + """ + assert_format bad, good, @short_length + + bad = "foo == bar == baz" + good = """ + foo == bar == + baz + """ + assert_format bad, good, @short_length + + bad = "(foo == (bar == baz))" + good = """ + foo == + (bar == + baz) + """ + assert_format bad, good, @short_length + end + + test "with multiple of the same entry and right associative" do + assert_same "foo ++ bar ++ baz" + + bad = "a ++ b ++ c" + good = """ + a ++ + b ++ c + """ + assert_format bad, good, @short_length + + bad = "((a ++ b) ++ c)" + good = """ + (a ++ b) ++ + c + """ + assert_format bad, good, @short_length + + bad = "foo ++ bar ++ baz" + good = """ + foo ++ + bar ++ + baz + """ + assert_format bad, good, @short_length + + bad = "((foo ++ bar) ++ baz)" + good = """ + (foo ++ + bar) ++ + baz + """ + assert_format bad, good, @short_length + end + + test "with precedence" do + assert_format "(a + b) == (c + d)", "a + b == c + d" + assert_format "a + (b == c) + d", "a + (b == c) + d" + + bad = "(a + b) == (c + d)" + good = """ + a + b == + c + d + """ + assert_format bad, good, @short_length + + bad = "a * (b + c) * d" + good = """ + a * + (b + c) * + d + """ + assert_format bad, good, @short_length + + bad = "(one + two) == (three + four)" + good = """ + one + two == + three + four + """ + assert_format bad, good, @medium_length + + bad = "one * (two + three) * four" + good = """ + one * (two + three) * + four + """ + assert_format bad, good, @medium_length + + bad = "one * (two + three + four) * five" + good = """ + one * + (two + three + + four) * five + """ + assert_format bad, good, @medium_length + + bad = "var = one * (two + three + four) * five" + good = """ + var = + one * + (two + three + + four) * five + """ + assert_format bad, good, @medium_length + end + + test "with required parens" do + assert_same "(a |> b) ++ (c |> d)" + assert_format "a + b |> c + d", "(a + b) |> (c + d)" + assert_format "a ++ b |> c ++ d", "(a ++ b) |> (c ++ d)" + assert_format "a |> b ++ c |> d", "a |> (b ++ c) |> d" + end + + test "with required parens skips on no parens" do + assert_same "1..2 ++ 3..4" + assert_same "1..2 |> 3..4" + end + + test "with logical operators" do + assert_same "a or b or c" + assert_format "a or b and c", "a or (b and c)" + assert_format "a and b or c", "(a and b) or c" + end + + test "mixed before and after lines" do + bad = "var :: a | b and c | d" + good = """ + var :: + a + | b and + c + | d + """ + assert_format bad, good, @short_length + + bad = "var :: a | b and c + d + e + f | g" + good = """ + var :: + a + | b and + c + d + e + f + | g + """ + assert_format bad, good, @medium_length + end + end + + # Theoretically it fits under operators but the goal of + # this section is to test common idioms. + describe "match" do + test "with calls" do + bad = "var = fun(one, two, three)" + good = """ + var = + fun( + one, + two, + three + ) + """ + assert_format bad, good, @short_length + + bad = "fun(one, two, three) = var" + good = """ + fun( + one, + two, + three + ) = var + """ + assert_format bad, good, @short_length + + bad = "fun(foo, bar) = fun(baz, bat)" + good = """ + fun( + foo, + bar + ) = + fun( + baz, + bat + ) + """ + assert_format bad, good, @short_length + + bad = "fun(foo, bar) = fun(baz, bat)" + good = """ + fun(foo, bar) = + fun(baz, bat) + """ + assert_format bad, good, @medium_length + end + + test "with containers" do + bad = "var = {one, two, three}" + good = """ + var = { + one, + two, + three + } + """ + assert_format bad, good, @short_length + + bad = "{one, two, three} = var" + good = """ + { + one, + two, + three + } = var + """ + assert_format bad, good, @short_length + + bad = "{one, two, three} = foo(bar, baz)" + good = """ + {one, two, three} = + foo(bar, baz) + """ + assert_format bad, good, @medium_length + end + + test "with heredoc" do + assert_same ~S""" + var = ''' + one + ''' + """, @short_length + + assert_same ~S""" + var = ''' + #{one} + ''' + """, @short_length + end + + test "with anonymous functions" do + bad = "var = fn arg1 -> body1; arg2 -> body2 end" + + good = """ + var = fn + arg1 -> + body1 + + arg2 -> + body2 + end + """ + assert_format bad, good, @short_length + + good = """ + var = fn + arg1 -> body1 + arg2 -> body2 + end + """ + assert_format bad, good, @medium_length + end + + test "with do-end blocks" do + assert_same """ + var = + case true do + foo -> bar + baz -> bat + end + """ + end + end + + describe "module attributes" do + test "when reading" do + assert_format "@ my_attribute", "@my_attribute" + end + + test "when setting" do + assert_format "@ my_attribute(:some_value)", "@my_attribute :some_value" + end + + test "doesn't split when reading on line limit" do + assert_same "@my_long_attribute", @short_length + end + + test "doesn't split when setting on line limit" do + assert_same "@my_long_attribute :some_value", @short_length + end + + test "with do-end block" do + assert_same """ + @attr (for x <- y do + z + end) + """ + end + + test "is parenthesized when setting inside a call" do + assert_same "my_fun(@foo(bar), baz)" + end + + test "fall back to @ as an operator when needed" do + assert_same "@(1 + 1)" + assert_same "@:foo" + assert_same "+@foo" + assert_same "@@foo" + assert_same "@(+foo)" + assert_same "!(@(1 + 1))" + assert_same "(@Foo).Baz" + assert_same "@bar(1, 2)" + + assert_format "@+1", "@(+1)" + assert_format "@Foo.Baz", "(@Foo).Baz" + assert_format "@(Foo.Bar).Baz", "(@(Foo.Bar)).Baz" + end + + test "with next break fits" do + assert_same """ + @doc ''' + foo + ''' + """, @short_length + + assert_same """ + @doc foo: ''' + bar + ''' + """, @short_length + end + + test "without next break fits" do + bad = "@really_long_expr foo + bar" + + good = """ + @really_long_expr foo + + bar + """ + + assert_format bad, good, @short_length + end + + test "with do end blocks" do + assert_same """ + @doc do + :ok + end + """, @short_length + + assert_same """ + use (@doc do + :end + end) + """, @short_length + end + end + + describe "capture" do + test "with integers" do + assert_same "&1" + assert_format "&(&1)", "& &1" + assert_format "&(&1.foo)", "& &1.foo" + end + + test "with operators inside" do + assert_format "&(+1)", "&+1" + assert_format "& a ++ b", "&(a ++ b)" + assert_format "& &1 && &2", "&(&1 && &2)" + assert_same "&(&1 | &2)" + end + + test "with operators outside" do + assert_same "(& &1) == (& &2)" + assert_same "[&IO.puts/1 | &IO.puts/2]" + end + + test "with call expressions" do + assert_format "& local(&1, &2)", "&local(&1, &2)" + end + + test "with blocks" do + bad = "&(1; 2)" + good = """ + &( + 1 + 2 + ) + """ + assert_format bad, good + end + + test "with no parens" do + assert_same """ + &assert foo, + bar + """, @short_length + end + + test "precedence when combined with calls" do + assert_same "(&Foo).Bar" + assert_format "&(Foo).Bar", "&Foo.Bar" + assert_format "&(Foo.Bar).Baz", "&Foo.Bar.Baz" + end + + test "local/arity" do + assert_format "&(foo/1)", "&foo/1" + assert_format "&(foo/bar)", "&(foo / bar)" + end + + test "operator/arity" do + assert_same "&+/2" + assert_same "&and/2" + assert_same "& &&/2" + end + + test "Module.remote/arity" do + assert_format "&(Mod.foo/1)", "&Mod.foo/1" + assert_format "&(Mod.++/1)", "&Mod.++/1" + assert_format ~s[&(Mod."foo bar"/1)], ~s[&Mod."foo bar"/1] + + # Invalid + assert_format "& Mod.foo/bar", "&(Mod.foo() / bar)" + + # This is "invalid" as a special form but we don't + # have enough knowledge to know that, so let's just + # make sure we format it properly with proper wrapping. + assert_same "&(1 + 2).foo/1" + + assert_same "&my_function.foo.bar/3", @short_length + end + end + + describe "when" do + test "with keywords" do + assert_same "foo when bar: :baz" + end + + test "with keywords on line breaks" do + bad = "foo when one: :two, three: :four" + good = """ + foo + when one: :two, + three: :four + """ + assert_format bad, good, @short_length + end + end +end diff --git a/lib/elixir/test/elixir/code_identifier_test.exs b/lib/elixir/test/elixir/code_identifier_test.exs new file mode 100644 index 00000000000..f838b99c481 --- /dev/null +++ b/lib/elixir/test/elixir/code_identifier_test.exs @@ -0,0 +1,6 @@ +Code.require_file "test_helper.exs", __DIR__ + +defmodule Code.IdentifierTest do + use ExUnit.Case, async: true + doctest Code.Identifier +end diff --git a/lib/elixir/test/elixir/test_helper.exs b/lib/elixir/test/elixir/test_helper.exs index 4a069da41df..f6c39efa127 100644 --- a/lib/elixir/test/elixir/test_helper.exs +++ b/lib/elixir/test/elixir/test_helper.exs @@ -71,3 +71,19 @@ defmodule PathHelpers do def redirect_std_err_on_win, do: "" end end + +defmodule CodeFormatterHelpers do + defmacro assert_same(good, opts \\ []) do + quote bind_quoted: [good: good, opts: opts] do + assert IO.iodata_to_binary(Code.format_string!(good, opts)) == String.trim(good) + end + end + + defmacro assert_format(bad, good, opts \\ []) do + quote bind_quoted: [bad: bad, good: good, opts: opts] do + result = String.trim(good) + assert IO.iodata_to_binary(Code.format_string!(bad, opts)) == result + assert IO.iodata_to_binary(Code.format_string!(good, opts)) == result + end + end +end