Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
7 changes: 4 additions & 3 deletions lib/elixir/lib/access.ex
Original file line number Diff line number Diff line change
Expand Up @@ -517,8 +517,8 @@ defmodule Access do
An error is raised if the accessed structure is not a map or a struct:

iex> get_in([], [Access.key(:foo)])
** (BadMapError) expected a map, got: []

** (BadMapError) expected a map, got:
...
"""
@spec key(key, term) :: access_fun(data :: struct | map, current_value :: term)
def key(key, default \\ nil) do
Expand Down Expand Up @@ -556,7 +556,8 @@ defmodule Access do
iex> pop_in(map, [Access.key!(:user), Access.key!(:name)])
{"john", %{user: %{}}}
iex> get_in(map, [Access.key!(:user), Access.key!(:unknown)])
** (KeyError) key :unknown not found in: %{name: \"john\"}
** (KeyError) key :unknown not found in:
...

The examples above could be partially written as:

Expand Down
58 changes: 49 additions & 9 deletions lib/elixir/lib/exception.ex
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,22 @@ defmodule Exception do
end
end

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should mark it as @doc false. Preferably underscore it too.

@doc false
@spec _format_message_with_term(String.t(), any) :: String.t()
def _format_message_with_term(message, term) do
inspected =
term
|> inspect(pretty: true)
|> String.split("\n")
|> Enum.map(fn
"" -> ""
line -> " " <> line
end)
|> Enum.join("\n")

message <> "\n\n" <> inspected
end

@doc """
Attaches information to exceptions for extra debugging.

Expand Down Expand Up @@ -1431,7 +1447,10 @@ defmodule BadStructError do

@impl true
def message(exception) do
"expected a struct named #{inspect(exception.struct)}, got: #{inspect(exception.term)}"
Exception._format_message_with_term(
"expected a struct named #{inspect(exception.struct)}, got:",
exception.term
)
end
end

Expand All @@ -1451,7 +1470,10 @@ defmodule BadMapError do

@impl true
def message(exception) do
"expected a map, got: #{inspect(exception.term)}"
Exception._format_message_with_term(
"expected a map, got:",
exception.term
)
end
end

Expand All @@ -1470,7 +1492,10 @@ defmodule BadBooleanError do

@impl true
def message(exception) do
"expected a boolean on left-side of \"#{exception.operator}\", got: #{inspect(exception.term)}"
Exception._format_message_with_term(
"expected a boolean on left-side of \"#{exception.operator}\", got:",
exception.term
)
end
end

Expand All @@ -1492,7 +1517,10 @@ defmodule MatchError do

@impl true
def message(exception) do
"no match of right hand side value: #{inspect(exception.term)}"
Exception._format_message_with_term(
"no match of right hand side value:",
exception.term
)
end
end

Expand All @@ -1518,7 +1546,10 @@ defmodule CaseClauseError do

@impl true
def message(exception) do
"no case clause matching: #{inspect(exception.term)}"
Exception._format_message_with_term(
"no case clause matching:",
exception.term
)
end
end

Expand Down Expand Up @@ -1548,7 +1579,10 @@ defmodule WithClauseError do

@impl true
def message(exception) do
"no with clause matching: #{inspect(exception.term)}"
Exception._format_message_with_term(
"no with clause matching:",
exception.term
)
end
end

Expand Down Expand Up @@ -1598,7 +1632,10 @@ defmodule TryClauseError do

@impl true
def message(exception) do
"no try clause matching: #{inspect(exception.term)}"
Exception._format_message_with_term(
"no try clause matching:",
exception.term
)
end
end

Expand Down Expand Up @@ -2160,7 +2197,10 @@ defmodule KeyError do
"make sure to add parentheses after the function name)"

true ->
message <> " in: #{inspect(term, pretty: true, limit: :infinity)}"
Exception._format_message_with_term(
message <> " in:",
term
)
end
end

Expand Down Expand Up @@ -2202,7 +2242,7 @@ defmodule KeyError do

case suggestions do
[] -> []
suggestions -> [". Did you mean:\n\n" | format_suggestions(suggestions)]
suggestions -> ["\n\nDid you mean:\n\n" | format_suggestions(suggestions)]
end
end

Expand Down
3 changes: 2 additions & 1 deletion lib/elixir/lib/kernel/special_forms.ex
Original file line number Diff line number Diff line change
Expand Up @@ -742,7 +742,8 @@ defmodule Kernel.SpecialForms do
iex> x = 1
iex> ^x = List.first([1])
iex> ^x = List.first([2])
** (MatchError) no match of right hand side value: 2
** (MatchError) no match of right hand side value:
...

Note that `^x` always refers to the value of `x` prior to the match. The
following example will match:
Expand Down
15 changes: 10 additions & 5 deletions lib/elixir/lib/keyword.ex
Original file line number Diff line number Diff line change
Expand Up @@ -529,7 +529,8 @@ defmodule Keyword do
iex> Keyword.get_and_update!([a: 1], :b, fn current_value ->
...> {current_value, "new value!"}
...> end)
** (KeyError) key :b not found in: [a: 1]
** (KeyError) key :b not found in:
...

iex> Keyword.get_and_update!([a: 1], :a, fn _ ->
...> :pop
Expand Down Expand Up @@ -596,7 +597,8 @@ defmodule Keyword do
iex> Keyword.fetch!([a: 1], :a)
1
iex> Keyword.fetch!([a: 1], :b)
** (KeyError) key :b not found in: [a: 1]
** (KeyError) key :b not found in:
...

"""
@spec fetch!(t, key) :: value
Expand Down Expand Up @@ -879,7 +881,8 @@ defmodule Keyword do
[a: 1, b: :new, c: 3]

iex> Keyword.replace!([a: 1], :b, 2)
** (KeyError) key :b not found in: [a: 1]
** (KeyError) key :b not found in:
...

"""
@doc since: "1.5.0"
Expand Down Expand Up @@ -1135,7 +1138,8 @@ defmodule Keyword do
[a: 1, b: 4, c: 3]

iex> Keyword.update!([a: 1], :b, &(&1 * 2))
** (KeyError) key :b not found in: [a: 1]
** (KeyError) key :b not found in:
...

"""
@spec update!(t, key, (current_value :: value -> new_value :: value)) :: t
Expand Down Expand Up @@ -1348,7 +1352,8 @@ defmodule Keyword do
iex> Keyword.pop!([a: 1, a: 2], :a)
{1, []}
iex> Keyword.pop!([a: 1], :b)
** (KeyError) key :b not found in: [a: 1]
** (KeyError) key :b not found in:
...

"""
@doc since: "1.10.0"
Expand Down
15 changes: 10 additions & 5 deletions lib/elixir/lib/map.ex
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ defmodule Map do
map.foo
#=> "bar"
map.non_existing_key
** (KeyError) key :non_existing_key not found in: %{baz: "bong", foo: "bar"}
** (KeyError) key :non_existing_key not found in:
...

> #### Avoid parentheses {: .warning}
>
Expand Down Expand Up @@ -388,7 +389,8 @@ defmodule Map do
%{a: 3, b: 2}

iex> Map.replace!(%{a: 1}, :b, 2)
** (KeyError) key :b not found in: %{a: 1}
** (KeyError) key :b not found in:
...

"""
@doc since: "1.5.0"
Expand Down Expand Up @@ -725,7 +727,8 @@ defmodule Map do
iex> Map.pop!(%{a: 1, b: 2}, :a)
{1, %{b: 2}}
iex> Map.pop!(%{a: 1}, :b)
** (KeyError) key :b not found in: %{a: 1}
** (KeyError) key :b not found in:
...

"""
@doc since: "1.10.0"
Expand Down Expand Up @@ -911,7 +914,8 @@ defmodule Map do
%{a: 2}

iex> Map.update!(%{a: 1}, :b, &(&1 * 2))
** (KeyError) key :b not found in: %{a: 1}
** (KeyError) key :b not found in:
...

"""
@spec update!(map, key, (existing_value :: value -> new_value :: value)) :: map
Expand Down Expand Up @@ -986,7 +990,8 @@ defmodule Map do
iex> Map.get_and_update!(%{a: 1}, :b, fn current_value ->
...> {current_value, "new value!"}
...> end)
** (KeyError) key :b not found in: %{a: 1}
** (KeyError) key :b not found in:
...

iex> Map.get_and_update!(%{a: 1}, :a, fn _ ->
...> :pop
Expand Down
50 changes: 43 additions & 7 deletions lib/elixir/test/elixir/exception_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -701,33 +701,69 @@ defmodule ExceptionTest do
message = blame_message(%{first: nil, second: nil}, fn map -> map.firts end)

assert message == """
key :firts not found in: %{first: nil, second: nil}. Did you mean:
key :firts not found in:

%{first: nil, second: nil}

Did you mean:

* :first
"""

message = blame_message(%{"first" => nil, "second" => nil}, fn map -> map.firts end)

assert message == "key :firts not found in: %{\"first\" => nil, \"second\" => nil}"
assert message == """
key :firts not found in:

%{"first" => nil, "second" => nil}\
"""

message =
blame_message(%{"first" => nil, "second" => nil}, fn map -> Map.fetch!(map, "firts") end)

assert message == "key \"firts\" not found in: %{\"first\" => nil, \"second\" => nil}"
assert message ==
"""
key "firts" not found in:

%{"first" => nil, "second" => nil}\
"""

message =
blame_message([first: nil, second: nil], fn kwlist -> Keyword.fetch!(kwlist, :firts) end)
blame_message(
[
created_at: nil,
updated_at: nil,
deleted_at: nil,
started_at: nil,
finished_at: nil
],
fn kwlist ->
Keyword.fetch!(kwlist, :inserted_at)
end
)

assert message == """
key :firts not found in: [first: nil, second: nil]. Did you mean:
key :inserted_at not found in:

* :first
[
created_at: nil,
updated_at: nil,
deleted_at: nil,
started_at: nil,
finished_at: nil
]

Did you mean:

* :created_at
* :finished_at
* :started_at
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also an issues:
I think it should be something like:

key :firts not found, did you mean:

    * :first

in:

    [ data: :structure]
...

This is more natural imo - you first check the suggestion and then go to the printed structure.
With the current implementation, you have to go back after reading the suggestion.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not change the order for now but we definitely need to fix the line breaking.

"""
end

test "annotates key error with suggestions for structs" do
message = blame_message(%URI{}, fn map -> map.schema end)
assert message =~ "key :schema not found in: %URI{"
assert message =~ "key :schema not found in:\n\n %URI{"
assert message =~ "Did you mean:"
assert message =~ "* :scheme"
end
Expand Down
Loading
Loading