Skip to content

Commit

Permalink
Allow packages to provide custom hints for Exceptions (#35094)
Browse files Browse the repository at this point in the history
Package authors may be able to predict likely user errors, and it can
be nice to create an informative hint. This PR makes it possible for packages to register
hint-handlers for a variety of error types via `register_error_hint`. For packages that
create their own custom Exception types, there is also `show_error_hints` which may
be called from the `showerror` method.
  • Loading branch information
timholy authored Mar 20, 2020
1 parent d33c5a5 commit 1d2ef16
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 1 deletion.
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 ")
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

2 comments on commit 1d2ef16

@nanosoldier
Copy link
Collaborator

Choose a reason for hiding this comment

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

Executing the daily package evaluation, I will reply here when finished:

@nanosoldier runtests(ALL, isdaily = true)

@nanosoldier
Copy link
Collaborator

Choose a reason for hiding this comment

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

Your package evaluation job has completed - possible new issues were detected. A full report can be found here. cc @maleadt

Please sign in to comment.