Skip to content

Commit 95de5ba

Browse files
author
José Valim
committed
Merge branch 'iex-exceptions-take-2' of github.com:pragdave/elixir into pragdave-iex-exceptions-take-2
2 parents 7cdb3b0 + 8456140 commit 95de5ba

File tree

6 files changed

+253
-54
lines changed

6 files changed

+253
-54
lines changed

lib/elixir/lib/exception.ex

Lines changed: 118 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -223,35 +223,46 @@ defmodule Exception do
223223
@doc """
224224
Receives a tuple representing a stacktrace entry and formats it.
225225
"""
226-
def format_stacktrace_entry(entry)
226+
def format_stacktrace_entry(entry) do
227+
format_stacktrace_entry_into_fields(entry)
228+
|> tuple_to_list
229+
|> Enum.filter(fn field -> field && field != "" end)
230+
|> Enum.join(" ")
231+
end
232+
233+
@doc """
234+
Returns the fields from a single frame in a stack trace as a list of
235+
`[ app, location, mfa/module/file ]` where all but location can be nil.
236+
Intended for use inside the Elixir libraries and iex only
237+
"""
227238

228239
# From Macro.Env.stacktrace
229-
def format_stacktrace_entry({ module, :__MODULE__, 0, location }) do
230-
format_location(location) <> inspect(module) <> " (module)"
240+
def format_stacktrace_entry_into_fields({ module, :__MODULE__, 0, location }) do
241+
{ nil, format_location(location), inspect(module) <> " (module)" }
231242
end
232243

233244
# From :elixir_compiler_*
234-
def format_stacktrace_entry({ _module, :__MODULE__, 1, location }) do
235-
format_location(location) <> "(module)"
245+
def format_stacktrace_entry_into_fields({ _module, :__MODULE__, 1, location }) do
246+
{ nil, format_location(location), "(module)" }
236247
end
237248

238249
# From :elixir_compiler_*
239-
def format_stacktrace_entry({ _module, :__FILE__, 1, location }) do
240-
format_location(location) <> "(file)"
250+
def format_stacktrace_entry_into_fields({ _module, :__FILE__, 1, location }) do
251+
{ nil, format_location(location), "(file)" }
241252
end
242253

243-
def format_stacktrace_entry({module, fun, arity, location}) do
244-
format_application(module) <> format_location(location) <> format_mfa(module, fun, arity)
254+
def format_stacktrace_entry_into_fields({module, fun, arity, location}) do
255+
{ format_application(module), format_location(location), format_mfa(module, fun, arity) }
245256
end
246257

247-
def format_stacktrace_entry({fun, arity, location}) do
248-
format_location(location) <> format_fa(fun, arity)
258+
def format_stacktrace_entry_into_fields({fun, arity, location}) do
259+
{ nil, format_location(location), format_fa(fun, arity) }
249260
end
250261

251262
defp format_application(module) do
252263
case :application.get_application(module) do
253-
{ :ok, app } -> "(" <> atom_to_binary(app) <> ") "
254-
:undefined -> ""
264+
{ :ok, app } -> "(" <> atom_to_binary(app) <> ")"
265+
:undefined -> nil
255266
end
256267
end
257268

@@ -268,7 +279,6 @@ defmodule Exception do
268279
catch
269280
:stacktrace -> Enum.drop(:erlang.get_stacktrace, 1)
270281
end
271-
272282
case trace do
273283
[] -> "\n"
274284
s -> " " <> Enum.map_join(s, "\n ", &format_stacktrace_entry(&1)) <> "\n"
@@ -309,74 +319,97 @@ defmodule Exception do
309319
310320
"""
311321
def format_fa(fun, arity) do
312-
if is_list(arity) do
313-
inspected = lc x inlist arity, do: inspect(x)
314-
"#{inspect fun}(#{Enum.join(inspected, ", ")})"
315-
else
316-
"#{inspect fun}/#{arity}"
317-
end
322+
"#{inspect fun}#{format_arity(arity)}"
318323
end
319324

320325
@doc """
321326
Receives a module, fun and arity and formats it
322327
as shown in stacktraces. The arity may also be a list
323328
of arguments.
324-
329+
325330
## Examples
326-
327331
iex> Exception.format_mfa Foo, :bar, 1
328332
"Foo.bar/1"
329333
iex> Exception.format_mfa Foo, :bar, []
330334
"Foo.bar()"
331335
iex> Exception.format_mfa nil, :bar, []
332336
"nil.bar()"
333337
338+
Anonymous functions are reported as -func/arity-anonfn-count-,
339+
where func is the name of the enclosing function. Convert to
340+
"nth fn in func/arity"
334341
"""
335-
def format_mfa(module, fun, arity) do
336-
fun =
337-
case inspect(fun) do
338-
<< ?:, erl :: binary >> -> erl
339-
elixir -> elixir
340-
end
341342

342-
if is_list(arity) do
343-
inspected = lc x inlist arity, do: inspect(x)
344-
"#{inspect module}.#{fun}(#{Enum.join(inspected, ", ")})"
345-
else
346-
"#{inspect module}.#{fun}/#{arity}"
347-
end
343+
def format_mfa(module, nil, arity),
344+
do: do_format_mfa(module, "nil", arity)
345+
346+
def format_mfa(module, fun, arity) when is_atom(fun),
347+
do: do_format_mfa(module, to_string(fun), arity)
348+
349+
defp do_format_mfa(module, fun, arity) when not(is_binary(fun)),
350+
do: format_mfa(module, inspect(fun), arity)
351+
352+
defp do_format_mfa(module, "-" <> fun, arity) do
353+
[ outer_fun, "fun", count, "" ] = String.split(fun, "-")
354+
"#{format_nth(count)} anonymous fn#{format_arity(arity)} in #{inspect module}.#{outer_fun}"
348355
end
349356

357+
# Erlang internal
358+
defp do_format_mfa(module, ":" <> fun, arity),
359+
do: format_mfa(module, maybe_quote_name(fun), arity)
360+
361+
defp do_format_mfa(module, fun, arity) do
362+
"#{inspect module}.#{maybe_quote_name(fun)}#{format_arity(arity)}"
363+
end
364+
365+
defp format_arity(arity) when is_list(arity) do
366+
inspected = lc x inlist arity, do: inspect(x)
367+
"(#{Enum.join(inspected, ", ")})"
368+
end
369+
370+
defp format_arity(arity), do: "/#{arity}"
371+
372+
defp format_nth("0"), do: "first"
373+
defp format_nth("1"), do: "second"
374+
defp format_nth("2"), do: "third"
375+
defp format_nth(n), do: "#{binary_to_integer(n)+1}th"
376+
377+
350378
@doc """
351379
Formats the given file and line as shown in stacktraces.
352-
If any of the values are nil, they are omitted.
380+
If any of the values are nil, they are omitted. If the
381+
optional suffix is omitted, a space is appended to
382+
the result.
353383
354384
## Examples
355385
356386
iex> Exception.format_file_line("foo", 1)
357387
"foo:1: "
358388
389+
iex> Exception.format_file_line("foo", 1, "")
390+
"foo:1:"
391+
359392
iex> Exception.format_file_line("foo", nil)
360393
"foo: "
361394
362395
iex> Exception.format_file_line(nil, nil)
363396
""
364397
365398
"""
366-
def format_file_line(file, line) do
399+
def format_file_line(file, line, suffix // " ") do
367400
if file do
368401
if line && line != 0 do
369-
"#{file}:#{line}: "
402+
"#{file}:#{line}:#{suffix}"
370403
else
371-
"#{file}: "
404+
"#{file}:#{suffix}"
372405
end
373406
else
374407
""
375408
end
376409
end
377410

378411
defp format_location(opts) do
379-
format_file_line Keyword.get(opts, :file), Keyword.get(opts, :line)
412+
format_file_line Keyword.get(opts, :file), Keyword.get(opts, :line), ""
380413
end
381414

382415
defp from_stacktrace([{ module, function, args, _ }|_]) when is_list(args) do
@@ -390,4 +423,50 @@ defmodule Exception do
390423
defp from_stacktrace(_) do
391424
{ nil, nil, nil }
392425
end
426+
427+
428+
# have to use :re here because exceptions may be triggered before Regexp
429+
# module is compiled.
430+
@function_name_re :re.compile(
431+
%S{
432+
\A(
433+
[\w]+[?!]?
434+
| ->
435+
| <-
436+
| ::
437+
| \|{1,3}
438+
| =
439+
| &&&?
440+
| <=?
441+
| >=?
442+
| ===?
443+
| !==?
444+
| =~
445+
| <<<
446+
| >>>
447+
| \+\+?
448+
| --?
449+
| <>
450+
| \+
451+
| -
452+
| \*
453+
| //?
454+
| ^^^
455+
| !
456+
| \^
457+
| &
458+
| ~~~
459+
| @
460+
)\z}, [:extended] )
461+
462+
defp maybe_quote_name(fun) do
463+
name = to_string(fun)
464+
{:ok, re} = @function_name_re
465+
case :re.run(name, re) do
466+
{ :match, _} -> name
467+
_ -> inspect name
468+
end
469+
end
470+
471+
393472
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: 100]} = 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)