diff --git a/NEWS.md b/NEWS.md index aedf39a84cad1..5f00cd97877f6 100644 --- a/NEWS.md +++ b/NEWS.md @@ -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 ---------------- diff --git a/base/errorshow.jl b/base/errorshow.jl index 2327a3732c906..c8f745fe90a57 100644 --- a/base/errorshow.jl +++ b/base/errorshow.jl @@ -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) @@ -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) @@ -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) @@ -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 @@ -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 ]...} @@ -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 @@ -731,4 +817,3 @@ function show(io::IO, ip::InterpreterIP) print(io, " in $(ip.code) at statement $(Int(ip.stmt))") end end - diff --git a/base/exports.jl b/base/exports.jl index 6754fe08b3079..2e8f2ccc3d72c 100644 --- a/base/exports.jl +++ b/base/exports.jl @@ -695,8 +695,10 @@ export backtrace, catch_backtrace, error, + register_error_hint, rethrow, retry, + show_error_hints, systemerror, # stack traces diff --git a/doc/src/base/base.md b/doc/src/base/base.md index 1035ba519c511..015b3729c6a8d 100644 --- a/doc/src/base/base.md +++ b/doc/src/base/base.md @@ -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 diff --git a/test/errorshow.jl b/test/errorshow.jl index ad47dad903f81..86b02dda7f4d6 100644 --- a/test/errorshow.jl +++ b/test/errorshow.jl @@ -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)