summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJosé Valim <jose.valim@dashbit.co>2023-05-15 23:35:07 +0200
committerJosé Valim <jose.valim@dashbit.co>2023-05-15 23:35:07 +0200
commit647635524607b779f14d1b62af24430da9a6da4f (patch)
tree94fe66c00cb461ff2409f495415b0fb64bd90058
parentbfa66559876041a797722f9ad4563e221c5ed765 (diff)
downloadelixir-647635524607b779f14d1b62af24430da9a6da4f.tar.gz
Keep stacktrace in diagnostics
-rw-r--r--lib/elixir/lib/calendar/datetime.ex2
-rw-r--r--lib/elixir/lib/io.ex20
-rw-r--r--lib/elixir/lib/kernel/parallel_compiler.ex25
-rw-r--r--lib/elixir/lib/module/parallel_checker.ex54
-rw-r--r--lib/elixir/src/elixir_errors.erl121
-rw-r--r--lib/elixir/test/elixir/module/types/integration_test.exs2
-rw-r--r--lib/mix/lib/mix/compilers/elixir.ex14
-rw-r--r--lib/mix/lib/mix/task.compiler.ex13
-rw-r--r--lib/mix/test/mix/tasks/compile.elixir_test.exs50
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