summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJosé Valim <jose.valim@dashbit.co>2023-05-10 12:32:25 +0200
committerJosé Valim <jose.valim@dashbit.co>2023-05-10 12:32:25 +0200
commit48e2efd2d9479ce6a79ae2badf91f24d87e71b88 (patch)
tree89ccbf2a85770613c2caff5509a0faa6b55407e3
parentfb1f1830b0eb5c2381c1fa14dba27431092152c8 (diff)
downloadelixir-48e2efd2d9479ce6a79ae2badf91f24d87e71b88.tar.gz
Return diagnostics from Kernel.ParallelCompiler
-rw-r--r--lib/elixir/lib/io.ex5
-rw-r--r--lib/elixir/lib/kernel/parallel_compiler.ex88
-rw-r--r--lib/elixir/lib/module/parallel_checker.ex33
-rw-r--r--lib/elixir/src/elixir_errors.erl51
-rw-r--r--lib/mix/lib/mix/compilers/elixir.ex30
5 files changed, 117 insertions, 90 deletions
diff --git a/lib/elixir/lib/io.ex b/lib/elixir/lib/io.ex
index 351d8bc6f..a6ba705de 100644
--- a/lib/elixir/lib/io.ex
+++ b/lib/elixir/lib/io.ex
@@ -327,7 +327,7 @@ defmodule IO do
def warn(message, []) do
message = [to_chardata(message), ?\n]
- :elixir_errors.print_warning(0, nil, message, message)
+ :elixir_errors.print_diagnostic(:warning, 0, nil, message, message)
end
def warn(message, %Macro.Env{} = env) do
@@ -357,7 +357,8 @@ defmodule IO do
line = opts[:line]
file = opts[:file]
- :elixir_errors.print_warning(
+ :elixir_errors.print_diagnostic(
+ :warning,
line || 0,
file && List.to_string(file),
message,
diff --git a/lib/elixir/lib/kernel/parallel_compiler.ex b/lib/elixir/lib/kernel/parallel_compiler.ex
index db02906f3..7b8c88e0b 100644
--- a/lib/elixir/lib/kernel/parallel_compiler.ex
+++ b/lib/elixir/lib/kernel/parallel_compiler.ex
@@ -5,15 +5,24 @@ defmodule Kernel.ParallelCompiler do
@typedoc "The line. 0 indicates no line."
@type line() :: non_neg_integer()
- @type location() :: line() | {pos_integer(), column :: non_neg_integer}
- @type warning() :: {file :: Path.t(), location(), message :: String.t()}
- @type error() :: {file :: Path.t(), location(), message :: String.t()}
+ @type position() :: line() | {pos_integer(), column :: non_neg_integer}
+
+ @type diagnostic(severity) :: %{
+ file: Path.t(),
+ severity: severity,
+ message: String.t(),
+ position: position
+ }
@type info :: %{
- runtime_warnings: [warning],
- compile_warnings: [warning]
+ runtime_warnings: [diagnostic(:warning)],
+ compile_warnings: [diagnostic(:warning)]
}
+ # Deprecated types
+ @type warning() :: {file :: Path.t(), position(), message :: String.t()}
+ @type error() :: {file :: Path.t(), position(), message :: String.t()}
+
@doc """
Starts a task for parallel compilation.
@@ -62,17 +71,14 @@ defmodule Kernel.ParallelCompiler do
It returns `{:ok, modules, warnings}` or `{:error, errors, warnings}`
by default but we recommend using `return_maps: true` so it returns
- a map as third element instead of a list of warnings. The map has the
- shape of:
+ diagnostics as maps as well as a map of compilation information.
+ The map has the shape of:
%{
runtime_warnings: [warning],
compile_warnings: [warning]
}
- Both errors and warnings are a list of three-element tuples containing
- the file, line and the formatted error/warning.
-
## Options
* `:each_file` - for each file compiled, invokes the callback passing the
@@ -109,12 +115,13 @@ defmodule Kernel.ParallelCompiler do
* `:beam_timestamp` - the modification timestamp to give all BEAM files
* `:return_maps` (since v1.15.0) - returns maps with information instead of
- a list of warnings
+ a list of warnings and returns diagnostics as maps instead of tuples
"""
@doc since: "1.6.0"
@spec compile([Path.t()], keyword()) ::
- {:ok, [atom], [warning] | info()} | {:error, [error], [warning] | info()}
+ {:ok, [atom], [warning] | info()}
+ | {:error, [error] | [diagnostic(:error)], [warning] | info()}
def compile(files, options \\ []) when is_list(options) do
spawn_workers(files, :compile, options)
end
@@ -126,7 +133,8 @@ defmodule Kernel.ParallelCompiler do
"""
@doc since: "1.6.0"
@spec compile_to_path([Path.t()], Path.t(), keyword()) ::
- {:ok, [atom], [warning] | info()} | {:error, [error], [warning] | info()}
+ {:ok, [atom], [warning] | info()}
+ | {:error, [error] | [diagnostic(:error)], [warning] | info()}
def compile_to_path(files, path, options \\ []) when is_binary(path) and is_list(options) do
spawn_workers(files, {:compile, path}, options)
end
@@ -139,17 +147,14 @@ defmodule Kernel.ParallelCompiler do
It returns `{:ok, modules, warnings}` or `{:error, errors, warnings}`
by default but we recommend using `return_maps: true` so it returns
- a map as third element instead of a list of warnings. The map has the
- shape of:
+ diagnostics as maps as well as a map of compilation information.
+ The map has the shape of:
%{
runtime_warnings: [warning],
compile_warnings: [warning]
}
- Both errors and warnings are a list of three-element tuples containing
- the file, line and the formatted error/warning.
-
## Options
* `:each_file` - for each file compiled, invokes the callback passing the
@@ -167,12 +172,17 @@ defmodule Kernel.ParallelCompiler do
end
@doc """
- Prints a warning returned by the compiler.
+ Prints a diagnostic returned by the compiler into stderr.
"""
- @doc since: "1.13.0"
- @spec print_warning(warning) :: :ok
+ @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)
+ end
+
+ @doc false
+ # TODO: Deprecate me on Elixir v1.19
def print_warning({file, location, warning}) do
- :elixir_errors.print_warning_no_diagnostic(location, file, warning)
+ :elixir_errors.print_diagnostic_no_capture(:warning, location, file, warning)
end
@doc false
@@ -209,7 +219,8 @@ defmodule Kernel.ParallelCompiler do
"Compilation failed due to warnings while using the --warnings-as-errors option"
IO.puts(:stderr, message)
- {:error, r_warnings ++ c_warnings, %{info | runtime_warnings: [], compile_warnings: []}}
+ errors = Enum.map(r_warnings ++ c_warnings, &Map.replace!(&1, :severity, :error))
+ {:error, errors, %{info | runtime_warnings: [], compile_warnings: []}}
{{:ok, outcome, info}, _} ->
beam_timestamp = Keyword.get(options, :beam_timestamp)
@@ -230,7 +241,12 @@ defmodule Kernel.ParallelCompiler do
if Keyword.get(options, :return_maps, false) do
{status, modules_or_errors, info}
else
- {status, modules_or_errors, info.runtime_warnings ++ info.compile_warnings}
+ to_tuples = &Enum.map(&1, fn diag -> {diag.file, diag.position, diag.message} end)
+
+ modules_or_errors =
+ if status == :ok, do: modules_or_errors, else: to_tuples.(modules_or_errors)
+
+ {status, modules_or_errors, to_tuples.(info.runtime_warnings ++ info.compile_warnings)}
end
end
@@ -633,19 +649,13 @@ defmodule Kernel.ParallelCompiler do
state = %{state | timer_ref: timer_ref}
spawn_workers(queue, spawned, waiting, files, result, warnings, errors, state)
- {:diagnostic, type, file, location, message} ->
- file = file && Path.absname(file)
- message = :unicode.characters_to_binary(message)
+ {:diagnostic, %{severity: :warning} = diagnostic} ->
+ warnings = [diagnostic | warnings]
+ wait_for_messages(queue, spawned, waiting, files, result, warnings, errors, state)
- case type do
- :warning ->
- warnings = [{file, location, message} | warnings]
- wait_for_messages(queue, spawned, waiting, files, result, warnings, errors, state)
-
- :error ->
- errors = [{file, location, message} | errors]
- wait_for_messages(queue, spawned, waiting, files, result, warnings, errors, state)
- end
+ {:diagnostic, %{severity: :error} = diagnostic} ->
+ errors = [diagnostic | errors]
+ wait_for_messages(queue, spawned, waiting, files, result, warnings, errors, state)
{:file_ok, child_pid, ref, file, lexical} ->
state.each_file.(file, lexical)
@@ -816,7 +826,9 @@ defmodule Kernel.ParallelCompiler do
"and that the modules they reference exist and are correctly named\n"
)
- for {file, _, description} <- deadlock, do: {Path.absname(file), nil, description}
+ for {file, _, description} <- deadlock do
+ %{severity: :error, file: Path.absname(file), position: nil, message: description}
+ end
end
defp terminate(files) do
@@ -842,7 +854,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, line || 0, message}
+ %{file: file, position: line || 0, message: message, severity: :error}
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 58a571a49..2e52a6b98 100644
--- a/lib/elixir/lib/module/parallel_checker.ex
+++ b/lib/elixir/lib/module/parallel_checker.ex
@@ -147,24 +147,21 @@ defmodule Module.ParallelChecker do
spawn({self(), checker}, module, file)
end
- modules = :gen_server.call(checker, :start, :infinity)
- collect_results(modules, [])
+ count = :gen_server.call(checker, :start, :infinity)
+ collect_results(count, [])
end
- defp collect_results([], warnings) do
- warnings
+ defp collect_results(0, diagnostics) do
+ diagnostics
end
- defp collect_results(modules, warnings) do
+ defp collect_results(count, diagnostics) do
receive do
- {:diagnostic, _type, file, location, message} ->
- file = file && Path.absname(file)
- message = :unicode.characters_to_binary(message)
- warning = {file, location, message}
- collect_results(modules, [warning | warnings])
-
- {__MODULE__, _module, new_warnings} ->
- collect_results(tl(modules), new_warnings ++ warnings)
+ {:diagnostic, diagnostic} ->
+ collect_results(count, [diagnostic | diagnostics])
+
+ {__MODULE__, _module, new_diagnostics} ->
+ collect_results(count - 1, new_diagnostics ++ diagnostics)
end
end
@@ -294,10 +291,14 @@ 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_warning_no_diagnostic([message, ?\n, format_locations(locations)])
+
+ :elixir_errors.print_diagnostic_no_capture(
+ :warning,
+ [message, ?\n, format_locations(locations)]
+ )
Enum.map(locations, fn {file, line, _mfa} ->
- {file, line, message}
+ %{severity: :warning, file: file, position: line, message: message}
end)
end)
end
@@ -485,7 +486,7 @@ defmodule Module.ParallelChecker do
end
end
- {:reply, modules, run_checkers(state)}
+ {:reply, length(modules), run_checkers(state)}
end
def handle_call(:ets, _from, state) do
diff --git a/lib/elixir/src/elixir_errors.erl b/lib/elixir/src/elixir_errors.erl
index d3f5b28ef..758fffb67 100644
--- a/lib/elixir/src/elixir_errors.erl
+++ b/lib/elixir/src/elixir_errors.erl
@@ -7,7 +7,7 @@
-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_warning/4, print_warning_no_diagnostic/1, print_warning_no_diagnostic/3]).
+-export([print_diagnostic/5, print_diagnostic_no_capture/2, print_diagnostic_no_capture/4]).
-include("elixir.hrl").
-type location() :: non_neg_integer() | {non_neg_integer(), non_neg_integer()}.
@@ -17,21 +17,22 @@ 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_warning_no_diagnostic(Location, File, Warning).
+ print_diagnostic_no_capture(warning, Location, File, Warning).
--spec print_warning_no_diagnostic(location(), unicode:chardata(), unicode:chardata()) -> ok.
-print_warning_no_diagnostic(Location, File, Warning) ->
- print_warning_no_diagnostic([Warning, "\n ", file_format(Location, File), $\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]).
--spec print_warning_no_diagnostic(unicode:chardata()) -> ok.
-print_warning_no_diagnostic(Message) ->
- io:put_chars(standard_error, [warning_prefix(), Message, $\n]),
+-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]),
ok.
--spec print_warning(location(), unicode:chardata() | nil, unicode:chardata(), unicode:chardata()) -> ok.
-print_warning(Location, File, DiagMessage, PrintMessage) when is_binary(File) or (File == nil) ->
- send_diagnostic(warning, Location, File, DiagMessage),
- print_warning_no_diagnostic(PrintMessage).
+-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).
%% Compilation error/warn handling.
@@ -44,8 +45,8 @@ file_warn(Meta, E, Module, Desc) when is_list(Meta) ->
true -> ok;
false ->
{EnvLine, EnvFile, EnvLocation} = env_format(Meta, E),
- Warning = Module:format_error(Desc),
- print_warning(EnvLine, EnvFile, Warning, [Warning, "\n ", EnvLocation, $\n])
+ Message = Module:format_error(Desc),
+ print_diagnostic(warning, EnvLine, EnvFile, Message, [Message, "\n ", EnvLocation, $\n])
end.
-spec file_error(list(), binary() | #{file := binary(), _ => _}, module(), any()) -> no_return().
@@ -78,8 +79,7 @@ function_error(Meta, Env, Module, Desc) ->
print_error(Meta, Env, Module, Desc) ->
{EnvLine, EnvFile, EnvLocation} = env_format(Meta, Env),
Message = Module:format_error(Desc),
- send_diagnostic(error, EnvLine, EnvFile, Message),
- io:put_chars(standard_error, [error_prefix(), Message, "\n ", EnvLocation, $\n, $\n]),
+ print_diagnostic(error, EnvLine, EnvFile, Message, [Message, "\n ", EnvLocation, $\n]),
ok.
%% Compilation error.
@@ -210,20 +210,27 @@ snippet(InputString, Location, StartLine, StartColumn) ->
%% Helpers
-send_diagnostic(Type, Line, File, Message) ->
+send_diagnostic(Severity, Position, File, Message) ->
case get(elixir_compiler_info) of
undefined -> ok;
- {CompilerPid, _} -> CompilerPid ! {diagnostic, Type, File, Line, Message}
+ {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.
-warning_prefix() ->
+prefix(warning) ->
case application:get_env(elixir, ansi_enabled, false) of
true -> <<"\e[33mwarning: \e[0m">>;
false -> <<"warning: ">>
- end.
-
-error_prefix() ->
+ end;
+prefix(error) ->
case application:get_env(elixir, ansi_enabled, false) of
true -> <<"\e[31merror: \e[0m">>;
false -> <<"error: ">>
diff --git a/lib/mix/lib/mix/compilers/elixir.ex b/lib/mix/lib/mix/compilers/elixir.ex
index 8accc7cba..c4a48742a 100644
--- a/lib/mix/lib/mix/compilers/elixir.ex
+++ b/lib/mix/lib/mix/compilers/elixir.ex
@@ -1,7 +1,7 @@
defmodule Mix.Compilers.Elixir do
@moduledoc false
- @manifest_vsn 18
+ @manifest_vsn 19
@checkpoint_vsn 2
import Record
@@ -190,14 +190,14 @@ defmodule Mix.Compilers.Elixir do
put_compile_env(sources)
info_warnings = info.runtime_warnings ++ info.compile_warnings
- all_warnings = previous_warnings ++ Enum.map(info_warnings, &diagnostic(&1, :warning))
+ all_warnings = previous_warnings ++ Enum.map(info_warnings, &diagnostic/1)
unless_previous_warnings_as_errors(previous_warnings, opts, {:ok, all_warnings})
{:error, errors, %{runtime_warnings: r_warnings, compile_warnings: c_warnings}, state} ->
# In case of errors, we show all previous warnings and all new ones.
{_, _, sources, _, _} = state
- errors = Enum.map(errors, &diagnostic(&1, :error))
- warnings = Enum.map(r_warnings ++ c_warnings, &diagnostic(&1, :warning))
+ errors = Enum.map(errors, &diagnostic/1)
+ warnings = Enum.map(r_warnings ++ c_warnings, &diagnostic/1)
all_warnings = Keyword.get(opts, :all_warnings, true)
{:error, previous_warnings(sources, all_warnings) ++ warnings ++ errors}
after
@@ -697,21 +697,27 @@ defmodule Mix.Compilers.Elixir do
runtime_warnings: runtime_warnings
) <- sources,
file = Path.absname(source),
- {location, message} <- compile_warnings ++ runtime_warnings do
- warning = {file, location, message}
+ {position, message} <- compile_warnings ++ runtime_warnings do
+ diagnostic = %Mix.Task.Compiler.Diagnostic{
+ severity: :warning,
+ file: file,
+ position: position,
+ message: message,
+ compiler_name: "Elixir"
+ }
if print? do
Mix.shell().print_app()
- Kernel.ParallelCompiler.print_warning(warning)
+ Kernel.ParallelCompiler.print_diagnostic(diagnostic)
end
- diagnostic(warning, :warning)
+ diagnostic
end
end
defp apply_warnings(sources, %{runtime_warnings: r_warnings, compile_warnings: c_warnings}) do
- runtime_group = Enum.group_by(r_warnings, &elem(&1, 0), &{elem(&1, 1), elem(&1, 2)})
- compile_group = Enum.group_by(c_warnings, &elem(&1, 0), &{elem(&1, 1), elem(&1, 2)})
+ runtime_group = Enum.group_by(r_warnings, & &1.file, &{&1.position, &1.message})
+ compile_group = Enum.group_by(c_warnings, & &1.file, &{&1.position, &1.message})
for source(
source: source_path,
@@ -727,10 +733,10 @@ defmodule Mix.Compilers.Elixir do
end
end
- defp diagnostic({file, location, message}, severity) do
+ defp diagnostic(%{file: file, position: position, message: message, severity: severity}) do
%Mix.Task.Compiler.Diagnostic{
file: file,
- position: location,
+ position: position,
message: message,
severity: severity,
compiler_name: "Elixir"