diff --git a/base/errorshow.jl b/base/errorshow.jl index 35815bcd35fd0..56624c5b39868 100644 --- a/base/errorshow.jl +++ b/base/errorshow.jl @@ -538,15 +538,6 @@ function show_method_candidates(io::IO, ex::MethodError, @nospecialize kwargs=() end end -function show_trace_entry(io, frame, n; prefix = "") - if haskey(io, :LAST_SHOWN_LINE_INFOS) - push!(io[:LAST_SHOWN_LINE_INFOS], (string(frame.file), frame.line)) - end - print(io, "\n", prefix) - show(io, frame, full_path=true) - n > 1 && print(io, " (repeats ", n, " times)") -end - # In case the line numbers in the source code have changed since the code was compiled, # allow packages to set a callback function that corrects them. # (Used by Revise and perhaps other packages.) @@ -558,9 +549,44 @@ end # replace `sf` as needed. const update_stackframes_callback = Ref{Function}(identity) +function replaceuserpath(str) + str = replace(str, homedir() => "~") + # seems to be necessary for some paths with small letter drive c:// etc + str = replace(str, lowercasefirst(homedir()) => "~") + return str +end + +const STACKTRACE_MODULECOLORS = [:light_blue, :light_yellow, + :light_magenta, :light_green, :light_cyan, :light_red, + :blue, :yellow, :magenta, :green, :cyan, :red] +stacktrace_expand_basepaths()::Bool = + tryparse(Bool, get(ENV, "JULIA_STACKTRACE_EXPAND_BASEPATHS", "false")) === true +stacktrace_contract_userdir()::Bool = + tryparse(Bool, get(ENV, "JULIA_STACKTRACE_CONTRACT_HOMEDIR", "true")) === true +stacktrace_linebreaks()::Bool = + tryparse(Bool, get(ENV, "JULIA_STACKTRACE_LINEBREAKS", "false")) === true + +function show_full_backtrace(io::IO, trace; print_linebreaks::Bool) + n = length(trace) + ndigits_max = ndigits(n) + + modulecolordict = Dict{Module, Symbol}() + modulecolorcycler = Iterators.Stateful(Iterators.cycle(STACKTRACE_MODULECOLORS)) + + println(io, "\nStacktrace:") + + for (i, frame) in enumerate(trace) + print_stackframe(io, i, frame, 1, ndigits_max, modulecolordict, modulecolorcycler) + if i < n + println(io) + print_linebreaks && println(io) + end + end +end + const BIG_STACKTRACE_SIZE = 50 # Arbitrary constant chosen here -function show_reduced_backtrace(io::IO, t::Vector, with_prefix::Bool) +function show_reduced_backtrace(io::IO, t::Vector) recorded_positions = IdDict{UInt, Vector{Int}}() #= For each frame of hash h, recorded_positions[h] is the list of indices i such that hash(t[i-1]) == h, ie the list of positions in which the @@ -606,32 +632,133 @@ function show_reduced_backtrace(io::IO, t::Vector, with_prefix::Bool) try invokelatest(update_stackframes_callback[], displayed_stackframes) catch end + println(io, "\nStacktrace:") + + ndigits_max = ndigits(length(t)) + + modulecolordict = Dict{Module, Symbol}() + modulecolorcycler = Iterators.Stateful(Iterators.cycle(STACKTRACE_MODULECOLORS)) + push!(repeated_cycle, (0,0,0)) # repeated_cycle is never empty frame_counter = 1 for i in 1:length(displayed_stackframes) (frame, n) = displayed_stackframes[i] - if with_prefix - show_trace_entry(io, frame, n, prefix = string(" [", frame_counter, "] ")) - else - show_trace_entry(io, frame, n) + + print_stackframe(io, frame_counter, frame, n, ndigits_max, modulecolordict, modulecolorcycler) + + if i < length(displayed_stackframes) + println(io) + stacktrace_linebreaks() && println(io) end + while repeated_cycle[1][1] == i # never empty because of the initial (0,0,0) cycle_length = repeated_cycle[1][2] repetitions = repeated_cycle[1][3] popfirst!(repeated_cycle) - print(io, "\n ... (the last ", cycle_length, " lines are repeated ", - repetitions, " more time", repetitions>1 ? "s)" : ")") + printstyled(io, + "--- the last ", cycle_length, " lines are repeated ", + repetitions, " more time", repetitions>1 ? "s" : "", " ---", color = :light_black) + if i < length(displayed_stackframes) + println(io) + stacktrace_linebreaks() && println(io) + end frame_counter += cycle_length * repetitions end frame_counter += 1 end end + +# Print a stack frame where the module color is determined by looking up the parent module in +# `modulecolordict`. If the module does not have a color, yet, a new one can be drawn +# from `modulecolorcycler`. +function print_stackframe(io, i, frame, n, digit_align_width, modulecolordict, modulecolorcycler) + m = Base.parentmodule(frame) + if m !== nothing + while parentmodule(m) !== m + pm = parentmodule(m) + pm == Main && break + m = pm + end + if !haskey(modulecolordict, m) + modulecolordict[m] = popfirst!(modulecolorcycler) + end + modulecolor = modulecolordict[m] + else + modulecolor = :default + end + print_stackframe(io, i, frame, n, digit_align_width, modulecolor) +end + + +# Print a stack frame where the module color is set manually with `modulecolor`. +function print_stackframe(io, i, frame, n, digit_align_width, modulecolor) + file, line = string(frame.file), frame.line + stacktrace_expand_basepaths() && (file = something(find_source_file(file), file)) + stacktrace_contract_userdir() && (file = replaceuserpath(file)) + + # Used by the REPL to make it possible to open + # the location of a stackframe/method in the editor. + if haskey(io, :LAST_SHOWN_LINE_INFOS) + push!(io[:LAST_SHOWN_LINE_INFOS], (string(frame.file), frame.line)) + end + + inlined = getfield(frame, :inlined) + modul = parentmodule(frame) + + # frame number + print(io, " ", lpad("[" * string(i) * "]", digit_align_width + 2)) + print(io, " ") + + StackTraces.show_spec_linfo(IOContext(io, :backtrace=>true), frame) + if n > 1 + printstyled(io, " (repeats $n times)"; color=:light_black) + end + println(io) + + # @ + printstyled(io, " " ^ (digit_align_width + 2) * "@ ", color = :light_black) + + # module + if modul !== nothing + printstyled(io, modul, color = modulecolor) + print(io, " ") + end + + # filepath + pathparts = splitpath(file) + folderparts = pathparts[1:end-1] + if !isempty(folderparts) + printstyled(io, joinpath(folderparts...) * (Sys.iswindows() ? "\\" : "/"), color = :light_black) + end + + # filename, separator, line + # use escape codes for formatting, printstyled can't do underlined and color + # codes are bright black (90) and underlined (4) + function print_underlined(io::IO, s...) + colored = get(io, :color, false)::Bool + start_s = colored ? "\033[90;4m" : "" + end_s = colored ? "\033[0m" : "" + print(io, start_s, s..., end_s) + end + print_underlined(io, pathparts[end], ":", line) + + # inlined + printstyled(io, inlined ? " [inlined]" : "", color = :light_black) +end + + function show_backtrace(io::IO, t::Vector) if haskey(io, :LAST_SHOWN_LINE_INFOS) resize!(io[:LAST_SHOWN_LINE_INFOS], 0) end - filtered = process_backtrace(t) + + # t is a pre-processed backtrace (ref #12856) + if t isa Vector{Any} + filtered = t + else + filtered = process_backtrace(t) + end isempty(filtered) && return if length(filtered) == 1 && StackTraces.is_top_level_frame(filtered[1][1]) @@ -642,32 +769,18 @@ function show_backtrace(io::IO, t::Vector) end end - print(io, "\nStacktrace:") - if length(filtered) < BIG_STACKTRACE_SIZE - # Fast track: no duplicate stack frame detection. - try invokelatest(update_stackframes_callback[], filtered) catch end - frame_counter = 0 - for (last_frame, n) in filtered - frame_counter += 1 - show_trace_entry(IOContext(io, :backtrace => true), last_frame, n, prefix = string(" [", frame_counter, "] ")) - end + if length(filtered) > BIG_STACKTRACE_SIZE + show_reduced_backtrace(IOContext(io, :backtrace => true), filtered) return end - show_reduced_backtrace(IOContext(io, :backtrace => true), filtered, true) + try invokelatest(update_stackframes_callback[], filtered) catch end + # process_backtrace returns a Vector{Tuple{Frame, Int}} + frames = first.(filtered) + show_full_backtrace(io, frames; print_linebreaks = stacktrace_linebreaks()) + return end -function show_backtrace(io::IO, t::Vector{Any}) - # t is a pre-processed backtrace (ref #12856) - if length(t) < BIG_STACKTRACE_SIZE - try invokelatest(update_stackframes_callback[], t) catch end - for entry in t - show_trace_entry(io, entry...) - end - else - show_reduced_backtrace(io, t, false) - end -end function is_kw_sorter_name(name::Symbol) sn = string(name) diff --git a/base/loading.jl b/base/loading.jl index 1ca0b0b800f07..03094b867508d 100644 --- a/base/loading.jl +++ b/base/loading.jl @@ -648,7 +648,7 @@ end function find_source_file(path::AbstractString) (isabspath(path) || isfile(path)) && return path base_path = joinpath(Sys.BINDIR::String, DATAROOTDIR, "julia", "base", path) - return isfile(base_path) ? base_path : nothing + return isfile(base_path) ? normpath(base_path) : nothing end cache_file_entry(pkg::PkgId) = joinpath( diff --git a/base/show.jl b/base/show.jl index faaf6773fbe3d..7597e5f52fc8a 100644 --- a/base/show.jl +++ b/base/show.jl @@ -2011,11 +2011,17 @@ function show_signature_function(io::IO, @nospecialize(ft), demangle=false, farg nothing end -function show_tuple_as_call(io::IO, name::Symbol, sig::Type, demangle=false, kwargs=nothing) +function print_within_stacktrace(io, s...; color, bold=false) + if get(io, :backtrace, false)::Bool + printstyled(io, s...; color, bold) + else + print(io, s...) + end +end +function show_tuple_as_call(io::IO, name::Symbol, sig::Type, demangle=false, kwargs=nothing, argnames=nothing) # print a method signature tuple for a lambda definition - color = get(io, :color, false) && get(io, :backtrace, false) ? stackframe_function_color() : :nothing if sig === Tuple - printstyled(io, demangle ? demangle_function_name(name) : name, "(...)", color=color) + print(io, demangle ? demangle_function_name(name) : name, "(...)") return end tv = Any[] @@ -2026,16 +2032,18 @@ function show_tuple_as_call(io::IO, name::Symbol, sig::Type, demangle=false, kwa sig = sig.body end sig = sig.parameters - with_output_color(color, env_io) do io - show_signature_function(io, sig[1], demangle) - end + show_signature_function(env_io, sig[1], demangle) first = true - print_style = get(io, :color, false) && get(io, :backtrace, false) ? :bold : :nothing - printstyled(io, "(", color=print_style) + print_within_stacktrace(io, "(", color=:light_black) + show_argnames = argnames !== nothing && length(argnames) == length(sig) for i = 2:length(sig) # fixme (iter): `eachindex` with offset? first || print(io, ", ") first = false - print(env_io, "::", sig[i]) + if show_argnames + print_within_stacktrace(io, argnames[i]; bold=true, color=:light_black) + end + print(io, "::") + print_within_stacktrace(env_io, sig[i]; color=:light_black) end if kwargs !== nothing print(io, "; ") @@ -2043,11 +2051,12 @@ function show_tuple_as_call(io::IO, name::Symbol, sig::Type, demangle=false, kwa for (k, t) in kwargs first || print(io, ", ") first = false - print(io, k, "::") - show(io, t) + print_within_stacktrace(io, k; bold=true, color=:light_black) + print(io, "::") + print_within_stacktrace(io, t; color=:light_black) end end - printstyled(io, ")", color=print_style) + print_within_stacktrace(io, ")", color=:light_black) show_method_params(io, tv) nothing end diff --git a/base/stacktraces.jl b/base/stacktraces.jl index 9c13355ce789c..0837dc116e89b 100644 --- a/base/stacktraces.jl +++ b/base/stacktraces.jl @@ -216,30 +216,28 @@ function show_spec_linfo(io::IO, frame::StackFrame) elseif frame.func === top_level_scope_sym print(io, "top-level scope") else - color = get(io, :color, false) && get(io, :backtrace, false) ? - Base.stackframe_function_color() : - :nothing - printstyled(io, Base.demangle_function_name(string(frame.func)), color=color) + print(io, Base.demangle_function_name(string(frame.func))) end elseif frame.linfo isa MethodInstance def = frame.linfo.def if isa(def, Method) sig = frame.linfo.specTypes + argnames = Base.method_argnames(def) if def.nkw > 0 # rearrange call kw_impl(kw_args..., func, pos_args...) to func(pos_args...) kwarg_types = Any[ fieldtype(sig, i) for i = 2:(1+def.nkw) ] uw = Base.unwrap_unionall(sig) pos_sig = Base.rewrap_unionall(Tuple{uw.parameters[(def.nkw+2):end]...}, sig) - kwnames = Base.method_argnames(def)[2:(def.nkw+1)] + kwnames = argnames[2:(def.nkw+1)] for i = 1:length(kwnames) str = string(kwnames[i]) if endswith(str, "...") kwnames[i] = Symbol(str[1:end-3]) end end - Base.show_tuple_as_call(io, def.name, pos_sig, true, zip(kwnames, kwarg_types)) + Base.show_tuple_as_call(io, def.name, pos_sig, true, zip(kwnames, kwarg_types), argnames[def.nkw+2:end]) else - Base.show_tuple_as_call(io, def.name, sig, true) + Base.show_tuple_as_call(io, def.name, sig, true, nothing, argnames) end else Base.show(io, frame.linfo) @@ -249,18 +247,16 @@ function show_spec_linfo(io::IO, frame::StackFrame) end end -function show(io::IO, frame::StackFrame; full_path::Bool=false) +function show(io::IO, frame::StackFrame) show_spec_linfo(io, frame) if frame.file !== empty_sym - file_info = full_path ? string(frame.file) : basename(string(frame.file)) + file_info = basename(string(frame.file)) print(io, " at ") - Base.with_output_color(get(io, :color, false) && get(io, :backtrace, false) ? Base.stackframe_lineinfo_color() : :nothing, io) do io - print(io, file_info, ":") - if frame.line >= 0 - print(io, frame.line) - else - print(io, "?") - end + print(io, file_info, ":") + if frame.line >= 0 + print(io, frame.line) + else + print(io, "?") end end if frame.inlined diff --git a/doc/src/manual/environment-variables.md b/doc/src/manual/environment-variables.md index 7139c35cde09f..c434740906fe8 100644 --- a/doc/src/manual/environment-variables.md +++ b/doc/src/manual/environment-variables.md @@ -245,16 +245,6 @@ should have at the terminal. The formatting `Base.answer_color()` (default: normal, `"\033[0m"`) that output should have at the terminal. -### `JULIA_STACKFRAME_LINEINFO_COLOR` - -The formatting `Base.stackframe_lineinfo_color()` (default: bold, `"\033[1m"`) -that line info should have during a stack trace at the terminal. - -### `JULIA_STACKFRAME_FUNCTION_COLOR` - -The formatting `Base.stackframe_function_color()` (default: bold, `"\033[1m"`) -that function calls should have during a stack trace at the terminal. - ## Debugging and profiling ### `JULIA_DEBUG` diff --git a/stdlib/Logging/test/runtests.jl b/stdlib/Logging/test/runtests.jl index 373038844fb17..5c442de32c896 100644 --- a/stdlib/Logging/test/runtests.jl +++ b/stdlib/Logging/test/runtests.jl @@ -201,7 +201,7 @@ end │ exception = │ DivideError: integer division error │ Stacktrace: - │ [1] func1() at""") + │ [1] func1()""") @testset "Limiting large data structures" begin diff --git a/stdlib/REPL/test/repl.jl b/stdlib/REPL/test/repl.jl index 849a897e37687..bf4a33897bc2c 100644 --- a/stdlib/REPL/test/repl.jl +++ b/stdlib/REPL/test/repl.jl @@ -194,8 +194,8 @@ fake_repl(options = REPL.Options(confirm_exit=false,hascolor=false)) do stdin_wr @test occursin("shell> ", s) # check for the echo of the prompt @test occursin("'", s) # check for the echo of the input s = readuntil(stdout_read, "\n\n") - @test startswith(s, "\e[0mERROR: unterminated single quote\nStacktrace:\n [1] ") || - startswith(s, "\e[0m\e[1m\e[91mERROR: \e[39m\e[22m\e[91munterminated single quote\e[39m\nStacktrace:\n [1] ") + @test startswith(s, "\e[0mERROR: unterminated single quote\nStacktrace:\n [1] ") || + startswith(s, "\e[0m\e[1m\e[91mERROR: \e[39m\e[22m\e[91munterminated single quote\e[39m\nStacktrace:\n [1] ") end # issue #27293 diff --git a/test/errorshow.jl b/test/errorshow.jl index 21a78d27a62b6..761fa3cdf4efe 100644 --- a/test/errorshow.jl +++ b/test/errorshow.jl @@ -641,16 +641,16 @@ pop!(Base.Experimental._hint_handlers[DomainError]) # order is undefined, don't io = IOBuffer() Base.show_backtrace(io, bt) output = split(String(take!(io)), '\n') - @test output[3][1:4] == " [1]" + @test lstrip(output[3])[1:3] == "[1]" @test occursin("g28442", output[3]) - @test output[4][1:4] == " [2]" - @test occursin("f28442", output[4]) + @test lstrip(output[5])[1:3] == "[2]" + @test occursin("f28442", output[5]) # Issue #30233 # Note that we can't use @test_broken on FreeBSD here, because the tests actually do # pass with some compilation options, e.g. with assertions enabled if !Sys.isfreebsd() - @test occursin("the last 2 lines are repeated 5000 more times", output[5]) - @test output[6][1:8] == " [10003]" + @test occursin("the last 2 lines are repeated 5000 more times", output[7]) + @test lstrip(output[8])[1:7] == "[10003]" end end @@ -669,7 +669,7 @@ end output0 = split(String(take!(io)), '\n') function getline(output) idx = findfirst(str->occursin("getbt", str), output) - return parse(Int, match(r":(\d*)$", output[idx]).captures[1]) + return parse(Int, match(r":(\d*)$", output[idx+1]).captures[1]) end @test getline(outputc) == getline(output0) + 2 end @@ -698,7 +698,7 @@ let t1 = @async(error(1)), showerror(buf, e) s = String(take!(buf)) @test length(findall("Stacktrace:", s)) == 2 - @test occursin("[1] error(::Int", s) + @test occursin("[1] error(s::Int", s) end module TestMethodShadow @@ -727,3 +727,26 @@ let bt = try @test occursin(" include(", bt_str) @test !occursin(" _include(", bt_str) end + +# Test backtrace printing +module B + module C + f(x; y=2.0) = error() + end + module D + import ..C: f + g() = f(2.0; y=3.0) + end +end + +@testset "backtrace" begin + bt = try B.D.g() + catch + catch_backtrace() + end + bt_str = sprint(Base.show_backtrace, bt) + m = @__MODULE__ + @test contains(bt_str, "f(x::Float64; y::Float64)") + @test contains(bt_str, "@ $m.B.C") + @test contains(bt_str, "@ $m.B.D") +end diff --git a/test/stacktraces.jl b/test/stacktraces.jl index b3ec4db44a3ef..e6fe2736d95cc 100644 --- a/test/stacktraces.jl +++ b/test/stacktraces.jl @@ -177,17 +177,17 @@ let bt catch bt = stacktrace(catch_backtrace()) end - @test any(s->startswith(string(s), "f33065(::Float32; b::Float64, a::String)"), bt) + @test any(s->startswith(string(s), "f33065(x::Float32; b::Float64, a::String)"), bt) try f33065(0.0f0, b=:x) catch bt = stacktrace(catch_backtrace()) end - @test any(s->startswith(string(s), "f33065(::Float32; b::Symbol, a::String)"), bt) + @test any(s->startswith(string(s), "f33065(x::Float32; b::Symbol, a::String)"), bt) try f33065(0.0f0, 0.0f0, z=0) catch bt = stacktrace(catch_backtrace()) end - @test any(s->startswith(string(s), "f33065(::Float32, ::Float32; b::Float64, a::String, c::"), bt) + @test any(s->startswith(string(s), "f33065(x::Float32, y::Float32; b::Float64, a::String, c::"), bt) end