Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions lib/elixir/lib/code/normalizer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
defmodule Code.Normalizer do
@moduledoc false

@do_end_keywords [:rescue, :catch, :else, :after]

defguard is_literal(x)
when is_integer(x) or
is_float(x) or
Expand Down Expand Up @@ -347,18 +349,20 @@ defmodule Code.Normalizer do
args = normalize_args(args, %{state | parent_meta: meta})
{form, meta, args}

Keyword.has_key?(meta, :do) ->
Keyword.has_key?(meta, :do) and kw_blocks?(last) ->
# def foo do :ok end
# def foo, do: :ok
normalize_kw_blocks(form, meta, args, state)

match?([{:do, _} | _], last) and Keyword.keyword?(last) ->
match?([{:do, _} | _], last) and kw_blocks?(last) ->
# Non normalized kw blocks
line = state.parent_meta[:line] || meta[:line]
meta = meta ++ [do: [line: line], end: [line: line]]
normalize_kw_blocks(form, meta, args, state)

true ->
# The formatter renders do-end blocks from the meta alone
meta = Keyword.drop(meta, [:do, :end])
args = normalize_args(args, %{state | parent_meta: meta})
{last_arg, leading_args} = List.pop_at(args, -1, [])

Expand Down Expand Up @@ -397,6 +401,17 @@ defmodule Code.Normalizer do
defp block_keyword?([]), do: true
defp block_keyword?(_), do: false

# Anything after the do block that is not a block keyword makes it a keyword list
defp kw_blocks?([{:do, _} | rest] = kw) do
Keyword.keyword?(kw) and Enum.all?(rest, &match?({key, _} when key in @do_end_keywords, &1))
end

defp kw_blocks?([{{:__block__, _, [:do]}, _} | rest]) do
Enum.all?(rest, &match?({{:__block__, _, [key]}, _} when key in @do_end_keywords, &1))
end

defp kw_blocks?(_), do: false

defp allow_keyword?(:when, 2), do: true
defp allow_keyword?(:{}, _), do: false
defp allow_keyword?(op, arity), do: not is_atom(op) or not Macro.operator?(op, arity)
Expand Down
56 changes: 56 additions & 0 deletions lib/elixir/test/elixir/code_normalizer/quoted_ast_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,62 @@ defmodule Code.Normalizer.QuotedASTTest do
assert quoted_to_string(quote(do: foo |> [bar: :baz])) == "foo |> [bar: :baz]"
end

test "keyword args that start with do:" do
assert quoted_to_string(quote(do: foo(do: a, bar: b))) == "foo(do: a, bar: b)"
assert quoted_to_string(quote(do: foo(x, do: a, bar: b))) == "foo(x, do: a, bar: b)"
assert quoted_to_string(quote(do: foo(bar: b, do: a))) == "foo(bar: b, do: a)"

assert quoted_to_string({:foo, [], [[do: 1, do: 2]]}) == "foo(do: 1, do: 2)"
assert quoted_to_string({:foo, [], [[rescue: 1]]}) == "foo(rescue: 1)"

assert quoted_to_string({:foo, [], [[do: {:__block__, [], [1, 2]}, bar: 3]]}) ==
"foo(\n do:\n (\n 1\n 2\n ),\n bar: 3\n)"

assert quoted_to_string(quote(do: foo(do: a))) == "foo do\n a\nend"
assert quoted_to_string(quote(do: foo(do: a, else: b))) == "foo do\n a\nelse\n b\nend"
assert quoted_to_string(quote(do: foo(do: a, catch: b))) == "foo do\n a\ncatch\n b\nend"
assert quoted_to_string(quote(do: foo(do: a, after: b))) == "foo do\n a\nafter\n b\nend"

assert quoted_to_string(quote(do: foo(do: a, rescue: b, after: c))) ==
"foo do\n a\nrescue\n b\nafter\n c\nend"

assert quoted_to_string({:case, [], [{:x, [], nil}, [do: [{:->, [], [[1], 2]}], other: 9]]}) ==
"case x, do: (1 -> 2), other: 9"

assert quoted_to_string(quote(do: receive(do: (x -> x), after: (100 -> nil)))) ==
"receive do\n x -> x\nafter\n 100 -> nil\nend"
end

test "keyword args that start with do: with do/end in the metadata" do
meta = [do: [line: 1], end: [line: 1]]

assert quoted_to_string({:foo, meta, [[do: 1, bar: 2]]}) == "foo(do: 1, bar: 2)"
assert quoted_to_string({:foo, meta, [[do: 1, else: 2]]}) == "foo do\n 1\nelse\n 2\nend"
end

test "keyword args that start with do: are not forced into do-end blocks" do
assert quoted_to_string({:foo, [], [[do: 1, bar: 2]]}, force_do_end_blocks: true) ==
"foo(do: 1, bar: 2)"

assert quoted_to_string({:foo, [], [[do: 1, else: 2]]}, force_do_end_blocks: true) ==
"foo do\n 1\nelse\n 2\nend"
end

test "keyword args that start with do: round-trip through the parser" do
asts = [
quote(do: foo(do: a, bar: b)),
quote(do: for(x <- y, do: x, into: "")),
quote(do: foo(do: a, else: b)),
{:foo, [], [[do: 1, do: 2]]},
{:foo, [do: [line: 1], end: [line: 1]], [[do: 1, bar: 2]]}
]

for ast <- asts do
string = quoted_to_string(ast)
assert string |> Code.string_to_quoted!() |> quoted_to_string() == string
end
end
Comment thread
josevalim marked this conversation as resolved.

test "keyword arg with cursor" do
input = "def foo, do: :bar, __cursor__()"
expected = "def foo, [{:do, :bar}, __cursor__()]"
Expand Down
Loading