• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

JuliaLang / julia / #37996

25 Jan 2025 02:50AM UTC coverage: 85.981% (-0.02%) from 86.001%
#37996

push

local

web-flow
bpart: Give a warning when accessing a backdated const binding (#57133)

This implements the strategy proposed in
https://github.com/JuliaLang/julia/pull/57102#issuecomment-2605511266.
Example:
```
julia> function foo(i)
           eval(:(const x = $i))
           x
       end
foo (generic function with 1 method)

julia> foo(1)
WARNING: Detected access to binding Main.x in a world prior to its definition world.
  Julia 1.12 has introduced more strict world age semantics for global bindings.
  !!! This code may malfunction under Revise.
  !!! This code will error in future versions of Julia.
Hint: Add an appropriate `invokelatest` around the access to this binding.
1
```

The warning is triggered once per binding to avoid spamming for repeated
access.

17 of 24 new or added lines in 6 files covered. (70.83%)

459 existing lines in 18 files now uncovered.

52029 of 60512 relevant lines covered (85.98%)

12098050.55 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.62
/stdlib/REPL/src/docview.jl
1
# This file is a part of Julia. License is MIT: https://julialang.org/license
2

3
## Code for searching and viewing documentation
4

5
using Markdown
6

7
using Base.Docs: catdoc, modules, DocStr, Binding, MultiDoc, keywords, isfield, namify, bindingexpr,
8
    defined, resolve, getdoc, meta, aliasof, signature
9

10
import Base.Docs: doc, formatdoc, parsedoc, apropos
11

12
using Base: with_output_color, mapany, isdeprecated, isexported
13

14
using Base.Filesystem: _readdirx
15

16
using InteractiveUtils: subtypes
17

18
using Unicode: normalize
19

20
## Help mode ##
21

22
# This is split into helpmode and _helpmode to easier unittest _helpmode
23
function helpmode(io::IO, line::AbstractString, mod::Module=Main)
25✔
24
    internal_accesses = Set{Pair{Module,Symbol}}()
30✔
25
    quote
25✔
26
        docs = $Markdown.insert_hlines($(REPL._helpmode(io, line, mod, internal_accesses)))
27
        $REPL.insert_internal_warning(docs, $internal_accesses)
28
    end
29
end
30
helpmode(line::AbstractString, mod::Module=Main) = helpmode(stdout, line, mod)
2✔
31

32
# A hack to make the line entered at the REPL available at trimdocs without
33
# passing the string through the entire mechanism.
34
const extended_help_on = Ref{Any}(nothing)
35

36
function _helpmode(io::IO, line::AbstractString, mod::Module=Main, internal_accesses::Union{Nothing, Set{Pair{Module,Symbol}}}=nothing)
87✔
37
    line = strip(line)
113✔
38
    ternary_operator_help = (line == "?" || line == "?:")
138✔
39
    if startswith(line, '?') && !ternary_operator_help
134✔
40
        line = line[2:end]
1✔
41
        extended_help_on[] = nothing
1✔
42
        brief = false
1✔
43
    else
44
        extended_help_on[] = line
68✔
45
        brief = true
68✔
46
    end
47
    # interpret anything starting with # or #= as asking for help on comments
48
    if startswith(line, "#")
69✔
49
        if startswith(line, "#=")
×
50
            line = "#="
×
51
        else
52
            line = "#"
×
53
        end
54
    end
55
    x = Meta.parse(line, raise = false, depwarn = false)
69✔
56
    assym = Symbol(line)
69✔
57
    expr =
190✔
58
        if haskey(keywords, assym) || Base.isoperator(assym) || isexpr(x, :error) ||
59
            isexpr(x, :invalid) || isexpr(x, :incomplete)
60
            # Docs for keywords must be treated separately since trying to parse a single
61
            # keyword such as `function` would throw a parse error due to the missing `end`.
62
            assym
13✔
63
        elseif isexpr(x, (:using, :import))
56✔
64
            (x::Expr).head
4✔
65
        else
66
            # Retrieving docs for macros requires us to make a distinction between the text
67
            # `@macroname` and `@macroname()`. These both parse the same, but are used by
68
            # the docsystem to return different results. The first returns all documentation
69
            # for `@macroname`, while the second returns *only* the docs for the 0-arg
70
            # definition if it exists.
71
            (isexpr(x, :macrocall, 1) && !endswith(line, "()")) ? quot(x) : x
125✔
72
        end
73
    # the following must call repl(io, expr) via the @repl macro
74
    # so that the resulting expressions are evaluated in the Base.Docs namespace
75
    :($REPL.@repl $io $expr $brief $mod $internal_accesses)
69✔
76
end
77
_helpmode(line::AbstractString, mod::Module=Main) = _helpmode(stdout, line, mod)
28✔
78

79
function formatdoc(d::DocStr)
6,388✔
80
    buffer = IOBuffer()
6,388✔
81
    for part in d.text
12,776✔
82
        formatdoc(buffer, d, part)
7,944✔
83
    end
9,500✔
84
    md = Markdown.MD(Any[Markdown.parse(seekstart(buffer))])
12,776✔
85
    assume_julia_code!(md)
6,388✔
86
end
87
@noinline formatdoc(buffer, d, part) = print(buffer, part)
7,942✔
88

89
function parsedoc(d::DocStr)
12,805✔
90
    if d.object === nothing
12,805✔
91
        md = formatdoc(d)
6,388✔
92
        md.meta[:module] = d.data[:module]
6,388✔
93
        md.meta[:path]   = d.data[:path]
6,388✔
94
        d.object = md
6,388✔
95
    end
96
    d.object
12,805✔
97
end
98

99
"""
100
    assume_julia_code!(doc::Markdown.MD) -> doc
101

102
Assume that code blocks with no language specified are Julia code.
103
"""
104
function assume_julia_code!(doc::Markdown.MD)
105
    assume_julia_code!(doc.content)
12,776✔
106
    doc
×
107
end
108

109
function assume_julia_code!(blocks::Vector)
12,776✔
110
    for (i, block) in enumerate(blocks)
12,776✔
111
        if block isa Markdown.Code && block.language == ""
31,105✔
112
            blocks[i] = Markdown.Code("julia", block.code)
6,281✔
113
        elseif block isa Vector || block isa Markdown.MD
49,648✔
114
            assume_julia_code!(block)
6,388✔
115
        end
116
    end
49,434✔
117
    blocks
12,776✔
118
end
119

120
## Trimming long help ("# Extended help")
121

122
struct Message  # For direct messages to the terminal
123
    msg    # AbstractString
2✔
124
    fmt    # keywords to `printstyled`
125
end
126
Message(msg) = Message(msg, ())
×
127

128
function Markdown.term(io::IO, msg::Message, columns)
×
129
    printstyled(io, msg.msg; msg.fmt...)
×
130
end
131

132
trimdocs(doc, brief::Bool) = doc
2✔
133

134
function trimdocs(md::Markdown.MD, brief::Bool)
61✔
135
    brief || return md
70✔
136
    md, trimmed = _trimdocs(md, brief)
52✔
137
    if trimmed
52✔
138
        line = extended_help_on[]
2✔
139
        line = isa(line, AbstractString) ? line : ""
2✔
140
        push!(md.content, Message("Extended help is available with `??$line`", (color=Base.info_color(), bold=true)))
2✔
141
    end
142
    return md
52✔
143
end
144

145
function _trimdocs(md::Markdown.MD, brief::Bool)
92✔
146
    content, trimmed = [], false
92✔
147
    for c in md.content
92✔
148
        if isa(c, Markdown.Header{1}) && isa(c.text, AbstractArray) && !isempty(c.text)
215✔
149
            item = c.text[1]
19✔
150
            if isa(item, AbstractString) &&
19✔
151
                lowercase(item) ∈ ("extended help",
152
                                   "extended documentation",
153
                                   "extended docs")
154
                trimmed = true
2✔
155
                break
2✔
156
            end
157
        end
158
        c, trm = _trimdocs(c, brief)
213✔
159
        trimmed |= trm
213✔
160
        push!(content, c)
213✔
161
    end
213✔
162
    return Markdown.MD(content, md.meta), trimmed
92✔
163
end
164

165
_trimdocs(md, brief::Bool) = md, false
173✔
166

167

168
is_tuple(expr) = false
×
169
is_tuple(expr::Expr) = expr.head == :tuple
×
170

171
struct Logged{F}
172
    f::F
48✔
173
    mod::Module
174
    collection::Set{Pair{Module,Symbol}}
175
end
176
function (la::Logged)(m::Module, s::Symbol)
35✔
177
    m !== la.mod && Base.isdefined(m, s) && !Base.ispublic(m, s) && push!(la.collection, m => s)
35✔
178
    la.f(m, s)
35✔
179
end
180
(la::Logged)(args...) = la.f(args...)
×
181

182
function log_nonpublic_access(expr::Expr, mod::Module, internal_access::Set{Pair{Module,Symbol}})
232✔
183
    if expr.head === :. && length(expr.args) == 2 && !is_tuple(expr.args[2])
232✔
184
        Expr(:call, Logged(getproperty, mod, internal_access), log_nonpublic_access.(expr.args, (mod,), (internal_access,))...)
24✔
185
    elseif expr.head === :call && expr.args[1] === Base.Docs.Binding
208✔
186
        Expr(:call, Logged(Base.Docs.Binding, mod, internal_access), log_nonpublic_access.(expr.args[2:end], (mod,), (internal_access,))...)
24✔
187
    else
188
        Expr(expr.head, log_nonpublic_access.(expr.args, (mod,), (internal_access,))...)
184✔
189
    end
190
end
191
log_nonpublic_access(expr, ::Module, _) = expr
172✔
192

193
function insert_internal_warning(md::Markdown.MD, internal_access::Set{Pair{Module,Symbol}})
25✔
194
    if !isempty(internal_access)
25✔
195
        items = Any[Any[Markdown.Paragraph(Any[Markdown.Code("", s)])] for s in sort!(["$mod.$sym" for (mod, sym) in internal_access])]
6✔
196
        admonition = Markdown.Admonition("warning", "Warning", Any[
12✔
197
            Markdown.Paragraph(Any["The following bindings may be internal; they may change or be removed in future versions:"]),
198
            Markdown.List(items, -1, false)])
199
        pushfirst!(md.content, admonition)
12✔
200
    end
201
    md
25✔
202
end
203
function insert_internal_warning(other, internal_access::Set{Pair{Module,Symbol}})
×
204
    # We don't know how to insert an internal symbol warning into non-markdown
205
    # content, so we don't.
206
    other
×
207
end
208

209
function doc(binding::Binding, sig::Type = Union{})
565✔
210
    if defined(binding)
565✔
211
        result = getdoc(resolve(binding), sig)
457✔
212
        result === nothing || return result
460✔
213
    end
214
    results, groups = DocStr[], MultiDoc[]
467✔
215
    # Lookup `binding` and `sig` for matches in all modules of the docsystem.
216
    for mod in modules
467✔
217
        dict = meta(mod; autoinit=false)
77,662✔
218
        isnothing(dict) && continue
77,662✔
219
        if haskey(dict, binding)
38,831✔
220
            multidoc = dict[binding]
272✔
221
            push!(groups, multidoc)
272✔
222
            for msig in multidoc.order
272✔
223
                sig <: msig && push!(results, multidoc.docs[msig])
319✔
224
            end
319✔
225
        end
226
    end
38,831✔
227
    if isempty(groups)
467✔
228
        # When no `MultiDoc`s are found that match `binding` then we check whether `binding`
229
        # is an alias of some other `Binding`. When it is we then re-run `doc` with that
230
        # `Binding`, otherwise if it's not an alias then we generate a summary for the
231
        # `binding` and display that to the user instead.
232
        alias = aliasof(binding)
201✔
233
        alias == binding ? summarize(alias, sig) : doc(alias, sig)
201✔
234
    else
235
        # There was at least one match for `binding` while searching. If there weren't any
236
        # matches for `sig` then we concatenate *all* the docs from the matching `Binding`s.
237
        if isempty(results)
266✔
238
            for group in groups, each in group.order
13✔
239
                push!(results, group.docs[each])
16✔
240
            end
16✔
241
        end
242
        # Get parsed docs and concatenate them.
243
        md = catdoc(mapany(parsedoc, results)...)
283✔
244
        # Save metadata in the generated markdown.
245
        if isa(md, Markdown.MD)
266✔
246
            md.meta[:results] = results
263✔
247
            md.meta[:binding] = binding
263✔
248
            md.meta[:typesig] = sig
263✔
249
        end
250
        return md
266✔
251
    end
252
end
253

254
# Some additional convenience `doc` methods that take objects rather than `Binding`s.
255
doc(obj::UnionAll) = doc(Base.unwrap_unionall(obj))
2✔
256
doc(object, sig::Type = Union{}) = doc(aliasof(object, typeof(object)), sig)
671✔
257
doc(object, sig...)              = doc(object, Tuple{sig...})
×
258

259
function lookup_doc(ex)
154✔
260
    if isa(ex, Expr) && ex.head !== :(.) && Base.isoperator(ex.head)
203✔
261
        # handle syntactic operators, e.g. +=, ::, .=
262
        ex = ex.head
×
263
    end
264
    if haskey(keywords, ex)
197✔
265
        return parsedoc(keywords[ex])
6✔
266
    elseif Meta.isexpr(ex, :incomplete)
148✔
267
        return :($(Markdown.md"No documentation found."))
1✔
268
    elseif !isa(ex, Expr) && !isa(ex, Symbol)
147✔
269
        return :($(doc)($(typeof)($(esc(ex)))))
2✔
270
    end
271
    if isa(ex, Symbol) && Base.isoperator(ex)
174✔
272
        str = string(ex)
6✔
273
        isdotted = startswith(str, ".")
6✔
274
        if endswith(str, "=") && Base.operator_precedence(ex) == Base.prec_assignment && ex !== :(:=)
6✔
275
            op = chop(str)
2✔
276
            eq = isdotted ? ".=" : "="
2✔
277
            return Markdown.parse("`x $op= y` is a synonym for `x $eq x $op y`")
2✔
278
        elseif isdotted && ex !== :(..)
4✔
279
            op = str[2:end]
2✔
280
            if op in ("&&", "||")
2✔
281
                return Markdown.parse("`x $ex y` broadcasts the boolean operator `$op` to `x` and `y`. See [`broadcast`](@ref).")
×
282
            else
283
                return Markdown.parse("`x $ex y` is akin to `broadcast($op, x, y)`. See [`broadcast`](@ref).")
1✔
284
            end
285
        end
286
    end
287
    binding = esc(bindingexpr(namify(ex)))
171✔
288
    if isexpr(ex, :call) || isexpr(ex, :macrocall) || isexpr(ex, :where)
234✔
289
        sig = esc(signature(ex))
41✔
290
        :($(doc)($binding, $sig))
41✔
291
    else
292
        :($(doc)($binding))
101✔
293
    end
294
end
295

296
# Object Summaries.
297
# =================
298

299
function summarize(binding::Binding, sig)
202✔
300
    io = IOBuffer()
202✔
301
    if defined(binding)
202✔
302
        binding_res = resolve(binding)
188✔
303
        if !isa(binding_res, Module)
188✔
304
            varstr = "$(binding.mod).$(binding.var)"
24✔
305
            if Base.ispublic(binding.mod, binding.var)
24✔
306
                println(io, "No documentation found for public binding `$varstr`.\n")
5✔
307
            else
308
                println(io, "No documentation found for private binding `$varstr`.\n")
19✔
309
            end
310
        end
311
        summarize(io, binding_res, binding)
188✔
312
    else
313
        println(io, "No documentation found.\n")
14✔
314
        quot = any(isspace, sprint(print, binding)) ? "'" : ""
14✔
315
        if Base.isbindingresolved(binding.mod, binding.var)
14✔
316
            println(io, "Binding ", quot, "`", binding, "`", quot, " exists, but has not been assigned a value.")
1✔
317
        else
318
            println(io, "Binding ", quot, "`", binding, "`", quot, " does not exist.")
13✔
319
        end
320
    end
321
    md = Markdown.parse(seekstart(io))
404✔
322
    # Save metadata in the generated markdown.
323
    md.meta[:results] = DocStr[]
202✔
324
    md.meta[:binding] = binding
202✔
325
    md.meta[:typesig] = sig
202✔
326
    return md
202✔
327
end
328

329
function summarize(io::IO, λ::Function, binding::Binding)
5✔
330
    kind = startswith(string(binding.var), '@') ? "macro" : "`Function`"
10✔
331
    println(io, "`", binding, "` is a ", kind, ".")
5✔
332
    println(io, "```\n", methods(λ), "\n```")
5✔
333
end
334

335
function summarize(io::IO, TT::Type, binding::Binding)
19✔
336
    println(io, "# Summary")
19✔
337
    T = Base.unwrap_unionall(TT)
19✔
338
    if T isa DataType
19✔
339
        println(io, "```")
16✔
340
        print(io,
27✔
341
            Base.isabstracttype(T) ? "abstract type " :
342
            Base.ismutabletype(T)  ? "mutable struct " :
343
            Base.isstructtype(T) ? "struct " :
344
            "primitive type ")
345
        supert = supertype(T)
16✔
346
        println(io, T)
16✔
347
        println(io, "```")
16✔
348
        if !Base.isabstracttype(T) && T.name !== Tuple.name && !isempty(fieldnames(T))
16✔
349
            println(io, "# Fields")
6✔
350
            println(io, "```")
6✔
351
            pad = maximum(length(string(f)) for f in fieldnames(T))
6✔
352
            for (f, t) in zip(fieldnames(T), fieldtypes(T))
6✔
353
                println(io, rpad(f, pad), " :: ", t)
11✔
354
            end
11✔
355
            println(io, "```")
6✔
356
        end
357
        subt = subtypes(TT)
16✔
358
        if !isempty(subt)
21✔
359
            println(io, "# Subtypes")
5✔
360
            println(io, "```")
5✔
361
            for t in subt
5✔
362
                println(io, Base.unwrap_unionall(t))
12✔
363
            end
12✔
364
            println(io, "```")
5✔
365
        end
366
        if supert != Any
16✔
367
            println(io, "# Supertype Hierarchy")
12✔
368
            println(io, "```")
12✔
369
            Base.show_supertypes(io, T)
12✔
370
            println(io)
12✔
371
            println(io, "```")
12✔
372
        end
373
    elseif T isa Union
3✔
374
        println(io, "`", binding, "` is of type `", typeof(TT), "`.\n")
3✔
375
        println(io, "# Union Composed of Types")
3✔
376
        for T1 in Base.uniontypes(T)
3✔
377
            println(io, " - `", Base.rewrap_unionall(T1, TT), "`")
10✔
378
        end
10✔
379
    else # unreachable?
UNCOV
380
        println(io, "`", binding, "` is of type `", typeof(TT), "`.\n")
×
381
    end
382
end
383

384
function find_readme(m::Module)::Union{String, Nothing}
164✔
385
    mpath = pathof(m)
164✔
386
    isnothing(mpath) && return nothing
190✔
387
    !isfile(mpath) && return nothing # modules in sysimage, where src files are omitted
52✔
388
    path = dirname(mpath)
26✔
389
    top_path = pkgdir(m)
26✔
390
    while true
52✔
391
        for entry in _readdirx(path; sort=true)
52✔
392
            isfile(entry) && (lowercase(entry.name) in ["readme.md", "readme"]) || continue
850✔
393
            return entry.path
12✔
394
        end
318✔
395
        path == top_path && break # go no further than pkgdir
80✔
396
        path = dirname(path) # work up through nested modules
26✔
397
    end
26✔
398
    return nothing
14✔
399
end
400
function summarize(io::IO, m::Module, binding::Binding; nlines::Int = 200)
328✔
401
    readme_path = find_readme(m)
164✔
402
    public = Base.ispublic(binding.mod, binding.var) ? "public" : "internal"
164✔
403
    if isnothing(readme_path)
176✔
404
        println(io, "No docstring or readme file found for $public module `$m`.\n")
152✔
405
    else
406
        println(io, "No docstring found for $public module `$m`.")
12✔
407
    end
408
    exports = filter!(!=(nameof(m)), names(m))
328✔
409
    if isempty(exports)
164✔
410
        println(io, "Module does not have any public names.")
51✔
411
    else
412
        println(io, "# Public names")
113✔
413
        print(io, "  `")
113✔
414
        join(io, exports, "`, `")
113✔
415
        println(io, "`\n")
113✔
416
    end
417
    if !isnothing(readme_path)
176✔
418
        readme_lines = readlines(readme_path)
12✔
419
        isempty(readme_lines) && return  # don't say we are going to print empty file
12✔
420
        println(io)
12✔
421
        println(io, "---")
12✔
422
        println(io, "_Package description from `$(basename(readme_path))`:_")
12✔
423
        for line in first(readme_lines, nlines)
12✔
424
            println(io, line)
292✔
425
        end
292✔
426
        length(readme_lines) > nlines && println(io, "\n[output truncated to first $nlines lines]")
12✔
427
    end
428
end
429

430
function summarize(io::IO, @nospecialize(T), binding::Binding)
4✔
431
    T = typeof(T)
4✔
432
    println(io, "`", binding, "` is of type `", T, "`.\n")
4✔
433
    summarize(io, T, binding)
4✔
434
end
435

436
# repl search and completions for help
437

438
# This type is returned from `accessible` and denotes a binding that is accessible within
439
# some context. It differs from `Base.Docs.Binding`, which is also used by the REPL, in
440
# that it doesn't track the defining module for a symbol unless the symbol is public but
441
# not exported, i.e. it's accessible but requires qualification. Using this type rather
442
# than `Base.Docs.Binding` simplifies things considerably, partially because REPL searching
443
# is based on `String`s, which this type stores, but `Base.Docs.Binding` stores a module
444
# and symbol and does not have any notion of the context from which the binding is accessed.
445
struct AccessibleBinding
446
    source::Union{String,Nothing}
244,293✔
447
    name::String
448
end
449

450
function AccessibleBinding(mod::Module, name::Symbol)
58,919✔
451
    m = isexported(mod, name) ? nothing : String(nameof(mod))
62,349✔
452
    return AccessibleBinding(m, String(name))
58,919✔
453
end
454
AccessibleBinding(name::Symbol) = AccessibleBinding(nothing, String(name))
115,477✔
455

456
function Base.show(io::IO, b::AccessibleBinding)
457
    b.source === nothing || print(io, b.source, '.')
31,424✔
458
    print(io, b.name)
29,557✔
459
end
460

461
quote_spaces(x) = any(isspace, x) ? "'" * x * "'" : x
69,295✔
462
quote_spaces(x::AccessibleBinding) = AccessibleBinding(x.source, quote_spaces(x.name))
70,346✔
463

464
function repl_search(io::IO, s::Union{Symbol,String}, mod::Module)
26✔
465
    pre = "search:"
26✔
466
    print(io, pre)
26✔
467
    printmatches(io, s, map(quote_spaces, doc_completions(s, mod)), cols = _displaysize(io)[2] - length(pre))
26✔
468
    println(io, "\n")
26✔
469
end
470

471
# TODO: document where this is used
UNCOV
472
repl_search(s, mod::Module) = repl_search(stdout, s, mod)
×
473

474
function repl_corrections(io::IO, s, mod::Module)
8✔
475
    print(io, "Couldn't find ")
8✔
476
    quot = any(isspace, s) ? "'" : ""
8✔
477
    print(io, quot)
8✔
478
    printstyled(io, s, color=:cyan)
8✔
479
    print(io, quot)
8✔
480
    if Base.identify_package(s) === nothing
9✔
481
        print(io, '\n')
7✔
482
    else
483
        print(io, ", but a loadable package with that name exists. If you are looking for the package docs load the package first.\n")
1✔
484
    end
485
    print_correction(io, s, mod)
8✔
486
end
UNCOV
487
repl_corrections(s) = repl_corrections(stdout, s)
×
488

489
# inverse of latex_symbols Dict, lazily created as needed
490
const symbols_latex = Dict{String,String}()
491
function symbol_latex(s::String)
55✔
492
    if isempty(symbols_latex)
55✔
UNCOV
493
        for (k,v) in Iterators.flatten((REPLCompletions.latex_symbols,
×
494
                                        REPLCompletions.emoji_symbols))
495
            symbols_latex[v] = k
×
UNCOV
496
        end
×
497

498
        # Overwrite with canonical mapping when a symbol has several completions (#39148)
UNCOV
499
        merge!(symbols_latex, REPLCompletions.symbols_latex_canonical)
×
500
    end
501

502
    return get(symbols_latex, s, "")
55✔
503
end
504
function repl_latex(io::IO, s0::String)
30✔
505
    # This has rampant `Core.Box` problems (#15276). Use the tricks of
506
    # https://docs.julialang.org/en/v1/manual/performance-tips/#man-performance-captured
507
    # We're changing some of the values so the `let` trick isn't applicable.
508
    s::String = s0
30✔
509
    latex::String = symbol_latex(s)
30✔
510
    if isempty(latex)
30✔
511
        # Decompose NFC-normalized identifier to match tab-completion
512
        # input if the first search came up empty.
513
        s = normalize(s, :NFD)
25✔
514
        latex = symbol_latex(s)
25✔
515
    end
516
    if !isempty(latex)
30✔
517
        print(io, "\"")
5✔
518
        printstyled(io, s, color=:cyan)
5✔
519
        print(io, "\" can be typed by ")
5✔
520
        printstyled(io, latex, "<tab>", color=:cyan)
5✔
521
        println(io, '\n')
5✔
522
    elseif any(c -> haskey(symbols_latex, string(c)), s)
128✔
523
        print(io, "\"")
3✔
524
        printstyled(io, s, color=:cyan)
3✔
525
        print(io, "\" can be typed by ")
3✔
526
        state::Char = '\0'
3✔
527
        with_output_color(:cyan, io) do io
3✔
528
            for c in s
5✔
529
                cstr = string(c)
13✔
530
                if haskey(symbols_latex, cstr)
26✔
531
                    latex = symbols_latex[cstr]
11✔
532
                    if length(latex) == 3 && latex[2] in ('^','_')
15✔
533
                        # coalesce runs of sub/superscripts
534
                        if state != latex[2]
14✔
535
                            '\0' != state && print(io, "<tab>")
3✔
536
                            print(io, latex[1:2])
6✔
537
                            state = latex[2]
6✔
538
                        end
539
                        print(io, latex[3])
14✔
540
                    else
541
                        if '\0' != state
4✔
542
                            print(io, "<tab>")
1✔
543
                            state = '\0'
1✔
544
                        end
545
                        print(io, latex, "<tab>")
4✔
546
                    end
547
                else
548
                    if '\0' != state
2✔
549
                        print(io, "<tab>")
×
UNCOV
550
                        state = '\0'
×
551
                    end
552
                    print(io, c)
2✔
553
                end
554
            end
13✔
555
            '\0' != state && print(io, "<tab>")
3✔
556
        end
557
        println(io, '\n')
3✔
558
    end
559
end
UNCOV
560
repl_latex(s::String) = repl_latex(stdout, s)
×
561

562
macro repl(ex, brief::Bool=false, mod::Module=Main) repl(ex; brief, mod) end
12✔
563
macro repl(io, ex, brief, mod, internal_accesses) repl(io, ex; brief, mod, internal_accesses) end
55✔
564

565
function repl(io::IO, s::Symbol; brief::Bool=true, mod::Module=Main, internal_accesses::Union{Nothing, Set{Pair{Module,Symbol}}}=nothing)
52✔
566
    str = string(s)
26✔
567
    quote
34✔
568
        repl_latex($io, $str)
569
        repl_search($io, $str, $mod)
570
        $(if !isdefined(mod, s) && !Base.isbindingresolved(mod, s) && !haskey(keywords, s) && !Base.isoperator(s)
571
               # n.b. we call isdefined for the side-effect of resolving the binding, if possible
572
               :(repl_corrections($io, $str, $mod))
8✔
573
          end)
574
        $(_repl(s, brief, mod, internal_accesses))
575
    end
576
end
577
isregex(x) = isexpr(x, :macrocall, 3) && x.args[1] === Symbol("@r_str") && !isempty(x.args[3])
32✔
578

579
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)
64✔
580
repl(io::IO, str::AbstractString; brief::Bool=true, mod::Module=Main, internal_accesses::Union{Nothing, Set{Pair{Module,Symbol}}}=nothing) = :(apropos($io, $str))
2✔
581
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
4✔
582
#repl(io::IO, other) = lookup_doc(other) # TODO
583

584
repl(x; brief::Bool=true, mod::Module=Main) = repl(stdout, x; brief, mod)
12✔
585

586
function _repl(x, brief::Bool=true, mod::Module=Main, internal_accesses::Union{Nothing, Set{Pair{Module,Symbol}}}=nothing)
65✔
587
    if isexpr(x, :call)
65✔
588
        x = x::Expr
6✔
589
        # determine the types of the values
590
        kwargs = nothing
6✔
591
        pargs = Any[]
6✔
592
        for arg in x.args[2:end]
6✔
593
            if isexpr(arg, :parameters)
9✔
UNCOV
594
                kwargs = mapany(arg.args) do kwarg
×
595
                    if kwarg isa Symbol
596
                        kwarg = :($kwarg::Any)
597
                    elseif isexpr(kwarg, :kw)
598
                        lhs = kwarg.args[1]
599
                        rhs = kwarg.args[2]
600
                        if lhs isa Symbol
601
                            if rhs isa Symbol
602
                                kwarg.args[1] = :($lhs::(@isdefined($rhs) ? typeof($rhs) : Any))
603
                            else
604
                                kwarg.args[1] = :($lhs::typeof($rhs))
605
                            end
606
                        end
607
                    end
608
                    kwarg
609
                end
610
            elseif isexpr(arg, :kw)
9✔
611
                if kwargs === nothing
1✔
612
                    kwargs = Any[]
1✔
613
                end
614
                lhs = arg.args[1]
1✔
615
                rhs = arg.args[2]
1✔
616
                if lhs isa Symbol
1✔
617
                    if rhs isa Symbol
1✔
UNCOV
618
                        arg.args[1] = :($lhs::(@isdefined($rhs) ? typeof($rhs) : Any))
×
619
                    else
620
                        arg.args[1] = :($lhs::typeof($rhs))
1✔
621
                    end
622
                end
623
                push!(kwargs, arg)
1✔
624
            else
625
                if arg isa Symbol
8✔
626
                    arg = :($arg::(@isdefined($arg) ? typeof($arg) : Any))
1✔
627
                elseif !isexpr(arg, :(::))
7✔
628
                    arg = :(::typeof($arg))
4✔
629
                end
630
                push!(pargs, arg)
8✔
631
            end
632
        end
9✔
633
        if kwargs === nothing
6✔
634
            x.args = Any[x.args[1], pargs...]
5✔
635
        else
636
            x.args = Any[x.args[1], Expr(:parameters, kwargs...), pargs...]
1✔
637
        end
638
    end
639
    #docs = lookup_doc(x) # TODO
640
    docs = esc(:(@doc $x))
61✔
641
    docs = if isfield(x)
61✔
642
        quote
22✔
643
            if isa($(esc(x.args[1])), DataType)
644
                fielddoc($(esc(x.args[1])), $(esc(x.args[2])))
645
            else
646
                $docs
647
            end
648
        end
649
    else
650
        docs
74✔
651
    end
652
    docs = log_nonpublic_access(macroexpand(mod, docs), mod, internal_accesses)
61✔
653
    :(REPL.trimdocs($docs, $brief))
61✔
654
end
655

656
"""
657
    fielddoc(binding, field)
658

659
Return documentation for a particular `field` of a type if it exists.
660
"""
661
function fielddoc(binding::Binding, field::Symbol)
6✔
662
    for mod in modules
6✔
663
        dict = meta(mod; autoinit=false)
912✔
664
        isnothing(dict) && continue
912✔
665
        multidoc = get(dict, binding, nothing)
456✔
666
        if multidoc !== nothing
456✔
667
            structdoc = get(multidoc.docs, Union{}, nothing)
2✔
668
            if structdoc !== nothing
2✔
669
                fieldsdoc = get(structdoc.data, :fields, nothing)
2✔
670
                if fieldsdoc !== nothing
2✔
671
                    fielddoc = get(fieldsdoc, field, nothing)
1✔
672
                    if fielddoc !== nothing
1✔
673
                        return isa(fielddoc, Markdown.MD) ?
1✔
674
                            fielddoc : Markdown.parse(fielddoc)
675
                    end
676
                end
677
            end
678
        end
679
    end
455✔
680
    fs = fieldnames(resolve(binding))
5✔
681
    fields = isempty(fs) ? "no fields" : (length(fs) == 1 ? "field " : "fields ") *
8✔
682
                                          join(("`$f`" for f in fs), ", ", ", and ")
683
    Markdown.parse("`$(resolve(binding))` has $fields.")
5✔
684
end
685

686
# As with the additional `doc` methods, this converts an object to a `Binding` first.
687
fielddoc(object, field::Symbol) = fielddoc(aliasof(object, typeof(object)), field)
6✔
688

689

690
# Search & Rescue
691
# Utilities for correcting user mistakes and (eventually)
692
# doing full documentation searches from the repl.
693

694
# Fuzzy Search Algorithm
695

696
function matchinds(needle, haystack; acronym::Bool = false)
534✔
697
    chars = collect(needle)
267✔
698
    is = Int[]
267✔
699
    lastc = '\0'
267✔
700
    for (i, char) in enumerate(haystack)
534✔
701
        while !isempty(chars) && isspace(first(chars))
2,442✔
702
            popfirst!(chars) # skip spaces
4✔
703
        end
4✔
704
        isempty(chars) && break
1,231✔
705
        if lowercase(char) == lowercase(chars[1]) &&
1,348✔
706
           (!acronym || !isletter(lastc))
707
            push!(is, i)
342✔
708
            popfirst!(chars)
342✔
709
        end
710
        lastc = char
1,207✔
711
    end
1,207✔
712
    return is
267✔
713
end
714

UNCOV
715
matchinds(needle, (; name)::AccessibleBinding; acronym::Bool=false) =
×
716
    matchinds(needle, name; acronym)
717

718
longer(x, y) = length(x) ≥ length(y) ? (x, true) : (y, false)
133✔
719

720
bestmatch(needle, haystack) =
133✔
721
    longer(matchinds(needle, haystack, acronym = true),
722
           matchinds(needle, haystack))
723

724
# Optimal string distance: Counts the minimum number of insertions, deletions,
725
# transpositions or substitutions to go from one string to the other.
726
function string_distance(a::AbstractString, lena::Integer, b::AbstractString, lenb::Integer)
112,192✔
727
    if lena > lenb
112,192✔
728
        a, b = b, a
13,191✔
729
        lena, lenb = lenb, lena
13,191✔
730
    end
731
    start = 0
112,192✔
732
    for (i, j) in zip(a, b)
222,939✔
733
        if a == b
110,918✔
734
            start += 1
228✔
735
        else
736
            break
110,690✔
737
        end
738
    end
399✔
739
    start == lena && return lenb - start
112,192✔
740
    vzero = collect(1:(lenb - start))
1,133,108✔
741
    vone = similar(vzero)
110,690✔
742
    prev_a, prev_b = first(a), first(b)
219,467✔
743
    current = 0
110,690✔
744
    for (i, ai) in enumerate(a)
221,380✔
745
        i > start || (prev_a = ai; continue)
362,961✔
746
        left = i - start - 1
362,961✔
747
        current = i - start
362,961✔
748
        transition_next = 0
362,961✔
749
        for (j, bj) in enumerate(b)
725,922✔
750
            j > start || (prev_b = bj; continue)
3,915,857✔
751
            # No need to look beyond window of lower right diagonal
752
            above = current
3,915,857✔
753
            this_transition = transition_next
3,915,857✔
754
            transition_next = vone[j - start]
3,915,857✔
755
            vone[j - start] = current = left
3,915,857✔
756
            left = vzero[j - start]
3,915,857✔
757
            if ai != bj
3,915,857✔
758
                # Minimum between substitution, deletion and insertion
759
                current = min(current + 1, above + 1, left + 1)
3,788,339✔
760
                if i > start + 1 && j > start + 1 && ai == prev_b && prev_a == bj
3,788,339✔
761
                    current = min(current, (this_transition += 1))
5,134✔
762
                end
763
            end
764
            vzero[j - start] = current
3,915,857✔
765
            prev_b = bj
3,915,857✔
766
        end
7,468,753✔
767
        prev_a = ai
362,961✔
768
    end
615,232✔
769
    current
110,690✔
770
end
771

772
function fuzzyscore(needle::AbstractString, haystack::AbstractString)
4✔
773
    lena, lenb = length(needle), length(haystack)
112,192✔
774
    1 - (string_distance(needle, lena, haystack, lenb) / max(lena, lenb))
112,192✔
775
end
776

777
function fuzzyscore(needle::AbstractString, haystack::AccessibleBinding)
112,188✔
778
    score = fuzzyscore(needle, haystack.name)
112,188✔
779
    haystack.source === nothing && return score
112,188✔
780
    # Apply a "penalty" of half an edit if the comparator binding is public but not
781
    # exported so that exported/local names that exactly match the search query are
782
    # listed first
783
    penalty = 1 / (2 * max(length(needle), length(haystack.name)))
3,335✔
784
    return max(score - penalty, 0)
3,335✔
785
end
786

787
function fuzzysort(search::String, candidates::Vector{AccessibleBinding})
46✔
788
    scores = map(cand -> fuzzyscore(search, cand), candidates)
96,369✔
789
    candidates[sortperm(scores)] |> reverse
46✔
790
end
791

792
# Levenshtein Distance
793

794
function levenshtein(s1, s2)
16,175✔
795
    a, b = collect(s1), collect(s2)
16,175✔
796
    m = length(a)
16,175✔
797
    n = length(b)
16,175✔
798
    d = Matrix{Int}(undef, m+1, n+1)
16,175✔
799

800
    d[1:m+1, 1] = 0:m
82,558✔
801
    d[1, 1:n+1] = 0:n
168,154✔
802

803
    for i = 1:m, j = 1:n
16,175✔
804
        d[i+1,j+1] = min(d[i  , j+1] + 1,
585,886✔
805
                         d[i+1, j  ] + 1,
806
                         d[i  , j  ] + (a[i] != b[j]))
807
    end
636,094✔
808

809
    return d[m+1, n+1]
16,175✔
810
end
811

812
function levsort(search::String, candidates::Vector{AccessibleBinding})
8✔
813
    scores = map(candidates) do cand
8✔
814
        (Float64(levenshtein(search, cand.name)), -fuzzyscore(search, cand))
15,708✔
815
    end
816
    candidates = candidates[sortperm(scores)]
8✔
817
    i = 0
8✔
818
    for outer i = 1:length(candidates)
8✔
819
        levenshtein(search, candidates[i].name) > 3 && break
467✔
820
    end
459✔
821
    return candidates[1:i]
8✔
822
end
823

824
# Result printing
825

826
function printmatch(io::IO, word, match)
133✔
827
    is, _ = bestmatch(word, match)
133✔
828
    for (i, char) = enumerate(match)
266✔
829
        if i in is
1,681✔
830
            printstyled(io, char, bold=true)
270✔
831
        else
832
            print(io, char)
356✔
833
        end
834
    end
626✔
835
end
836

837
function printmatch(io::IO, word, match::AccessibleBinding)
838
    match.source === nothing || print(io, match.source, '.')
139✔
839
    printmatch(io, word, match.name)
133✔
840
end
841

842
function matchlength(x::AccessibleBinding)
843
    n = length(x.name)
231✔
844
    if x.source !== nothing
231✔
845
        n += length(x.source) + 1  # the +1 is for the `.` separator
10✔
846
    end
847
    return n
231✔
848
end
UNCOV
849
matchlength(x) = length(x)
×
850

851
function printmatches(io::IO, word, matches; cols::Int = _displaysize(io)[2])
52✔
852
    total = 0
26✔
853
    for match in matches
26✔
854
        ml = matchlength(match)
159✔
855
        total + ml + 1 > cols && break
159✔
856
        fuzzyscore(word, match) < 0.5 && break
153✔
857
        print(io, " ")
133✔
858
        printmatch(io, word, match)
139✔
859
        total += ml + 1
133✔
860
    end
133✔
861
end
862

UNCOV
863
printmatches(args...; cols::Int = _displaysize(stdout)[2]) = printmatches(stdout, args..., cols = cols)
×
864

865
function print_joined_cols(io::IO, ss::Vector{AccessibleBinding}, delim = "", last = delim; cols::Int = _displaysize(io)[2])
16✔
866
    i = 0
8✔
867
    total = 0
8✔
868
    for outer i = 1:length(ss)
8✔
869
        total += matchlength(ss[i])
72✔
870
        total + max(i-2,0)*length(delim) + (i>1 ? 1 : 0)*length(last) > cols && (i-=1; break)
72✔
871
    end
67✔
872
    join(io, ss[1:i], delim, last)
8✔
873
end
874

UNCOV
875
print_joined_cols(args...; cols::Int = _displaysize(stdout)[2]) = print_joined_cols(stdout, args...; cols=cols)
×
876

877
function print_correction(io::IO, word::String, mod::Module)
8✔
878
    cors = map(quote_spaces, levsort(word, accessible(mod)))
8✔
879
    pre = "Perhaps you meant "
8✔
880
    print(io, pre)
8✔
881
    print_joined_cols(io, cors, ", ", " or "; cols = _displaysize(io)[2] - length(pre))
8✔
882
    println(io)
8✔
883
    return
8✔
884
end
885

886
# TODO: document where this is used
UNCOV
887
print_correction(word, mod::Module) = print_correction(stdout, word, mod)
×
888

889
# Completion data
890

891
moduleusings(mod) = ccall(:jl_module_usings, Any, (Any,), mod)
55✔
892

893
function accessible(mod::Module)
55✔
894
    bindings = Set(AccessibleBinding(s) for s in names(mod; all=true, imported=true)
55✔
895
                   if !isdeprecated(mod, s))
896
    for used in moduleusings(mod)
55✔
897
        union!(bindings, (AccessibleBinding(used, s) for s in names(used)
660✔
898
                          if !isdeprecated(used, s)))
899
    end
660✔
900
    union!(bindings, (AccessibleBinding(k) for k in keys(Base.Docs.keywords)))
55✔
901
    filter!(b -> !occursin('#', b.name), bindings)
227,161✔
902
    return collect(bindings)
55✔
903
end
904

905
function doc_completions(name, mod::Module=Main)
49✔
906
    res = fuzzysort(name, accessible(mod))
49✔
907

908
    # to insert an entry like `raw""` for `"@raw_str"` in `res`
909
    ms = map(c -> match(r"^@(.*?)_str$", c.name), res)
96,369✔
910
    idxs = findall(!isnothing, ms)
92✔
911

912
    # avoid messing up the order while inserting
913
    for i in reverse!(idxs)
46✔
914
        c = only((ms[i]::AbstractMatch).captures)
600✔
915
        insert!(res, i, AccessibleBinding(res[i].source, "$(c)\"\""))
600✔
916
    end
600✔
917
    res
46✔
918
end
UNCOV
919
doc_completions(name::Symbol) = doc_completions(string(name), mod)
×
920

921

922
# Searching and apropos
923

924
# Docsearch simply returns true or false if an object contains the given needle
925
docsearch(haystack::AbstractString, needle) = occursin(needle, haystack)
12,878✔
926
docsearch(haystack::Symbol, needle) = docsearch(string(haystack), needle)
×
927
docsearch(::Nothing, needle) = false
×
UNCOV
928
function docsearch(haystack::Array, needle)
×
929
    for elt in haystack
×
930
        docsearch(elt, needle) && return true
×
931
    end
×
UNCOV
932
    false
×
933
end
UNCOV
934
function docsearch(haystack, needle)
×
UNCOV
935
    @warn "Unable to search documentation of type $(typeof(haystack))" maxlog=1
×
UNCOV
936
    false
×
937
end
938

939
## Searching specific documentation objects
940
function docsearch(haystack::MultiDoc, needle)
11,243✔
941
    for v in values(haystack.docs)
22,486✔
942
        docsearch(v, needle) && return true
12,495✔
943
    end
9,116✔
944
    false
945
end
946

947
function docsearch(haystack::DocStr, needle)
12,495✔
948
    docsearch(parsedoc(haystack), needle) && return true
12,495✔
949
    if haskey(haystack.data, :fields)
18,232✔
950
        for doc in values(haystack.data[:fields])
812✔
951
            docsearch(doc, needle) && return true
54✔
952
        end
54✔
953
    end
954
    false
955
end
956

957
## doc search
958

959
## Markdown search simply strips all markup and searches plain text version
960
docsearch(haystack::Markdown.MD, needle) = docsearch(stripmd(haystack.content), needle)
12,822✔
961

962
"""
963
    stripmd(x)
964

965
Strip all Markdown markup from x, leaving the result in plain text. Used
966
internally by apropos to make docstrings containing more than one markdown
967
element searchable.
968
"""
969
stripmd(@nospecialize x) = string(x) # for random objects interpolated into the docstring
6✔
970
stripmd(x::AbstractString) = x  # base case
191,511✔
971
stripmd(x::Nothing) = " "
100✔
972
stripmd(x::Vector) = string(map(stripmd, x)...)
68,242✔
973

974
stripmd(x::Markdown.BlockQuote) = "$(stripmd(x.content))"
4✔
975
stripmd(x::Markdown.Admonition) = "$(stripmd(x.content))"
2,774✔
976
stripmd(x::Markdown.Bold) = "$(stripmd(x.text))"
76✔
977
stripmd(x::Markdown.Code) = "$(stripmd(x.code))"
88,031✔
978
stripmd(x::Markdown.Header) = stripmd(x.text)
4,810✔
979
stripmd(x::Markdown.HorizontalRule) = " "
148✔
980
stripmd(x::Markdown.Image) = "$(stripmd(x.alt)) $(x.url)"
48✔
981
stripmd(x::Markdown.Italic) = "$(stripmd(x.text))"
597✔
982
stripmd(x::Markdown.LaTeX) = "$(x.formula)"
853✔
983
stripmd(x::Markdown.LineBreak) = " "
76✔
984
stripmd(x::Markdown.Link) = "$(stripmd(x.text)) $(x.url)"
13,066✔
985
stripmd(x::Markdown.List) = join(map(stripmd, x.items), " ")
1,127✔
986
stripmd(x::Markdown.MD) = join(map(stripmd, x.content), " ")
12,831✔
987
stripmd(x::Markdown.Paragraph) = stripmd(x.content)
29,374✔
988
stripmd(x::Markdown.Footnote) = "$(stripmd(x.id)) $(stripmd(x.text))"
196✔
989
stripmd(x::Markdown.Table) =
45✔
990
    join([join(map(stripmd, r), " ") for r in x.rows], " ")
991

UNCOV
992
apropos(string) = apropos(stdout, string)
×
993
apropos(io::IO, string) = apropos(io, Regex("\\Q$string", "i"))
2✔
994

995
function apropos(io::IO, needle::Regex)
4✔
996
    for mod in modules
4✔
997
        # Module doc might be in README.md instead of the META dict
998
        docsearch(doc(mod), needle) && println(io, mod)
329✔
999
        dict = meta(mod; autoinit=false)
658✔
1000
        isnothing(dict) && continue
658✔
1001
        for (k, v) in dict
656✔
1002
            docsearch(v, needle) && println(io, k)
11,243✔
1003
        end
11,243✔
1004
    end
329✔
1005
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc