diff options
author | José Valim <jose.valim@dashbit.co> | 2023-05-16 11:52:59 +0200 |
---|---|---|
committer | José Valim <jose.valim@dashbit.co> | 2023-05-16 11:52:59 +0200 |
commit | a0f0f75ee86d01a7ccc7012a1e6cd3f019bd3d43 (patch) | |
tree | 873da86913bd03642d0d1048174362167c0e84b1 | |
parent | 77c95d529bcb91b2e9190b1546ad60de6c0fb84b (diff) | |
download | elixir-a0f0f75ee86d01a7ccc7012a1e6cd3f019bd3d43.tar.gz |
Add Code.with_diagnostics/2, closes #12276
-rw-r--r-- | lib/elixir/lib/code.ex | 64 | ||||
-rw-r--r-- | lib/elixir/lib/code/fragment.ex | 2 | ||||
-rw-r--r-- | lib/elixir/lib/kernel/parallel_compiler.ex | 40 | ||||
-rw-r--r-- | lib/elixir/lib/module/parallel_checker.ex | 31 | ||||
-rw-r--r-- | lib/elixir/src/elixir.erl | 6 | ||||
-rw-r--r-- | lib/elixir/src/elixir_errors.erl | 8 | ||||
-rw-r--r-- | lib/elixir/src/elixir_module.erl | 13 | ||||
-rw-r--r-- | lib/elixir/test/elixir/code_test.exs | 265 | ||||
-rw-r--r-- | lib/mix/lib/mix/compilers/elixir.ex | 4 |
9 files changed, 293 insertions, 140 deletions
diff --git a/lib/elixir/lib/code.ex b/lib/elixir/lib/code.ex index f92da34e5..c5c5b63d2 100644 --- a/lib/elixir/lib/code.ex +++ b/lib/elixir/lib/code.ex @@ -194,6 +194,21 @@ defmodule Code do """ @type binding :: [{atom() | tuple(), any}] + @typedoc """ + Diagnostics returned by the compiler and code evaluation. + """ + @type diagnostic(severity) :: %{ + file: Path.t(), + severity: severity, + message: String.t(), + position: position, + stacktrace: Exception.stacktrace() + } + + @typedoc "The line. 0 indicates no line." + @type line() :: non_neg_integer() + @type position() :: line() | {pos_integer(), column :: non_neg_integer} + @boolean_compiler_options [ :docs, :debug_info, @@ -533,6 +548,51 @@ defmodule Code do end) end + @doc """ + Executes the given `fun` and capture all diagnostics. + + Diagnostics are warnings and errors emitted by the compiler + and by functions such as `IO.warn/2`. + + ## Options + + * `:log` - if the diagnostics should be logged as they happen. + Defaults to `false`. + + """ + @spec with_diagnostics(keyword(), (-> result)) :: {result, [diagnostic(:warning | :error)]} + when result: term() + def with_diagnostics(opts \\ [], fun) do + value = :erlang.get(:elixir_code_diagnostics) + log = Keyword.get(opts, :log, false) + :erlang.put(:elixir_code_diagnostics, {[], log}) + + try do + result = fun.() + {diagnostics, _log?} = :erlang.get(:elixir_code_diagnostics) + {result, Enum.reverse(diagnostics)} + after + if value == :undefined do + :erlang.erase(:elixir_code_diagnostics) + else + :erlang.put(:elixir_code_diagnostics, value) + end + end + end + + @doc """ + Prints a diagnostic into the standard error. + + A diagnostic is either returned by `Kernel.ParallelCompiler` + or by `Code.with_diagnostics/2`. + """ + @doc since: "1.15.0" + @spec print_diagnostic(diagnostic(:warning | :error)) :: :ok + def print_diagnostic(diagnostic) do + :elixir_errors.print_diagnostic(diagnostic) + :ok + end + @doc ~S""" Formats the given code `string`. @@ -876,7 +936,7 @@ defmodule Code do warn_on_unnecessary_quotes: false, literal_encoder: &{:ok, {:__block__, &2, [&1]}}, token_metadata: true, - emit_warnings: false + warnings: false ] ++ opts {forms, comments} = string_to_quoted_with_comments!(string, to_quoted_opts) @@ -1688,7 +1748,7 @@ defmodule Code do ## Examples - iex> Code.ensure_loaded?(Atom) + iex> Code.ensure_loaded?(String) true """ diff --git a/lib/elixir/lib/code/fragment.ex b/lib/elixir/lib/code/fragment.ex index 222694fd8..ed30dd3a4 100644 --- a/lib/elixir/lib/code/fragment.ex +++ b/lib/elixir/lib/code/fragment.ex @@ -1054,6 +1054,6 @@ defmodule Code.Fragment do opts = Keyword.take(opts, [:file, :line, :column, :columns, :token_metadata, :literal_encoder]) - Code.string_to_quoted(fragment, [cursor_completion: true, emit_warnings: false] ++ opts) + Code.string_to_quoted(fragment, [cursor_completion: true, warnings: false] ++ opts) end end diff --git a/lib/elixir/lib/kernel/parallel_compiler.ex b/lib/elixir/lib/kernel/parallel_compiler.ex index fc9a512d1..a8491220c 100644 --- a/lib/elixir/lib/kernel/parallel_compiler.ex +++ b/lib/elixir/lib/kernel/parallel_compiler.ex @@ -3,26 +3,14 @@ defmodule Kernel.ParallelCompiler do A module responsible for compiling and requiring files in parallel. """ - @typedoc "The line. 0 indicates no line." - @type line() :: non_neg_integer() - @type position() :: line() | {pos_integer(), column :: non_neg_integer} - - @type diagnostic(severity) :: %{ - file: Path.t(), - severity: severity, - message: String.t(), - position: position, - stacktrace: Exception.stacktrace() - } - @type info :: %{ - runtime_warnings: [diagnostic(:warning)], - compile_warnings: [diagnostic(:warning)] + runtime_warnings: [Code.diagnostic(:warning)], + compile_warnings: [Code.diagnostic(:warning)] } # Deprecated types - @type warning() :: {file :: Path.t(), position(), message :: String.t()} - @type error() :: {file :: Path.t(), position(), message :: String.t()} + @type warning() :: {file :: Path.t(), Code.position(), message :: String.t()} + @type error() :: {file :: Path.t(), Code.position(), message :: String.t()} @doc """ Starts a task for parallel compilation. @@ -71,7 +59,7 @@ defmodule Kernel.ParallelCompiler do resolved. It returns `{:ok, modules, warnings}` or `{:error, errors, warnings}` - by default but we recommend using `return_maps: true` so it returns + by default but we recommend using `return_diagnostics: true` so it returns diagnostics as maps as well as a map of compilation information. The map has the shape of: @@ -115,14 +103,14 @@ 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 + * `:return_diagnostics` (since v1.15.0) - returns maps with information instead of 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] | [diagnostic(:error)], [warning] | info()} + | {:error, [error] | [Code.diagnostic(:error)], [warning] | info()} def compile(files, options \\ []) when is_list(options) do spawn_workers(files, :compile, options) end @@ -135,7 +123,7 @@ defmodule Kernel.ParallelCompiler do @doc since: "1.6.0" @spec compile_to_path([Path.t()], Path.t(), keyword()) :: {:ok, [atom], [warning] | info()} - | {:error, [error] | [diagnostic(:error)], [warning] | info()} + | {:error, [error] | [Code.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 @@ -147,7 +135,7 @@ defmodule Kernel.ParallelCompiler do automatically solved between files. It returns `{:ok, modules, warnings}` or `{:error, errors, warnings}` - by default but we recommend using `return_maps: true` so it returns + by default but we recommend using `return_diagnostics: true` so it returns diagnostics as maps as well as a map of compilation information. The map has the shape of: @@ -172,14 +160,6 @@ defmodule Kernel.ParallelCompiler do spawn_workers(files, :require, options) end - @doc """ - Prints a diagnostic returned by the compiler into stderr. - """ - @doc since: "1.15.0" - 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 @@ -239,7 +219,7 @@ defmodule Kernel.ParallelCompiler do end # TODO: Require this to be set from Elixir v1.19 - if Keyword.get(options, :return_maps, false) do + if Keyword.get(options, :return_diagnostics, false) do {status, modules_or_errors, info} else to_tuples = &Enum.map(&1, fn diag -> {diag.file, diag.position, diag.message} end) diff --git a/lib/elixir/lib/module/parallel_checker.ex b/lib/elixir/lib/module/parallel_checker.ex index 20f146931..f5e93a25f 100644 --- a/lib/elixir/lib/module/parallel_checker.ex +++ b/lib/elixir/lib/module/parallel_checker.ex @@ -48,7 +48,7 @@ defmodule Module.ParallelChecker do @doc """ Spawns a process that runs the parallel checker. """ - def spawn({pid, checker}, module, info) do + def spawn({pid, checker}, module, info, log?) do ref = make_ref() spawned = @@ -79,7 +79,7 @@ defmodule Module.ParallelChecker do warnings = if module_map do - check_module(module_map, {checker, ets}) + check_module(module_map, {checker, ets}, log?) else [] end @@ -143,12 +143,23 @@ defmodule Module.ParallelChecker do """ @spec verify(pid(), [{module(), Path.t()}]) :: [warning()] def verify(checker, runtime_files) do + value = :erlang.get(:elixir_code_diagnostics) + log? = not match?({_, false}, value) + for {module, file} <- runtime_files do - spawn({self(), checker}, module, file) + spawn({self(), checker}, module, file, log?) end count = :gen_server.call(checker, :start, :infinity) - collect_results(count, []) + diagnostics = collect_results(count, []) + + case :erlang.get(:elixir_code_diagnostics) do + :undefined -> :ok + {tail, true} -> :erlang.put(:elixir_code_diagnostics, {diagnostics ++ tail, true}) + {tail, false} -> :erlang.put(:elixir_code_diagnostics, {diagnostics ++ tail, false}) + end + + diagnostics end defp collect_results(0, diagnostics) do @@ -221,7 +232,7 @@ defmodule Module.ParallelChecker do ## Module checking - defp check_module(module_map, cache) do + defp check_module(module_map, cache, log?) do %{ module: module, file: file, @@ -252,7 +263,7 @@ defmodule Module.ParallelChecker do |> Module.Types.warnings(file, definitions, no_warn_undefined, cache) |> Kernel.++(behaviour_warnings) |> group_warnings() - |> emit_warnings() + |> emit_warnings(log?) module_map |> Map.get(:after_verify, []) @@ -278,7 +289,7 @@ defmodule Module.ParallelChecker do ## Warning helpers - def group_warnings(warnings) do + defp group_warnings(warnings) do warnings |> Enum.reduce(%{}, fn {module, warning, location}, acc -> locations = MapSet.new([location]) @@ -288,11 +299,11 @@ defmodule Module.ParallelChecker do |> Enum.sort() end - def emit_warnings(warnings) do + defp emit_warnings(warnings, log?) do Enum.flat_map(warnings, fn {module, warning, locations} -> message = module.format_warning(warning) diagnostics = Enum.map(locations, &to_diagnostic(message, &1)) - :elixir_errors.print_warning([message, ?\n, format_stacktraces(diagnostics)]) + log? and :elixir_errors.print_warning([message, ?\n, format_stacktraces(diagnostics)]) diagnostics end) end @@ -317,7 +328,7 @@ defmodule Module.ParallelChecker do severity: :warning, file: file, position: line, - message: message, + message: IO.iodata_to_binary(message), stacktrace: [to_stacktrace(file, line, mfa)] } end diff --git a/lib/elixir/src/elixir.erl b/lib/elixir/src/elixir.erl index 46bb07d2d..463f544e2 100644 --- a/lib/elixir/src/elixir.erl +++ b/lib/elixir/src/elixir.erl @@ -426,7 +426,7 @@ string_to_tokens(String, StartLine, StartColumn, File, Opts) when is_integer(Sta {ok, _Line, _Column, [], Tokens} -> {ok, Tokens}; {ok, _Line, _Column, Warnings, Tokens} -> - (lists:keyfind(emit_warnings, 1, Opts) /= {emit_warnings, false}) andalso + (lists:keyfind(warnings, 1, Opts) /= {warnings, false}) andalso [elixir_errors:erl_warn(L, File, M) || {L, M} <- lists:reverse(Warnings)], {ok, Tokens}; {error, {Line, Column, {ErrorPrefix, ErrorSuffix}, Token}, _Rest, _Warnings, _SoFar} -> @@ -486,8 +486,8 @@ to_binary(Atom) when is_atom(Atom) -> atom_to_binary(Atom). handle_parsing_opts(File, Opts) -> WarningFile = - case lists:keyfind(emit_warnings, 1, Opts) of - {emit_warnings, false} -> nil; + case lists:keyfind(warnings, 1, Opts) of + {warnings, false} -> nil; _ -> File end, LiteralEncoder = diff --git a/lib/elixir/src/elixir_errors.erl b/lib/elixir/src/elixir_errors.erl index 4a907f207..16874a1d6 100644 --- a/lib/elixir/src/elixir_errors.erl +++ b/lib/elixir/src/elixir_errors.erl @@ -33,7 +33,7 @@ print_diagnostic(#{severity := Severity, message := Message, stacktrace := Stack [["\n ", 'Elixir.Exception':format_stacktrace_entry(E)] || E <- Stacktrace] end, io:put_chars(standard_error, [prefix(Severity), Message, Location, "\n\n"]), - ok. + Diagnostic. emit_diagnostic(Severity, Position, File, Message, Stacktrace) -> Diagnostic = #{ @@ -44,7 +44,11 @@ emit_diagnostic(Severity, Position, File, Message, Stacktrace) -> stacktrace => Stacktrace }, - print_diagnostic(Diagnostic), + case get(elixir_code_diagnostics) of + undefined -> print_diagnostic(Diagnostic); + {Tail, true} -> put(elixir_code_diagnostics, {[print_diagnostic(Diagnostic) | Tail], true}); + {Tail, false} -> put(elixir_code_diagnostics, {[Diagnostic | Tail], false}) + end, case get(elixir_compiler_info) of undefined -> ok; diff --git a/lib/elixir/src/elixir_module.erl b/lib/elixir/src/elixir_module.erl index ca4bef4ed..88a66f14a 100644 --- a/lib/elixir/src/elixir_module.erl +++ b/lib/elixir/src/elixir_module.erl @@ -506,13 +506,20 @@ beam_location(ModuleAsCharlist) -> checker_info() -> case get(elixir_checker_info) of undefined -> undefined; - _ -> 'Elixir.Module.ParallelChecker':get() + _ -> + Log = + case erlang:get(elixir_code_diagnostics) of + {_, false} -> false; + _ -> true + end, + + {'Elixir.Module.ParallelChecker':get(), Log} end. spawn_parallel_checker(undefined, _Module, _ModuleMap) -> nil; -spawn_parallel_checker(CheckerInfo, Module, ModuleMap) -> - 'Elixir.Module.ParallelChecker':spawn(CheckerInfo, Module, ModuleMap). +spawn_parallel_checker({CheckerInfo, Log}, Module, ModuleMap) -> + 'Elixir.Module.ParallelChecker':spawn(CheckerInfo, Module, ModuleMap, Log). make_module_available(Module, Binary) -> case get(elixir_module_binaries) of diff --git a/lib/elixir/test/elixir/code_test.exs b/lib/elixir/test/elixir/code_test.exs index 4b1e2e439..3e528d451 100644 --- a/lib/elixir/test/elixir/code_test.exs +++ b/lib/elixir/test/elixir/code_test.exs @@ -21,6 +21,41 @@ defmodule CodeTest do Code.eval_quoted(contents, [], file: "sample.ex", line: 13) + describe "with_diagnostics/2" do + test "captures warnings" do + assert {:warn, [%{message: "hello"}]} = + Code.with_diagnostics(fn -> + IO.warn("hello") + :warn + end) + end + + test "captures and logs warnings" do + assert ExUnit.CaptureIO.capture_io(:stderr, fn -> + assert {:warn, [%{message: "hello"}]} = + Code.with_diagnostics([log: true], fn -> + IO.warn("hello") + :warn + end) + end) =~ "hello" + end + + test "can be nested" do + assert {:warn, [%{message: "hello"}]} = + Code.with_diagnostics(fn -> + IO.warn("hello") + + assert {:nested, [%{message: "world"}]} = + Code.with_diagnostics(fn -> + IO.warn("world") + :nested + end) + + :warn + end) + end + end + describe "eval_string/1,2,3" do test "correctly evaluates a string of code" do assert Code.eval_string("1 + 2") == {3, []} @@ -111,28 +146,45 @@ defmodule CodeTest do end) assert output =~ "incompatible types" + after + :code.purge(CodeTest.CheckerWarning) + :code.delete(CodeTest.CheckerWarning) end - end - test "eval_quoted/1" do - assert Code.eval_quoted(quote(do: 1 + 2)) == {3, []} - assert CodeTest.Sample.eval_quoted_info() == {CodeTest.Sample, "sample.ex", 13} + test "captures checker diagnostics" do + {{{:module, _, _, _}, _}, diagnostics} = + Code.with_diagnostics(fn -> + Code.eval_string(File.read!(fixture_path("checker_warning.exs")), []) + end) + + assert [%{message: "incompatible types:" <> _}] = diagnostics + after + :code.purge(CodeTest.CheckerWarning) + :code.delete(CodeTest.CheckerWarning) + end end - test "eval_quoted/2 with %Macro.Env{} at runtime" do - alias :lists, as: MyList - quoted = quote(do: MyList.flatten([[1, 2, 3]])) + describe "eval_quoted/1" do + test "evaluates expression" do + assert Code.eval_quoted(quote(do: 1 + 2)) == {3, []} + assert CodeTest.Sample.eval_quoted_info() == {CodeTest.Sample, "sample.ex", 13} + end + + test "with %Macro.Env{} at runtime" do + alias :lists, as: MyList + quoted = quote(do: MyList.flatten([[1, 2, 3]])) - assert Code.eval_quoted(quoted, [], __ENV__) == {[1, 2, 3], []} + assert Code.eval_quoted(quoted, [], __ENV__) == {[1, 2, 3], []} - # Let's check it discards tracers since the lexical tracker is explicitly nil - assert Code.eval_quoted(quoted, [], %{__ENV__ | tracers: [:bad]}) == {[1, 2, 3], []} - end + # Let's check it discards tracers since the lexical tracker is explicitly nil + assert Code.eval_quoted(quoted, [], %{__ENV__ | tracers: [:bad]}) == {[1, 2, 3], []} + end - test "eval_quoted/2 with %Macro.Env{} at compile time" do - defmodule CompileTimeEnv do - alias String.Chars - {"foo", []} = Code.eval_string("Chars.to_string(:foo)", [], __ENV__) + test "with %Macro.Env{} at compile time" do + defmodule CompileTimeEnv do + alias String.Chars + {"foo", []} = Code.eval_string("Chars.to_string(:foo)", [], __ENV__) + end end end @@ -144,101 +196,137 @@ defmodule CodeTest do end end - test "eval_quoted_with_env/3" do - alias :lists, as: MyList - quoted = quote(do: MyList.flatten([[1, 2, 3]])) - env = Code.env_for_eval(__ENV__) - assert Code.eval_quoted_with_env(quoted, [], env) == {[1, 2, 3], [], env} + describe "eval_quoted_with_env/3" do + test "returns results, bindings, and env" do + alias :lists, as: MyList + quoted = quote(do: MyList.flatten([[1, 2, 3]])) + env = Code.env_for_eval(__ENV__) + assert Code.eval_quoted_with_env(quoted, [], env) == {[1, 2, 3], [], env} - quoted = quote(do: alias(:dict, as: MyDict)) - {:dict, [], env} = Code.eval_quoted_with_env(quoted, [], env) - assert Macro.Env.fetch_alias(env, :MyDict) == {:ok, :dict} - end + quoted = quote(do: alias(:dict, as: MyDict)) + {:dict, [], env} = Code.eval_quoted_with_env(quoted, [], env) + assert Macro.Env.fetch_alias(env, :MyDict) == {:ok, :dict} + end - test "eval_quoted_with_env/3 with vars" do - env = Code.env_for_eval(__ENV__) - {1, [x: 1], env} = Code.eval_quoted_with_env(quote(do: var!(x) = 1), [], env) - assert Macro.Env.vars(env) == [{:x, nil}] - end + test "manages env vars" do + env = Code.env_for_eval(__ENV__) + {1, [x: 1], env} = Code.eval_quoted_with_env(quote(do: var!(x) = 1), [], env) + assert Macro.Env.vars(env) == [{:x, nil}] + end - test "eval_quoted_with_env/3 with pruning" do - env = Code.env_for_eval(__ENV__) + test "prunes vars" do + env = Code.env_for_eval(__ENV__) - fun = fn quoted, binding -> - {_, binding, env} = Code.eval_quoted_with_env(quoted, binding, env, prune_binding: true) - {binding, Macro.Env.vars(env)} - end + fun = fn quoted, binding -> + {_, binding, env} = Code.eval_quoted_with_env(quoted, binding, env, prune_binding: true) + {binding, Macro.Env.vars(env)} + end - assert fun.(quote(do: 123), []) == {[], []} - assert fun.(quote(do: 123), x: 2, y: 3) == {[], []} + assert fun.(quote(do: 123), []) == {[], []} + assert fun.(quote(do: 123), x: 2, y: 3) == {[], []} - assert fun.(quote(do: var!(x) = 1), []) == {[x: 1], [x: nil]} - assert fun.(quote(do: var!(x) = 1), x: 2, y: 3) == {[x: 1], [x: nil]} + assert fun.(quote(do: var!(x) = 1), []) == {[x: 1], [x: nil]} + assert fun.(quote(do: var!(x) = 1), x: 2, y: 3) == {[x: 1], [x: nil]} - assert fun.(quote(do: var!(x, :foo) = 1), []) == {[{{:x, :foo}, 1}], [x: :foo]} - assert fun.(quote(do: var!(x, :foo) = 1), x: 2, y: 3) == {[{{:x, :foo}, 1}], [x: :foo]} + assert fun.(quote(do: var!(x, :foo) = 1), []) == {[{{:x, :foo}, 1}], [x: :foo]} + assert fun.(quote(do: var!(x, :foo) = 1), x: 2, y: 3) == {[{{:x, :foo}, 1}], [x: :foo]} - assert fun.(quote(do: var!(x, :foo) = 1), [{{:x, :foo}, 2}, {{:y, :foo}, 3}]) == - {[{{:x, :foo}, 1}], [x: :foo]} + assert fun.(quote(do: var!(x, :foo) = 1), [{{:x, :foo}, 2}, {{:y, :foo}, 3}]) == + {[{{:x, :foo}, 1}], [x: :foo]} - assert fun.(quote(do: fn -> var!(x, :foo) = 1 end), []) == {[], []} - assert fun.(quote(do: fn -> var!(x, :foo) = 1 end), x: 1, y: 2) == {[], []} + assert fun.(quote(do: fn -> var!(x, :foo) = 1 end), []) == {[], []} + assert fun.(quote(do: fn -> var!(x, :foo) = 1 end), x: 1, y: 2) == {[], []} - assert fun.(quote(do: fn -> var!(x) end), x: 2, y: 3) == {[x: 2], [x: nil]} + assert fun.(quote(do: fn -> var!(x) end), x: 2, y: 3) == {[x: 2], [x: nil]} - assert fun.(quote(do: fn -> var!(x, :foo) end), [{{:x, :foo}, 2}, {{:y, :foo}, 3}]) == - {[{{:x, :foo}, 2}], [x: :foo]} - end + assert fun.(quote(do: fn -> var!(x, :foo) end), [{{:x, :foo}, 2}, {{:y, :foo}, 3}]) == + {[{{:x, :foo}, 2}], [x: :foo]} + end - defmodule Tracer do - def trace(event, env) do - send(self(), {:trace, event, env}) - :ok + defmodule Tracer do + def trace(event, env) do + send(self(), {:trace, event, env}) + :ok + end end - end - test "eval_quoted_with_env/3 with tracing and pruning" do - env = %{Code.env_for_eval(__ENV__) | tracers: [Tracer], function: nil} - binding = [x: 1, y: 2, z: 3] + test "with tracing and pruning" do + env = %{Code.env_for_eval(__ENV__) | tracers: [Tracer], function: nil} + binding = [x: 1, y: 2, z: 3] - quoted = - quote do - defmodule Elixir.CodeTest.TracingPruning do - var!(y) = :updated - var!(y) - var!(x) + quoted = + quote do + defmodule Elixir.CodeTest.TracingPruning do + var!(y) = :updated + var!(y) + var!(x) + end end - end - {_, binding, env} = Code.eval_quoted_with_env(quoted, binding, env, prune_binding: true) - assert Enum.sort(binding) == [] - assert env.versioned_vars == %{} + {_, binding, env} = Code.eval_quoted_with_env(quoted, binding, env, prune_binding: true) + assert Enum.sort(binding) == [] + assert env.versioned_vars == %{} - assert_receive {:trace, {:on_module, _, _}, %{module: CodeTest.TracingPruning} = trace_env} - assert trace_env.versioned_vars == %{{:result, Kernel} => 5, {:x, nil} => 1, {:y, nil} => 4} - end + assert_receive {:trace, {:on_module, _, _}, %{module: CodeTest.TracingPruning} = trace_env} + assert trace_env.versioned_vars == %{{:result, Kernel} => 5, {:x, nil} => 1, {:y, nil} => 4} + end - test "eval_quoted_with_env/3 with defguard" do - require Integer - env = Code.env_for_eval(__ENV__) - quoted = quote do: Integer.is_even(1) - {false, binding, env} = Code.eval_quoted_with_env(quoted, [], env, prune_binding: true) - assert binding == [] - assert Macro.Env.vars(env) == [] + test "with defguard" do + require Integer + env = Code.env_for_eval(__ENV__) + quoted = quote do: Integer.is_even(1) + {false, binding, env} = Code.eval_quoted_with_env(quoted, [], env, prune_binding: true) + assert binding == [] + assert Macro.Env.vars(env) == [] + end end - test "compile_file/1" do - assert Code.compile_file(fixture_path("code_sample.exs")) == [] - refute fixture_path("code_sample.exs") in Code.required_files() - end + describe "compile_file/1" do + test "compiles the given path" do + assert Code.compile_file(fixture_path("code_sample.exs")) == [] + refute fixture_path("code_sample.exs") in Code.required_files() + end + + test "emits checker warnings" do + output = + ExUnit.CaptureIO.capture_io(:stderr, fn -> + Code.compile_file(fixture_path("checker_warning.exs")) + end) + + assert output =~ "incompatible types" + after + :code.purge(CodeTest.CheckerWarning) + :code.delete(CodeTest.CheckerWarning) + end + + test "captures checker diagnostics" do + {[{CodeTest.CheckerWarning, _}], diagnostics} = + Code.with_diagnostics(fn -> + Code.compile_file(fixture_path("checker_warning.exs")) + end) + + assert [%{message: "incompatible types:" <> _}] = diagnostics + after + :code.purge(CodeTest.CheckerWarning) + :code.delete(CodeTest.CheckerWarning) + end - test "compile_file/1 also emits checker warnings" do - output = - ExUnit.CaptureIO.capture_io(:stderr, fn -> - Code.compile_file(fixture_path("checker_warning.exs")) - end) + test "captures checker diagnostics with logging" do + output = + ExUnit.CaptureIO.capture_io(:stderr, fn -> + {[{CodeTest.CheckerWarning, _}], diagnostics} = + Code.with_diagnostics([log: true], fn -> + Code.compile_file(fixture_path("checker_warning.exs")) + end) + + assert [%{message: "incompatible types:" <> _}] = diagnostics + end) - assert output =~ "incompatible types" + assert output =~ "incompatible types" + after + :code.purge(CodeTest.CheckerWarning) + :code.delete(CodeTest.CheckerWarning) + end end test "require_file/1" do @@ -317,6 +405,9 @@ defmodule CodeTest do end) assert output =~ "incompatible types" + after + :code.purge(CodeTest.CheckerWarning) + :code.delete(CodeTest.CheckerWarning) end test "works across lexical scopes" do diff --git a/lib/mix/lib/mix/compilers/elixir.ex b/lib/mix/lib/mix/compilers/elixir.ex index c53d5f109..2bfd19ff2 100644 --- a/lib/mix/lib/mix/compilers/elixir.ex +++ b/lib/mix/lib/mix/compilers/elixir.ex @@ -709,7 +709,7 @@ defmodule Mix.Compilers.Elixir do if print? do Mix.shell().print_app() - Kernel.ParallelCompiler.print_diagnostic(diagnostic) + Code.print_diagnostic(diagnostic) end diagnostic @@ -957,7 +957,7 @@ defmodule Mix.Compilers.Elixir do long_compilation_threshold: threshold, profile: profile, beam_timestamp: timestamp, - return_maps: true + return_diagnostics: true ] response = Kernel.ParallelCompiler.compile_to_path(stale, dest, compile_opts) |