From 0da6662b5ecd3ccc9031945da5fac4ecfcd5142c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20M=C3=BCller?= Date: Mon, 19 Jun 2023 09:07:59 +0000 Subject: [PATCH] Improvide diagnostics on lexer exceptions (#12670) --- lib/elixir/lib/exception.ex | 43 ++-- lib/elixir/src/elixir_errors.erl | 112 +++++++++-- .../test/elixir/kernel/diagnostics_test.exs | 180 +++++++++++++++++ lib/elixir/test/elixir/test_helper.exs | 2 +- lib/ex_unit/lib/ex_unit/formatter.ex | 7 +- lib/ex_unit/test/ex_unit/doc_test_test.exs | 183 +++++++++++------- lib/iex/test/iex/interaction_test.exs | 15 +- .../test/mix/tasks/compile.elixir_test.exs | 2 +- lib/mix/test/mix/tasks/compile_test.exs | 2 +- lib/mix/test/mix/tasks/format_test.exs | 40 +++- 10 files changed, 467 insertions(+), 119 deletions(-) create mode 100644 lib/elixir/test/elixir/kernel/diagnostics_test.exs diff --git a/lib/elixir/lib/exception.ex b/lib/elixir/lib/exception.ex index 16d45ab47a..5e52e64c4a 100644 --- a/lib/elixir/lib/exception.ex +++ b/lib/elixir/lib/exception.ex @@ -776,17 +776,6 @@ defmodule Exception do end end - @doc false - def format_snippet(snippet, error_line) do - line_digits = error_line |> Integer.to_string() |> byte_size() - placeholder = String.duplicate(" ", max(line_digits, 2)) - padding = if line_digits < 2, do: " " - - " #{placeholder} |\n" <> - " #{padding}#{error_line} | #{snippet.content}\n" <> - " #{placeholder} | #{String.duplicate(" ", snippet.offset)}^" - end - defp format_location(opts) when is_list(opts) do case opts[:column] do nil -> format_file_line(Keyword.get(opts, :file), Keyword.get(opts, :line), " ") @@ -902,8 +891,11 @@ defmodule SyntaxError do snippet: snippet }) when not is_nil(snippet) and not is_nil(column) do - Exception.format_file_line_column(Path.relative_to_cwd(file), line, column) <> - " " <> description <> "\n" <> Exception.format_snippet(snippet, line) + fancy = + :elixir_errors.format_snippet(file, line, column, description, snippet) + + location = Exception.format_file_line_column(Path.relative_to_cwd(file), line, column) + format_fancy(location, fancy) end @impl true @@ -913,8 +905,13 @@ defmodule SyntaxError do column: column, description: description }) do - Exception.format_file_line_column(Path.relative_to_cwd(file), line, column) <> - " " <> description + fancy = :elixir_errors.format_snippet(file, line, column, description) + location = Exception.format_file_line_column(Path.relative_to_cwd(file), line, column) + format_fancy(location, fancy) + end + + defp format_fancy(location, fancy) do + "invalid syntax found on " <> location <> "\n" <> fancy <> "\n" end end @@ -930,8 +927,11 @@ defmodule TokenMissingError do snippet: snippet }) when not is_nil(snippet) and not is_nil(column) do - Exception.format_file_line_column(Path.relative_to_cwd(file), line, column) <> - " " <> description <> "\n" <> Exception.format_snippet(snippet, line) + fancy = + :elixir_errors.format_snippet(file, line, column, description, snippet) + + location = Exception.format_file_line_column(Path.relative_to_cwd(file), line, column) + format_fancy(location, fancy) end @impl true @@ -941,8 +941,13 @@ defmodule TokenMissingError do column: column, description: description }) do - Exception.format_file_line_column(file && Path.relative_to_cwd(file), line, column) <> - " " <> description + fancy = :elixir_errors.format_snippet(file, line, column, description) + location = Exception.format_file_line_column(Path.relative_to_cwd(file), line, column) + format_fancy(location, fancy) + end + + defp format_fancy(location, fancy) do + "token missing on " <> location <> "\n" <> fancy <> "\n" end end diff --git a/lib/elixir/src/elixir_errors.erl b/lib/elixir/src/elixir_errors.erl index 7d9ae702fd..ef4dc185d5 100644 --- a/lib/elixir/src/elixir_errors.erl +++ b/lib/elixir/src/elixir_errors.erl @@ -8,6 +8,7 @@ -export([function_error/4, module_error/4, file_error/4]). -export([erl_warn/3, file_warn/4]). -export([print_diagnostic/1, emit_diagnostic/5]). +-export([format_snippet/4, format_snippet/5]). -export([print_warning/1, print_warning/3]). -include("elixir.hrl"). -type location() :: non_neg_integer() | {non_neg_integer(), non_neg_integer()}. @@ -27,7 +28,10 @@ print_diagnostic(#{severity := Severity, message := Message, stacktrace := Stack case (Stacktrace =:= []) orelse elixir_config:is_bootstrap() of true -> #{position := Position, file := File} = Diagnostic, - file_format(Position, File); + case File of + nil -> []; + File -> ["\n ", file_format(Position, File)] + end; false -> [["\n ", 'Elixir.Exception':format_stacktrace_entry(E)] || E <- Stacktrace] @@ -57,6 +61,82 @@ emit_diagnostic(Severity, Position, File, Message, Stacktrace) -> ok. +%% Format snippets + +format_snippet(File, LineNumber, Column, Description, Snippet) -> + #{content := Content, offset := Offset} = Snippet, + LineDigits = get_line_number_digits(LineNumber, 1), + Spacing = n_spaces(LineDigits + 1), + {FormattedSnippet, ColumnsTrimmed} = format_line(Content), + + Formatted = io_lib:format( + " ~ts┌─ ~ts~ts\n" + " ~ts│\n" + " ~p │ ~ts\n" + " ~ts│ ~ts\n" + " ~ts│\n" + " ~ts~ts", + [ + Spacing, prefix(error), file_format({LineNumber, Column}, File), + Spacing, + LineNumber, FormattedSnippet, + Spacing, highlight_below_line(Offset - ColumnsTrimmed, error), + Spacing, + Spacing, format_message(Description, LineDigits) + ]), + + unicode:characters_to_binary(Formatted). + +format_snippet(File, LineNumber, Column, Message) -> + Formatted = no_line_diagnostic({LineNumber, Column}, File, Message, error), + % Left pad so we stay aligned with "** (Exception)" banner + Padded = [" ", string:replace(Formatted, "\n", "\n ")], + unicode:characters_to_binary(Padded). + +no_line_diagnostic(Position, File, Message, Severity) -> + io_lib:format( + " ┌─ ~ts~ts\n" + " ~ts", + [ + prefix(Severity), file_format(Position, File), + Message + ] + ). + +format_line(Line) -> + {ok, Re} = re:compile("\s*"), + {match, [{_Start, SpacesMatched}]} = re:run(Line, Re, [{capture, all, index}]), + case SpacesMatched >= 27 of + true -> + {Trimmed, _} = trim_line(Line), + ColumnsTrimmed = SpacesMatched - 22, + {["...", n_spaces(19), Trimmed], ColumnsTrimmed}; + false -> + {Line, 0} + end. + +trim_line(Line) -> + Trimmed = string:trim(Line, leading), + {Trimmed, string:length(Line) - string:length(Trimmed) + 1}. + +format_message(Message, NDigits) -> + Padding = list_to_binary(["\n ", n_spaces(NDigits)]), + Bin = unicode:characters_to_binary(Message), + binary:replace(Bin, <<"\n">>, Padding, [global]). + +highlight_below_line(Column, Severity) -> + ErrorLength = 1, + case Severity of + warning -> highlight([n_spaces(Column), lists:duplicate(ErrorLength, $~)], warning); + error -> highlight([n_spaces(Column), lists:duplicate(ErrorLength, $^)], error) + end. + +get_line_number_digits(Number, Acc) when Number < 10 -> Acc; +get_line_number_digits(Number, Acc) -> + get_line_number_digits(Number div 10, Acc + 1). + +n_spaces(N) -> lists:duplicate(N, " "). + %% Compilation error/warn handling. %% Low-level warning, should be used only from Erlang passes. @@ -240,17 +320,19 @@ snippet(InputString, Location, StartLine, StartColumn) -> %% Helpers -prefix(warning) -> - case application:get_env(elixir, ansi_enabled, false) of - true -> <<"\e[33mwarning: \e[0m">>; - false -> <<"warning: ">> - end; -prefix(error) -> - case application:get_env(elixir, ansi_enabled, false) of - true -> <<"\e[31merror: \e[0m">>; - false -> <<"error: ">> +prefix(warning) -> highlight(<<"warning: ">>, warning); +prefix(error) -> highlight(<<"error: ">>, error). + +highlight(Message, Severity) -> + case {Severity, application:get_env(elixir, ansi_enabled, false)} of + {warning, true} -> yellow(Message); + {error, true} -> red(Message); + _ -> Message end. +yellow(Msg) -> io_lib:format("\e[33m~ts\e[0m", [Msg]). +red(Msg) -> io_lib:format("\e[31m~ts\e[0m", [Msg]). + env_format(Meta, #{file := EnvFile} = E) -> {File, Position} = meta_location(Meta, EnvFile), Line = ?line(Position), @@ -273,13 +355,15 @@ env_format(Meta, #{file := EnvFile} = E) -> file_format(_, nil) -> ""; file_format({0, _Column}, File) -> - ["\n ", elixir_utils:relative_to_cwd(File)]; + elixir_utils:relative_to_cwd(File); +file_format({Line, nil}, File) -> + file_format(Line, File); file_format({Line, Column}, File) -> - io_lib:format("\n ~ts:~w:~w", [elixir_utils:relative_to_cwd(File), Line, Column]); + io_lib:format("~ts:~w:~w", [elixir_utils:relative_to_cwd(File), Line, Column]); file_format(0, File) -> - ["\n ", elixir_utils:relative_to_cwd(File)]; + elixir_utils:relative_to_cwd(File); file_format(Line, File) -> - io_lib:format("\n ~ts:~w", [elixir_utils:relative_to_cwd(File), Line]). + io_lib:format("~ts:~w", [elixir_utils:relative_to_cwd(File), Line]). meta_location(Meta, File) -> case elixir_utils:meta_keep(Meta) of diff --git a/lib/elixir/test/elixir/kernel/diagnostics_test.exs b/lib/elixir/test/elixir/kernel/diagnostics_test.exs new file mode 100644 index 0000000000..9bd4cd1dee --- /dev/null +++ b/lib/elixir/test/elixir/kernel/diagnostics_test.exs @@ -0,0 +1,180 @@ +Code.require_file("../test_helper.exs", __DIR__) + +defmodule Kernel.DiagnosticsTest do + use ExUnit.Case, async: false + + setup do + Application.put_env(:elixir, :ansi_enabled, false) + on_exit(fn -> Application.put_env(:elixir, :ansi_enabled, true) end) + end + + describe "compile-time exceptions" do + test "SyntaxError (snippet)" do + expected = """ + ** (SyntaxError) invalid syntax found on nofile:1:17: + ┌─ error: nofile:1:17 + │ + 1 │ [1, 2, 3, 4, 5, *] + │ ^ + │ + syntax error before: '*' + """ + + output = + capture_raise( + """ + [1, 2, 3, 4, 5, *] + """, + SyntaxError + ) + + assert output == expected + end + + test "TokenMissingError (snippet)" do + expected = """ + ** (TokenMissingError) token missing on nofile:1:4: + ┌─ error: nofile:1:4 + │ + 1 │ 1 + + │ ^ + │ + syntax error: expression is incomplete + """ + + output = + capture_raise( + """ + 1 + + """, + TokenMissingError + ) + + assert output == expected + end + + test "TokenMissingError (no snippet)" do + expected = """ + ** (TokenMissingError) token missing on nofile:2:1: + ┌─ error: nofile:2:1 + missing terminator: end (for "fn" starting at line 1) + """ + + output = + capture_raise( + """ + fn a + """, + TokenMissingError + ) + + assert output == expected + end + + test "keeps trailing whitespace if under threshold" do + expected = """ + ** (SyntaxError) invalid syntax found on nofile:1:23: + ┌─ error: nofile:1:23 + │ + 1 │ a + 😎 + │ ^ + │ + unexpected token: "😎" (column 23, code point U+****) + """ + + output = + capture_raise( + """ + a + 😎 + """, + SyntaxError + ) + + assert output == expected + end + + test "limits trailing whitespace if too many" do + expected = """ + ** (SyntaxError) invalid syntax found on nofile:1:43: + ┌─ error: nofile:1:43 + │ + 1 │ ... a + 😎 + │ ^ + │ + unexpected token: "😎" (column 43, code point U+****) + """ + + output = + capture_raise( + """ + a + 😎 + """, + SyntaxError + ) + + assert output == expected + end + + test "shows stacktrace if present" do + fake_stacktrace = [ + {:fake, :fun, 3, [file: "nofile", line: 10]}, + {:real, :fun, 2, [file: "nofile", line: 10]} + ] + + expected = """ + ** (TokenMissingError) token missing on nofile:1:4: + ┌─ error: nofile:1:4 + │ + 1 │ 1 - + │ ^ + │ + syntax error: expression is incomplete + + nofile:10: :fake.fun/3 + nofile:10: :real.fun/2 + """ + + output = + capture_raise( + """ + 1 - + """, + TokenMissingError, + fake_stacktrace + ) + + assert output == expected + end + + test "handles unicode" do + source = """ + defmodule Sample do + def a do + 10 + 😎 + end + end + """ + + output = capture_raise(source, SyntaxError) + + assert output =~ "😎" + after + purge(Sample) + end + end + + defp capture_raise(source, exception, mock_stacktrace \\ []) do + e = + assert_raise exception, fn -> + ast = Code.string_to_quoted!(source, columns: true) + Code.eval_quoted(ast) + end + + Exception.format(:error, e, mock_stacktrace) + end + + defp purge(module) when is_atom(module) do + :code.purge(module) + :code.delete(module) + end +end diff --git a/lib/elixir/test/elixir/test_helper.exs b/lib/elixir/test/elixir/test_helper.exs index 40f0bff2c0..66661c0116 100644 --- a/lib/elixir/test/elixir/test_helper.exs +++ b/lib/elixir/test/elixir/test_helper.exs @@ -57,7 +57,7 @@ defmodule PathHelpers do defp run_cmd(executable, args) do ~c"#{executable} #{IO.chardata_to_string(args)}#{redirect_std_err_on_win()}" |> :os.cmd() - |> :binary.list_to_bin() + |> :unicode.characters_to_binary() end defp executable_path(name) do diff --git a/lib/ex_unit/lib/ex_unit/formatter.ex b/lib/ex_unit/lib/ex_unit/formatter.ex index d18122cb22..7a688d7cfe 100644 --- a/lib/ex_unit/lib/ex_unit/formatter.ex +++ b/lib/ex_unit/lib/ex_unit/formatter.ex @@ -461,7 +461,12 @@ defmodule ExUnit.Formatter do defp format_message(value, formatter) do value = String.replace(value, "\n", "\n" <> @counter_padding) - formatter.(:error_info, value) + + if String.contains?(value, IO.ANSI.reset()) do + value + else + formatter.(:error_info, value) + end end defp format_args(args, width) do diff --git a/lib/ex_unit/test/ex_unit/doc_test_test.exs b/lib/ex_unit/test/ex_unit/doc_test_test.exs index 70965fa2bd..918ec1bca7 100644 --- a/lib/ex_unit/test/ex_unit/doc_test_test.exs +++ b/lib/ex_unit/test/ex_unit/doc_test_test.exs @@ -557,14 +557,19 @@ defmodule ExUnit.DocTestTest do ExUnit.configure(seed: 0, colors: [enabled: false]) output = capture_io(fn -> ExUnit.run() end) + output = String.replace(output, [IO.ANSI.red(), IO.ANSI.reset()], "") assert output =~ """ 1) doctest module ExUnit.DocTestTest.Failure (1) (ExUnit.DocTestTest.FailureCompiled) test/ex_unit/doc_test_test.exs:#{doctest_line} - Doctest did not compile, got: (SyntaxError) test/ex_unit/doc_test_test.exs:#{starting_line}:6: syntax error before: '*' - #{line_placeholder(starting_line)} | - #{starting_line} | 1 + * 1 - #{line_placeholder(starting_line)} | ^ + Doctest did not compile, got: (SyntaxError) invalid syntax found on test/ex_unit/doc_test_test.exs:#{starting_line}:6: + ┌─ error: test/ex_unit/doc_test_test.exs:#{starting_line}:6 + │ + #{starting_line} │ 1 + * 1 + │ ^ + │ + syntax error before: '*' + doctest: iex> 1 + * 1 1 @@ -675,78 +680,99 @@ defmodule ExUnit.DocTestTest do ExUnit.configure(seed: 0, colors: [enabled: false]) output = capture_io(fn -> ExUnit.run() end) + output = String.replace(output, [IO.ANSI.red(), IO.ANSI.reset()], "") assert output =~ """ 1) doctest ExUnit.DocTestTest.Invalid.a/0 (1) (ExUnit.DocTestTest.InvalidCompiled) - test/ex_unit/doc_test_test.exs:#{doctest_line} - Doctest did not compile, got: (SyntaxError) test/ex_unit/doc_test_test.exs:#{starting_line}:6: syntax error before: '*' - #{line_placeholder(starting_line)} | - #{starting_line} | 1 + * 1 - #{line_placeholder(starting_line)} | ^ + test/ex_unit/doc_test_test.exs:675 + Doctest did not compile, got: (SyntaxError) invalid syntax found on test/ex_unit/doc_test_test.exs:186:6: + ┌─ error: test/ex_unit/doc_test_test.exs:186:6 + │ + 186 │ 1 + * 1 + │ ^ + │ + syntax error before: '*' + doctest: iex> 1 + * 1 1 stacktrace: - test/ex_unit/doc_test_test.exs:#{starting_line}: ExUnit.DocTestTest.Invalid (module) + test/ex_unit/doc_test_test.exs:186: ExUnit.DocTestTest.Invalid (module) """ assert output =~ """ 2) doctest ExUnit.DocTestTest.Invalid.b/0 (2) (ExUnit.DocTestTest.InvalidCompiled) - test/ex_unit/doc_test_test.exs:#{doctest_line} - Doctest did not compile, got: (SyntaxError) test/ex_unit/doc_test_test.exs:#{starting_line + 6}:6: syntax error before: '*' - #{line_placeholder(starting_line + 6)} | - #{starting_line + 6} | 1 + * 1 - #{line_placeholder(starting_line + 6)} | ^ + test/ex_unit/doc_test_test.exs:675 + Doctest did not compile, got: (SyntaxError) invalid syntax found on test/ex_unit/doc_test_test.exs:192:6: + ┌─ error: test/ex_unit/doc_test_test.exs:192:6 + │ + 192 │ 1 + * 1 + │ ^ + │ + syntax error before: '*' + doctest: iex> 1 + * 1 1 stacktrace: - test/ex_unit/doc_test_test.exs:#{starting_line + 6}: ExUnit.DocTestTest.Invalid (module) + test/ex_unit/doc_test_test.exs:192: ExUnit.DocTestTest.Invalid (module) """ assert output =~ """ 3) doctest ExUnit.DocTestTest.Invalid.indented_not_enough/0 (3) (ExUnit.DocTestTest.InvalidCompiled) - test/ex_unit/doc_test_test.exs:#{doctest_line} - Doctest did not compile, got: (SyntaxError) test/ex_unit/doc_test_test.exs:#{starting_line + 15}:1: unexpected token: "`" (column 1, code point U+0060) - #{line_placeholder(starting_line + 15)} | - #{starting_line + 15} | ` - #{line_placeholder(starting_line + 15)} | ^ + test/ex_unit/doc_test_test.exs:675 + Doctest did not compile, got: (SyntaxError) invalid syntax found on test/ex_unit/doc_test_test.exs:201:1: + ┌─ error: test/ex_unit/doc_test_test.exs:201:1 + │ + 201 │ ` + │ ^ + │ + unexpected token: "`" (column 1, code point U+0060) + doctest: iex> 1 + 2 3 ` stacktrace: - test/ex_unit/doc_test_test.exs:#{starting_line + 14}: ExUnit.DocTestTest.Invalid (module) + test/ex_unit/doc_test_test.exs:200: ExUnit.DocTestTest.Invalid (module) """ assert output =~ """ 4) doctest ExUnit.DocTestTest.Invalid.indented_too_much/0 (4) (ExUnit.DocTestTest.InvalidCompiled) - test/ex_unit/doc_test_test.exs:#{doctest_line} - Doctest did not compile, got: (SyntaxError) test/ex_unit/doc_test_test.exs:#{starting_line + 23}:3: unexpected token: "`" (column 3, code point U+0060) - #{line_placeholder(starting_line + 23)} | - #{starting_line + 23} | ``` - #{line_placeholder(starting_line + 23)} | ^ + test/ex_unit/doc_test_test.exs:675 + Doctest did not compile, got: (SyntaxError) invalid syntax found on test/ex_unit/doc_test_test.exs:209:3: + ┌─ error: test/ex_unit/doc_test_test.exs:209:3 + │ + 209 │ ``` + │ ^ + │ + unexpected token: "`" (column 3, code point U+0060) + doctest: iex> 1 + 2 3 ``` stacktrace: - test/ex_unit/doc_test_test.exs:#{starting_line + 22}: ExUnit.DocTestTest.Invalid (module) + test/ex_unit/doc_test_test.exs:208: ExUnit.DocTestTest.Invalid (module) """ assert output =~ """ 5) doctest ExUnit.DocTestTest.Invalid.dedented_past_fence/0 (5) (ExUnit.DocTestTest.InvalidCompiled) - test/ex_unit/doc_test_test.exs:#{doctest_line} - Doctest did not compile, got: (SyntaxError) test/ex_unit/doc_test_test.exs:#{starting_line + 31}:5: unexpected token: "`" (column 5, code point U+0060) - #{line_placeholder(starting_line + 31)} | - #{starting_line + 31} | ``` - #{line_placeholder(starting_line + 31)} | ^ + test/ex_unit/doc_test_test.exs:675 + Doctest did not compile, got: (SyntaxError) invalid syntax found on test/ex_unit/doc_test_test.exs:217:5: + ┌─ error: test/ex_unit/doc_test_test.exs:217:5 + │ + 217 │ ``` + │ ^ + │ + unexpected token: "`" (column 5, code point U+0060) + doctest: iex> 1 + 2 3 ``` stacktrace: - test/ex_unit/doc_test_test.exs:#{starting_line + 30}: ExUnit.DocTestTest.Invalid (module) + test/ex_unit/doc_test_test.exs:216: ExUnit.DocTestTest.Invalid (module) """ assert output =~ """ @@ -759,74 +785,94 @@ defmodule ExUnit.DocTestTest do assert output =~ """ 7) doctest ExUnit.DocTestTest.Invalid.misplaced_opaque_type/0 (7) (ExUnit.DocTestTest.InvalidCompiled) - test/ex_unit/doc_test_test.exs:#{doctest_line} - Doctest did not compile, got: (TokenMissingError) test/ex_unit/doc_test_test.exs:#{starting_line + 43}:7: missing terminator: } (for "{" starting at line #{starting_line + 43}) - #{line_placeholder(starting_line + 43)} | - #{starting_line + 43} | {:ok, #Inspect<[]>} - #{line_placeholder(starting_line + 43)} | ^ + test/ex_unit/doc_test_test.exs:675 + Doctest did not compile, got: (TokenMissingError) token missing on test/ex_unit/doc_test_test.exs:229:7: + ┌─ error: test/ex_unit/doc_test_test.exs:229:7 + │ + 229 │ {:ok, #Inspect<[]>} + │ ^ + │ + missing terminator: } (for "{" starting at line 229) + If you are planning to assert on the result of an iex> expression which contains a value inspected as #Name<...>, please make sure the inspected value is placed at the beginning of the expression; otherwise Elixir will treat it as a comment due to the leading sign #. doctest: iex> {:ok, :oops} {:ok, #Inspect<[]>} stacktrace: - test/ex_unit/doc_test_test.exs:#{starting_line + 43}: ExUnit.DocTestTest.Invalid (module) + test/ex_unit/doc_test_test.exs:229: ExUnit.DocTestTest.Invalid (module) """ assert output =~ """ 8) doctest ExUnit.DocTestTest.Invalid.t/0 (8) (ExUnit.DocTestTest.InvalidCompiled) - test/ex_unit/doc_test_test.exs:#{doctest_line} - Doctest did not compile, got: (SyntaxError) test/ex_unit/doc_test_test.exs:#{starting_line + 48}:6: syntax error before: '*' - #{line_placeholder(starting_line + 48)} | - #{starting_line + 48} | 1 + * 1 - #{line_placeholder(starting_line + 48)} | ^ + test/ex_unit/doc_test_test.exs:675 + Doctest did not compile, got: (SyntaxError) invalid syntax found on test/ex_unit/doc_test_test.exs:234:6: + ┌─ error: test/ex_unit/doc_test_test.exs:234:6 + │ + 234 │ 1 + * 1 + │ ^ + │ + syntax error before: '*' + doctest: iex> 1 + * 1 1 stacktrace: - test/ex_unit/doc_test_test.exs:#{starting_line + 48}: ExUnit.DocTestTest.Invalid (module) + test/ex_unit/doc_test_test.exs:234: ExUnit.DocTestTest.Invalid (module) """ assert output =~ """ 9) doctest ExUnit.DocTestTest.Invalid.result/0 (9) (ExUnit.DocTestTest.InvalidCompiled) - test/ex_unit/doc_test_test.exs:#{doctest_line} - Doctest did not compile, got: (SyntaxError) test/ex_unit/doc_test_test.exs:#{starting_line + 56}:5: syntax error before: '*' - #{line_placeholder(starting_line + 56)} | - #{starting_line + 56} | 1 + * 1 - #{line_placeholder(starting_line + 56)} | ^ + test/ex_unit/doc_test_test.exs:675 + Doctest did not compile, got: (SyntaxError) invalid syntax found on test/ex_unit/doc_test_test.exs:242:5: + ┌─ error: test/ex_unit/doc_test_test.exs:242:5 + │ + 242 │ 1 + * 1 + │ ^ + │ + syntax error before: '*' + doctest: iex> :foo iex> :bar 1 + * 1 stacktrace: - test/ex_unit/doc_test_test.exs:#{starting_line + 56}: ExUnit.DocTestTest.Invalid (module) + test/ex_unit/doc_test_test.exs:242: ExUnit.DocTestTest.Invalid (module) """ assert output =~ """ 10) doctest ExUnit.DocTestTest.Invalid.mixed/0 (10) (ExUnit.DocTestTest.InvalidCompiled) - test/ex_unit/doc_test_test.exs:#{doctest_line} - Doctest did not compile, got: (TokenMissingError) test/ex_unit/doc_test_test.exs:#{starting_line + 61}:6: syntax error: expression is incomplete - #{line_placeholder(starting_line + 61)} | - #{starting_line + 61} | 123 + - #{line_placeholder(starting_line + 61)} | ^ + test/ex_unit/doc_test_test.exs:675 + Doctest did not compile, got: (TokenMissingError) token missing on test/ex_unit/doc_test_test.exs:247:6: + ┌─ error: test/ex_unit/doc_test_test.exs:247:6 + │ + 247 │ 123 + + │ ^ + │ + syntax error: expression is incomplete + doctest: iex> 123 + :mixed stacktrace: - test/ex_unit/doc_test_test.exs:#{starting_line + 61}: ExUnit.DocTestTest.Invalid (module) + test/ex_unit/doc_test_test.exs:247: ExUnit.DocTestTest.Invalid (module) """ assert output =~ """ 11) doctest ExUnit.DocTestTest.Invalid.invalid_second/0 (11) (ExUnit.DocTestTest.InvalidCompiled) - test/ex_unit/doc_test_test.exs:#{doctest_line} - Doctest did not compile, got: (TokenMissingError) test/ex_unit/doc_test_test.exs:#{starting_line + 69}:6: syntax error: expression is incomplete - #{line_placeholder(starting_line + 69)} | - #{starting_line + 69} | 123 + - #{line_placeholder(starting_line + 69)} | ^ + test/ex_unit/doc_test_test.exs:675 + Doctest did not compile, got: (TokenMissingError) token missing on test/ex_unit/doc_test_test.exs:255:6: + ┌─ error: test/ex_unit/doc_test_test.exs:255:6 + │ + 255 │ 123 + + │ ^ + │ + syntax error: expression is incomplete + doctest: iex> 123 + :mixed stacktrace: - test/ex_unit/doc_test_test.exs:#{starting_line + 69}: ExUnit.DocTestTest.Invalid (module) + test/ex_unit/doc_test_test.exs:255: ExUnit.DocTestTest.Invalid (module) """ assert output =~ "11 doctests, 11 failures" @@ -1135,13 +1181,4 @@ defmodule ExUnit.DocTestTest do [head | _] = Direct.__ex_unit__().tests assert apply(Direct, head.name, [%{}]) == {:ok, 2} end - - defp line_placeholder(line_number) do - digits = - line_number - |> Integer.digits() - |> length() - - String.duplicate(" ", digits) - end end diff --git a/lib/iex/test/iex/interaction_test.exs b/lib/iex/test/iex/interaction_test.exs index 98aaf0a589..b42bba5feb 100644 --- a/lib/iex/test/iex/interaction_test.exs +++ b/lib/iex/test/iex/interaction_test.exs @@ -20,8 +20,13 @@ defmodule IEx.InteractionTest do end test "invalid input" do - assert capture_iex("if true do ) false end") =~ - "** (SyntaxError) iex:1:12: unexpected token: )" + output = capture_iex("if true do ) false end") + + assert output =~ "** (SyntaxError) invalid syntax found on iex:1:12:" + assert output =~ "error: iex:1:12" + assert output =~ "if true do ) false end" + assert output =~ "unexpected token: )" + assert output =~ ~s/HINT: the "do" on line 1 is missing terminator "end"/ end test "multiple vars" do @@ -61,7 +66,11 @@ defmodule IEx.InteractionTest do #iex:break """ - assert capture_iex(input) =~ "** (TokenMissingError) iex:1: incomplete expression" + output = capture_iex(input) + + assert output =~ "** (TokenMissingError) token missing on iex:1:" + assert output =~ "error: iex:1\n" + assert output =~ "incomplete expression" end test "module definition" do diff --git a/lib/mix/test/mix/tasks/compile.elixir_test.exs b/lib/mix/test/mix/tasks/compile.elixir_test.exs index 06809ad4be..ea406cf6d0 100644 --- a/lib/mix/test/mix/tasks/compile.elixir_test.exs +++ b/lib/mix/test/mix/tasks/compile.elixir_test.exs @@ -1467,7 +1467,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do file: ^file, severity: :error, position: {2, 20}, - message: "** (SyntaxError) lib/a.ex:2:" <> _, + message: "** (SyntaxError) invalid syntax found on lib/a.ex:2:" <> _, compiler_name: "Elixir" } = diagnostic end) diff --git a/lib/mix/test/mix/tasks/compile_test.exs b/lib/mix/test/mix/tasks/compile_test.exs index cdc40977c7..5b3d23544d 100644 --- a/lib/mix/test/mix/tasks/compile_test.exs +++ b/lib/mix/test/mix/tasks/compile_test.exs @@ -157,7 +157,7 @@ defmodule Mix.Tasks.CompileTest do file: ^file, severity: :error, position: {2, 20}, - message: "** (SyntaxError) lib/a.ex:2:" <> _, + message: "** (SyntaxError) invalid syntax found on lib/a.ex:2:" <> _, compiler_name: "Elixir" } = diagnostic end) diff --git a/lib/mix/test/mix/tasks/format_test.exs b/lib/mix/test/mix/tasks/format_test.exs index 254ac47c90..8d7cda34c6 100644 --- a/lib/mix/test/mix/tasks/format_test.exs +++ b/lib/mix/test/mix/tasks/format_test.exs @@ -840,8 +840,22 @@ defmodule Mix.Tasks.FormatTest do defmodule <%= module %>.Bar do end """) - assert_raise SyntaxError, ~r"a.ex:1:13: syntax error before: '='", fn -> - Mix.Tasks.Format.run(["a.ex"]) + messages = [ + "invalid syntax found on a.ex:1:13:", + "defmodule <%= module %>.Bar do end", + "^", + "syntax error before: '='" + ] + + e = + assert_raise SyntaxError, fn -> + Mix.Tasks.Format.run(["a.ex"]) + end + + output = Exception.format(:error, e, []) + + for msg <- messages do + assert output =~ msg end assert_received {:mix_shell, :error, ["mix format failed for file: a.ex"]} @@ -850,10 +864,24 @@ defmodule Mix.Tasks.FormatTest do test "raises SyntaxError when parsing invalid stdin", context do in_tmp(context.test, fn -> - assert_raise SyntaxError, ~r"stdin.exs:1:13: syntax error before: '='", fn -> - capture_io("defmodule <%= module %>.Bar do end", fn -> - Mix.Tasks.Format.run(["-"]) - end) + e = + assert_raise SyntaxError, fn -> + capture_io("defmodule <%= module %>.Bar do end", fn -> + Mix.Tasks.Format.run(["-"]) + end) + end + + messages = [ + "invalid syntax found on stdin.exs:1:13:", + "defmodule <%= module %>.Bar do end", + "^", + "syntax error before: '='" + ] + + output = Exception.format(:error, e, []) + + for msg <- messages do + assert output =~ msg end assert_received {:mix_shell, :error, ["mix format failed for stdin"]}