Skip to content
Open
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
30 changes: 18 additions & 12 deletions src/abbreviations.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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")
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions src/utilities.jl
Original file line number Diff line number Diff line change
Expand Up @@ -113,15 +113,15 @@ 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)
end
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
Expand Down Expand Up @@ -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 &&
Expand Down
75 changes: 75 additions & 0 deletions test/tests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Loading