Skip to content

Commit 6359193

Browse files
committed
feat: even more error handling
1 parent 5e1f96b commit 6359193

File tree

2 files changed

+178
-25
lines changed

2 files changed

+178
-25
lines changed

lib/spitfire.ex

Lines changed: 72 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ defmodule Spitfire do
175175
:")" -> parser
176176
:"]" -> parser
177177
:"}" -> parser
178+
:end -> parser
178179
_ -> next_token(parser)
179180
end
180181

@@ -313,15 +314,17 @@ defmodule Spitfire do
313314
defp parse_comma_list(parser, opts \\ []) do
314315
opts = Keyword.put(opts, :precedence, @comma)
315316
{expr, parser} = parse_expression(parser, opts)
316-
items = [expr]
317+
# we zip together the expression and parser state so that we can potentially
318+
# backtrack later
319+
items = [{expr, parser}]
317320

318321
{items, parser} =
319322
while peek_token(parser) == :"," <- {items, parser} do
320323
parser = parser |> next_token() |> next_token()
321324

322325
{item, parser} = parse_expression(parser, opts)
323326

324-
{[item | items], parser}
327+
{[{item, parser} | items], parser}
325328
end
326329

327330
{Enum.reverse(items), parser}
@@ -472,6 +475,7 @@ defmodule Spitfire do
472475
defp parse_comma(parser, lhs) do
473476
parser = parser |> next_token() |> eat_eol()
474477
{exprs, parser} = parse_comma_list(parser)
478+
{exprs, _} = Enum.unzip(exprs)
475479

476480
{{:comma, [], [lhs | exprs]}, eat_eol(parser)}
477481
end
@@ -654,8 +658,6 @@ defmodule Spitfire do
654658

655659
parser = dec_stab_depth(parser)
656660

657-
dbg(parser)
658-
659661
parser =
660662
case peek_token(parser) do
661663
:end ->
@@ -676,6 +678,7 @@ defmodule Spitfire do
676678
{ast, next_token(parser)}
677679
else
678680
{pairs, parser} = parse_comma_list(parser |> next_token() |> eat_eol())
681+
{pairs, _} = Enum.unzip(pairs)
679682
ast = {{:., [], [lhs]}, [], pairs}
680683

681684
{ast, parser |> next_token() |> eat_eol()}
@@ -802,6 +805,7 @@ defmodule Spitfire do
802805
{{:%{}, meta, []}, parser}
803806
else
804807
{pairs, parser} = parse_comma_list(parser, is_map: true)
808+
{pairs, _} = Enum.unzip(pairs)
805809

806810
parser = eat_at(parser, :eol, 1)
807811

@@ -820,29 +824,71 @@ defmodule Spitfire do
820824

821825
defp parse_tuple_literal(%{current_token: {:"{", _}} = parser) do
822826
meta = current_meta(parser)
827+
orig_parser = parser
823828
parser = parser |> next_token() |> eat_eol()
824829

825-
if current_token(parser) == :"}" do
826-
{{:{}, meta, []}, parser}
827-
else
828-
{pairs, parser} = parse_comma_list(parser)
830+
cond do
831+
current_token(parser) == :"}" ->
832+
{{:{}, meta, []}, parser}
829833

830-
parser = eat_at(parser, :eol, 1)
834+
current_token(parser) in [:end, :"]", :")"] ->
835+
# if the current token is the wrong kind of ending delimiter, we revert to the previous parser
836+
# state, put an error, and inject a closing brace to simulate a completed tuple
837+
parser = put_error(orig_parser, {meta, "missing closing brace for tuple"})
831838

832-
parser =
833-
case peek_token(parser) do
834-
:"}" ->
835-
parser |> next_token() |> eat_eol()
839+
parser = next_token(parser)
836840

837-
_ ->
838-
put_error(parser, {current_meta(parser), "missing closing brace for tuple"})
839-
end
841+
parser =
842+
parser
843+
|> put_in([:current_token], {:fake_closing_brace, nil})
844+
|> put_in([:peek_token], parser.current_token)
845+
|> update_in([:tokens], &[parser.peek_token | &1])
840846

841-
if length(pairs) == 2 do
842-
{pairs |> List.wrap() |> List.to_tuple(), parser}
843-
else
844-
{{:{}, meta, List.wrap(pairs)}, parser}
845-
end
847+
{{:{}, meta, []}, parser}
848+
849+
true ->
850+
{pairs, parser} = parse_comma_list(parser)
851+
852+
parser = eat_at(parser, :eol, 1)
853+
854+
{pairs, parser} =
855+
case peek_token(parser) do
856+
:"}" ->
857+
pairs = pairs |> Enum.unzip() |> elem(0)
858+
{pairs, parser |> next_token() |> eat_eol()}
859+
860+
_ ->
861+
[{potential_error, parser}, {item, parser_for_errors} | rest] = all_pairs = Enum.reverse(pairs)
862+
863+
# if the last item is an unknown token error, that means that it parsed past the
864+
# recovery point and we need to insert a fake closing brace, and backtrack
865+
# the errors from the previous item.
866+
{pairs, parser} =
867+
case potential_error do
868+
{:__error__, _, ["unknown token: " <> _]} ->
869+
{[{item, parser} | rest],
870+
parser
871+
|> put_in([:current_token], {:fake_closing_brace, nil})
872+
|> put_in([:peek_token], parser.current_token)
873+
|> put_in([:errors], parser_for_errors.errors)
874+
|> update_in([:tokens], &[parser.peek_token | &1])}
875+
876+
_ ->
877+
{all_pairs, parser}
878+
end
879+
880+
parser = put_error(parser, {meta, "missing closing brace for tuple"})
881+
882+
{pairs, _} = pairs |> Enum.reverse() |> Enum.unzip()
883+
884+
{pairs, parser}
885+
end
886+
887+
if length(pairs) == 2 do
888+
{pairs |> List.wrap() |> List.to_tuple(), parser}
889+
else
890+
{{:{}, meta, List.wrap(pairs)}, parser}
891+
end
846892
end
847893
end
848894

@@ -853,6 +899,7 @@ defmodule Spitfire do
853899
{[], parser}
854900
else
855901
{pairs, parser} = parse_comma_list(parser, is_list: true)
902+
{pairs, _} = Enum.unzip(pairs)
856903

857904
parser = eat_at(parser, :eol, 1)
858905

@@ -889,6 +936,8 @@ defmodule Spitfire do
889936
|> eat_eol()
890937
|> parse_comma_list()
891938

939+
{pairs, _} = Enum.unzip(pairs)
940+
892941
parser = eat_at(parser, :eol, 1)
893942

894943
parser =
@@ -974,7 +1023,8 @@ defmodule Spitfire do
9741023
parser = pop_nesting(parser)
9751024

9761025
if parser.nestings == [] && current_token(parser) == :do do
977-
parse_do_block(parser, {token, meta, Enum.reverse(args)})
1026+
res = parse_do_block(parser, {token, meta, Enum.reverse(args)})
1027+
res
9781028
else
9791029
{{token, meta, Enum.reverse(args)}, parser}
9801030
end
@@ -1093,8 +1143,6 @@ defmodule Spitfire do
10931143
end
10941144

10951145
def eat_at(%{tokens: tokens} = parser, token, idx) when is_list(tokens) do
1096-
dbg()
1097-
10981146
tokens =
10991147
case Enum.at(tokens, idx) do
11001148
{^token, _, _} ->

test/spitfire_test.exs

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2391,7 +2391,7 @@ defmodule SpitfireTest do
23912391

23922392
assert Spitfire.parse(code) ==
23932393
{:error, {1, {:++, [line: 1, column: 8], [2, [4]]}},
2394-
[{[line: 1, column: 13], "missing closing brace for tuple"}]}
2394+
[{[line: 1, column: 2], "missing closing brace for tuple"}]}
23952395
end
23962396

23972397
test "missing closing map brace" do
@@ -2553,5 +2553,110 @@ defmodule SpitfireTest do
25532553
]
25542554
}
25552555
end
2556+
2557+
test "example from github issue" do
2558+
code = ~S'''
2559+
defmodule Foo do
2560+
import Baz
2561+
2562+
def bat do
2563+
var = 123
2564+
{
2565+
end
2566+
2567+
def local_function do
2568+
# ...
2569+
end
2570+
end
2571+
'''
2572+
2573+
assert {:error, _ast, _} = result = Spitfire.parse(code)
2574+
2575+
assert result ==
2576+
{:error,
2577+
{:defmodule, [do: [line: 1, column: 15], end: [line: 12, column: 1], line: 1, column: 1],
2578+
[
2579+
{:__aliases__, [line: 1, column: 11], [:Foo]},
2580+
[
2581+
do:
2582+
{:__block__, [],
2583+
[
2584+
{:import, [line: 2, column: 3], [{:__aliases__, [line: 2, column: 10], [:Baz]}]},
2585+
{:def, [do: [line: 4, column: 11], end: [line: 7, column: 3], line: 4, column: 3],
2586+
[
2587+
{:bat, [line: 4, column: 7], Elixir},
2588+
[
2589+
do:
2590+
{:__block__, [],
2591+
[
2592+
{:=, [line: 5, column: 9], [{:var, [line: 5, column: 5], Elixir}, 123]},
2593+
{:{}, [line: 6, column: 5], []}
2594+
]}
2595+
]
2596+
]},
2597+
{:def, [do: [line: 9, column: 22], end: [line: 11, column: 3], line: 9, column: 3],
2598+
[{:local_function, [line: 9, column: 7], Elixir}, [do: {:__block__, [], []}]]}
2599+
]}
2600+
]
2601+
]}, [{[line: 6, column: 5], "missing closing brace for tuple"}]}
2602+
end
2603+
2604+
test "example from github issue with tuple elements" do
2605+
code = ~S'''
2606+
defmodule Foo do
2607+
import Baz
2608+
2609+
def bat do
2610+
var = 123
2611+
{var,
2612+
end
2613+
2614+
def local_function do
2615+
# ...
2616+
end
2617+
end
2618+
'''
2619+
2620+
assert {:error, _ast, _} = result = Spitfire.parse(code)
2621+
2622+
assert result ==
2623+
{
2624+
:error,
2625+
{
2626+
:defmodule,
2627+
[do: [line: 1, column: 15], end: [line: 12, column: 1], line: 1, column: 1],
2628+
[
2629+
{:__aliases__, [line: 1, column: 11], [:Foo]},
2630+
[
2631+
do: {
2632+
:__block__,
2633+
[],
2634+
[
2635+
{:import, [line: 2, column: 3], [{:__aliases__, [line: 2, column: 10], [:Baz]}]},
2636+
{
2637+
:def,
2638+
[do: [line: 4, column: 11], end: [line: 7, column: 3], line: 4, column: 3],
2639+
[
2640+
{:bat, [line: 4, column: 7], Elixir},
2641+
[
2642+
do:
2643+
{:__block__, [],
2644+
[
2645+
{:=, [line: 5, column: 9], [{:var, [line: 5, column: 5], Elixir}, 123]},
2646+
{:{}, [line: 6, column: 5], [{:var, [line: 6, column: 6], Elixir}]}
2647+
]}
2648+
]
2649+
]
2650+
},
2651+
{:def, [do: [line: 9, column: 22], end: [line: 11, column: 3], line: 9, column: 3],
2652+
[{:local_function, [line: 9, column: 7], Elixir}, [do: {:__block__, [], []}]]}
2653+
]
2654+
}
2655+
]
2656+
]
2657+
},
2658+
[{[line: 6, column: 5], "missing closing brace for tuple"}]
2659+
}
2660+
end
25562661
end
25572662
end

0 commit comments

Comments
 (0)