Skip to content

Commit 0513a5d

Browse files
authored
fix: recover from incomplete keyword lists (#71)
1 parent bebd845 commit 0513a5d

File tree

2 files changed

+54
-14
lines changed

2 files changed

+54
-14
lines changed

lib/spitfire.ex

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -653,13 +653,7 @@ defmodule Spitfire do
653653

654654
{value, parser} = parse_expression(parser, @kw_identifier, false, false, false)
655655

656-
{kvs, parser} =
657-
while2 peek_token(parser) == :"," <- parser do
658-
parser = parser |> next_token() |> next_token()
659-
{pair, parser} = parse_kw_identifier(parser)
660-
661-
{pair, parser}
662-
end
656+
{kvs, parser} = parse_kw_list_continuation(parser)
663657

664658
{[{token, value} | kvs], parser}
665659
end
@@ -680,18 +674,39 @@ defmodule Spitfire do
680674

681675
{value, parser} = parse_expression(parser, @kw_identifier, false, false, false)
682676

683-
{kvs, parser} =
684-
while2 peek_token(parser) == :"," <- parser do
685-
parser = parser |> next_token() |> next_token()
686-
{pair, parser} = parse_kw_identifier(parser)
687-
688-
{pair, parser}
689-
end
677+
{kvs, parser} = parse_kw_list_continuation(parser)
690678

691679
{[{atom, value} | kvs], parser}
692680
end
693681
end
694682

683+
defp parse_kw_list_continuation(parser) do
684+
while2 peek_token(parser) == :"," <- parser do
685+
parser = parser |> next_token() |> next_token()
686+
687+
case current_token_type(parser) do
688+
type when type in [:kw_identifier, :kw_identifier_unsafe] ->
689+
parse_kw_identifier(parser)
690+
691+
_ ->
692+
parser =
693+
if match?({:paren_identifier, _, :__cursor__}, parser.current_token) do
694+
parser
695+
else
696+
put_error(
697+
parser,
698+
{current_meta(parser),
699+
"unexpected expression after keyword list. Keyword lists must always come as the last argument. " <>
700+
"Therefore, this is not allowed:\n\n function_call(1, some: :option, 2)\n\n" <>
701+
"Instead, wrap the keyword in brackets:\n\n function_call(1, [some: :option], 2)"}
702+
)
703+
end
704+
705+
parse_expression(parser, @kw_identifier, true, false, false)
706+
end
707+
end
708+
end
709+
695710
defp parse_assoc_op(%{current_token: {:assoc_op, _, _token}} = parser, key) do
696711
trace "parse_assoc_op", trace_meta(parser) do
697712
assoc_meta = current_meta(parser)

test/spitfire_test.exs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2844,6 +2844,31 @@ defmodule SpitfireTest do
28442844
assert {:ok, _} = Spitfire.parse(code)
28452845
end
28462846

2847+
test "unexpected expression after keyword list" do
2848+
assert {:error, _ast, errors} = Spitfire.parse(~S|foo(a: 1, b)|)
2849+
2850+
assert Enum.any?(errors, fn {_, msg} ->
2851+
String.contains?(msg, "unexpected expression after keyword list")
2852+
end)
2853+
2854+
assert {:error, _ast, errors} = Spitfire.parse(~S|foo(a: 1, :bar)|)
2855+
2856+
assert Enum.any?(errors, fn {_, msg} ->
2857+
String.contains?(msg, "unexpected expression after keyword list")
2858+
end)
2859+
2860+
assert {:error, _ast, errors} = Spitfire.parse(~S|@tag foo: bar, baz|)
2861+
2862+
assert Enum.any?(errors, fn {_, msg} ->
2863+
String.contains?(msg, "unexpected expression after keyword list")
2864+
end)
2865+
end
2866+
2867+
test "__cursor__ after keyword list does not crash" do
2868+
code = ~S|foo(a: 1, __cursor__())|
2869+
assert {:ok, {:foo, _, [[{:a, 1}, {:__cursor__, _, []}]]}} = Spitfire.parse(code)
2870+
end
2871+
28472872
test "weird characters" do
28482873
code = """
28492874
[«]

0 commit comments

Comments
 (0)