diff --git a/lib/iex/lib/iex/evaluator.ex b/lib/iex/lib/iex/evaluator.ex index 9c04e1a5281..d34fea366b4 100644 --- a/lib/iex/lib/iex/evaluator.ex +++ b/lib/iex/lib/iex/evaluator.ex @@ -89,7 +89,8 @@ defmodule IEx.Evaluator do result = with {:ok, tokens} <- :elixir.string_to_tokens(input, line, column, file, opts), - {:ok, adjusted_tokens} <- adjust_operator(tokens, line, column, file, opts, last_op), + {:ok, adjusted_tokens, adjusted_op} <- + adjust_operator(tokens, line, column, file, opts, last_op), {:ok, forms} <- :elixir.tokens_to_quoted(adjusted_tokens, file, opts) do last_op = case forms do @@ -97,6 +98,16 @@ defmodule IEx.Evaluator do _ -> :other end + forms = + if adjusted_op != nil do + quote do + IEx.Evaluator.assert_no_error!() + unquote(forms) + end + else + forms + end + {:ok, forms, last_op} end @@ -129,10 +140,20 @@ defmodule IEx.Evaluator do defp adjust_operator([{op_type, _, _} | _] = tokens, line, column, file, opts, _last_op) when op_type in @op_tokens do {:ok, prefix} = :elixir.string_to_tokens(~c"v(-1)", line, column, file, opts) - {:ok, prefix ++ tokens} + {:ok, prefix ++ tokens, op_type} end - defp adjust_operator(tokens, _line, _column, _file, _opts, _last_op), do: {:ok, tokens} + defp adjust_operator(tokens, _line, _column, _file, _opts, _last_op), do: {:ok, tokens, nil} + + @doc """ + Raises an error if the last iex result was itself an error + """ + def assert_no_error!() do + if Process.get(:iex_error, false) do + message = "skipping evaluation of expression because pipeline has failed" + reraise RuntimeError.exception(message), [] + end + end @doc """ Gets a value out of the binding, using the provided @@ -183,6 +204,10 @@ defmodule IEx.Evaluator do defp loop(%{server: server, ref: ref} = state) do receive do + {:reader_errored, ^server} -> + Process.put(:iex_error, true) + loop(state) + {:eval, ^server, code, counter} -> {status, state} = safe_eval_and_inspect(code, counter, state) send(server, {:evaled, self(), status}) @@ -293,9 +318,12 @@ defmodule IEx.Evaluator do defp safe_eval_and_inspect(forms, counter, state) do put_history(state) put_whereami(state) - {:ok, eval_and_inspect(forms, counter, state)} + result = eval_and_inspect(forms, counter, state) + Process.delete(:iex_error) + {:ok, result} catch kind, error -> + Process.put(:iex_error, true) print_error(kind, error, __STACKTRACE__) {:error, state} after diff --git a/lib/iex/lib/iex/server.ex b/lib/iex/lib/iex/server.ex index fe0d938ee03..ff28bde7a8d 100644 --- a/lib/iex/lib/iex/server.ex +++ b/lib/iex/lib/iex/server.ex @@ -150,6 +150,8 @@ defmodule IEx.Server do end {:io_reply, ^input, {:error, kind, error, stacktrace}} -> + send(evaluator, {:reader_errored, self()}) + banner = Exception.format_banner(kind, error, stacktrace) banner = diff --git a/lib/iex/test/iex/interaction_test.exs b/lib/iex/test/iex/interaction_test.exs index e6c5cd3aa50..26635c4f005 100644 --- a/lib/iex/test/iex/interaction_test.exs +++ b/lib/iex/test/iex/interaction_test.exs @@ -327,4 +327,41 @@ defmodule IEx.InteractionTest do File.write!(path, contents) path end + + describe "pipeline error safety" do + test "syntax error in pipeline prevents subsequent operations" do + input = """ + 1, 2, 3] + |> Enum.map(&(&1 * 2)) + """ + + output = capture_iex(input) + assert output =~ "skipping evaluation of expression because pipeline has failed" + end + + test "runtime error in first pipeline step prevents subsequent operations" do + input = """ + :not_a_list + |> Enum.map(&(&1 * 2)) + |> Enum.sum() + """ + + output = capture_iex(input) + assert output =~ "skipping evaluation of expression because pipeline has failed" + end + + test "error state is cleared after successful evaluation" do + input = """ + :not_a_list + |> Enum.map(&(&1 * 2)) + |> Enum.sum() + [1, 2, 3] + |> Enum.sum() + """ + + output = capture_iex(input) + assert output =~ "skipping evaluation of expression because pipeline has failed" + assert String.ends_with?(output, "6") + end + end end