Skip to content

Commit bebd845

Browse files
authored
fix: infinite loop parsing incomplete struct (#66)
* fix: infinite loop parsing incomplete struct * fix: recover from invalid struct code * fix: assert equal instead of match
1 parent 792e9c9 commit bebd845

File tree

2 files changed

+284
-44
lines changed

2 files changed

+284
-44
lines changed

lib/spitfire.ex

Lines changed: 87 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1735,19 +1735,9 @@ defmodule Spitfire do
17351735

17361736
case prefix do
17371737
{left, parser} ->
1738-
terminals = [:eol, :eof, :"}", :")", :"]", :">>"]
1739-
1740-
{parser, is_valid} = validate_peek(parser, current_token_type(parser))
1741-
1742-
if is_valid do
1743-
while peek_token(parser) not in terminals && calc_prec(parser, associativity, precedence) <- {left, parser} do
1744-
case peek_token_type(parser) do
1745-
:. -> parse_dot_expression(next_token(parser), left)
1746-
_ -> {left, parser}
1747-
end
1748-
end
1749-
else
1750-
{left, parser}
1738+
while peek_token(parser) == :. &&
1739+
calc_prec(parser, associativity, precedence) <- {left, parser} do
1740+
parse_dot_expression(next_token(parser), left)
17511741
end
17521742

17531743
nil ->
@@ -1776,51 +1766,105 @@ defmodule Spitfire do
17761766
end
17771767
end
17781768

1769+
# Formats a struct type AST to a string for error messages
1770+
defp format_struct_type({:__aliases__, _, parts}) do
1771+
Enum.map_join(parts, ".", fn
1772+
part when is_atom(part) -> Atom.to_string(part)
1773+
{:__MODULE__, _, _} -> "__MODULE__"
1774+
{name, _, _} when is_atom(name) -> Atom.to_string(name)
1775+
_ -> "?"
1776+
end)
1777+
end
1778+
1779+
defp format_struct_type({:@, _, [{name, _, _}]}) do
1780+
"@#{name}"
1781+
end
1782+
1783+
defp format_struct_type({name, _, _}) when is_atom(name) do
1784+
Atom.to_string(name)
1785+
end
1786+
1787+
defp format_struct_type(_), do: nil
1788+
17791789
defp parse_struct_literal(%{current_token: {:%, _}} = parser) do
17801790
trace "parse_struct_literal", trace_meta(parser) do
17811791
meta = current_meta(parser)
17821792
parser = next_token(parser)
17831793
{type, parser} = parse_struct_type(parser)
17841794

1785-
parser = next_token(parser)
1795+
valid_type? = type != {:__block__, [], []}
1796+
struct_name = format_struct_type(type)
17861797

1787-
brace_meta = current_meta(parser)
1788-
parser = next_token(parser)
1798+
case peek_token(parser) do
1799+
:"{" ->
1800+
parser = next_token(parser)
1801+
brace_meta = current_meta(parser)
1802+
parser = next_token(parser)
17891803

1790-
newlines =
1791-
case current_newlines(parser) do
1792-
nil -> []
1793-
nl -> [newlines: nl]
1794-
end
1804+
newlines =
1805+
case current_newlines(parser) do
1806+
nil -> []
1807+
nl -> [newlines: nl]
1808+
end
17951809

1796-
parser = eat_eol(parser)
1810+
parser = eat_eol(parser)
1811+
old_nesting = parser.nesting
1812+
parser = Map.put(parser, :nesting, 0)
17971813

1798-
old_nesting = parser.nesting
1799-
parser = Map.put(parser, :nesting, 0)
1814+
if current_token(parser) == :"}" do
1815+
closing = current_meta(parser)
1816+
ast = {:%, meta, [type, {:%{}, newlines ++ [{:closing, closing} | brace_meta], []}]}
1817+
parser = Map.put(parser, :nesting, old_nesting)
1818+
{ast, parser}
1819+
else
1820+
{pairs, parser} = parse_comma_list(parser, @list_comma, false, true)
1821+
parser = eat_eol_at(parser, 1)
18001822

1801-
if current_token(parser) == :"}" do
1802-
closing = current_meta(parser)
1803-
ast = {:%, meta, [type, {:%{}, newlines ++ [{:closing, closing} | brace_meta], []}]}
1804-
parser = Map.put(parser, :nesting, old_nesting)
1805-
{ast, parser}
1806-
else
1807-
{pairs, parser} = parse_comma_list(parser, @list_comma, false, true)
1823+
parser =
1824+
case peek_token(parser) do
1825+
:"}" -> next_token(parser)
1826+
_ -> put_error(parser, {current_meta(parser), "missing closing brace for struct %#{struct_name}"})
1827+
end
18081828

1809-
parser = eat_eol_at(parser, 1)
1829+
closing = current_meta(parser)
1830+
ast = {:%, meta, [type, {:%{}, newlines ++ [{:closing, closing} | brace_meta], pairs}]}
1831+
parser = Map.put(parser, :nesting, old_nesting)
1832+
{ast, parser}
1833+
end
18101834

1811-
parser =
1812-
case peek_token(parser) do
1813-
:"}" ->
1814-
next_token(parser)
1835+
token when token in [:kw_identifier, :kw_identifier_unsafe, :identifier] and valid_type? ->
1836+
parser = put_error(parser, {current_meta(parser), "missing opening brace for struct %#{struct_name}"})
1837+
parser = next_token(parser)
1838+
brace_meta = current_meta(parser)
18151839

1816-
_ ->
1817-
put_error(parser, {current_meta(parser), "missing closing brace for struct"})
1818-
end
1840+
old_nesting = parser.nesting
1841+
parser = Map.put(parser, :nesting, 0)
18191842

1820-
closing = current_meta(parser)
1821-
ast = {:%, meta, [type, {:%{}, newlines ++ [{:closing, closing} | brace_meta], pairs}]}
1822-
parser = Map.put(parser, :nesting, old_nesting)
1823-
{ast, parser}
1843+
{pairs, parser} = parse_comma_list(parser, @list_comma, false, true)
1844+
parser = eat_eol_at(parser, 1)
1845+
1846+
{parser, closing_meta} =
1847+
case peek_token(parser) do
1848+
:"}" ->
1849+
parser = next_token(parser)
1850+
{parser, [{:closing, current_meta(parser)} | brace_meta]}
1851+
1852+
_ ->
1853+
parser = put_error(parser, {current_meta(parser), "missing closing brace for struct %#{struct_name}"})
1854+
{parser, brace_meta}
1855+
end
1856+
1857+
ast = {:%, meta, [type, {:%{}, closing_meta, pairs}]}
1858+
parser = Map.put(parser, :nesting, old_nesting)
1859+
{ast, parser}
1860+
1861+
_ ->
1862+
parser =
1863+
if valid_type?,
1864+
do: put_error(parser, {current_meta(parser), "missing opening brace for struct %#{struct_name}"}),
1865+
else: parser
1866+
1867+
{{:%, meta, [type, {:%{}, [], []}]}, parser}
18241868
end
18251869
end
18261870
end

test/spitfire_test.exs

Lines changed: 197 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2607,7 +2607,7 @@ defmodule SpitfireTest do
26072607
<% end %>
26082608
'''
26092609

2610-
assert Spitfire.parse(code) == {:error, :no_fuel_remaining}
2610+
assert {:error, _ast, _errors} = Spitfire.parse(code)
26112611
end
26122612

26132613
test "doesn't drop the cursor node" do
@@ -2883,6 +2883,202 @@ defmodule SpitfireTest do
28832883

28842884
assert {:error, _ast, [{[line: 1, column: 4], "missing closing parentheses"}]} = Spitfire.parse(code)
28852885
end
2886+
2887+
test "missing braces for struct" do
2888+
assert {:error,
2889+
{:=, [line: 1, column: 6],
2890+
[
2891+
{:%, [line: 1, column: 1],
2892+
[
2893+
{:__aliases__, [last: [line: 1, column: 2], line: 1, column: 2], [:Foo]},
2894+
{:%{}, [], []}
2895+
]},
2896+
{:x, [line: 1, column: 8], nil}
2897+
]},
2898+
[{[line: 1, column: 2], "missing opening brace for struct %Foo"}]} ==
2899+
Spitfire.parse("%Foo = x")
2900+
2901+
assert {:error,
2902+
{:%, [line: 1, column: 1],
2903+
[
2904+
{:__aliases__, [last: [line: 1, column: 2], line: 1, column: 2], [:Foo]},
2905+
{:%{}, [], []}
2906+
]},
2907+
[{[line: 1, column: 2], "missing opening brace for struct %Foo"}]} ==
2908+
Spitfire.parse("%Foo")
2909+
2910+
assert {:error,
2911+
{:=, [line: 1, column: 10],
2912+
[
2913+
{:%, [line: 1, column: 1],
2914+
[
2915+
{:__aliases__, [last: [line: 1, column: 6], line: 1, column: 2], [:Foo, :Bar]},
2916+
{:%{}, [], []}
2917+
]},
2918+
{:x, [line: 1, column: 12], nil}
2919+
]},
2920+
[{[line: 1, column: 6], "missing opening brace for struct %Foo.Bar"}]} ==
2921+
Spitfire.parse("%Foo.Bar = x")
2922+
2923+
assert {:error,
2924+
{:=, [line: 1, column: 13],
2925+
[
2926+
{:%, [line: 1, column: 1],
2927+
[
2928+
{:__aliases__, [last: [line: 1, column: 2], line: 1, column: 2], [:Foo]},
2929+
{:%{}, [closing: [line: 1, column: 11], line: 1, column: 6], [a: 42]}
2930+
]},
2931+
{:x, [line: 1, column: 15], nil}
2932+
]},
2933+
[{[line: 1, column: 2], "missing opening brace for struct %Foo"}]} ==
2934+
Spitfire.parse("%Foo a: 42} = x")
2935+
2936+
assert {:error,
2937+
{:%, [line: 1, column: 1],
2938+
[
2939+
{:__aliases__, [last: [line: 1, column: 2], line: 1, column: 2], [:Foo]},
2940+
{:%{}, [line: 1, column: 6], [a: 1]}
2941+
]},
2942+
[
2943+
{[line: 1, column: 2], "missing opening brace for struct %Foo"},
2944+
{[line: 1, column: 9], "missing closing brace for struct %Foo"}
2945+
]} == Spitfire.parse("%Foo a: 1")
2946+
2947+
assert {:error,
2948+
{:%, [line: 1, column: 1],
2949+
[
2950+
{:__aliases__, [last: [line: 1, column: 2], line: 1, column: 2], [:Foo]},
2951+
{:%{}, [closing: [line: 1, column: 16], line: 1, column: 6], [a: 1, b: 2]}
2952+
]},
2953+
[{[line: 1, column: 2], "missing opening brace for struct %Foo"}]} ==
2954+
Spitfire.parse("%Foo a: 1, b: 2}")
2955+
2956+
assert {:error,
2957+
{:%, [line: 1, column: 1],
2958+
[
2959+
{:__aliases__, [last: [line: 1, column: 2], line: 1, column: 2], [:Foo]},
2960+
{:%{}, [closing: [line: 1, column: 14], line: 1, column: 6],
2961+
[{:|, [line: 1, column: 8], [{:x, [line: 1, column: 6], nil}, [a: 1]]}]}
2962+
]},
2963+
[{[line: 1, column: 2], "missing opening brace for struct %Foo"}]} ==
2964+
Spitfire.parse("%Foo x | a: 1}")
2965+
2966+
assert {:error,
2967+
{:%, [line: 1, column: 1],
2968+
[
2969+
{:__aliases__, [last: [line: 1, column: 2], line: 1, column: 2], [:Foo]},
2970+
{:%{}, [line: 1, column: 6], [{:|, [line: 1, column: 8], [{:x, [line: 1, column: 6], nil}, [a: 1]]}]}
2971+
]},
2972+
[
2973+
{[line: 1, column: 2], "missing opening brace for struct %Foo"},
2974+
{[line: 1, column: 13], "missing closing brace for struct %Foo"}
2975+
]} == Spitfire.parse("%Foo x | a: 1")
2976+
2977+
assert {:error,
2978+
{:foo, [closing: [line: 1, column: 15], line: 1, column: 1],
2979+
[
2980+
{:%, [line: 1, column: 5],
2981+
[
2982+
{:__aliases__, [last: [line: 1, column: 6], line: 1, column: 6], [:Bar]},
2983+
{:%{}, [closing: [line: 1, column: 14], line: 1, column: 10], [a: 1]}
2984+
]}
2985+
]},
2986+
[{[line: 1, column: 6], "missing opening brace for struct %Bar"}]} ==
2987+
Spitfire.parse("foo(%Bar a: 1})")
2988+
2989+
assert {:error,
2990+
{:|>, [line: 1, column: 3],
2991+
[
2992+
{:x, [line: 1, column: 1], nil},
2993+
{:%, [line: 1, column: 6],
2994+
[
2995+
{:__aliases__, [last: [line: 1, column: 7], line: 1, column: 7], [:Foo]},
2996+
{:%{}, [closing: [line: 1, column: 15], line: 1, column: 11], [a: 1]}
2997+
]}
2998+
]},
2999+
[{[line: 1, column: 7], "missing opening brace for struct %Foo"}]} ==
3000+
Spitfire.parse("x |> %Foo a: 1}")
3001+
3002+
# Nested structs
3003+
assert {:error,
3004+
{:%, [line: 1, column: 1],
3005+
[
3006+
{:__aliases__, [last: [line: 1, column: 2], line: 1, column: 2], [:Outer]},
3007+
{:%{}, [closing: [line: 1, column: 26], line: 1, column: 7],
3008+
[
3009+
inner:
3010+
{:%, [line: 1, column: 15],
3011+
[
3012+
{:__aliases__, [last: [line: 1, column: 16], line: 1, column: 16], [:Inner]},
3013+
{:%{}, [closing: [line: 1, column: 26], line: 1, column: 22], [a: 1]}
3014+
]}
3015+
]}
3016+
]},
3017+
[
3018+
{[line: 1, column: 16], "missing opening brace for struct %Inner"},
3019+
{[line: 1, column: 26], "missing closing brace for struct %Outer"}
3020+
]} == Spitfire.parse("%Outer{inner: %Inner a: 1}")
3021+
3022+
assert {:error,
3023+
{:%, [line: 1, column: 1],
3024+
[
3025+
{:__aliases__, [last: [line: 1, column: 2], line: 1, column: 2], [:Outer]},
3026+
{:%{}, [closing: [line: 1, column: 27], line: 1, column: 8],
3027+
[
3028+
inner:
3029+
{:%, [line: 1, column: 15],
3030+
[
3031+
{:__aliases__, [last: [line: 1, column: 16], line: 1, column: 16], [:Inner]},
3032+
{:%{}, [closing: [line: 1, column: 26], line: 1, column: 21], [a: 1]}
3033+
]}
3034+
]}
3035+
]},
3036+
[{[line: 1, column: 2], "missing opening brace for struct %Outer"}]} ==
3037+
Spitfire.parse("%Outer inner: %Inner{a: 1}}")
3038+
3039+
assert {:error,
3040+
{:%, [line: 1, column: 1],
3041+
[
3042+
{:__aliases__, [last: [line: 1, column: 2], line: 1, column: 2], [:Outer]},
3043+
{:%{}, [closing: [line: 1, column: 27], line: 1, column: 8],
3044+
[
3045+
inner:
3046+
{:%, [line: 1, column: 15],
3047+
[
3048+
{:__aliases__, [last: [line: 1, column: 16], line: 1, column: 16], [:Inner]},
3049+
{:%{}, [closing: [line: 1, column: 26], line: 1, column: 22], [a: 1]}
3050+
]}
3051+
]}
3052+
]},
3053+
[
3054+
{[line: 1, column: 2], "missing opening brace for struct %Outer"},
3055+
{[line: 1, column: 16], "missing opening brace for struct %Inner"}
3056+
]} == Spitfire.parse("%Outer inner: %Inner a: 1}}")
3057+
3058+
# Module attribute struct
3059+
assert {:error,
3060+
{:%, [line: 1, column: 1],
3061+
[
3062+
{:@, [line: 1, column: 2], [{:foo, [line: 1, column: 3], nil}]},
3063+
{:%{}, [line: 1, column: 7], [a: 1]}
3064+
]},
3065+
[
3066+
{[line: 1, column: 3], "missing opening brace for struct %@foo"},
3067+
{[line: 1, column: 10], "missing closing brace for struct %@foo"}
3068+
]} == Spitfire.parse("%@foo a: 1")
3069+
3070+
# __MODULE__ struct
3071+
assert {:error,
3072+
{:%, [line: 1, column: 1],
3073+
[
3074+
{:__MODULE__, [line: 1, column: 2], nil},
3075+
{:%{}, [line: 1, column: 13], [a: 1]}
3076+
]},
3077+
[
3078+
{[line: 1, column: 2], "missing opening brace for struct %__MODULE__"},
3079+
{[line: 1, column: 16], "missing closing brace for struct %__MODULE__"}
3080+
]} == Spitfire.parse("%__MODULE__ a: 1")
3081+
end
28863082
end
28873083

28883084
describe "&parse_with_comments/2" do

0 commit comments

Comments
 (0)