Skip to content

Commit 8eba863

Browse files
committed
Properly track stab inside container_cursor_to_quoted, closes #13826
1 parent a7264aa commit 8eba863

File tree

5 files changed

+156
-52
lines changed

5 files changed

+156
-52
lines changed

lib/elixir/lib/code/fragment.ex

Lines changed: 93 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1099,6 +1099,13 @@ defmodule Code.Fragment do
10991099
iex> Code.Fragment.container_cursor_to_quoted("foo +")
11001100
{:ok, {:+, [line: 1], [{:foo, [line: 1], nil}, {:__cursor__, [line: 1], []}]}}
11011101
1102+
In order to parse the left-side of `->` properly, which appears both
1103+
in anonymous functions and do-end blocks, the trailing fragment option
1104+
must be given with the rest of the contents:
1105+
1106+
iex> Code.Fragment.container_cursor_to_quoted("fn x", trailing_fragment: " -> :ok end")
1107+
{:ok, {:fn, [line: 1], [{:->, [line: 1], [[{:__cursor__, [line: 1], []}], :ok]}]}}
1108+
11021109
## Options
11031110
11041111
* `:file` - the filename to be reported in case of parsing errors.
@@ -1121,46 +1128,108 @@ defmodule Code.Fragment do
11211128
* `:literal_encoder` - a function to encode literals in the AST.
11221129
See the documentation for `Code.string_to_quoted/2` for more information.
11231130
1131+
* `:trailing_fragment` (since v1.18.0) - the rest of the contents after
1132+
the cursor. This is necessary to correctly complete anonymous functions
1133+
and the left-hand side of `->`
1134+
11241135
"""
11251136
@doc since: "1.13.0"
11261137
@spec container_cursor_to_quoted(List.Chars.t(), keyword()) ::
11271138
{:ok, Macro.t()} | {:error, {location :: keyword, binary | {binary, binary}, binary}}
11281139
def container_cursor_to_quoted(fragment, opts \\ []) do
1140+
{trailing_fragment, opts} = Keyword.pop(opts, :trailing_fragment)
11291141
opts = Keyword.take(opts, [:columns, :token_metadata, :literal_encoder])
1130-
opts = [cursor_completion: true, emit_warnings: false] ++ opts
1142+
opts = [check_terminators: {:cursor, []}, emit_warnings: false] ++ opts
11311143

11321144
file = Keyword.get(opts, :file, "nofile")
11331145
line = Keyword.get(opts, :line, 1)
11341146
column = Keyword.get(opts, :column, 1)
11351147

11361148
case :elixir_tokenizer.tokenize(to_charlist(fragment), line, column, opts) do
1149+
{:ok, line, column, _warnings, rev_tokens, rev_terminators}
1150+
when trailing_fragment == nil ->
1151+
{rev_tokens, rev_terminators} =
1152+
with [close, open, {_, _, :__cursor__} = cursor | rev_tokens] <- rev_tokens,
1153+
{_, [_ | after_fn]} <- Enum.split_while(rev_terminators, &(elem(&1, 0) != :fn)),
1154+
true <- maybe_missing_stab?(rev_tokens),
1155+
[_ | rev_tokens] <- Enum.drop_while(rev_tokens, &(elem(&1, 0) != :fn)) do
1156+
{[close, open, cursor | rev_tokens], after_fn}
1157+
else
1158+
_ -> {rev_tokens, rev_terminators}
1159+
end
1160+
1161+
tokens = reverse_tokens(line, column, rev_tokens, rev_terminators)
1162+
:elixir.tokens_to_quoted(tokens, file, opts)
1163+
11371164
{:ok, line, column, _warnings, rev_tokens, rev_terminators} ->
1138-
tokens = :lists.reverse(rev_tokens, rev_terminators)
1139-
1140-
case :elixir.tokens_to_quoted(tokens, file, opts) do
1141-
{:ok, ast} ->
1142-
{:ok, ast}
1143-
1144-
{:error, error} ->
1145-
# In case parsing fails, we give it another shot but handling fn/do/else/catch/rescue/after.
1146-
tokens =
1147-
:lists.reverse(
1148-
rev_tokens,
1149-
[{:stab_op, {line, column, nil}, :->}, {nil, {line, column + 2, nil}}] ++
1150-
Enum.map(rev_terminators, fn tuple ->
1151-
{line, column, info} = elem(tuple, 1)
1152-
put_elem(tuple, 1, {line, column + 5, info})
1153-
end)
1154-
)
1155-
1156-
case :elixir.tokens_to_quoted(tokens, file, opts) do
1157-
{:ok, ast} -> {:ok, ast}
1158-
{:error, _} -> {:error, error}
1159-
end
1160-
end
1165+
tokens =
1166+
with {before_start, [_ | _] = after_start} <-
1167+
Enum.split_while(rev_terminators, &(elem(&1, 0) not in [:do, :fn])),
1168+
true <- maybe_missing_stab?(rev_tokens),
1169+
opts =
1170+
Keyword.put(opts, :check_terminators, {:cursor, before_start}),
1171+
{:error, {meta, _, ~c"end"}, _rest, _warnings, trailing_rev_tokens} <-
1172+
:elixir_tokenizer.tokenize(to_charlist(trailing_fragment), line, column, opts) do
1173+
trailing_tokens =
1174+
reverse_tokens(meta[:line], meta[:column], trailing_rev_tokens, after_start)
1175+
1176+
Enum.reverse(rev_tokens, drop_tokens(trailing_tokens, 0))
1177+
else
1178+
_ -> reverse_tokens(line, column, rev_tokens, rev_terminators)
1179+
end
1180+
1181+
:elixir.tokens_to_quoted(tokens, file, opts)
11611182

11621183
{:error, info, _rest, _warnings, _so_far} ->
11631184
{:error, :elixir.format_token_error(info)}
11641185
end
11651186
end
1187+
1188+
defp reverse_tokens(line, column, tokens, terminators) do
1189+
{terminators, _} =
1190+
Enum.map_reduce(terminators, column, fn {start, _, _}, column ->
1191+
atom = :elixir_tokenizer.terminator(start)
1192+
1193+
{{atom, {line, column, nil}}, column + length(Atom.to_charlist(atom))}
1194+
end)
1195+
1196+
Enum.reverse(tokens, terminators)
1197+
end
1198+
1199+
defp drop_tokens([{:"}", _} | _] = tokens, 0), do: tokens
1200+
defp drop_tokens([{:"]", _} | _] = tokens, 0), do: tokens
1201+
defp drop_tokens([{:")", _} | _] = tokens, 0), do: tokens
1202+
defp drop_tokens([{:">>", _} | _] = tokens, 0), do: tokens
1203+
defp drop_tokens([{:end, _} | _] = tokens, 0), do: tokens
1204+
defp drop_tokens([{:",", _} | _] = tokens, 0), do: tokens
1205+
defp drop_tokens([{:stab_op, _, :->} | _] = tokens, 0), do: tokens
1206+
1207+
defp drop_tokens([{:"}", _} | tokens], counter), do: drop_tokens(tokens, counter - 1)
1208+
defp drop_tokens([{:"]", _} | tokens], counter), do: drop_tokens(tokens, counter - 1)
1209+
defp drop_tokens([{:")", _} | tokens], counter), do: drop_tokens(tokens, counter - 1)
1210+
defp drop_tokens([{:">>", _} | tokens], counter), do: drop_tokens(tokens, counter - 1)
1211+
defp drop_tokens([{:end, _} | tokens], counter), do: drop_tokens(tokens, counter - 1)
1212+
1213+
defp drop_tokens([{:"{", _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
1214+
defp drop_tokens([{:"[", _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
1215+
defp drop_tokens([{:"(", _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
1216+
defp drop_tokens([{:"<<", _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
1217+
defp drop_tokens([{:fn, _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
1218+
defp drop_tokens([{:do, _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
1219+
1220+
defp drop_tokens([_ | tokens], counter), do: drop_tokens(tokens, counter)
1221+
defp drop_tokens([], 0), do: []
1222+
1223+
defp maybe_missing_stab?([{:after, _} | _]), do: true
1224+
defp maybe_missing_stab?([{:do, _} | _]), do: true
1225+
defp maybe_missing_stab?([{:fn, _} | _]), do: true
1226+
defp maybe_missing_stab?([{:else, _} | _]), do: true
1227+
defp maybe_missing_stab?([{:catch, _} | _]), do: true
1228+
defp maybe_missing_stab?([{:rescue, _} | _]), do: true
1229+
1230+
defp maybe_missing_stab?([{:stab_op, _, :->} | _]), do: false
1231+
defp maybe_missing_stab?([{:eol, _}, next | _]) when elem(next, 0) != :",", do: false
1232+
1233+
defp maybe_missing_stab?([_ | tail]), do: maybe_missing_stab?(tail)
1234+
defp maybe_missing_stab?([]), do: false
11661235
end

lib/elixir/src/elixir.erl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -460,8 +460,8 @@ quoted_to_erl(Quoted, ErlS, ExS, Env) ->
460460

461461
string_to_tokens(String, StartLine, StartColumn, File, Opts) when is_integer(StartLine), is_binary(File) ->
462462
case elixir_tokenizer:tokenize(String, StartLine, StartColumn, Opts) of
463-
{ok, _Line, _Column, [], Tokens, Terminators} ->
464-
{ok, lists:reverse(Tokens, Terminators)};
463+
{ok, _Line, _Column, [], Tokens, []} ->
464+
{ok, lists:reverse(Tokens)};
465465
{ok, _Line, _Column, Warnings, Tokens, Terminators} ->
466466
(lists:keyfind(emit_warnings, 1, Opts) /= {emit_warnings, false}) andalso
467467
[elixir_errors:erl_warn(L, File, M) || {L, M} <- lists:reverse(Warnings)],

lib/elixir/src/elixir_interpolation.erl

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ extract([$#, ${ | Rest], Buffer, Output, Line, Column, Scope, true, Last) ->
6464
{error, Reason};
6565
{ok, EndLine, EndColumn, Warnings, Tokens, Terminators} when Scope#elixir_tokenizer.cursor_completion /= false ->
6666
NewScope = Scope#elixir_tokenizer{warnings=Warnings, cursor_completion=noprune},
67-
Output2 = build_interpol(Line, Column, EndLine, EndColumn, lists:reverse(Tokens, Terminators), Output1),
67+
{CursorTerminators, _} = cursor_complete(EndLine, EndColumn, Terminators),
68+
Output2 = build_interpol(Line, Column, EndLine, EndColumn, lists:reverse(Tokens, CursorTerminators), Output1),
6869
extract([], [], Output2, EndLine, EndColumn, NewScope, true, Last);
6970
{ok, _, _, _, _, _} ->
7071
{error, {string, Line, Column, "missing interpolation terminator: \"}\"", []}}
@@ -117,6 +118,16 @@ strip_horizontal_space([H | T], Buffer, Counter) when H =:= $\s; H =:= $\t ->
117118
strip_horizontal_space(T, Buffer, Counter) ->
118119
{T, Buffer, Counter}.
119120

121+
cursor_complete(Line, Column, Terminators) ->
122+
lists:mapfoldl(
123+
fun({Start, _, _}, AccColumn) ->
124+
End = elixir_tokenizer:terminator(Start),
125+
{{End, {Line, AccColumn, nil}}, AccColumn + length(erlang:atom_to_list(End))}
126+
end,
127+
Column,
128+
Terminators
129+
).
130+
120131
%% Unescape a series of tokens as returned by extract.
121132

122133
unescape_tokens(Tokens) ->

lib/elixir/src/elixir_tokenizer.erl

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,9 @@ tokenize(String, Line, Column, Opts) ->
115115
Scope =
116116
lists:foldl(fun
117117
({check_terminators, false}, Acc) ->
118-
Acc#elixir_tokenizer{terminators=none};
119-
({cursor_completion, true}, Acc) ->
120-
Acc#elixir_tokenizer{cursor_completion=prune_and_cursor};
118+
Acc#elixir_tokenizer{cursor_completion=false, terminators=none};
119+
({check_terminators, {cursor, Terminators}}, Acc) ->
120+
Acc#elixir_tokenizer{cursor_completion=prune_and_cursor, terminators=Terminators};
121121
({existing_atoms_only, ExistingAtomsOnly}, Acc) when is_boolean(ExistingAtomsOnly) ->
122122
Acc#elixir_tokenizer{existing_atoms_only=ExistingAtomsOnly};
123123
({static_atoms_encoder, StaticAtomsEncoder}, Acc) when is_function(StaticAtomsEncoder) ->
@@ -138,11 +138,10 @@ tokenize(String, Line, Opts) ->
138138
tokenize([], Line, Column, #elixir_tokenizer{cursor_completion=Cursor} = Scope, Tokens) when Cursor /= false ->
139139
#elixir_tokenizer{ascii_identifiers_only=Ascii, terminators=Terminators, warnings=Warnings} = Scope,
140140

141-
{CursorColumn, CursorTerminators, AccTokens} =
141+
{CursorColumn, AccTerminators, AccTokens} =
142142
add_cursor(Line, Column, Cursor, Terminators, Tokens),
143143

144144
AllWarnings = maybe_unicode_lint_warnings(Ascii, Tokens, Warnings),
145-
{AccTerminators, _AccColumn} = cursor_complete(Line, CursorColumn, CursorTerminators),
146145
{ok, Line, CursorColumn, AllWarnings, AccTokens, AccTerminators};
147146

148147
tokenize([], EndLine, EndColumn, #elixir_tokenizer{terminators=[{Start, {StartLine, StartColumn, _}, _} | _]} = Scope, Tokens) ->
@@ -1747,16 +1746,6 @@ error(Reason, Rest, #elixir_tokenizer{warnings=Warnings}, Tokens) ->
17471746

17481747
%% Cursor handling
17491748

1750-
cursor_complete(Line, Column, Terminators) ->
1751-
lists:mapfoldl(
1752-
fun({Start, _, _}, AccColumn) ->
1753-
End = terminator(Start),
1754-
{{End, {Line, AccColumn, nil}}, AccColumn + length(erlang:atom_to_list(End))}
1755-
end,
1756-
Column,
1757-
Terminators
1758-
).
1759-
17601749
add_cursor(_Line, Column, noprune, Terminators, Tokens) ->
17611750
{Column, Terminators, Tokens};
17621751
add_cursor(Line, Column, prune_and_cursor, Terminators, Tokens) ->

lib/elixir/test/elixir/code_fragment_test.exs

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1228,7 +1228,9 @@ defmodule CodeFragmentTest do
12281228
assert cc2q!("foo(bar do baz ") == s2q!("foo(bar do baz(__cursor__()) end)")
12291229
assert cc2q!("foo(bar do baz(") == s2q!("foo(bar do baz(__cursor__()) end)")
12301230
assert cc2q!("foo(bar do baz bat,") == s2q!("foo(bar do baz(bat, __cursor__()) end)")
1231-
assert cc2q!("foo(bar do baz, bat") == s2q!("foo(bar do baz, __cursor__() -> nil end)")
1231+
1232+
assert cc2q!("foo(bar do baz, bat", trailing_fragment: " -> :ok end") ==
1233+
s2q!("foo(bar do baz, __cursor__() -> :ok end)")
12321234
end
12331235

12341236
test "keyword lists" do
@@ -1283,6 +1285,48 @@ defmodule CodeFragmentTest do
12831285
assert cc2q!("<<foo, bar::baz") == s2q!("<<foo, bar::__cursor__()>>")
12841286
end
12851287

1288+
test "anonymous functions" do
1289+
assert cc2q!("(fn", trailing_fragment: "-> end)") == s2q!("(fn __cursor__() -> nil end)")
1290+
1291+
assert cc2q!("(fn", trailing_fragment: "-> 1 + 2 end)") ==
1292+
s2q!("(fn __cursor__() -> 1 + 2 end)")
1293+
1294+
assert cc2q!("(fn x", trailing_fragment: "-> :ok end)") ==
1295+
s2q!("(fn __cursor__() -> :ok end)")
1296+
1297+
assert cc2q!("(fn x", trailing_fragment: ", y -> :ok end)") ==
1298+
s2q!("(fn __cursor__(), y -> :ok end)")
1299+
1300+
assert cc2q!("(fn x,", trailing_fragment: "y -> :ok end)") ==
1301+
s2q!("(fn x, __cursor__() -> :ok end)")
1302+
1303+
assert cc2q!("(fn x,", trailing_fragment: "\ny -> :ok end)") ==
1304+
s2q!("(fn x, __cursor__()\n -> :ok end)")
1305+
1306+
assert cc2q!("(fn x, {", trailing_fragment: "y, z} -> :ok end)") ==
1307+
s2q!("(fn x, {__cursor__(), z} -> :ok end)")
1308+
1309+
assert cc2q!("(fn x, {y", trailing_fragment: ", z} -> :ok end)") ==
1310+
s2q!("(fn x, {__cursor__(), z} -> :ok end)")
1311+
1312+
assert cc2q!("(fn x, {y, ", trailing_fragment: "z} -> :ok end)") ==
1313+
s2q!("(fn x, {y, __cursor__()} -> :ok end)")
1314+
1315+
assert cc2q!("(fn x ->", trailing_fragment: ":ok end)") ==
1316+
s2q!("(fn x -> __cursor__() end)")
1317+
1318+
assert cc2q!("(fn x ->", trailing_fragment: ":ok end)") ==
1319+
s2q!("(fn x -> __cursor__() end)")
1320+
1321+
assert cc2q!("(fn") == s2q!("(__cursor__())")
1322+
assert cc2q!("(fn x") == s2q!("(__cursor__())")
1323+
assert cc2q!("(fn x,") == s2q!("(__cursor__())")
1324+
assert cc2q!("(fn x ->") == s2q!("(fn x -> __cursor__() end)")
1325+
assert cc2q!("(fn x -> x") == s2q!("(fn x -> __cursor__() end)")
1326+
assert cc2q!("(fn x, y -> x + y") == s2q!("(fn x, y -> x + __cursor__() end)")
1327+
assert cc2q!("(fn x, y -> x + y end") == s2q!("(__cursor__())")
1328+
end
1329+
12861330
test "removes tokens until opening" do
12871331
assert cc2q!("(123") == s2q!("(__cursor__())")
12881332
assert cc2q!("[foo") == s2q!("[__cursor__()]")
@@ -1299,15 +1343,6 @@ defmodule CodeFragmentTest do
12991343
assert cc2q!("foo bar, :atom") == s2q!("foo(bar, __cursor__())")
13001344
end
13011345

1302-
test "removes anonymous functions" do
1303-
assert cc2q!("(fn") == s2q!("(fn __cursor__() -> nil end)")
1304-
assert cc2q!("(fn x") == s2q!("(fn __cursor__() -> nil end)")
1305-
assert cc2q!("(fn x ->") == s2q!("(fn x -> __cursor__() end)")
1306-
assert cc2q!("(fn x -> x") == s2q!("(fn x -> __cursor__() end)")
1307-
assert cc2q!("(fn x, y -> x + y") == s2q!("(fn x, y -> x + __cursor__() end)")
1308-
assert cc2q!("(fn x, y -> x + y end") == s2q!("(__cursor__())")
1309-
end
1310-
13111346
test "removes closed terminators" do
13121347
assert cc2q!("foo([1, 2, 3]") == s2q!("foo(__cursor__())")
13131348
assert cc2q!("foo({1, 2, 3}") == s2q!("foo(__cursor__())")

0 commit comments

Comments
 (0)