Skip to content

Commit d23e42e

Browse files
committed
Normalize token missing and mismatched delimiter exceptions
Closes #13183. Closes #13185. Closes #13186. Closes #13187.
1 parent 872efb1 commit d23e42e

File tree

6 files changed

+187
-148
lines changed

6 files changed

+187
-148
lines changed

lib/elixir/lib/exception.ex

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -795,12 +795,10 @@ defmodule Exception do
795795
end
796796

797797
@doc false
798-
def format_expected_delimiter(opening_delimiter) do
799-
terminator = :elixir_tokenizer.terminator(opening_delimiter)
800-
801-
if terminator |> Atom.to_string() |> String.contains?(["\"", "'"]),
802-
do: terminator,
803-
else: ~s("#{terminator}")
798+
def format_delimiter(delimiter) do
799+
if delimiter |> Atom.to_string() |> String.contains?(["\"", "'"]),
800+
do: delimiter,
801+
else: ~s("#{delimiter}")
804802
end
805803

806804
@doc false
@@ -1159,8 +1157,23 @@ defmodule MismatchedDelimiterError do
11591157
An exception raised when a mismatched delimiter is found when parsing code.
11601158
11611159
For example:
1162-
- `[1, 2, 3}`
1163-
- `fn a -> )`
1160+
1161+
* `[1, 2, 3}`
1162+
* `fn a -> )`
1163+
1164+
The following fields of this exceptions are public and can be accessed freely:
1165+
1166+
* `:file` (`t:Path.t/0` or `nil`) - the file where the error occurred, or `nil` if
1167+
the error occurred in code that did not come from a file
1168+
* `:line` - the line for the opening delimiter
1169+
* `:column` - the column for the opening delimiter
1170+
* `:end_line` - the line for the mismatched closing delimiter
1171+
* `:end_column` - the column for the mismatched closing delimiter
1172+
* `:opening_delimiter` - an atom representing the opening delimiter
1173+
* `:closing_delimiter` - an atom representing the mismatched closing delimiter
1174+
* `:expected_delimiter` - an atom representing the closing delimiter
1175+
* `:description` - a description of the mismatched delimiter error
1176+
11641177
"""
11651178

11661179
defexception [
@@ -1172,6 +1185,7 @@ defmodule MismatchedDelimiterError do
11721185
:end_column,
11731186
:opening_delimiter,
11741187
:closing_delimiter,
1188+
:expected_delimiter,
11751189
:snippet,
11761190
description: "mismatched delimiter error"
11771191
]
@@ -1184,15 +1198,14 @@ defmodule MismatchedDelimiterError do
11841198
end_column: end_column,
11851199
line_offset: line_offset,
11861200
description: description,
1187-
opening_delimiter: opening_delimiter,
1188-
closing_delimiter: _closing_delimiter,
1201+
expected_delimiter: expected_delimiter,
11891202
file: file,
11901203
snippet: snippet
11911204
}) do
11921205
start_pos = {start_line, start_column}
11931206
end_pos = {end_line, end_column}
11941207
lines = String.split(snippet, "\n")
1195-
expected_delimiter = Exception.format_expected_delimiter(opening_delimiter)
1208+
expected_delimiter = Exception.format_delimiter(expected_delimiter)
11961209

11971210
start_message = "└ unclosed delimiter"
11981211
end_message = ~s/└ mismatched closing delimiter (expected #{expected_delimiter})/
@@ -1226,8 +1239,9 @@ defmodule SyntaxError do
12261239
12271240
* `:file` (`t:Path.t/0` or `nil`) - the file where the error occurred, or `nil` if
12281241
the error occurred in code that did not come from a file
1229-
* `:line` (`t:non_neg_integer/0`) - the line where the error occurred
1230-
* `:column` (`t:non_neg_integer/0`) - the column where the error occurred
1242+
* `:line` - the line where the error occurred
1243+
* `:column` - the column where the error occurred
1244+
* `:description` - a description of the syntax error
12311245
12321246
"""
12331247

@@ -1276,19 +1290,25 @@ defmodule TokenMissingError do
12761290
12771291
* `:file` (`t:Path.t/0` or `nil`) - the file where the error occurred, or `nil` if
12781292
the error occurred in code that did not come from a file
1279-
* `:line` (`t:non_neg_integer/0`) - the line where the error occurred
1280-
* `:column` (`t:non_neg_integer/0`) - the column where the error occurred
1281-
1293+
* `:line` - the line for the opening delimiter
1294+
* `:column` - the column for the opening delimiter
1295+
* `:end_line` - the line for the end of the string
1296+
* `:end_column` - the column for the end of the string
1297+
* `:opening_delimiter` - an atom representing the opening delimiter
1298+
* `:expected_delimiter` - an atom representing the expected delimiter
1299+
* `:description` - a description of the missing token error
12821300
"""
12831301

12841302
defexception [
12851303
:file,
12861304
:line,
12871305
:column,
12881306
:end_line,
1307+
:end_column,
12891308
:line_offset,
12901309
:snippet,
12911310
:opening_delimiter,
1311+
:expected_delimiter,
12921312
description: "expression is incomplete"
12931313
]
12941314

@@ -1300,7 +1320,7 @@ defmodule TokenMissingError do
13001320
end_line: end_line,
13011321
line_offset: line_offset,
13021322
description: description,
1303-
opening_delimiter: opening_delimiter,
1323+
expected_delimiter: expected_delimiter,
13041324
snippet: snippet
13051325
})
13061326
when not is_nil(snippet) and not is_nil(column) and not is_nil(end_line) do
@@ -1315,7 +1335,7 @@ defmodule TokenMissingError do
13151335

13161336
start_pos = {line, column}
13171337
end_pos = {end_line, end_column}
1318-
expected_delimiter = Exception.format_expected_delimiter(opening_delimiter)
1338+
expected_delimiter = Exception.format_delimiter(expected_delimiter)
13191339

13201340
start_message = ~s/└ unclosed delimiter/
13211341
end_message = ~s/└ missing closing delimiter (expected #{expected_delimiter})/

lib/elixir/lib/kernel/parallel_compiler.ex

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -890,7 +890,7 @@ defmodule Kernel.ParallelCompiler do
890890
end
891891

892892
defp to_error(file, kind, reason, stack) do
893-
line = get_line(file, reason, stack)
893+
{line, span} = get_line_span(file, reason, stack)
894894
file = Path.absname(file)
895895
message = :unicode.characters_to_binary(Kernel.CLI.format_error(kind, reason, stack))
896896

@@ -900,43 +900,55 @@ defmodule Kernel.ParallelCompiler do
900900
message: message,
901901
severity: :error,
902902
stacktrace: stack,
903-
span: nil,
903+
span: span,
904904
exception: get_exception(reason)
905905
}
906906
end
907907

908908
defp get_exception(exception) when is_exception(exception), do: exception
909909
defp get_exception(_reason), do: nil
910910

911-
defp get_line(_file, %{line: line, column: column}, _stack)
911+
defp get_line_span(
912+
_file,
913+
%{line: line, column: column, end_line: end_line, end_column: end_column},
914+
_stack
915+
)
916+
when is_integer(line) and line > 0 and is_integer(column) and column >= 0 and
917+
is_integer(end_line) and end_line > 0 and is_integer(end_column) and end_column >= 0 do
918+
{{line, column}, {end_line, end_column}}
919+
end
920+
921+
defp get_line_span(_file, %{line: line, column: column}, _stack)
912922
when is_integer(line) and line > 0 and is_integer(column) and column >= 0 do
913-
{line, column}
923+
{{line, column}, nil}
914924
end
915925

916-
defp get_line(_file, %{line: line}, _stack) when is_integer(line) and line > 0 do
917-
line
926+
defp get_line_span(_file, %{line: line}, _stack) when is_integer(line) and line > 0 do
927+
{line, nil}
918928
end
919929

920-
defp get_line(file, :undef, [{_, _, _, []}, {_, _, _, info} | _]) do
921-
if Keyword.get(info, :file) == to_charlist(Path.relative_to_cwd(file)) do
922-
Keyword.get(info, :line)
923-
end
930+
defp get_line_span(file, :undef, [{_, _, _, []}, {_, _, _, info} | _]) do
931+
get_line_span_from_stacktrace_info(info, file)
924932
end
925933

926-
defp get_line(file, _reason, [{_, _, _, [file: expanding]}, {_, _, _, info} | _])
934+
defp get_line_span(file, _reason, [{_, _, _, [file: expanding]}, {_, _, _, info} | _])
927935
when expanding in [~c"expanding macro", ~c"expanding struct"] do
928-
if Keyword.get(info, :file) == to_charlist(Path.relative_to_cwd(file)) do
929-
Keyword.get(info, :line)
930-
end
936+
get_line_span_from_stacktrace_info(info, file)
931937
end
932938

933-
defp get_line(file, _reason, [{_, _, _, info} | _]) do
934-
if Keyword.get(info, :file) == to_charlist(Path.relative_to_cwd(file)) do
935-
Keyword.get(info, :line)
936-
end
939+
defp get_line_span(file, _reason, [{_, _, _, info} | _]) do
940+
get_line_span_from_stacktrace_info(info, file)
941+
end
942+
943+
defp get_line_span(_, _, _) do
944+
{nil, nil}
937945
end
938946

939-
defp get_line(_, _, _) do
940-
nil
947+
defp get_line_span_from_stacktrace_info(info, file) do
948+
if Keyword.get(info, :file) == to_charlist(Path.relative_to_cwd(file)) do
949+
{Keyword.get(info, :line), nil}
950+
else
951+
{nil, nil}
952+
end
941953
end
942954
end

lib/elixir/src/elixir_tokenizer.erl

Lines changed: 31 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -142,18 +142,20 @@ tokenize([], Line, Column, #elixir_tokenizer{cursor_completion=Cursor} = Scope,
142142
AccTokens = cursor_complete(Line, CursorColumn, CursorTerminators, CursorTokens),
143143
{ok, Line, Column, AllWarnings, AccTokens};
144144

145-
tokenize([], EndLine, _, #elixir_tokenizer{terminators=[{Start, {StartLine, StartColumn, _}, _} | _]} = Scope, Tokens) ->
145+
tokenize([], EndLine, EndColumn, #elixir_tokenizer{terminators=[{Start, {StartLine, StartColumn, _}, _} | _]} = Scope, Tokens) ->
146146
End = terminator(Start),
147147
Hint = missing_terminator_hint(Start, End, Scope),
148148
Message = "missing terminator: ~ts",
149149
Formatted = io_lib:format(Message, [End]),
150150
Meta = [
151-
{error_type, unclosed_delimiter},
152-
{opening_delimiter, Start},
153-
{line, StartLine},
154-
{column, StartColumn},
155-
{end_line, EndLine}
156-
],
151+
{error_type, unclosed_delimiter},
152+
{opening_delimiter, Start},
153+
{expected_delimiter, End},
154+
{line, StartLine},
155+
{column, StartColumn},
156+
{end_line, EndLine},
157+
{end_column, EndColumn}
158+
],
157159
error({Meta, [Formatted, Hint], []}, [], Scope, Tokens);
158160

159161
tokenize([], Line, Column, #elixir_tokenizer{} = Scope, Tokens) ->
@@ -531,7 +533,7 @@ tokenize([$:, H | T] = Original, Line, Column, Scope, Tokens) when ?is_quote(H)
531533

532534
{error, Reason} ->
533535
Message = " (for atom starting at line ~B)",
534-
interpolation_error(Reason, Original, Scope, Tokens, Message, [Line])
536+
interpolation_error(Reason, Original, Scope, Tokens, Message, [Line], Line, Column + 1, [H], [H])
535537
end;
536538

537539
tokenize([$: | String] = Original, Line, Column, Scope, Tokens) ->
@@ -772,7 +774,7 @@ handle_heredocs(T, Line, Column, H, Scope, Tokens) ->
772774
handle_strings(T, Line, Column, H, Scope, Tokens) ->
773775
case elixir_interpolation:extract(Line, Column, Scope, true, T, H) of
774776
{error, Reason} ->
775-
interpolation_error(Reason, [H | T], Scope, Tokens, " (for string starting at line ~B)", [Line]);
777+
interpolation_error(Reason, [H | T], Scope, Tokens, " (for string starting at line ~B)", [Line], Line, Column-1, [H], [H]);
776778

777779
{NewLine, NewColumn, Parts, [$: | Rest], InterScope} when ?is_space(hd(Rest)) ->
778780
NewScope = case is_unnecessary_quote(Parts, InterScope) of
@@ -925,7 +927,7 @@ handle_dot([$., H | T] = Original, Line, Column, DotInfo, Scope, Tokens) when ?i
925927
Message = "interpolation is not allowed when calling function/macro. Found interpolation in a call starting with: ",
926928
error({?LOC(Line, Column), Message, [H]}, Rest, NewScope, Tokens);
927929
{error, Reason} ->
928-
interpolation_error(Reason, Original, Scope, Tokens, " (for function name starting at line ~B)", [Line])
930+
interpolation_error(Reason, Original, Scope, Tokens, " (for function name starting at line ~B)", [Line], Line, Column, [H], [H])
929931
end;
930932

931933
handle_dot([$. | Rest], Line, Column, DotInfo, Scope, Tokens) ->
@@ -1030,16 +1032,7 @@ extract_heredoc_with_interpolation(Line, Column, Scope, Interpol, T, H) ->
10301032
{ok, NewLine, NewColumn, tokens_to_binary(Parts2), Rest, NewScope};
10311033

10321034
{error, Reason} ->
1033-
{Position, Message, List} = interpolation_format(Reason, " (for heredoc starting at line ~B)", [Line]),
1034-
{line, EndLine} = lists:keyfind(line, 1, Position),
1035-
Meta = [
1036-
{error_type, unclosed_delimiter},
1037-
{opening_delimiter, '"""'},
1038-
{line, Line},
1039-
{column, Column},
1040-
{end_line, EndLine}
1041-
],
1042-
{error, {Meta, Message, List}}
1035+
{error, interpolation_format(Reason, " (for heredoc starting at line ~B)", [Line], Line, Column, [H, H, H], [H, H, H])}
10431036
end;
10441037

10451038
error ->
@@ -1352,12 +1345,21 @@ previous_was_eol(_) -> nil.
13521345

13531346
%% Error handling
13541347

1355-
interpolation_error(Reason, Rest, Scope, Tokens, Extension, Args) ->
1356-
error(interpolation_format(Reason, Extension, Args), Rest, Scope, Tokens).
1348+
interpolation_error(Reason, Rest, Scope, Tokens, Extension, Args, Line, Column, Opening, Closing) ->
1349+
error(interpolation_format(Reason, Extension, Args, Line, Column, Opening, Closing), Rest, Scope, Tokens).
13571350

1358-
interpolation_format({string, Line, Column, Message, Token}, Extension, Args) ->
1359-
{?LOC(Line, Column), [Message, io_lib:format(Extension, Args)], Token};
1360-
interpolation_format({_, _, _} = Reason, _Extension, _Args) ->
1351+
interpolation_format({string, EndLine, EndColumn, Message, Token}, Extension, Args, Line, Column, Opening, Closing) ->
1352+
Meta = [
1353+
{error_type, unclosed_delimiter},
1354+
{opening_delimiter, list_to_atom(Opening)},
1355+
{expected_delimiter, list_to_atom(Closing)},
1356+
{line, Line},
1357+
{column, Column},
1358+
{end_line, EndLine},
1359+
{end_column, EndColumn}
1360+
],
1361+
{Meta, [Message, io_lib:format(Extension, Args)], Token};
1362+
interpolation_format({_, _, _} = Reason, _Extension, _Args, _Line, _Column, _Opening, _Closing) ->
13611363
Reason.
13621364

13631365
%% Terminators
@@ -1429,15 +1431,16 @@ check_terminator({End, {EndLine, EndColumn, _}}, [{Start, {StartLine, StartColum
14291431
End ->
14301432
{ok, Scope#elixir_tokenizer{terminators=Terminators}};
14311433

1432-
_ExpectedEnd ->
1434+
ExpectedEnd ->
14331435
Meta = [
14341436
{line, StartLine},
14351437
{column, StartColumn},
14361438
{end_line, EndLine},
14371439
{end_column, EndColumn},
14381440
{error_type, mismatched_delimiter},
14391441
{opening_delimiter, Start},
1440-
{closing_delimiter, End}
1442+
{closing_delimiter, End},
1443+
{expected_delimiter, ExpectedEnd}
14411444
],
14421445
{error, {Meta, unexpected_token_or_reserved(End), [atom_to_list(End)]}}
14431446
end;
@@ -1490,7 +1493,6 @@ terminator('do') -> 'end';
14901493
terminator('(') -> ')';
14911494
terminator('[') -> ']';
14921495
terminator('{') -> '}';
1493-
terminator('"""') -> '"""';
14941496
terminator('<<') -> '>>'.
14951497

14961498
%% Keywords checking
@@ -1596,7 +1598,7 @@ tokenize_sigil_contents([H | T] = Original, [S | _] = SigilName, Line, Column, S
15961598
{error, Reason} ->
15971599
Sigil = [$~, S, H],
15981600
Message = " (for sigil ~ts starting at line ~B)",
1599-
interpolation_error(Reason, [$~] ++ SigilName ++ Original, Scope, Tokens, Message, [Sigil, Line])
1601+
interpolation_error(Reason, [$~] ++ SigilName ++ Original, Scope, Tokens, Message, [Sigil, Line], Line, Column, [H], [sigil_terminator(H)])
16001602
end;
16011603

16021604
tokenize_sigil_contents([H | _] = Original, SigilName, Line, Column, Scope, Tokens) ->

lib/elixir/test/elixir/code_test.exs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -498,9 +498,13 @@ defmodule CodeTest do
498498
end
499499

500500
test "string_to_quoted returns error on incomplete escaped string" do
501-
assert Code.string_to_quoted("\"\\") ==
502-
{:error,
503-
{[line: 1, column: 3], "missing terminator: \" (for string starting at line 1)", ""}}
501+
assert {:error, {meta, "missing terminator: \" (for string starting at line 1)", ""}} =
502+
Code.string_to_quoted("\"\\")
503+
504+
assert meta[:line] == 1
505+
assert meta[:column] == 1
506+
assert meta[:end_line] == 1
507+
assert meta[:end_column] == 3
504508
end
505509

506510
test "compile source" do

0 commit comments

Comments
 (0)