Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 36 additions & 4 deletions lib/iex/lib/iex/evaluator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -89,14 +89,25 @@ 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
{:=, _, [_, _]} -> :match
_ -> :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

Expand Down Expand Up @@ -129,10 +140,22 @@ 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
reraise RuntimeError.exception(
"skipping evaluation of expression because pipeline has failed"
),
[]
end
end

@doc """
Gets a value out of the binding, using the provided
Expand Down Expand Up @@ -183,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})
Expand Down Expand Up @@ -293,9 +320,14 @@ 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
Expand Down
2 changes: 2 additions & 0 deletions lib/iex/lib/iex/server.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
37 changes: 37 additions & 0 deletions lib/iex/test/iex/interaction_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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