Skip to content

Commit f310ed9

Browse files
authored
Make errors when piping expressions in IEx safer (#14795)
1 parent bba5542 commit f310ed9

File tree

3 files changed

+71
-4
lines changed

3 files changed

+71
-4
lines changed

lib/iex/lib/iex/evaluator.ex

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,14 +89,25 @@ defmodule IEx.Evaluator do
8989

9090
result =
9191
with {:ok, tokens} <- :elixir.string_to_tokens(input, line, column, file, opts),
92-
{:ok, adjusted_tokens} <- adjust_operator(tokens, line, column, file, opts, last_op),
92+
{:ok, adjusted_tokens, adjusted_op} <-
93+
adjust_operator(tokens, line, column, file, opts, last_op),
9394
{:ok, forms} <- :elixir.tokens_to_quoted(adjusted_tokens, file, opts) do
9495
last_op =
9596
case forms do
9697
{:=, _, [_, _]} -> :match
9798
_ -> :other
9899
end
99100

101+
forms =
102+
if adjusted_op != nil do
103+
quote do
104+
IEx.Evaluator.assert_no_error!()
105+
unquote(forms)
106+
end
107+
else
108+
forms
109+
end
110+
100111
{:ok, forms, last_op}
101112
end
102113

@@ -129,10 +140,20 @@ defmodule IEx.Evaluator do
129140
defp adjust_operator([{op_type, _, _} | _] = tokens, line, column, file, opts, _last_op)
130141
when op_type in @op_tokens do
131142
{:ok, prefix} = :elixir.string_to_tokens(~c"v(-1)", line, column, file, opts)
132-
{:ok, prefix ++ tokens}
143+
{:ok, prefix ++ tokens, op_type}
133144
end
134145

135-
defp adjust_operator(tokens, _line, _column, _file, _opts, _last_op), do: {:ok, tokens}
146+
defp adjust_operator(tokens, _line, _column, _file, _opts, _last_op), do: {:ok, tokens, nil}
147+
148+
@doc """
149+
Raises an error if the last iex result was itself an error
150+
"""
151+
def assert_no_error!() do
152+
if Process.get(:iex_error, false) do
153+
message = "skipping evaluation of expression because pipeline has failed"
154+
reraise RuntimeError.exception(message), []
155+
end
156+
end
136157

137158
@doc """
138159
Gets a value out of the binding, using the provided
@@ -183,6 +204,10 @@ defmodule IEx.Evaluator do
183204

184205
defp loop(%{server: server, ref: ref} = state) do
185206
receive do
207+
{:reader_errored, ^server} ->
208+
Process.put(:iex_error, true)
209+
loop(state)
210+
186211
{:eval, ^server, code, counter} ->
187212
{status, state} = safe_eval_and_inspect(code, counter, state)
188213
send(server, {:evaled, self(), status})
@@ -293,9 +318,12 @@ defmodule IEx.Evaluator do
293318
defp safe_eval_and_inspect(forms, counter, state) do
294319
put_history(state)
295320
put_whereami(state)
296-
{:ok, eval_and_inspect(forms, counter, state)}
321+
result = eval_and_inspect(forms, counter, state)
322+
Process.delete(:iex_error)
323+
{:ok, result}
297324
catch
298325
kind, error ->
326+
Process.put(:iex_error, true)
299327
print_error(kind, error, __STACKTRACE__)
300328
{:error, state}
301329
after

lib/iex/lib/iex/server.ex

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,8 @@ defmodule IEx.Server do
150150
end
151151

152152
{:io_reply, ^input, {:error, kind, error, stacktrace}} ->
153+
send(evaluator, {:reader_errored, self()})
154+
153155
banner = Exception.format_banner(kind, error, stacktrace)
154156

155157
banner =

lib/iex/test/iex/interaction_test.exs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,4 +327,41 @@ defmodule IEx.InteractionTest do
327327
File.write!(path, contents)
328328
path
329329
end
330+
331+
describe "pipeline error safety" do
332+
test "syntax error in pipeline prevents subsequent operations" do
333+
input = """
334+
1, 2, 3]
335+
|> Enum.map(&(&1 * 2))
336+
"""
337+
338+
output = capture_iex(input)
339+
assert output =~ "skipping evaluation of expression because pipeline has failed"
340+
end
341+
342+
test "runtime error in first pipeline step prevents subsequent operations" do
343+
input = """
344+
:not_a_list
345+
|> Enum.map(&(&1 * 2))
346+
|> Enum.sum()
347+
"""
348+
349+
output = capture_iex(input)
350+
assert output =~ "skipping evaluation of expression because pipeline has failed"
351+
end
352+
353+
test "error state is cleared after successful evaluation" do
354+
input = """
355+
:not_a_list
356+
|> Enum.map(&(&1 * 2))
357+
|> Enum.sum()
358+
[1, 2, 3]
359+
|> Enum.sum()
360+
"""
361+
362+
output = capture_iex(input)
363+
assert output =~ "skipping evaluation of expression because pipeline has failed"
364+
assert String.ends_with?(output, "6")
365+
end
366+
end
330367
end

0 commit comments

Comments
 (0)