Skip to content

Commit 3f30bf1

Browse files
committed
Add exception formatting and colorization to iex
1 parent c240a49 commit 3f30bf1

File tree

6 files changed

+254
-54
lines changed

6 files changed

+254
-54
lines changed

lib/elixir/lib/exception.ex

Lines changed: 117 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -216,35 +216,46 @@ defmodule Exception do
216216
@doc """
217217
Receives a tuple representing a stacktrace entry and formats it.
218218
"""
219-
def format_stacktrace_entry(entry)
219+
def format_stacktrace_entry(entry) do
220+
format_stacktrace_entry_into_fields(entry)
221+
|> tuple_to_list
222+
|> Enum.filter(fn field -> field && field != "" end)
223+
|> Enum.join(" ")
224+
end
225+
226+
@doc """
227+
Returns the fields from a single frame in a stack trace as a list of
228+
`[ app, location, mfa/module/file ]` where all but location can be nil.
229+
Intended for use inside the Elixir libraries and iex only
230+
"""
220231

221232
# From Macro.Env.stacktrace
222-
def format_stacktrace_entry({ module, :__MODULE__, 0, location }) do
223-
format_location(location) <> inspect(module) <> " (module)"
233+
def format_stacktrace_entry_into_fields({ module, :__MODULE__, 0, location }) do
234+
{ nil, format_location(location), inspect(module) <> " (module)" }
224235
end
225236

226237
# From :elixir_compiler_*
227-
def format_stacktrace_entry({ _module, :__MODULE__, 1, location }) do
228-
format_location(location) <> "(module)"
238+
def format_stacktrace_entry_into_fields({ _module, :__MODULE__, 1, location }) do
239+
{ nil, format_location(location), "(module)" }
229240
end
230241

231242
# From :elixir_compiler_*
232-
def format_stacktrace_entry({ _module, :__FILE__, 1, location }) do
233-
format_location(location) <> "(file)"
243+
def format_stacktrace_entry_into_fields({ _module, :__FILE__, 1, location }) do
244+
{ nil, format_location(location), "(file)" }
234245
end
235246

236-
def format_stacktrace_entry({module, fun, arity, location}) do
237-
format_application(module) <> format_location(location) <> format_mfa(module, fun, arity)
247+
def format_stacktrace_entry_into_fields({module, fun, arity, location}) do
248+
{ format_application(module), format_location(location), format_mfa(module, fun, arity) }
238249
end
239250

240-
def format_stacktrace_entry({fun, arity, location}) do
241-
format_location(location) <> format_fa(fun, arity)
251+
def format_stacktrace_entry_into_fields({fun, arity, location}) do
252+
{ nil, format_location(location), format_fa(fun, arity) }
242253
end
243254

244255
defp format_application(module) do
245256
case :application.get_application(module) do
246-
{ :ok, app } -> "(" <> atom_to_binary(app) <> ") "
247-
:undefined -> ""
257+
{ :ok, app } -> "(" <> atom_to_binary(app) <> ")"
258+
:undefined -> nil
248259
end
249260
end
250261

@@ -261,7 +272,6 @@ defmodule Exception do
261272
catch
262273
:stacktrace -> Enum.drop(:erlang.get_stacktrace, 1)
263274
end
264-
265275
case trace do
266276
[] -> "\n"
267277
s -> " " <> Enum.map_join(s, "\n ", &format_stacktrace_entry(&1)) <> "\n"
@@ -303,74 +313,97 @@ defmodule Exception do
303313
304314
"""
305315
def format_fa(fun, arity) do
306-
if is_list(arity) do
307-
inspected = lc x inlist arity, do: inspect(x)
308-
"#{inspect fun}(#{Enum.join(inspected, ", ")})"
309-
else
310-
"#{inspect fun}/#{arity}"
311-
end
316+
"#{inspect fun}#{format_arity(arity)}"
312317
end
313318

314319
@doc """
315320
Receives a module, fun and arity and formats it
316321
as shown in stacktraces. The arity may also be a list
317322
of arguments.
318-
323+
319324
## Examples
320-
321325
iex> Exception.format_mfa Foo, :bar, 1
322326
"Foo.bar/1"
323327
iex> Exception.format_mfa Foo, :bar, []
324328
"Foo.bar()"
325329
iex> Exception.format_mfa nil, :bar, []
326330
"nil.bar()"
327331
332+
Anonymous functions are reported as -func/arity-anonfn-count-,
333+
where func is the name of the enclosing function. Convert to
334+
"nth fn in func/arity"
328335
"""
329-
def format_mfa(module, fun, arity) do
330-
fun =
331-
case inspect(fun) do
332-
<< ?:, erl :: binary >> -> erl
333-
elixir -> elixir
334-
end
335336

336-
if is_list(arity) do
337-
inspected = lc x inlist arity, do: inspect(x)
338-
"#{inspect module}.#{fun}(#{Enum.join(inspected, ", ")})"
339-
else
340-
"#{inspect module}.#{fun}/#{arity}"
341-
end
337+
def format_mfa(module, nil, arity),
338+
do: do_format_mfa(module, "nil", arity)
339+
340+
def format_mfa(module, fun, arity) when is_atom(fun),
341+
do: do_format_mfa(module, to_string(fun), arity)
342+
343+
defp do_format_mfa(module, fun, arity) when not(is_binary(fun)),
344+
do: format_mfa(module, inspect(fun), arity)
345+
346+
defp do_format_mfa(module, "-" <> fun, arity) do
347+
[ outer_fun, "fun", count, "" ] = String.split(fun, "-")
348+
"#{format_nth(count)} anonymous fn#{format_arity(arity)} in #{inspect module}.#{outer_fun}"
342349
end
343350

351+
# Erlang internal
352+
defp do_format_mfa(module, ":" <> fun, arity),
353+
do: format_mfa(module, maybe_quote_name(fun), arity)
354+
355+
defp do_format_mfa(module, fun, arity) do
356+
"#{inspect module}.#{maybe_quote_name(fun)}#{format_arity(arity)}"
357+
end
358+
359+
defp format_arity(arity) when is_list(arity) do
360+
inspected = lc x inlist arity, do: inspect(x)
361+
"(#{Enum.join(inspected, ", ")})"
362+
end
363+
364+
defp format_arity(arity), do: "/#{arity}"
365+
366+
defp format_nth("0"), do: "first"
367+
defp format_nth("1"), do: "second"
368+
defp format_nth("2"), do: "third"
369+
defp format_nth(n), do: "#{binary_to_integer(n)+1}th"
370+
371+
344372
@doc """
345373
Formats the given file and line as shown in stacktraces.
346-
If any of the values are nil, they are omitted.
374+
If any of the values are nil, they are omitted. If the
375+
optional suffix is omitted, a space is appended to
376+
the result.
347377
348378
## Examples
349379
350380
iex> Exception.format_file_line("foo", 1)
351381
"foo:1: "
352382
383+
iex> Exception.format_file_line("foo", 1, "")
384+
"foo:1:"
385+
353386
iex> Exception.format_file_line("foo", nil)
354387
"foo: "
355388
356389
iex> Exception.format_file_line(nil, nil)
357390
""
358391
359392
"""
360-
def format_file_line(file, line) do
393+
def format_file_line(file, line, suffix // " ") do
361394
if file do
362395
if line && line != 0 do
363-
"#{file}:#{line}: "
396+
"#{file}:#{line}:#{suffix}"
364397
else
365-
"#{file}: "
398+
"#{file}:#{suffix}"
366399
end
367400
else
368401
""
369402
end
370403
end
371404

372405
defp format_location(opts) do
373-
format_file_line Keyword.get(opts, :file), Keyword.get(opts, :line)
406+
format_file_line Keyword.get(opts, :file), Keyword.get(opts, :line), ""
374407
end
375408

376409
defp from_stacktrace([{ module, function, args, _ }|_]) when is_list(args) do
@@ -384,4 +417,49 @@ defmodule Exception do
384417
defp from_stacktrace(_) do
385418
{ nil, nil, nil }
386419
end
420+
421+
422+
def function_name_pattern do
423+
%r{
424+
\A(
425+
[\w]+[?!]?
426+
| ->
427+
| <-
428+
| ::
429+
| \|{1,3}
430+
| =
431+
| &&&?
432+
| <=?
433+
| >=?
434+
| ===?
435+
| !==?
436+
| =~
437+
| <<<
438+
| >>>
439+
| \+\+?
440+
| --?
441+
| <>
442+
| \+
443+
| -
444+
| \*
445+
| //?
446+
| ^^^
447+
| !
448+
| \^
449+
| &
450+
| ~~~
451+
| @
452+
)\z}x
453+
end
454+
455+
defp maybe_quote_name(fun) do
456+
name = to_string(fun)
457+
if Regex.match?(function_name_pattern, name) do
458+
name
459+
else
460+
inspect name
461+
end
462+
end
463+
464+
387465
end

lib/elixir/test/elixir/exception_test.exs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,13 @@ defmodule Kernel.ExceptionTest do
4242
end
4343

4444
test "format_stacktrace_entry with application" do
45-
assert Exception.format_stacktrace_entry({Exception, :bar, [], [file: 'file.ex']}) == "(elixir) file.ex: Exception.bar()"
46-
assert Exception.format_stacktrace_entry({Exception, :bar, [], [file: 'file.ex', line: 10]}) == "(elixir) file.ex:10: Exception.bar()"
47-
assert Exception.format_stacktrace_entry({:lists, :bar, [1, 2, 3], []}) == "(stdlib) :lists.bar(1, 2, 3)" end
45+
assert Exception.format_stacktrace_entry({Exception, :bar, [], [file: 'file.ex']}) ==
46+
"(elixir) file.ex: Exception.bar()"
47+
assert Exception.format_stacktrace_entry({Exception, :bar, [], [file: 'file.ex', line: 10]}) ==
48+
"(elixir) file.ex:10: Exception.bar()"
49+
assert Exception.format_stacktrace_entry({:lists, :bar, [1, 2, 3], []}) ==
50+
"(stdlib) :lists.bar(1, 2, 3)"
51+
end
4852

4953
test "format_stacktrace_entry with fun" do
5054
assert Exception.format_stacktrace_entry({fn(x) -> x end, [1], []}) =~ %r/#Function<.+>\(1\)/
@@ -98,10 +102,9 @@ defmodule Kernel.ExceptionTest do
98102
[top|_] = System.stacktrace
99103
top
100104
end
101-
102105
file = __ENV__.file |> Path.relative_to_cwd |> String.to_char_list!
103106
assert {Kernel.ExceptionTest, :"test raise preserves the stacktrace", _,
104-
[file: ^file, line: 96]} = stacktrace
107+
[file: ^file, line: _line]} = stacktrace
105108
end
106109

107110
test "defexception" do

lib/iex/lib/iex/evaluator.ex

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -200,14 +200,62 @@ defmodule IEx.Evaluator do
200200
io_error callback.()
201201
case prune_stacktrace(trace) do
202202
[] -> :ok
203-
other -> io_error Exception.format_stacktrace(other)
203+
other -> IO.puts(pretty_stacktrace(other))
204204
end
205205
catch
206-
_, _ ->
207-
io_error "** (IEx.Error) error when printing exception message and stacktrace"
206+
type, detail ->
207+
io_error "** (IEx.Error) #{str(type)} (#{str(detail)}) when printing exception message and stacktrace"
208208
end
209209
end
210210

211+
defp str(thing) when is_binary(thing), do: thing
212+
defp str(thing) do
213+
try do: to_string(thing), rescue: (_ -> inspect(thing))
214+
end
215+
216+
217+
218+
# at this point, the trace is nonempty
219+
def pretty_stacktrace(trace) do
220+
frames = (lc frame inlist trace do
221+
Exception.format_stacktrace_entry_into_fields(frame)
222+
end)
223+
col_0_width = calculate_width(frames, 0)
224+
col_1_width = calculate_width(frames, 1)
225+
lines = (lc frame inlist frames, do: pretty_print_frame(frame, col_0_width, col_1_width))
226+
" " <> Enum.join(lines, "\n ")
227+
end
228+
229+
defp pretty_print_frame({app, location, detail}, c1_width, c2_width) do
230+
s_app = String.rjust(app||"", c1_width)
231+
s_loc = String.ljust(location, c2_width)
232+
233+
"#{IEx.color(:eval_error, s_app)} #{IEx.color(:stack_loc, s_loc)} #{IEx.color(:stack_mfa, detail)}"
234+
end
235+
236+
# Look through all the entries in a particular column. If the
237+
# longest differs from the smallest by less than 8, return
238+
# the longest, so that smaller entries will be padded
239+
# and all entries for that column will be the same length.
240+
# If the difference is greater, return 0, and no padding
241+
# will be done. Public to allow testing
242+
@doc nil
243+
def calculate_width([ head | rest_of_lists ], column) do
244+
first_line_length = length_for(head, column)
245+
{min, max} = Enum.reduce(rest_of_lists,
246+
{first_line_length,first_line_length},
247+
fn (line, {min, max}) ->
248+
length = length_for(line, column)
249+
{min(min, length), max(max, length)}
250+
end)
251+
if max - min < 8, do: max, else: 0
252+
end
253+
254+
defp length_for(line, column) do
255+
string = elem line, column
256+
if string, do: String.length(string), else: 0
257+
end
258+
211259
defp prune_stacktrace([{ :erl_eval, _, _, _ }|_]), do: []
212260
defp prune_stacktrace([{ __MODULE__, _, _, _ }|_]), do: []
213261
defp prune_stacktrace([h|t]), do: [h|prune_stacktrace(t)]

lib/iex/lib/iex/options.ex

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,10 +123,12 @@ defmodule IEx.Options do
123123
124124
* `:enabled` - boolean value that allows for switching the coloring on and off
125125
* `:eval_result` - color for an expression's resulting value
126-
* `:eval_error` - color for error messages
127-
* `:eval_info` - color for various informational messages
128-
* `:ls_directory` - color for directory entries (ls helper)
129-
* `:ls_device` - color for device entries (ls helper)
126+
* `:eval_error` - … error messages
127+
* `:stack_loc` - … the location in a stack trace
128+
* `:error_mfa` - … the function name in a stack trace
129+
* `:eval_info` - … various informational messages
130+
* `:ls_directory` - … for directory entries (ls helper)
131+
* `:ls_device` - … device entries (ls helper)
130132
131133
When printing documentation, IEx will convert the markdown
132134
documentation to ANSI as well. Those can be configured via:

lib/iex/mix.exs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@ defmodule IEx.Mixfile do
1818

1919
# Used by default on evaluation cycle
2020
eval_interrupt: "yellow",
21-
eval_result: "yellow",
22-
eval_error: "red",
23-
eval_info: "normal",
21+
eval_result: "yellow",
22+
eval_error: "red",
23+
eval_info: "normal",
24+
stack_loc: "red,bright",
25+
stack_mfa: "green",
2426

2527
# Used by ls
2628
ls_directory: "blue",

0 commit comments

Comments
 (0)