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
130 changes: 87 additions & 43 deletions lib/spitfire.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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 ->
Expand Down Expand Up @@ -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
Expand Down
198 changes: 197 additions & 1 deletion test/spitfire_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading