diff --git a/lib/spitfire.ex b/lib/spitfire.ex index a109164..2e3aebb 100644 --- a/lib/spitfire.ex +++ b/lib/spitfire.ex @@ -1735,19 +1735,9 @@ defmodule Spitfire do case prefix do {left, parser} -> - terminals = [:eol, :eof, :"}", :")", :"]", :">>"] - - {parser, is_valid} = validate_peek(parser, current_token_type(parser)) - - if is_valid do - while peek_token(parser) not in terminals && calc_prec(parser, associativity, precedence) <- {left, parser} do - case peek_token_type(parser) do - :. -> parse_dot_expression(next_token(parser), left) - _ -> {left, parser} - end - end - else - {left, parser} + while peek_token(parser) == :. && + calc_prec(parser, associativity, precedence) <- {left, parser} do + parse_dot_expression(next_token(parser), left) end nil -> @@ -1776,51 +1766,105 @@ defmodule Spitfire do end end + # Formats a struct type AST to a string for error messages + defp format_struct_type({:__aliases__, _, parts}) do + Enum.map_join(parts, ".", fn + part when is_atom(part) -> Atom.to_string(part) + {:__MODULE__, _, _} -> "__MODULE__" + {name, _, _} when is_atom(name) -> Atom.to_string(name) + _ -> "?" + end) + end + + defp format_struct_type({:@, _, [{name, _, _}]}) do + "@#{name}" + end + + defp format_struct_type({name, _, _}) when is_atom(name) do + Atom.to_string(name) + end + + defp format_struct_type(_), do: nil + defp parse_struct_literal(%{current_token: {:%, _}} = parser) do trace "parse_struct_literal", trace_meta(parser) do meta = current_meta(parser) parser = next_token(parser) {type, parser} = parse_struct_type(parser) - parser = next_token(parser) + valid_type? = type != {:__block__, [], []} + struct_name = format_struct_type(type) - brace_meta = current_meta(parser) - parser = next_token(parser) + case peek_token(parser) do + :"{" -> + parser = next_token(parser) + brace_meta = current_meta(parser) + parser = next_token(parser) - newlines = - case current_newlines(parser) do - nil -> [] - nl -> [newlines: nl] - end + newlines = + case current_newlines(parser) do + nil -> [] + nl -> [newlines: nl] + end - parser = eat_eol(parser) + parser = eat_eol(parser) + old_nesting = parser.nesting + parser = Map.put(parser, :nesting, 0) - old_nesting = parser.nesting - parser = Map.put(parser, :nesting, 0) + if current_token(parser) == :"}" do + closing = current_meta(parser) + ast = {:%, meta, [type, {:%{}, newlines ++ [{:closing, closing} | brace_meta], []}]} + parser = Map.put(parser, :nesting, old_nesting) + {ast, parser} + else + {pairs, parser} = parse_comma_list(parser, @list_comma, false, true) + parser = eat_eol_at(parser, 1) - if current_token(parser) == :"}" do - closing = current_meta(parser) - ast = {:%, meta, [type, {:%{}, newlines ++ [{:closing, closing} | brace_meta], []}]} - parser = Map.put(parser, :nesting, old_nesting) - {ast, parser} - else - {pairs, parser} = parse_comma_list(parser, @list_comma, false, true) + parser = + case peek_token(parser) do + :"}" -> next_token(parser) + _ -> put_error(parser, {current_meta(parser), "missing closing brace for struct %#{struct_name}"}) + end - parser = eat_eol_at(parser, 1) + closing = current_meta(parser) + ast = {:%, meta, [type, {:%{}, newlines ++ [{:closing, closing} | brace_meta], pairs}]} + parser = Map.put(parser, :nesting, old_nesting) + {ast, parser} + end - parser = - case peek_token(parser) do - :"}" -> - next_token(parser) + token when token in [:kw_identifier, :kw_identifier_unsafe, :identifier] and valid_type? -> + parser = put_error(parser, {current_meta(parser), "missing opening brace for struct %#{struct_name}"}) + parser = next_token(parser) + brace_meta = current_meta(parser) - _ -> - put_error(parser, {current_meta(parser), "missing closing brace for struct"}) - end + old_nesting = parser.nesting + parser = Map.put(parser, :nesting, 0) - closing = current_meta(parser) - ast = {:%, meta, [type, {:%{}, newlines ++ [{:closing, closing} | brace_meta], pairs}]} - parser = Map.put(parser, :nesting, old_nesting) - {ast, parser} + {pairs, parser} = parse_comma_list(parser, @list_comma, false, true) + parser = eat_eol_at(parser, 1) + + {parser, closing_meta} = + case peek_token(parser) do + :"}" -> + parser = next_token(parser) + {parser, [{:closing, current_meta(parser)} | brace_meta]} + + _ -> + parser = put_error(parser, {current_meta(parser), "missing closing brace for struct %#{struct_name}"}) + {parser, brace_meta} + end + + ast = {:%, meta, [type, {:%{}, closing_meta, pairs}]} + parser = Map.put(parser, :nesting, old_nesting) + {ast, parser} + + _ -> + parser = + if valid_type?, + do: put_error(parser, {current_meta(parser), "missing opening brace for struct %#{struct_name}"}), + else: parser + + {{:%, meta, [type, {:%{}, [], []}]}, parser} end end end diff --git a/test/spitfire_test.exs b/test/spitfire_test.exs index a8fa2e4..6a81ef4 100644 --- a/test/spitfire_test.exs +++ b/test/spitfire_test.exs @@ -2607,7 +2607,7 @@ defmodule SpitfireTest do <% end %> ''' - assert Spitfire.parse(code) == {:error, :no_fuel_remaining} + assert {:error, _ast, _errors} = Spitfire.parse(code) end test "doesn't drop the cursor node" do @@ -2883,6 +2883,202 @@ defmodule SpitfireTest do assert {:error, _ast, [{[line: 1, column: 4], "missing closing parentheses"}]} = Spitfire.parse(code) end + + test "missing braces for struct" do + assert {:error, + {:=, [line: 1, column: 6], + [ + {:%, [line: 1, column: 1], + [ + {:__aliases__, [last: [line: 1, column: 2], line: 1, column: 2], [:Foo]}, + {:%{}, [], []} + ]}, + {:x, [line: 1, column: 8], nil} + ]}, + [{[line: 1, column: 2], "missing opening brace for struct %Foo"}]} == + Spitfire.parse("%Foo = x") + + assert {:error, + {:%, [line: 1, column: 1], + [ + {:__aliases__, [last: [line: 1, column: 2], line: 1, column: 2], [:Foo]}, + {:%{}, [], []} + ]}, + [{[line: 1, column: 2], "missing opening brace for struct %Foo"}]} == + Spitfire.parse("%Foo") + + assert {:error, + {:=, [line: 1, column: 10], + [ + {:%, [line: 1, column: 1], + [ + {:__aliases__, [last: [line: 1, column: 6], line: 1, column: 2], [:Foo, :Bar]}, + {:%{}, [], []} + ]}, + {:x, [line: 1, column: 12], nil} + ]}, + [{[line: 1, column: 6], "missing opening brace for struct %Foo.Bar"}]} == + Spitfire.parse("%Foo.Bar = x") + + assert {:error, + {:=, [line: 1, column: 13], + [ + {:%, [line: 1, column: 1], + [ + {:__aliases__, [last: [line: 1, column: 2], line: 1, column: 2], [:Foo]}, + {:%{}, [closing: [line: 1, column: 11], line: 1, column: 6], [a: 42]} + ]}, + {:x, [line: 1, column: 15], nil} + ]}, + [{[line: 1, column: 2], "missing opening brace for struct %Foo"}]} == + Spitfire.parse("%Foo a: 42} = x") + + assert {:error, + {:%, [line: 1, column: 1], + [ + {:__aliases__, [last: [line: 1, column: 2], line: 1, column: 2], [:Foo]}, + {:%{}, [line: 1, column: 6], [a: 1]} + ]}, + [ + {[line: 1, column: 2], "missing opening brace for struct %Foo"}, + {[line: 1, column: 9], "missing closing brace for struct %Foo"} + ]} == Spitfire.parse("%Foo a: 1") + + assert {:error, + {:%, [line: 1, column: 1], + [ + {:__aliases__, [last: [line: 1, column: 2], line: 1, column: 2], [:Foo]}, + {:%{}, [closing: [line: 1, column: 16], line: 1, column: 6], [a: 1, b: 2]} + ]}, + [{[line: 1, column: 2], "missing opening brace for struct %Foo"}]} == + Spitfire.parse("%Foo a: 1, b: 2}") + + assert {:error, + {:%, [line: 1, column: 1], + [ + {:__aliases__, [last: [line: 1, column: 2], line: 1, column: 2], [:Foo]}, + {:%{}, [closing: [line: 1, column: 14], line: 1, column: 6], + [{:|, [line: 1, column: 8], [{:x, [line: 1, column: 6], nil}, [a: 1]]}]} + ]}, + [{[line: 1, column: 2], "missing opening brace for struct %Foo"}]} == + Spitfire.parse("%Foo x | a: 1}") + + assert {:error, + {:%, [line: 1, column: 1], + [ + {:__aliases__, [last: [line: 1, column: 2], line: 1, column: 2], [:Foo]}, + {:%{}, [line: 1, column: 6], [{:|, [line: 1, column: 8], [{:x, [line: 1, column: 6], nil}, [a: 1]]}]} + ]}, + [ + {[line: 1, column: 2], "missing opening brace for struct %Foo"}, + {[line: 1, column: 13], "missing closing brace for struct %Foo"} + ]} == Spitfire.parse("%Foo x | a: 1") + + assert {:error, + {:foo, [closing: [line: 1, column: 15], line: 1, column: 1], + [ + {:%, [line: 1, column: 5], + [ + {:__aliases__, [last: [line: 1, column: 6], line: 1, column: 6], [:Bar]}, + {:%{}, [closing: [line: 1, column: 14], line: 1, column: 10], [a: 1]} + ]} + ]}, + [{[line: 1, column: 6], "missing opening brace for struct %Bar"}]} == + Spitfire.parse("foo(%Bar a: 1})") + + assert {:error, + {:|>, [line: 1, column: 3], + [ + {:x, [line: 1, column: 1], nil}, + {:%, [line: 1, column: 6], + [ + {:__aliases__, [last: [line: 1, column: 7], line: 1, column: 7], [:Foo]}, + {:%{}, [closing: [line: 1, column: 15], line: 1, column: 11], [a: 1]} + ]} + ]}, + [{[line: 1, column: 7], "missing opening brace for struct %Foo"}]} == + Spitfire.parse("x |> %Foo a: 1}") + + # Nested structs + assert {:error, + {:%, [line: 1, column: 1], + [ + {:__aliases__, [last: [line: 1, column: 2], line: 1, column: 2], [:Outer]}, + {:%{}, [closing: [line: 1, column: 26], line: 1, column: 7], + [ + inner: + {:%, [line: 1, column: 15], + [ + {:__aliases__, [last: [line: 1, column: 16], line: 1, column: 16], [:Inner]}, + {:%{}, [closing: [line: 1, column: 26], line: 1, column: 22], [a: 1]} + ]} + ]} + ]}, + [ + {[line: 1, column: 16], "missing opening brace for struct %Inner"}, + {[line: 1, column: 26], "missing closing brace for struct %Outer"} + ]} == Spitfire.parse("%Outer{inner: %Inner a: 1}") + + assert {:error, + {:%, [line: 1, column: 1], + [ + {:__aliases__, [last: [line: 1, column: 2], line: 1, column: 2], [:Outer]}, + {:%{}, [closing: [line: 1, column: 27], line: 1, column: 8], + [ + inner: + {:%, [line: 1, column: 15], + [ + {:__aliases__, [last: [line: 1, column: 16], line: 1, column: 16], [:Inner]}, + {:%{}, [closing: [line: 1, column: 26], line: 1, column: 21], [a: 1]} + ]} + ]} + ]}, + [{[line: 1, column: 2], "missing opening brace for struct %Outer"}]} == + Spitfire.parse("%Outer inner: %Inner{a: 1}}") + + assert {:error, + {:%, [line: 1, column: 1], + [ + {:__aliases__, [last: [line: 1, column: 2], line: 1, column: 2], [:Outer]}, + {:%{}, [closing: [line: 1, column: 27], line: 1, column: 8], + [ + inner: + {:%, [line: 1, column: 15], + [ + {:__aliases__, [last: [line: 1, column: 16], line: 1, column: 16], [:Inner]}, + {:%{}, [closing: [line: 1, column: 26], line: 1, column: 22], [a: 1]} + ]} + ]} + ]}, + [ + {[line: 1, column: 2], "missing opening brace for struct %Outer"}, + {[line: 1, column: 16], "missing opening brace for struct %Inner"} + ]} == Spitfire.parse("%Outer inner: %Inner a: 1}}") + + # Module attribute struct + assert {:error, + {:%, [line: 1, column: 1], + [ + {:@, [line: 1, column: 2], [{:foo, [line: 1, column: 3], nil}]}, + {:%{}, [line: 1, column: 7], [a: 1]} + ]}, + [ + {[line: 1, column: 3], "missing opening brace for struct %@foo"}, + {[line: 1, column: 10], "missing closing brace for struct %@foo"} + ]} == Spitfire.parse("%@foo a: 1") + + # __MODULE__ struct + assert {:error, + {:%, [line: 1, column: 1], + [ + {:__MODULE__, [line: 1, column: 2], nil}, + {:%{}, [line: 1, column: 13], [a: 1]} + ]}, + [ + {[line: 1, column: 2], "missing opening brace for struct %__MODULE__"}, + {[line: 1, column: 16], "missing closing brace for struct %__MODULE__"} + ]} == Spitfire.parse("%__MODULE__ a: 1") + end end describe "&parse_with_comments/2" do