From 834d77b82003568579a86d05244dc6d7bec8650f Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Sat, 27 Sep 2025 10:32:33 -0400 Subject: [PATCH 1/9] make errors when piping in expressions safer For example: If you pasted this into an iex shell, you might have a very very bad day. ```elixir MyApp.Accounts.User |> Ecto.Query.where(user], user.id == 1) # note the missing open bracket |> MyApp.Repo.delete_all() ``` --- lib/iex/lib/iex/evaluator.ex | 42 +++++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/lib/iex/lib/iex/evaluator.ex b/lib/iex/lib/iex/evaluator.ex index 9c04e1a5281..0a43d89d4f0 100644 --- a/lib/iex/lib/iex/evaluator.ex +++ b/lib/iex/lib/iex/evaluator.ex @@ -69,7 +69,7 @@ defmodule IEx.Evaluator do """ def parse(input, opts, parser_state) - def parse(input, opts, []), do: parse(input, opts, {[], :other}) + def parse(input, opts, []), do: parse(input, opts, {[], :other, nil}) def parse(@break_trigger, opts, _parser_state) do :elixir_errors.parse_error( @@ -81,7 +81,7 @@ defmodule IEx.Evaluator do ) end - def parse(input, opts, {buffer, last_op}) do + def parse(input, opts, {buffer, last_op, adjusted_op}) do input = buffer ++ input file = Keyword.get(opts, :file, "nofile") line = Keyword.get(opts, :line, 1) @@ -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,15 +98,37 @@ defmodule IEx.Evaluator do _ -> :other end + forms = + if adjusted_op do + quote do + case Process.get(:iex_error) do + {_kind, _error, _stacktrace} -> + IO.write(:stdio, [ + "Skippping evaluation of:\n\n", + unquote(input), + "\n\n", + "For safety, you cannot begin an expression with `#{unquote(adjusted_op)}` when the last expression was an error.\n" + ]) + + IEx.dont_display_result() + + _ -> + unquote(forms) + end + end + else + forms + end + {:ok, forms, last_op} end case result do {:ok, forms, last_op} -> - {:ok, forms, {[], last_op}} + {:ok, forms, {[], last_op, adjusted_op}} {:error, {_, _, ""}} -> - {:incomplete, {input, last_op}} + {:incomplete, {input, last_op, adjusted_op}} {:error, {location, error, token}} -> :elixir_errors.parse_error( @@ -129,10 +152,10 @@ 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 """ Gets a value out of the binding, using the provided @@ -293,9 +316,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, {kind, error, __STACKTRACE__}) print_error(kind, error, __STACKTRACE__) {:error, state} after From d3c5f2a21c28d89f8ef4bf86c6a1d912f0cdf83c Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Sat, 27 Sep 2025 10:54:21 -0400 Subject: [PATCH 2/9] Update lib/iex/lib/iex/evaluator.ex MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Valim --- lib/iex/lib/iex/evaluator.ex | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/lib/iex/lib/iex/evaluator.ex b/lib/iex/lib/iex/evaluator.ex index 0a43d89d4f0..293eb40b103 100644 --- a/lib/iex/lib/iex/evaluator.ex +++ b/lib/iex/lib/iex/evaluator.ex @@ -99,22 +99,9 @@ defmodule IEx.Evaluator do end forms = - if adjusted_op do + if adjusted_op != nil and Process.get(:iex_error) != nil do quote do - case Process.get(:iex_error) do - {_kind, _error, _stacktrace} -> - IO.write(:stdio, [ - "Skippping evaluation of:\n\n", - unquote(input), - "\n\n", - "For safety, you cannot begin an expression with `#{unquote(adjusted_op)}` when the last expression was an error.\n" - ]) - - IEx.dont_display_result() - - _ -> - unquote(forms) - end + reraise RuntimeError.exception("skipping evaluation of expression because pipeline has failed"), [] end else forms From a205531691c2151cc647471084bd3de96e03c9e4 Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Sat, 27 Sep 2025 11:08:58 -0400 Subject: [PATCH 3/9] simplify display and don't modify parser state --- lib/iex/lib/iex/evaluator.ex | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/lib/iex/lib/iex/evaluator.ex b/lib/iex/lib/iex/evaluator.ex index 293eb40b103..2577cef1955 100644 --- a/lib/iex/lib/iex/evaluator.ex +++ b/lib/iex/lib/iex/evaluator.ex @@ -69,7 +69,7 @@ defmodule IEx.Evaluator do """ def parse(input, opts, parser_state) - def parse(input, opts, []), do: parse(input, opts, {[], :other, nil}) + def parse(input, opts, []), do: parse(input, opts, {[], :other}) def parse(@break_trigger, opts, _parser_state) do :elixir_errors.parse_error( @@ -81,7 +81,7 @@ defmodule IEx.Evaluator do ) end - def parse(input, opts, {buffer, last_op, adjusted_op}) do + def parse(input, opts, {buffer, last_op}) do input = buffer ++ input file = Keyword.get(opts, :file, "nofile") line = Keyword.get(opts, :line, 1) @@ -99,9 +99,16 @@ defmodule IEx.Evaluator do end forms = - if adjusted_op != nil and Process.get(:iex_error) != nil do + if adjusted_op != nil do quote do - reraise RuntimeError.exception("skipping evaluation of expression because pipeline has failed"), [] + if Process.get(:iex_error?) == true do + reraise RuntimeError.exception( + "skipping evaluation of expression because pipeline has failed" + ), + [] + else + unquote(forms) + end end else forms @@ -112,10 +119,10 @@ defmodule IEx.Evaluator do case result do {:ok, forms, last_op} -> - {:ok, forms, {[], last_op, adjusted_op}} + {:ok, forms, {[], last_op}} {:error, {_, _, ""}} -> - {:incomplete, {input, last_op, adjusted_op}} + {:incomplete, {input, last_op}} {:error, {location, error, token}} -> :elixir_errors.parse_error( @@ -304,11 +311,11 @@ defmodule IEx.Evaluator do put_history(state) put_whereami(state) result = eval_and_inspect(forms, counter, state) - Process.delete(:iex_error) + Process.delete(:iex_error?) {:ok, result} catch kind, error -> - Process.put(:iex_error, {kind, error, __STACKTRACE__}) + Process.put(:iex_error?, true) print_error(kind, error, __STACKTRACE__) {:error, state} after From fbe941d18099e277d7910dadcf576f1d6e1a50c9 Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Sat, 27 Sep 2025 11:27:50 -0400 Subject: [PATCH 4/9] change from `iex_error?` to `iex_error` to handle syntax errors, send to the evaluator to set `iex_error` to `:force` if its set to `:force`, we downgrate it to `true` first. --- lib/iex/lib/iex/evaluator.ex | 12 +++++++++--- lib/iex/lib/iex/server.ex | 9 +++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/lib/iex/lib/iex/evaluator.ex b/lib/iex/lib/iex/evaluator.ex index 2577cef1955..2705643d3ed 100644 --- a/lib/iex/lib/iex/evaluator.ex +++ b/lib/iex/lib/iex/evaluator.ex @@ -101,7 +101,7 @@ defmodule IEx.Evaluator do forms = if adjusted_op != nil do quote do - if Process.get(:iex_error?) == true do + if Process.get(:iex_error) do reraise RuntimeError.exception( "skipping evaluation of expression because pipeline has failed" ), @@ -311,11 +311,17 @@ defmodule IEx.Evaluator do put_history(state) put_whereami(state) result = eval_and_inspect(forms, counter, state) - Process.delete(:iex_error?) + + if Process.get(:iex_error) == :force do + Process.put(:iex_error, true) + else + Process.delete(:iex_error) + end + {:ok, result} catch kind, error -> - Process.put(:iex_error?, true) + 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..de6cb6f43c5 100644 --- a/lib/iex/lib/iex/server.ex +++ b/lib/iex/lib/iex/server.ex @@ -150,6 +150,15 @@ defmodule IEx.Server do end {:io_reply, ^input, {:error, kind, error, stacktrace}} -> + send( + evaluator, + {:eval, self(), + quote do + Process.put(:iex_error, :force) + IEx.dont_display_result() + end, 0} + ) + banner = Exception.format_banner(kind, error, stacktrace) banner = From 4010e297e2ea754c6c6d485387f04e141f334988 Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Sat, 27 Sep 2025 11:31:06 -0400 Subject: [PATCH 5/9] use default for `Process.get(:iex_error)` --- lib/iex/lib/iex/evaluator.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/iex/lib/iex/evaluator.ex b/lib/iex/lib/iex/evaluator.ex index 2705643d3ed..b89dd6409fb 100644 --- a/lib/iex/lib/iex/evaluator.ex +++ b/lib/iex/lib/iex/evaluator.ex @@ -101,7 +101,7 @@ defmodule IEx.Evaluator do forms = if adjusted_op != nil do quote do - if Process.get(:iex_error) do + if Process.get(:iex_error, false) do reraise RuntimeError.exception( "skipping evaluation of expression because pipeline has failed" ), From c5bb3bdad021b1ab1f99204de8b163792d5fd271 Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Sat, 27 Sep 2025 11:33:21 -0400 Subject: [PATCH 6/9] minimize alterations to user code --- lib/iex/lib/iex/evaluator.ex | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/lib/iex/lib/iex/evaluator.ex b/lib/iex/lib/iex/evaluator.ex index b89dd6409fb..2ad5d78c051 100644 --- a/lib/iex/lib/iex/evaluator.ex +++ b/lib/iex/lib/iex/evaluator.ex @@ -101,14 +101,8 @@ defmodule IEx.Evaluator do forms = if adjusted_op != nil do quote do - if Process.get(:iex_error, false) do - reraise RuntimeError.exception( - "skipping evaluation of expression because pipeline has failed" - ), - [] - else - unquote(forms) - end + IEx.Evaluator.assert_no_error!() + unquote(forms) end else forms @@ -151,6 +145,18 @@ defmodule IEx.Evaluator do 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 + reraise RuntimeError.exception( + "skipping evaluation of expression because pipeline has failed" + ), + [] + end + end + @doc """ Gets a value out of the binding, using the provided variable name and map key path. From 3c5911f14c0cc9b57aea167feeca798ea3e2fa5e Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Sat, 27 Sep 2025 17:23:14 -0400 Subject: [PATCH 7/9] clean up interface --- lib/iex/lib/iex/evaluator.ex | 10 +++++----- lib/iex/lib/iex/server.ex | 9 +-------- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/lib/iex/lib/iex/evaluator.ex b/lib/iex/lib/iex/evaluator.ex index 2ad5d78c051..a886cfa9bc4 100644 --- a/lib/iex/lib/iex/evaluator.ex +++ b/lib/iex/lib/iex/evaluator.ex @@ -206,6 +206,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}) @@ -318,11 +322,7 @@ defmodule IEx.Evaluator do put_whereami(state) result = eval_and_inspect(forms, counter, state) - if Process.get(:iex_error) == :force do - Process.put(:iex_error, true) - else - Process.delete(:iex_error) - end + Process.delete(:iex_error) {:ok, result} catch diff --git a/lib/iex/lib/iex/server.ex b/lib/iex/lib/iex/server.ex index de6cb6f43c5..ff28bde7a8d 100644 --- a/lib/iex/lib/iex/server.ex +++ b/lib/iex/lib/iex/server.ex @@ -150,14 +150,7 @@ defmodule IEx.Server do end {:io_reply, ^input, {:error, kind, error, stacktrace}} -> - send( - evaluator, - {:eval, self(), - quote do - Process.put(:iex_error, :force) - IEx.dont_display_result() - end, 0} - ) + send(evaluator, {:reader_errored, self()}) banner = Exception.format_banner(kind, error, stacktrace) From 02d1376ae97558368eeaa27531176d2f18c9ae23 Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Sat, 27 Sep 2025 17:35:59 -0400 Subject: [PATCH 8/9] add tests for pipeline safety feature --- lib/iex/test/iex/interaction_test.exs | 37 +++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) 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 From e6f4d8fa6cd4673bd9681902fc8c3da7a44abb82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sun, 28 Sep 2025 16:09:20 +0200 Subject: [PATCH 9/9] Apply suggestions from code review --- lib/iex/lib/iex/evaluator.ex | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/iex/lib/iex/evaluator.ex b/lib/iex/lib/iex/evaluator.ex index a886cfa9bc4..d34fea366b4 100644 --- a/lib/iex/lib/iex/evaluator.ex +++ b/lib/iex/lib/iex/evaluator.ex @@ -150,10 +150,8 @@ defmodule IEx.Evaluator do """ def assert_no_error!() do if Process.get(:iex_error, false) do - reraise RuntimeError.exception( - "skipping evaluation of expression because pipeline has failed" - ), - [] + message = "skipping evaluation of expression because pipeline has failed" + reraise RuntimeError.exception(message), [] end end @@ -321,9 +319,7 @@ defmodule IEx.Evaluator do put_history(state) put_whereami(state) result = eval_and_inspect(forms, counter, state) - Process.delete(:iex_error) - {:ok, result} catch kind, error ->