diff --git a/src/abbreviations.jl b/src/abbreviations.jl index 00365a8..396370d 100644 --- a/src/abbreviations.jl +++ b/src/abbreviations.jl @@ -91,7 +91,7 @@ const TYPEDFIELDS = TypeFields(true) function format(abbrv::TypeFields, buf, doc) local docs = get(doc.data, :fields, Dict()) local binding = doc.data[:binding] - local object = Docs.resolve(binding) + local object = Base.invokelatest(Docs.resolve, binding) local fields = isabstracttype(object) ? Symbol[] : fieldnames(object) if !isempty(fields) println(buf) @@ -154,7 +154,7 @@ const EXPORTS = ModuleExports() function format(::ModuleExports, buf, doc) local binding = doc.data[:binding] - local object = Docs.resolve(binding) + local object = Base.invokelatest(Docs.resolve, binding) local exports = names(object) if !isempty(exports) println(buf) @@ -202,7 +202,7 @@ const IMPORTS = ModuleImports() function format(::ModuleImports, buf, doc) local binding = doc.data[:binding] - local object = Docs.resolve(binding) + local object = Base.invokelatest(Docs.resolve, binding) local imports = unique(ccall(:jl_module_usings, Any, (Any,), object)) if !isempty(imports) println(buf) @@ -254,7 +254,7 @@ function format(::MethodList, buf, doc) local binding = doc.data[:binding] local typesig = doc.data[:typesig] local modname = doc.data[:module] - local func = Docs.resolve(binding) + local func = Base.invokelatest(Docs.resolve, binding) local groups = methodgroups(func, typesig, modname; exact = false) if !isempty(groups) println(buf) @@ -314,7 +314,7 @@ function format(::MethodSignatures, buf, doc) local binding = doc.data[:binding] local typesig = doc.data[:typesig] local modname = doc.data[:module] - local func = Docs.resolve(binding) + local func = Base.invokelatest(Docs.resolve, binding) local groups = methodgroups(func, typesig, modname) if !isempty(groups) @@ -372,7 +372,7 @@ function format(tms::TypedMethodSignatures, buf, doc) local binding = doc.data[:binding] local typesig = doc.data[:typesig] local modname = doc.data[:module] - local func = Docs.resolve(binding) + local func = Base.invokelatest(Docs.resolve, binding) # TODO: why is methodgroups returning invalid methods? # the methodgroups always appears to return a Vector and the size depends on whether parametric types are used # and whether default arguments are used @@ -399,12 +399,18 @@ function format(tms::TypedMethodSignatures, buf, doc) end @static if Sys.iswindows() && VERSION < v"1.8" - t = tuples[findlast(f, tuples)] + idx = findlast(f, tuples) else - t = tuples[findfirst(f, tuples)] + idx = findfirst(f, tuples) + end + if idx === nothing + # Fall back to untyped signature if no matching tuple is found. + printmethod(buf, binding, func, method) + else + t = tuples[idx] + printmethod(buf, binding, func, method, t; + print_return_types=tms.return_types) end - printmethod(buf, binding, func, method, t; - print_return_types=tms.return_types) println(buf) end println(buf, "\n```\n") @@ -536,7 +542,7 @@ end function format(::TypeDefinition, buf, doc) local binding = doc.data[:binding] - local object = gettype(Docs.resolve(binding)) + local object = gettype(Base.invokelatest(Docs.resolve, binding)) if isa(object, DataType) println(buf, "\n```julia") if isprimitivetype(object) @@ -680,7 +686,7 @@ function template_key(doc::Docs.DocStr) _key(other, sig, binding) = :DEFAULT binding = doc.data[:binding] - obj = Docs.resolve(binding) + obj = Base.invokelatest(Docs.resolve, binding) name = objname(obj, binding) key = name === binding.var ? _key(obj, doc.data[:typesig], binding) : :CONSTANTS return key diff --git a/src/utilities.jl b/src/utilities.jl index 9f1ae71..5069a87 100644 --- a/src/utilities.jl +++ b/src/utilities.jl @@ -113,7 +113,7 @@ A helper method for [`getmethods`](@ref) that collects methods in `results`. """ function getmethods!(results, f, sig) if sig == Union{} - append!(results, methods(f)) + append!(results, Base.invokelatest(methods, f)) elseif isa(sig, Union) for each in uniontypes(sig) getmethods!(results, f, each) @@ -121,7 +121,7 @@ function getmethods!(results, f, sig) elseif isa(sig, UnionAll) getmethods!(results, f, Base.unwrap_unionall(sig)) else - append!(results, methods(f, sig)) + append!(results, Base.invokelatest(methods, f, sig)) end return results end @@ -396,7 +396,7 @@ function printmethod(buffer::IOBuffer, binding::Docs.Binding, func, method::Meth "$arg$type$suffix" end - rt = Base.return_types(func, typesig) + rt = Base.invokelatest(Base.return_types, func, typesig) return_type_string = if ( print_return_types && length(rt) >= 1 && diff --git a/test/tests.jl b/test/tests.jl index 33adebb..0b3d954 100644 --- a/test/tests.jl +++ b/test/tests.jl @@ -792,6 +792,81 @@ end end end end + @testset "world-age safety" begin + # Test that formatting works correctly when called from a stale world age, + # which is the scenario that triggers failures on Julia 1.12+ with binding + # partitions. We use invokelatest in the test to simulate the world-age gap + # that occurs when docstrings are formatted during macro expansion. + buf = IOBuffer() + + # Test TYPEDSIGNATURES with world-age gap + doc = Docs.DocStr(Core.svec(), nothing, Dict( + :binding => Docs.Binding(M, :h), + :typesig => Tuple{Int, Int, Int}, + :module => M, + )) + Base.invokelatest(DSE.format, DSE.TYPEDSIGNATURES, buf, doc) + str = String(take!(buf)) + @test occursin("```julia", str) + @test occursin("h(", str) + + # Test SIGNATURES with world-age gap + doc.data = Dict( + :binding => Docs.Binding(M, :f), + :typesig => Tuple{Any}, + :module => M, + ) + Base.invokelatest(DSE.format, DSE.SIGNATURES, buf, doc) + str = String(take!(buf)) + @test occursin("```julia", str) + @test occursin("f(x)", str) + + # Test METHODLIST with world-age gap + doc.data = Dict( + :binding => Docs.Binding(M, :f), + :typesig => Tuple{Any}, + :module => M, + ) + with_test_repo() do + Base.invokelatest(DSE.format, DSE.METHODLIST, buf, doc) + end + str = String(take!(buf)) + @test occursin("```julia", str) + @test occursin("f(x)", str) + + # Test FIELDS with world-age gap + doc.data = Dict( + :binding => Docs.Binding(M, :T), + :fields => Dict(:a => "one"), + ) + Base.invokelatest(DSE.format, DSE.FIELDS, buf, doc) + str = String(take!(buf)) + @test occursin("`a`", str) + + # Test TYPEDEF with world-age gap + doc.data = Dict( + :binding => Docs.Binding(M, :AbstractType1), + :typesig => Union{}, + :module => M, + ) + Base.invokelatest(DSE.format, DSE.TYPEDEF, buf, doc) + str = String(take!(buf)) + @test occursin("abstract type AbstractType1", str) + + # Test EXPORTS with world-age gap + doc.data = Dict( + :binding => Docs.Binding(Main, :M), + :typesig => Union{}, + ) + Base.invokelatest(DSE.format, DSE.EXPORTS, buf, doc) + str = String(take!(buf)) + @test occursin("[`f`](@ref)", str) + + # Test IMPORTS with world-age gap + Base.invokelatest(DSE.format, DSE.IMPORTS, buf, doc) + str = String(take!(buf)) + @test occursin("`Base`", str) + end end DSE.parsedocs(DSE)