Skip to content

Commit db5c7a7

Browse files
committed
docs: fix crashes on searching documentation of various expressions
Fix a crash when calling `@doc` on non-document-able expressions such as `:()` so that docs can be returned for the expression itself. Expands `astname` to handle valid expression types explicitly, removes the unused, undocumented `@var` export from Base.Docs, and updates the error message to be clearer. 🤖 Generated with some assistance from Claude Code.
1 parent f362f47 commit db5c7a7

File tree

4 files changed

+44
-24
lines changed

4 files changed

+44
-24
lines changed

base/docs/Docs.jl

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -295,16 +295,28 @@ catdoc(xs...) = vcat(xs...)
295295

296296
const keywords = Dict{Symbol, DocStr}()
297297

298-
namify(@nospecialize x) = astname(x, isexpr(x, :macro))::Union{Symbol,Expr,GlobalRef}
298+
namify(@nospecialize x) = astname(x, isexpr(x, :macro))
299299

300300
function astname(x::Expr, ismacro::Bool)
301301
head = x.head
302302
if head === :.
303303
ismacro ? macroname(x) : x
304-
elseif head === :call && isexpr(x.args[1], :(::))
305-
return astname((x.args[1]::Expr).args[end], ismacro)
304+
elseif head === :call && length(x.args) >= 1 && isexpr(x.args[1], :(::))
305+
# for documenting (x::y)(args...), extract the name from y
306+
# otherwise, for documenting `x::y`, it will be extracted from x
307+
astname((x.args[1]::Expr).args[end], ismacro)
306308
else
307-
n = isexpr(x, (:module, :struct)) ? 2 : 1
309+
n = if isexpr(x, (:module, :struct))
310+
2
311+
elseif isexpr(x, (:call, :macrocall, :function, :(=), :macro, :where, :curly,
312+
:(::), :(<:), :(>:), :local, :global, :const, :atomic,
313+
:copyast, :quote, :inert, :primitive, :abstract))
314+
# similar to is_function_def, but without -> and with various assignments, quoted statements, and miscellaneous that might be encountered in struct definitions also
315+
1
316+
else
317+
return x # nothing to see here--bindingexpr will convert this to an error if defining a doc
318+
end
319+
length(x.args) < n && return x
308320
astname(x.args[n], ismacro)
309321
end
310322
end
@@ -356,7 +368,7 @@ function metadata(__source__, __module__, expr, ismodule)
356368
if isa(eachex, Symbol) || isexpr(eachex, :(::))
357369
# a field declaration
358370
if last_docstr !== nothing
359-
push!(fields, P(namify(eachex::Union{Symbol,Expr}), last_docstr))
371+
push!(fields, P(namify(eachex), last_docstr))
360372
last_docstr = nothing
361373
end
362374
elseif isexpr(eachex, :function) || isexpr(eachex, :(=))
@@ -610,7 +622,13 @@ function simple_lookup_doc(ex)
610622
elseif !isa(ex, Expr) && !isa(ex, Symbol)
611623
return :($(_doc)($(typeof)($(esc(ex)))))
612624
end
613-
binding = esc(bindingexpr(namify(ex)))
625+
name = namify(ex)
626+
# If namify couldn't extract a meaningful name and returned an Expr
627+
# that can't be converted to a binding, treat it like a value
628+
if isa(name, Expr) && !isexpr(name, :(.))
629+
return :($(_doc)($(typeof)($(esc(ex)))))
630+
end
631+
binding = esc(bindingexpr(name))
614632
if isexpr(ex, :call) || isexpr(ex, :macrocall) || isexpr(ex, :where)
615633
sig = esc(signature(ex))
616634
:($(_doc)($binding, $sig))

base/docs/bindings.jl

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
# This file is a part of Julia. License is MIT: https://julialang.org/license
22

3-
export @var
4-
53
struct Binding
64
mod::Module
75
var::Symbol
@@ -20,17 +18,11 @@ defined(b::Binding) = invokelatest(isdefinedglobal, b.mod, b.var)
2018
resolve(b::Binding) = invokelatest(getglobal, b.mod, b.var)
2119

2220
function splitexpr(x::Expr)
23-
isexpr(x, :macrocall) ? splitexpr(x.args[1]) :
24-
isexpr(x, :.) ? (x.args[1], x.args[2]) :
25-
error("Invalid @var syntax `$x`.")
21+
isexpr(x, :.) ? (x.args[1], x.args[2]) : error("Could not find something to document in `$x`.")
2622
end
27-
splitexpr(s::Symbol) = Expr(:macrocall, getfield(Base, Symbol("@__MODULE__")), nothing), quot(s)
23+
splitexpr(s::Symbol) = :($Base.@__MODULE__), quot(s) # this somewhat complex form allows deferring resolving the Module for module docstring until after the module is created
2824
splitexpr(r::GlobalRef) = r.mod, quot(r.name)
29-
splitexpr(other) = error("Invalid @var syntax `$other`.")
30-
31-
macro var(x)
32-
esc(bindingexpr(x))
33-
end
25+
splitexpr(other) = error("Could not find something to document in `$other`.")
3426

3527
function Base.show(io::IO, b::Binding)
3628
if b.mod === Base.active_module()

stdlib/REPL/src/docview.jl

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ doc(obj::UnionAll) = doc(Base.unwrap_unionall(obj))
256256
doc(object, sig::Type = Union{}) = doc(aliasof(object, typeof(object)), sig)
257257
doc(object, sig...) = doc(object, Tuple{sig...})
258258

259-
function lookup_doc(ex)
259+
function lookup_doc(@nospecialize(ex))
260260
if isa(ex, Expr) && ex.head !== :(.) && Base.isoperator(ex.head)
261261
# handle syntactic operators, e.g. +=, ::, .=
262262
ex = ex.head
@@ -284,7 +284,13 @@ function lookup_doc(ex)
284284
end
285285
end
286286
end
287-
binding = esc(bindingexpr(namify(ex)))
287+
name = namify(ex)
288+
# If namify couldn't extract a meaningful name and returned an Expr
289+
# that can't be converted to a binding, treat it like a value
290+
if isa(name, Expr) && !isexpr(name, :(.))
291+
return :($(doc)($(typeof)($(esc(ex)))))
292+
end
293+
binding = esc(bindingexpr(name))
288294
if isexpr(ex, :call) || isexpr(ex, :macrocall) || isexpr(ex, :where)
289295
sig = esc(signature(ex))
290296
:($(doc)($binding, $sig))
@@ -579,7 +585,6 @@ isregex(x) = isexpr(x, :macrocall, 3) && x.args[1] === Symbol("@r_str") && !isem
579585
repl(io::IO, ex::Expr; brief::Bool=true, mod::Module=Main, internal_accesses::Union{Nothing, Set{Pair{Module,Symbol}}}=nothing) = isregex(ex) ? :(apropos($io, $ex)) : _repl(ex, brief, mod, internal_accesses)
580586
repl(io::IO, str::AbstractString; brief::Bool=true, mod::Module=Main, internal_accesses::Union{Nothing, Set{Pair{Module,Symbol}}}=nothing) = :(apropos($io, $str))
581587
repl(io::IO, other; brief::Bool=true, mod::Module=Main, internal_accesses::Union{Nothing, Set{Pair{Module,Symbol}}}=nothing) = esc(:(@doc $other)) # TODO: track internal_accesses
582-
#repl(io::IO, other) = lookup_doc(other) # TODO
583588

584589
repl(x; brief::Bool=true, mod::Module=Main) = repl(stdout, x; brief, mod)
585590

test/docs.jl

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
# This file is a part of Julia. License is MIT: https://julialang.org/license
22

3-
import Base.Docs: meta, @var, DocStr, parsedoc
3+
import Base.Docs: meta, DocStr, parsedoc, bindingexpr, namify
4+
5+
macro var(x) # just for testing bindingexpr/nameify more conveniently
6+
esc(bindingexpr(namify(x)))
7+
end
48

59
# check that @doc can work before REPL is loaded
610
@test !startswith(read(`$(Base.julia_cmd()) -E '@doc sin'`, String), "nothing")
@@ -58,6 +62,8 @@ macro macro_doctest() end
5862
@test (@eval @doc $(Meta.parse("``"))) == (@doc @cmd)
5963
@test (@eval @doc $(Meta.parse("123456789012345678901234567890"))) == (@doc @int128_str)
6064
@test (@eval @doc $(Meta.parse("1234567890123456789012345678901234567890"))) == (@doc @big_str)
65+
# Test that @doc doesn't crash on empty tuple expression (issue #XXXXX)
66+
@test (@doc :()) == (@doc Expr)
6167

6268
# test that random stuff interpolated into docstrings doesn't break search or other methods here
6369
@doc doc"""
@@ -1239,7 +1245,7 @@ end
12391245

12401246
# Bindings.
12411247

1242-
import Base.Docs: @var, Binding, defined
1248+
import Base.Docs: Binding, defined
12431249

12441250
let x = Binding(Base, Symbol("@inline"))
12451251
@test defined(x) == true
@@ -1565,8 +1571,7 @@ Base.@ccallable c51586_long()::Int = 3
15651571

15661572
@testset "Docs docstrings" begin
15671573
undoc = Docs.undocumented_names(Docs)
1568-
@test_broken isempty(undoc)
1569-
@test undoc == [Symbol("@var")]
1574+
@test isempty(undoc)
15701575
end
15711576

15721577
# Docing the macroception macro

0 commit comments

Comments
 (0)