diff options
author | José Valim <jose.valim@dashbit.co> | 2023-05-15 23:35:07 +0200 |
---|---|---|
committer | José Valim <jose.valim@dashbit.co> | 2023-05-15 23:35:07 +0200 |
commit | 647635524607b779f14d1b62af24430da9a6da4f (patch) | |
tree | 94fe66c00cb461ff2409f495415b0fb64bd90058 | |
parent | bfa66559876041a797722f9ad4563e221c5ed765 (diff) | |
download | elixir-647635524607b779f14d1b62af24430da9a6da4f.tar.gz |
Keep stacktrace in diagnostics
-rw-r--r-- | lib/elixir/lib/calendar/datetime.ex | 2 | ||||
-rw-r--r-- | lib/elixir/lib/io.ex | 20 | ||||
-rw-r--r-- | lib/elixir/lib/kernel/parallel_compiler.ex | 25 | ||||
-rw-r--r-- | lib/elixir/lib/module/parallel_checker.ex | 54 | ||||
-rw-r--r-- | lib/elixir/src/elixir_errors.erl | 121 | ||||
-rw-r--r-- | lib/elixir/test/elixir/module/types/integration_test.exs | 2 | ||||
-rw-r--r-- | lib/mix/lib/mix/compilers/elixir.ex | 14 | ||||
-rw-r--r-- | lib/mix/lib/mix/task.compiler.ex | 13 | ||||
-rw-r--r-- | lib/mix/test/mix/tasks/compile.elixir_test.exs | 50 |
9 files changed, 173 insertions, 128 deletions
diff --git a/lib/elixir/lib/calendar/datetime.ex b/lib/elixir/lib/calendar/datetime.ex index 44d74dee3..83dd5d801 100644 --- a/lib/elixir/lib/calendar/datetime.ex +++ b/lib/elixir/lib/calendar/datetime.ex @@ -169,7 +169,7 @@ defmodule DateTime do utc_now(unit, Calendar.ISO) calendar -> - utc_now(:native, calendar) + System.os_time() |> from_unix!(:native, calendar) end end diff --git a/lib/elixir/lib/io.ex b/lib/elixir/lib/io.ex index a6ba705de..d055b4d86 100644 --- a/lib/elixir/lib/io.ex +++ b/lib/elixir/lib/io.ex @@ -325,15 +325,14 @@ defmodule IO do :ok def warn(message, stacktrace_info) - def warn(message, []) do - message = [to_chardata(message), ?\n] - :elixir_errors.print_diagnostic(:warning, 0, nil, message, message) - end - def warn(message, %Macro.Env{} = env) do warn(message, Macro.Env.stacktrace(env)) end + def warn(message, []) do + :elixir_errors.emit_diagnostic(:warning, 0, nil, to_chardata(message), []) + end + def warn(message, [{_, _} | _] = keyword) do if file = keyword[:file] do warn( @@ -353,17 +352,10 @@ defmodule IO do def warn(message, [{_, _, _, opts} | _] = stacktrace) do message = to_chardata(message) - formatted_trace = Enum.map_join(stacktrace, "\n ", &Exception.format_stacktrace_entry(&1)) line = opts[:line] file = opts[:file] - - :elixir_errors.print_diagnostic( - :warning, - line || 0, - file && List.to_string(file), - message, - [message, ?\n, " ", formatted_trace, ?\n] - ) + file = file && List.to_string(file) + :elixir_errors.emit_diagnostic(:warning, line || 0, file, message, stacktrace) end @doc false diff --git a/lib/elixir/lib/kernel/parallel_compiler.ex b/lib/elixir/lib/kernel/parallel_compiler.ex index 7b8c88e0b..fc9a512d1 100644 --- a/lib/elixir/lib/kernel/parallel_compiler.ex +++ b/lib/elixir/lib/kernel/parallel_compiler.ex @@ -11,7 +11,8 @@ defmodule Kernel.ParallelCompiler do file: Path.t(), severity: severity, message: String.t(), - position: position + position: position, + stacktrace: Exception.stacktrace() } @type info :: %{ @@ -175,14 +176,14 @@ defmodule Kernel.ParallelCompiler do Prints a diagnostic returned by the compiler into stderr. """ @doc since: "1.15.0" - def print_diagnostic(%{file: file, position: position, message: message, severity: severity}) do - :elixir_errors.print_diagnostic_no_capture(severity, position, file, message) + def print_diagnostic(diagnostic) do + :elixir_errors.print_diagnostic(diagnostic) end @doc false # TODO: Deprecate me on Elixir v1.19 def print_warning({file, location, warning}) do - :elixir_errors.print_diagnostic_no_capture(:warning, location, file, warning) + :elixir_errors.print_warning(location, file, warning) end @doc false @@ -802,7 +803,7 @@ defmodule Kernel.ParallelCompiler do description = "deadlocked waiting on #{kind} #{inspect(on)}" error = CompileError.exception(description: description, file: nil, line: nil) print_error(file, :error, error, stacktrace) - {Path.relative_to_cwd(file), on, description} + {Path.relative_to_cwd(file), on, description, stacktrace} end IO.puts(:stderr, """ @@ -816,7 +817,7 @@ defmodule Kernel.ParallelCompiler do |> Enum.map(&(&1 |> elem(0) |> String.length())) |> Enum.max() - for {file, mod, _} <- deadlock do + for {file, mod, _, _} <- deadlock do IO.puts(:stderr, [" ", String.pad_leading(file, max), " => " | inspect(mod)]) end @@ -826,8 +827,14 @@ defmodule Kernel.ParallelCompiler do "and that the modules they reference exist and are correctly named\n" ) - for {file, _, description} <- deadlock do - %{severity: :error, file: Path.absname(file), position: nil, message: description} + for {file, _, description, stacktrace} <- deadlock do + %{ + severity: :error, + file: Path.absname(file), + position: nil, + message: description, + stacktrace: stacktrace + } end end @@ -854,7 +861,7 @@ defmodule Kernel.ParallelCompiler do line = get_line(file, reason, stack) file = Path.absname(file) message = :unicode.characters_to_binary(Kernel.CLI.format_error(kind, reason, stack)) - %{file: file, position: line || 0, message: message, severity: :error} + %{file: file, position: line || 0, message: message, severity: :error, stacktrace: stack} end defp get_line(_file, %{line: line, column: column}, _stack) diff --git a/lib/elixir/lib/module/parallel_checker.ex b/lib/elixir/lib/module/parallel_checker.ex index 2e52a6b98..20f146931 100644 --- a/lib/elixir/lib/module/parallel_checker.ex +++ b/lib/elixir/lib/module/parallel_checker.ex @@ -291,46 +291,48 @@ defmodule Module.ParallelChecker do def emit_warnings(warnings) do Enum.flat_map(warnings, fn {module, warning, locations} -> message = module.format_warning(warning) - - :elixir_errors.print_diagnostic_no_capture( - :warning, - [message, ?\n, format_locations(locations)] - ) - - Enum.map(locations, fn {file, line, _mfa} -> - %{severity: :warning, file: file, position: line, message: message} - end) + diagnostics = Enum.map(locations, &to_diagnostic(message, &1)) + :elixir_errors.print_warning([message, ?\n, format_stacktraces(diagnostics)]) + diagnostics end) end - defp format_locations([location]) do - format_location(location) + defp format_stacktraces([diagnostic]) do + format_diagnostic_stacktrace(diagnostic) end - defp format_locations(locations) do + defp format_stacktraces(diagnostics) do [ - "Invalid call found at #{length(locations)} locations:\n", - Enum.map(locations, &format_location/1) + "Invalid call found at #{length(diagnostics)} locations:\n", + Enum.map(diagnostics, &format_diagnostic_stacktrace/1) ] end - defp format_location({file, line, {module, fun, arity}}) do - mfa = Exception.format_mfa(module, fun, arity) - [format_file_line(file, line), ": ", mfa, ?\n] + defp format_diagnostic_stacktrace(%{stacktrace: [stacktrace]}) do + [" ", Exception.format_stacktrace_entry(stacktrace), ?\n] end - defp format_location({file, line, nil}) do - [format_file_line(file, line), ?\n] + defp to_diagnostic(message, {file, line, mfa}) do + %{ + severity: :warning, + file: file, + position: line, + message: message, + stacktrace: [to_stacktrace(file, line, mfa)] + } end - defp format_location({file, line, module}) do - [format_file_line(file, line), ": ", inspect(module), ?\n] - end + defp to_stacktrace(file, line, {module, fun, arity}), + do: {module, fun, arity, location(file, line)} + + defp to_stacktrace(file, line, nil), + do: {:elixir_compiler, :__FILE__, 1, location(file, line)} + + defp to_stacktrace(file, line, module), + do: {module, :__MODULE__, 0, location(file, line)} - defp format_file_line(file, line) do - file = Path.relative_to_cwd(file) - line = if line > 0, do: [?: | Integer.to_string(line)], else: [] - [" ", file, line] + defp location(file, line) do + [file: String.to_charlist(Path.relative_to_cwd(file)), line: line] end ## Cache diff --git a/lib/elixir/src/elixir_errors.erl b/lib/elixir/src/elixir_errors.erl index 758fffb67..4a907f207 100644 --- a/lib/elixir/src/elixir_errors.erl +++ b/lib/elixir/src/elixir_errors.erl @@ -7,35 +7,61 @@ -export([compile_error/1, compile_error/3, parse_error/5]). -export([function_error/4, module_error/4, file_error/4]). -export([erl_warn/3, file_warn/4]). --export([print_diagnostic/5, print_diagnostic_no_capture/2, print_diagnostic_no_capture/4]). +-export([print_diagnostic/1, emit_diagnostic/5]). +-export([print_warning/1, print_warning/3]). -include("elixir.hrl"). -type location() :: non_neg_integer() | {non_neg_integer(), non_neg_integer()}. -%% Low-level warning, should be used only from Erlang passes. --spec erl_warn(location() | none, unicode:chardata(), unicode:chardata()) -> ok. -erl_warn(none, File, Warning) -> - erl_warn(0, File, Warning); -erl_warn(Location, File, Warning) when is_binary(File) -> - send_diagnostic(warning, Location, File, Warning), - print_diagnostic_no_capture(warning, Location, File, Warning). +%% Diagnostic API + +%% TODO: Remove me on Elixir v2.0. +print_warning(Location, File, Message) -> + print_warning([Message, file_format(Location, File), $\n]). + +%% Used by parallel checker as it groups warnings. +print_warning(Message) -> + io:put_chars(standard_error, [prefix(warning), Message, $\n]). --spec print_diagnostic_no_capture(warning | error, location(), unicode:chardata(), unicode:chardata()) -> ok. -print_diagnostic_no_capture(Type, Location, File, Message) -> - print_diagnostic_no_capture(Type, [Message, "\n ", file_format(Location, File), $\n]). +print_diagnostic(#{severity := Severity, message := Message, stacktrace := Stacktrace} = Diagnostic) -> + Location = + case (Stacktrace =:= []) orelse elixir_config:is_bootstrap() of + true -> + #{position := Position, file := File} = Diagnostic, + file_format(Position, File); --spec print_diagnostic_no_capture(warning | error, unicode:chardata()) -> ok. -print_diagnostic_no_capture(Type, Message) -> - io:put_chars(standard_error, [prefix(Type), Message, $\n]), + false -> + [["\n ", 'Elixir.Exception':format_stacktrace_entry(E)] || E <- Stacktrace] + end, + io:put_chars(standard_error, [prefix(Severity), Message, Location, "\n\n"]), ok. --spec print_diagnostic(warning | error, location(), unicode:chardata() | nil, unicode:chardata(), unicode:chardata()) -> ok. -print_diagnostic(Type, Location, File, DiagMessage, PrintMessage) - when is_binary(File) or (File == nil) -> - send_diagnostic(Type, Location, File, DiagMessage), - print_diagnostic_no_capture(Type, PrintMessage). +emit_diagnostic(Severity, Position, File, Message, Stacktrace) -> + Diagnostic = #{ + severity => Severity, + file => if File =:= nil -> nil; true -> filename:absname(File) end, + position => Position, + message => unicode:characters_to_binary(Message), + stacktrace => Stacktrace + }, + + print_diagnostic(Diagnostic), + + case get(elixir_compiler_info) of + undefined -> ok; + {CompilerPid, _} -> CompilerPid ! {diagnostic, Diagnostic} + end, + + ok. %% Compilation error/warn handling. +%% Low-level warning, should be used only from Erlang passes. +-spec erl_warn(location() | none, unicode:chardata(), unicode:chardata()) -> ok. +erl_warn(none, File, Warning) -> + erl_warn(0, File, Warning); +erl_warn(Location, File, Warning) when is_binary(File) -> + emit_diagnostic(warning, Location, File, Warning, []). + -spec file_warn(list(), binary() | #{file := binary(), _ => _}, module(), any()) -> ok. file_warn(Meta, File, Module, Desc) when is_list(Meta), is_binary(File) -> file_warn(Meta, #{file => File}, Module, Desc); @@ -44,9 +70,9 @@ file_warn(Meta, E, Module, Desc) when is_list(Meta) -> case elixir_config:is_bootstrap() of true -> ok; false -> - {EnvLine, EnvFile, EnvLocation} = env_format(Meta, E), + {EnvLine, EnvFile, EnvStacktrace} = env_format(Meta, E), Message = Module:format_error(Desc), - print_diagnostic(warning, EnvLine, EnvFile, Message, [Message, "\n ", EnvLocation, $\n]) + emit_diagnostic(warning, EnvLine, EnvFile, Message, EnvStacktrace) end. -spec file_error(list(), binary() | #{file := binary(), _ => _}, module(), any()) -> no_return(). @@ -77,9 +103,9 @@ function_error(Meta, Env, Module, Desc) -> file_error(Meta, Env, Module, Desc). print_error(Meta, Env, Module, Desc) -> - {EnvLine, EnvFile, EnvLocation} = env_format(Meta, Env), + {EnvLine, EnvFile, EnvStacktrace} = env_format(Meta, Env), Message = Module:format_error(Desc), - print_diagnostic(error, EnvLine, EnvFile, Message, [Message, "\n ", EnvLocation, $\n]), + emit_diagnostic(error, EnvLine, EnvFile, Message, EnvStacktrace), ok. %% Compilation error. @@ -94,11 +120,11 @@ compile_error(#{file := File}) -> -spec compile_error(list(), binary(), binary() | unicode:charlist()) -> no_return(). compile_error(Meta, File, Message) when is_binary(Message) -> - MetaLocation = meta_location(Meta, File), - raise('Elixir.CompileError', Message, MetaLocation); + {File, Line} = meta_location(Meta, File), + raise('Elixir.CompileError', Message, [{file, File}, {line, Line}]); compile_error(Meta, File, Message) when is_list(Message) -> - MetaLocation = meta_location(Meta, File), - raise('Elixir.CompileError', elixir_utils:characters_to_binary(Message), MetaLocation). + {File, Line} = meta_location(Meta, File), + raise('Elixir.CompileError', elixir_utils:characters_to_binary(Message), [{file, File}, {line, Line}]). %% Tokenization parsing/errors. @@ -210,21 +236,6 @@ snippet(InputString, Location, StartLine, StartColumn) -> %% Helpers -send_diagnostic(Severity, Position, File, Message) -> - case get(elixir_compiler_info) of - undefined -> ok; - {CompilerPid, _} -> - Diagnostic = #{ - severity => Severity, - file => if File =:= nil -> nil; true -> filename:absname(File) end, - position => Position, - message => unicode:characters_to_binary(Message) - }, - - CompilerPid ! {diagnostic, Diagnostic} - end, - ok. - prefix(warning) -> case application:get_env(elixir, ansi_enabled, false) of true -> <<"\e[33mwarning: \e[0m">>; @@ -237,33 +248,35 @@ prefix(error) -> end. env_format(Meta, #{file := EnvFile} = E) -> - [{file, File}, {line, Line}] = meta_location(Meta, EnvFile), + {File, Line} = meta_location(Meta, EnvFile), - Location = + Stacktrace = case E of #{function := {Name, Arity}, module := Module} -> - [file_format(Line, File), ": ", 'Elixir.Exception':format_mfa(Module, Name, Arity)]; + [{Module, Name, Arity, [{file, elixir_utils:relative_to_cwd(File)}, {line, Line}]}]; #{module := Module} when Module /= nil -> - [file_format(Line, File), ": ", elixir_aliases:inspect(Module)]; + [{Module, '__MODULE__', 0, [{file, elixir_utils:relative_to_cwd(File)}, {line, Line}]}]; #{} -> - file_format(Line, File) + [] end, - {Line, File, Location}. + {Line, File, Stacktrace}. +file_format(_, nil) -> + ""; file_format({0, _Column}, File) -> - elixir_utils:relative_to_cwd(File); + ["\n ", elixir_utils:relative_to_cwd(File)]; file_format({Line, Column}, File) -> - io_lib:format("~ts:~w:~w", [elixir_utils:relative_to_cwd(File), Line, Column]); + io_lib:format("\n ~ts:~w:~w", [elixir_utils:relative_to_cwd(File), Line, Column]); file_format(0, File) -> - elixir_utils:relative_to_cwd(File); + ["\n ", elixir_utils:relative_to_cwd(File)]; file_format(Line, File) -> - io_lib:format("~ts:~w", [elixir_utils:relative_to_cwd(File), Line]). + io_lib:format("\n ~ts:~w", [elixir_utils:relative_to_cwd(File), Line]). meta_location(Meta, File) -> case elixir_utils:meta_keep(Meta) of - {F, L} -> [{file, F}, {line, L}]; - nil -> [{file, File}, {line, ?line(Meta)}] + {F, L} -> {F, L}; + nil -> {File, ?line(Meta)} end. raise(Kind, Message, Opts) when is_binary(Message) -> diff --git a/lib/elixir/test/elixir/module/types/integration_test.exs b/lib/elixir/test/elixir/module/types/integration_test.exs index 4414ea230..e835ac651 100644 --- a/lib/elixir/test/elixir/module/types/integration_test.exs +++ b/lib/elixir/test/elixir/module/types/integration_test.exs @@ -717,7 +717,7 @@ defmodule Module.Types.IntegrationTest do warning = """ warning: A.a/0 is deprecated. oops - b.ex:3: B + b.ex:3: B (module) """ diff --git a/lib/mix/lib/mix/compilers/elixir.ex b/lib/mix/lib/mix/compilers/elixir.ex index c4a48742a..c53d5f109 100644 --- a/lib/mix/lib/mix/compilers/elixir.ex +++ b/lib/mix/lib/mix/compilers/elixir.ex @@ -703,7 +703,8 @@ defmodule Mix.Compilers.Elixir do file: file, position: position, message: message, - compiler_name: "Elixir" + compiler_name: "Elixir", + stacktrace: [] } if print? do @@ -733,13 +734,20 @@ defmodule Mix.Compilers.Elixir do end end - defp diagnostic(%{file: file, position: position, message: message, severity: severity}) do + defp diagnostic(%{ + file: file, + position: position, + message: message, + severity: severity, + stacktrace: stacktrace + }) do %Mix.Task.Compiler.Diagnostic{ file: file, position: position, message: message, severity: severity, - compiler_name: "Elixir" + compiler_name: "Elixir", + stacktrace: stacktrace } end diff --git a/lib/mix/lib/mix/task.compiler.ex b/lib/mix/lib/mix/task.compiler.ex index ed4ed6125..dbaaeb0b7 100644 --- a/lib/mix/lib/mix/task.compiler.ex +++ b/lib/mix/lib/mix/task.compiler.ex @@ -39,7 +39,8 @@ defmodule Mix.Task.Compiler do message: IO.chardata(), position: position, compiler_name: String.t(), - details: any + details: any, + stacktrace: Exception.stacktrace() } @typedoc """ @@ -75,7 +76,15 @@ defmodule Mix.Task.Compiler do | {pos_integer, non_neg_integer, pos_integer, non_neg_integer} @enforce_keys [:file, :severity, :message, :position, :compiler_name] - defstruct [:file, :severity, :message, :position, :compiler_name, :details] + defstruct [ + :file, + :severity, + :message, + :position, + :compiler_name, + details: nil, + stacktrace: [] + ] end @type status :: :ok | :noop | :error diff --git a/lib/mix/test/mix/tasks/compile.elixir_test.exs b/lib/mix/test/mix/tasks/compile.elixir_test.exs index e18cefc00..06809ad4b 100644 --- a/lib/mix/test/mix/tasks/compile.elixir_test.exs +++ b/lib/mix/test/mix/tasks/compile.elixir_test.exs @@ -1390,23 +1390,35 @@ defmodule Mix.Tasks.Compile.ElixirTest do end """) - diagnostic = %Diagnostic{ - file: Path.absname("lib/a.ex"), - severity: :warning, - position: 2, - compiler_name: "Elixir", - message: - "variable \"unused\" is unused (if the variable is not meant to be used, prefix it with an underscore)" - } + file = Path.absname("lib/a.ex") + + message = + "variable \"unused\" is unused (if the variable is not meant to be used, prefix it with an underscore)" capture_io(:stderr, fn -> - assert {:ok, [^diagnostic]} = Mix.Tasks.Compile.Elixir.run([]) + assert {:ok, [diagnostic]} = Mix.Tasks.Compile.Elixir.run([]) + + assert %Diagnostic{ + file: ^file, + severity: :warning, + position: 2, + compiler_name: "Elixir", + message: ^message + } = diagnostic end) # Recompiling should return :noop status because nothing is stale, # but also include previous warning diagnostics capture_io(:stderr, fn -> - assert {:noop, [^diagnostic]} = Mix.Tasks.Compile.Elixir.run([]) + assert {:noop, [diagnostic]} = Mix.Tasks.Compile.Elixir.run([]) + + assert %Diagnostic{ + file: ^file, + severity: :warning, + position: 2, + compiler_name: "Elixir", + message: ^message + } = diagnostic end) end) end @@ -1419,16 +1431,18 @@ defmodule Mix.Tasks.Compile.ElixirTest do IO.warn "warning", [{nil, nil, 0, file: 'lib/foo.txt', line: 3}] """) - diagnostic = %Diagnostic{ - file: Path.absname("lib/foo.txt"), - severity: :warning, - position: 3, - compiler_name: "Elixir", - message: "warning" - } + file = Path.absname("lib/foo.txt") capture_io(:stderr, fn -> - assert {:ok, [^diagnostic]} = Mix.Tasks.Compile.Elixir.run([]) + assert {:ok, [diagnostic]} = Mix.Tasks.Compile.Elixir.run([]) + + assert %Diagnostic{ + file: ^file, + severity: :warning, + position: 3, + compiler_name: "Elixir", + message: "warning" + } = diagnostic end) end) end |