Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow packages to provide custom hints for Exceptions #35094

Merged
merged 8 commits into from
Mar 20, 2020
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
4 changes: 4 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ New language features
macros and matrix constructors, which are whitespace sensitive, because expressions like
`[a ±b]` now get parsed as `[a ±(b)]` instead of `[±(a, b)]`. ([#34200])

* Packages can now provide custom hints to help users resolve errors by using the
`register_error_hint` function. Packages that define custom exception types
can support hints by calling `show_error_hints` from their `showerror` method. ([#35094])

Language changes
----------------

Expand Down
87 changes: 86 additions & 1 deletion base/errorshow.jl
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,87 @@ ERROR: MyException: test exception
"""
showerror(io::IO, ex) = show(io, ex)

"""
register_error_hint(handler, exceptiontype)

Register a "hinting" function `handler(io, exception)` that can
suggest potential ways for users to circumvent errors. `handler`
should examine `exception` to see whether the conditions appropriate
for a hint are met, and if so generate output to `io`.
Packages should call `register_error_hint` from within their
`__init__` function.

For specific exception types, `handler` is required to accept additional arguments:

- `MethodError`: provide `handler(io, exc::MethodError, argtypes, kwargs)`,
which splits the combined arguments into positional and keyword arguments.

When issuing a hint, the output should typically start with `\\n`.

If you define custom exception types, your `showerror` method can
support hints by calling [`show_error_hints`](@ref).

# Example

```
julia> module Hinter

only_int(x::Int) = 1
any_number(x::Number) = 2

function __init__()
register_error_hint(MethodError) do io, exc, argtypes, kwargs
if exc.f == only_int
# Color is not necessary, this is just to show it's possible.
print(io, "\\nDid you mean to call ")
Copy link
Sponsor Member

Choose a reason for hiding this comment

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

should we recommend folks use @info?

Copy link
Member

Choose a reason for hiding this comment

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

It's printing to a buffer io so I don't think we can replace it with @info.

(Or you are thinking to temporarily create a "buffer" logger to collect messages within a dynamic scope? It might work but I feel it's too fancy to do during error printing.)

printstyled(io, "`any_number`?", color=:cyan)
end
end
end

end
```

Then if you call `Hinter.only_int` on something that isn't an `Int` (thereby triggering a `MethodError`), it issues the hint:

```
julia> Hinter.only_int(1.0)
ERROR: MethodError: no method matching only_int(::Float64)
Did you mean to call `any_number`?
Closest candidates are:
...
```

!!! compat "Julia 1.5"
Custom error hints are available as of Julia 1.5.
"""
function register_error_hint(handler, exct::Type)
list = get!(()->[], _hint_handlers, exct)
push!(list, handler)
return nothing
end

const _hint_handlers = IdDict{Type,Vector{Any}}()

"""
show_error_hints(io, ex, args...)

Invoke all handlers from [`register_error_hint`](@ref) for the particular
exception type `typeof(ex)`. `args` must contain any other arguments expected by
the handler for that type.
"""
function show_error_hints(io, ex, args...)
hinters = get!(()->[], _hint_handlers, typeof(ex))
for handler in hinters
try
Base.invokelatest(handler, io, ex, args...)
catch err
tn = typeof(handler).name
@error "Hint-handler $handler for $(typeof(ex)) in $(tn.module) caused an error"
end
end
end

function showerror(io::IO, ex::BoundsError)
print(io, "BoundsError")
if isdefined(ex, :a)
Expand All @@ -45,6 +126,7 @@ function showerror(io::IO, ex::BoundsError)
print(io, ']')
end
end
show_error_hints(io, ex)
end

function showerror(io::IO, ex::TypeError)
Expand All @@ -68,6 +150,7 @@ function showerror(io::IO, ex::TypeError)
end
print(io, ctx, ", expected ", ex.expected, ", got ", targs...)
end
show_error_hints(io, ex)
end

function showerror(io::IO, ex, bt; backtrace=true)
Expand Down Expand Up @@ -106,6 +189,7 @@ function showerror(io::IO, ex::DomainError)
if isdefined(ex, :msg)
print(io, ":\n", ex.msg)
end
show_error_hints(io, ex)
nothing
end

Expand Down Expand Up @@ -161,6 +245,7 @@ function showerror(io::IO, ex::InexactError)
print(io, "InexactError: ", ex.func, '(')
nameof(ex.T) === ex.func || print(io, ex.T, ", ")
print(io, ex.val, ')')
show_error_hints(io, ex)
end

typesof(args...) = Tuple{Any[ Core.Typeof(a) for a in args ]...}
Expand Down Expand Up @@ -311,6 +396,7 @@ function showerror(io::IO, ex::MethodError)
"\nYou can convert to a column vector with the vec() function.")
end
end
show_error_hints(io, ex, arg_types_param, kwargs)
try
show_method_candidates(io, ex, kwargs)
catch ex
Expand Down Expand Up @@ -731,4 +817,3 @@ function show(io::IO, ip::InterpreterIP)
print(io, " in $(ip.code) at statement $(Int(ip.stmt))")
end
end

2 changes: 2 additions & 0 deletions base/exports.jl
Original file line number Diff line number Diff line change
Expand Up @@ -695,8 +695,10 @@ export
backtrace,
catch_backtrace,
error,
register_error_hint,
rethrow,
retry,
show_error_hints,
systemerror,

# stack traces
Expand Down
2 changes: 2 additions & 0 deletions doc/src/base/base.md
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,8 @@ Base.backtrace
Base.catch_backtrace
Base.catch_stack
Base.@assert
Base.register_error_hint
Base.show_error_hints
Base.ArgumentError
Base.AssertionError
Core.BoundsError
Expand Down
35 changes: 35 additions & 0 deletions test/errorshow.jl
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,41 @@ end
end
end

# Custom hints
struct HasNoOne end
function recommend_oneunit(io, ex, arg_types, kwargs)
if ex.f === Base.one && length(arg_types) == 1 && arg_types[1] === HasNoOne
if isempty(kwargs)
print(io, "\nHasNoOne does not support `one`; did you mean `oneunit`?")
else
print(io, "\n`one` doesn't take keyword arguments, that would be silly")
end
end
end
@test register_error_hint(recommend_oneunit, MethodError) === nothing
let err_str
err_str = @except_str one(HasNoOne()) MethodError
@test occursin(r"MethodError: no method matching one\(::.*HasNoOne\)", err_str)
@test occursin("HasNoOne does not support `one`; did you mean `oneunit`?", err_str)
err_str = @except_str one(HasNoOne(); value=2) MethodError
@test occursin(r"MethodError: no method matching one\(::.*HasNoOne; value=2\)", err_str)
@test occursin("`one` doesn't take keyword arguments, that would be silly", err_str)
end
pop!(Base._hint_handlers[MethodError]) # order is undefined, don't copy this

function busted_hint(io, exc, notarg) # wrong number of args
print(io, "\nI don't have a hint for you, sorry")
end
@test register_error_hint(busted_hint, DomainError) === nothing
try
sqrt(-2)
catch ex
io = IOBuffer()
@test_logs (:error, "Hint-handler busted_hint for DomainError in $(@__MODULE__) caused an error") showerror(io, ex)
end
pop!(Base._hint_handlers[DomainError]) # order is undefined, don't copy this


# issue #28442
@testset "Long stacktrace printing" begin
f28442(c) = g28442(c + 1)
Expand Down