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

JuliaLang / julia / #37764

30 Apr 2024 04:54AM UTC coverage: 85.194% (-2.3%) from 87.452%
#37764

push

local

web-flow
inference: don't taint `:consistent` on use of undefined `isbitstype`-fields (#54136)

After #52169, there is no need to taint `:consistent`-cy on accessing
undefined `isbitstype` field since the value of the fields is freezed
and thus never transmute across multiple uses.

72817 of 85472 relevant lines covered (85.19%)

15106206.38 hits per line

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

51.77
/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
import REPL
17

18
using InteractiveUtils: subtypes
19

20
using Unicode: normalize
21

22
## Help mode ##
23

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

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

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

81
# Print horizontal lines between each docstring if there are multiple docs
82
function insert_hlines(docs)
×
83
    if !isa(docs, Markdown.MD) || !haskey(docs.meta, :results) || isempty(docs.meta[:results])
×
84
        return docs
×
85
    end
86
    docs = docs::Markdown.MD
×
87
    v = Any[]
×
88
    for (n, doc) in enumerate(docs.content)
×
89
        push!(v, doc)
×
90
        n == length(docs.content) || push!(v, Markdown.HorizontalRule())
×
91
    end
×
92
    return Markdown.MD(v)
×
93
end
94

95
function formatdoc(d::DocStr)
3,155✔
96
    buffer = IOBuffer()
3,155✔
97
    for part in d.text
6,310✔
98
        formatdoc(buffer, d, part)
3,884✔
99
    end
4,613✔
100
    Markdown.MD(Any[Markdown.parse(seekstart(buffer))])
3,155✔
101
end
102
@noinline formatdoc(buffer, d, part) = print(buffer, part)
3,882✔
103

104
function parsedoc(d::DocStr)
6,541✔
105
    if d.object === nothing
6,541✔
106
        md = formatdoc(d)
3,155✔
107
        md.meta[:module] = d.data[:module]
3,155✔
108
        md.meta[:path]   = d.data[:path]
3,155✔
109
        d.object = md
3,155✔
110
    end
111
    d.object
6,541✔
112
end
113

114
## Trimming long help ("# Extended help")
115

116
struct Message  # For direct messages to the terminal
117
    msg    # AbstractString
×
118
    fmt    # keywords to `printstyled`
119
end
120
Message(msg) = Message(msg, ())
×
121

122
function Markdown.term(io::IO, msg::Message, columns)
×
123
    printstyled(io, msg.msg; msg.fmt...)
×
124
end
125

126
trimdocs(doc, brief::Bool) = doc
×
127

128
function trimdocs(md::Markdown.MD, brief::Bool)
5✔
129
    brief || return md
10✔
130
    md, trimmed = _trimdocs(md, brief)
×
131
    if trimmed
×
132
        line = extended_help_on[]
×
133
        line = isa(line, AbstractString) ? line : ""
×
134
        push!(md.content, Message("Extended help is available with `??$line`", (color=Base.info_color(), bold=true)))
×
135
    end
136
    return md
×
137
end
138

139
function _trimdocs(md::Markdown.MD, brief::Bool)
×
140
    content, trimmed = [], false
×
141
    for c in md.content
×
142
        if isa(c, Markdown.Header{1}) && isa(c.text, AbstractArray) && !isempty(c.text)
×
143
            item = c.text[1]
×
144
            if isa(item, AbstractString) &&
×
145
                lowercase(item) ∈ ("extended help",
146
                                   "extended documentation",
147
                                   "extended docs")
148
                trimmed = true
×
149
                break
×
150
            end
151
        end
152
        c, trm = _trimdocs(c, brief)
×
153
        trimmed |= trm
×
154
        push!(content, c)
×
155
    end
×
156
    return Markdown.MD(content, md.meta), trimmed
×
157
end
158

159
_trimdocs(md, brief::Bool) = md, false
×
160

161

162
is_tuple(expr) = false
×
163
is_tuple(expr::Expr) = expr.head == :tuple
×
164

165
struct Logged{F}
166
    f::F
167
    mod::Module
168
    collection::Set{Pair{Module,Symbol}}
169
end
170
function (la::Logged)(m::Module, s::Symbol)
×
171
    m !== la.mod && Base.isdefined(m, s) && !Base.ispublic(m, s) && push!(la.collection, m => s)
×
172
    la.f(m, s)
×
173
end
174
(la::Logged)(args...) = la.f(args...)
×
175

176
function log_nonpublic_access(expr::Expr, mod::Module, internal_access::Set{Pair{Module,Symbol}})
×
177
    if expr.head === :. && length(expr.args) == 2 && !is_tuple(expr.args[2])
×
178
        Expr(:call, Logged(getproperty, mod, internal_access), log_nonpublic_access.(expr.args, (mod,), (internal_access,))...)
×
179
    elseif expr.head === :call && expr.args[1] === Base.Docs.Binding
×
180
        Expr(:call, Logged(Base.Docs.Binding, mod, internal_access), log_nonpublic_access.(expr.args[2:end], (mod,), (internal_access,))...)
×
181
    else
182
        Expr(expr.head, log_nonpublic_access.(expr.args, (mod,), (internal_access,))...)
×
183
    end
184
end
185
log_nonpublic_access(expr, ::Module, _) = expr
9✔
186

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

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

248
# Some additional convenience `doc` methods that take objects rather than `Binding`s.
249
doc(obj::UnionAll) = doc(Base.unwrap_unionall(obj))
2✔
250
doc(object, sig::Type = Union{}) = doc(aliasof(object, typeof(object)), sig)
369✔
251
doc(object, sig...)              = doc(object, Tuple{sig...})
×
252

253
function lookup_doc(ex)
100✔
254
    if isa(ex, Expr) && ex.head !== :(.) && Base.isoperator(ex.head)
130✔
255
        # handle syntactic operators, e.g. +=, ::, .=
256
        ex = ex.head
×
257
    end
258
    if haskey(keywords, ex)
115✔
259
        return parsedoc(keywords[ex])
×
260
    elseif Meta.isexpr(ex, :incomplete)
86✔
261
        return :($(Markdown.md"No documentation found."))
1✔
262
    elseif !isa(ex, Expr) && !isa(ex, Symbol)
85✔
263
        return :($(doc)($(typeof)($(esc(ex)))))
1✔
264
    end
265
    if isa(ex, Symbol) && Base.isoperator(ex)
112✔
266
        str = string(ex)
×
267
        isdotted = startswith(str, ".")
×
268
        if endswith(str, "=") && Base.operator_precedence(ex) == Base.prec_assignment && ex !== :(:=)
×
269
            op = chop(str)
×
270
            eq = isdotted ? ".=" : "="
×
271
            return Markdown.parse("`x $op= y` is a synonym for `x $eq x $op y`")
×
272
        elseif isdotted && ex !== :(..)
×
273
            op = str[2:end]
×
274
            if op in ("&&", "||")
×
275
                return Markdown.parse("`x $ex y` broadcasts the boolean operator `$op` to `x` and `y`. See [`broadcast`](@ref).")
×
276
            else
277
                return Markdown.parse("`x $ex y` is akin to `broadcast($op, x, y)`. See [`broadcast`](@ref).")
×
278
            end
279
        end
280
    end
281
    binding = esc(bindingexpr(namify(ex)))
123✔
282
    if isexpr(ex, :call) || isexpr(ex, :macrocall) || isexpr(ex, :where)
150✔
283
        sig = esc(signature(ex))
37✔
284
        :($(doc)($binding, $sig))
37✔
285
    else
286
        :($(doc)($binding))
61✔
287
    end
288
end
289

290
# Object Summaries.
291
# =================
292

293
function summarize(binding::Binding, sig)
114✔
294
    io = IOBuffer()
114✔
295
    if defined(binding)
114✔
296
        binding_res = resolve(binding)
113✔
297
        if !isa(binding_res, Module)
113✔
298
            if Base.ispublic(binding.mod, binding.var)
20✔
299
                println(io, "No documentation found for public symbol.\n")
5✔
300
            else
301
                println(io, "No documentation found for private symbol.\n")
15✔
302
            end
303
        end
304
        summarize(io, binding_res, binding)
113✔
305
    else
306
        println(io, "No documentation found.\n")
1✔
307
        quot = any(isspace, sprint(print, binding)) ? "'" : ""
1✔
308
        if Base.isbindingresolved(binding.mod, binding.var)
1✔
309
            println(io, "Binding ", quot, "`", binding, "`", quot, " exists, but has not been assigned a value.")
×
310
        else
311
            println(io, "Binding ", quot, "`", binding, "`", quot, " does not exist.")
1✔
312
        end
313
    end
314
    md = Markdown.parse(seekstart(io))
228✔
315
    # Save metadata in the generated markdown.
316
    md.meta[:results] = DocStr[]
114✔
317
    md.meta[:binding] = binding
114✔
318
    md.meta[:typesig] = sig
114✔
319
    return md
114✔
320
end
321

322
function summarize(io::IO, λ::Function, binding::Binding)
4✔
323
    kind = startswith(string(binding.var), '@') ? "macro" : "`Function`"
8✔
324
    println(io, "`", binding, "` is a ", kind, ".")
4✔
325
    println(io, "```\n", methods(λ), "\n```")
4✔
326
end
327

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

377
function find_readme(m::Module)::Union{String, Nothing}
93✔
378
    mpath = pathof(m)
93✔
379
    isnothing(mpath) && return nothing
95✔
380
    !isfile(mpath) && return nothing # modules in sysimage, where src files are omitted
2✔
381
    path = dirname(mpath)
2✔
382
    top_path = pkgdir(m)
2✔
383
    while true
4✔
384
        for entry in _readdirx(path; sort=true)
4✔
385
            isfile(entry) && (lowercase(entry.name) in ["readme.md", "readme"]) || continue
64✔
386
            return entry.path
2✔
387
        end
22✔
388
        path == top_path && break # go no further than pkgdir
4✔
389
        path = dirname(path) # work up through nested modules
2✔
390
    end
2✔
391
    return nothing
×
392
end
393
function summarize(io::IO, m::Module, binding::Binding; nlines::Int = 200)
186✔
394
    readme_path = find_readme(m)
93✔
395
    public = Base.ispublic(binding.mod, binding.var) ? "public" : "internal"
93✔
396
    if isnothing(readme_path)
95✔
397
        println(io, "No docstring or readme file found for $public module `$m`.\n")
91✔
398
    else
399
        println(io, "No docstring found for $public module `$m`.")
2✔
400
    end
401
    exports = filter!(!=(nameof(m)), names(m))
186✔
402
    if isempty(exports)
93✔
403
        println(io, "Module does not have any public names.")
33✔
404
    else
405
        println(io, "# Public names")
60✔
406
        print(io, "  `")
60✔
407
        join(io, exports, "`, `")
60✔
408
        println(io, "`\n")
60✔
409
    end
410
    if !isnothing(readme_path)
95✔
411
        readme_lines = readlines(readme_path)
2✔
412
        isempty(readme_lines) && return  # don't say we are going to print empty file
2✔
413
        println(io, "# Displaying contents of readme found at `$(readme_path)`")
2✔
414
        for line in first(readme_lines, nlines)
2✔
415
            println(io, line)
20✔
416
        end
20✔
417
        length(readme_lines) > nlines && println(io, "\n[output truncated to first $nlines lines]")
2✔
418
    end
419
end
420

421
function summarize(io::IO, @nospecialize(T), binding::Binding)
1✔
422
    T = typeof(T)
1✔
423
    println(io, "`", binding, "` is of type `", T, "`.\n")
1✔
424
    summarize(io, T, binding)
1✔
425
end
426

427
# repl search and completions for help
428

429
# This type is returned from `accessible` and denotes a binding that is accessible within
430
# some context. It differs from `Base.Docs.Binding`, which is also used by the REPL, in
431
# that it doesn't track the defining module for a symbol unless the symbol is public but
432
# not exported, i.e. it's accessible but requires qualification. Using this type rather
433
# than `Base.Docs.Binding` simplifies things considerably, partially because REPL searching
434
# is based on `String`s, which this type stores, but `Base.Docs.Binding` stores a module
435
# and symbol and does not have any notion of the context from which the binding is accessed.
436
struct AccessibleBinding
437
    source::Union{String,Nothing}
1,398✔
438
    name::String
439
end
440

441
function AccessibleBinding(mod::Module, name::Symbol)
1,236✔
442
    m = isexported(mod, name) ? nothing : String(nameof(mod))
1,320✔
443
    return AccessibleBinding(m, String(name))
1,236✔
444
end
445
AccessibleBinding(name::Symbol) = AccessibleBinding(nothing, String(name))
162✔
446

447
function Base.show(io::IO, b::AccessibleBinding)
448
    b.source === nothing || print(io, b.source, '.')
1,447✔
449
    print(io, b.name)
1,363✔
450
end
451

452
quote_spaces(x) = any(isspace, x) ? "'" * x * "'" : x
×
453
quote_spaces(x::AccessibleBinding) = AccessibleBinding(x.source, quote_spaces(x.name))
×
454

455
function repl_search(io::IO, s::Union{Symbol,String}, mod::Module)
×
456
    pre = "search:"
×
457
    print(io, pre)
×
458
    printmatches(io, s, map(quote_spaces, doc_completions(s, mod)), cols = _displaysize(io)[2] - length(pre))
×
459
    println(io, "\n")
×
460
end
461

462
# TODO: document where this is used
463
repl_search(s, mod::Module) = repl_search(stdout, s, mod)
×
464

465
function repl_corrections(io::IO, s, mod::Module)
×
466
    print(io, "Couldn't find ")
×
467
    quot = any(isspace, s) ? "'" : ""
×
468
    print(io, quot)
×
469
    printstyled(io, s, color=:cyan)
×
470
    print(io, quot, '\n')
×
471
    print_correction(io, s, mod)
×
472
end
473
repl_corrections(s) = repl_corrections(stdout, s)
×
474

475
# inverse of latex_symbols Dict, lazily created as needed
476
const symbols_latex = Dict{String,String}()
477
function symbol_latex(s::String)
6✔
478
    if isempty(symbols_latex) && isassigned(Base.REPL_MODULE_REF)
6✔
479
        for (k,v) in Iterators.flatten((REPLCompletions.latex_symbols,
1✔
480
                                        REPLCompletions.emoji_symbols))
481
            symbols_latex[v] = k
3,755✔
482
        end
3,757✔
483

484
        # Overwrite with canonical mapping when a symbol has several completions (#39148)
485
        merge!(symbols_latex, REPLCompletions.symbols_latex_canonical)
1✔
486
    end
487

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

548
macro repl(ex, brief::Bool=false, mod::Module=Main) repl(ex; brief, mod) end
12✔
549
macro repl(io, ex, brief, mod, internal_accesses) repl(io, ex; brief, mod, internal_accesses) end
550

551
function repl(io::IO, s::Symbol; brief::Bool=true, mod::Module=Main, internal_accesses::Union{Nothing, Set{Pair{Module,Symbol}}}=nothing)
×
552
    str = string(s)
×
553
    quote
×
554
        repl_latex($io, $str)
×
555
        repl_search($io, $str, $mod)
×
556
        $(if !isdefined(mod, s) && !Base.isbindingresolved(mod, s) && !haskey(keywords, s) && !Base.isoperator(s)
×
557
               # n.b. we call isdefined for the side-effect of resolving the binding, if possible
558
               :(repl_corrections($io, $str, $mod))
×
559
          end)
560
        $(_repl(s, brief, mod, internal_accesses))
×
561
    end
562
end
563
isregex(x) = isexpr(x, :macrocall, 3) && x.args[1] === Symbol("@r_str") && !isempty(x.args[3])
5✔
564

565
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)
10✔
566
repl(io::IO, str::AbstractString; brief::Bool=true, mod::Module=Main, internal_accesses::Union{Nothing, Set{Pair{Module,Symbol}}}=nothing) = :(apropos($io, $str))
×
567
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
2✔
568
#repl(io::IO, other) = lookup_doc(other) # TODO
569

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

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

642
"""
643
    fielddoc(binding, field)
644

645
Return documentation for a particular `field` of a type if it exists.
646
"""
647
function fielddoc(binding::Binding, field::Symbol)
1✔
648
    for mod in modules
1✔
649
        dict = meta(mod; autoinit=false)
152✔
650
        isnothing(dict) && continue
152✔
651
        if haskey(dict, binding)
76✔
652
            multidoc = dict[binding]
1✔
653
            if haskey(multidoc.docs, Union{})
1✔
654
                fields = multidoc.docs[Union{}].data[:fields]
1✔
655
                if haskey(fields, field)
1✔
656
                    doc = fields[field]
1✔
657
                    return isa(doc, Markdown.MD) ? doc : Markdown.parse(doc)
1✔
658
                end
659
            end
660
        end
661
    end
75✔
662
    fs = fieldnames(resolve(binding))
×
663
    fields = isempty(fs) ? "no fields" : (length(fs) == 1 ? "field " : "fields ") *
×
664
                                          join(("`$f`" for f in fs), ", ", ", and ")
665
    Markdown.parse("`$(resolve(binding))` has $fields.")
×
666
end
667

668
# As with the additional `doc` methods, this converts an object to a `Binding` first.
669
fielddoc(object, field::Symbol) = fielddoc(aliasof(object, typeof(object)), field)
1✔
670

671

672
# Search & Rescue
673
# Utilities for correcting user mistakes and (eventually)
674
# doing full documentation searches from the repl.
675

676
# Fuzzy Search Algorithm
677

678
function matchinds(needle, haystack; acronym::Bool = false)
×
679
    chars = collect(needle)
×
680
    is = Int[]
×
681
    lastc = '\0'
×
682
    for (i, char) in enumerate(haystack)
×
683
        while !isempty(chars) && isspace(first(chars))
×
684
            popfirst!(chars) # skip spaces
×
685
        end
×
686
        isempty(chars) && break
×
687
        if lowercase(char) == lowercase(chars[1]) &&
×
688
           (!acronym || !isletter(lastc))
689
            push!(is, i)
×
690
            popfirst!(chars)
×
691
        end
692
        lastc = char
×
693
    end
×
694
    return is
×
695
end
696

697
matchinds(needle, (; name)::AccessibleBinding; acronym::Bool=false) =
×
698
    matchinds(needle, name; acronym)
699

700
longer(x, y) = length(x) ≥ length(y) ? (x, true) : (y, false)
×
701

702
bestmatch(needle, haystack) =
×
703
    longer(matchinds(needle, haystack, acronym = true),
704
           matchinds(needle, haystack))
705

706
# Optimal string distance: Counts the minimum number of insertions, deletions,
707
# transpositions or substitutions to go from one string to the other.
708
function string_distance(a::AbstractString, lena::Integer, b::AbstractString, lenb::Integer)
×
709
    if lena > lenb
×
710
        a, b = b, a
×
711
        lena, lenb = lenb, lena
×
712
    end
713
    start = 0
×
714
    for (i, j) in zip(a, b)
×
715
        if a == b
×
716
            start += 1
×
717
        else
718
            break
×
719
        end
720
    end
×
721
    start == lena && return lenb - start
×
722
    vzero = collect(1:(lenb - start))
×
723
    vone = similar(vzero)
×
724
    prev_a, prev_b = first(a), first(b)
×
725
    current = 0
×
726
    for (i, ai) in enumerate(a)
×
727
        i > start || (prev_a = ai; continue)
×
728
        left = i - start - 1
×
729
        current = i - start
×
730
        transition_next = 0
×
731
        for (j, bj) in enumerate(b)
×
732
            j > start || (prev_b = bj; continue)
×
733
            # No need to look beyond window of lower right diagonal
734
            above = current
×
735
            this_transition = transition_next
×
736
            transition_next = vone[j - start]
×
737
            vone[j - start] = current = left
×
738
            left = vzero[j - start]
×
739
            if ai != bj
×
740
                # Minimum between substitution, deletion and insertion
741
                current = min(current + 1, above + 1, left + 1)
×
742
                if i > start + 1 && j > start + 1 && ai == prev_b && prev_a == bj
×
743
                    current = min(current, (this_transition += 1))
×
744
                end
745
            end
746
            vzero[j - start] = current
×
747
            prev_b = bj
×
748
        end
×
749
        prev_a = ai
×
750
    end
×
751
    current
×
752
end
753

754
function fuzzyscore(needle::AbstractString, haystack::AbstractString)
×
755
    lena, lenb = length(needle), length(haystack)
×
756
    1 - (string_distance(needle, lena, haystack, lenb) / max(lena, lenb))
×
757
end
758

759
function fuzzyscore(needle::AbstractString, haystack::AccessibleBinding)
×
760
    score = fuzzyscore(needle, haystack.name)
×
761
    haystack.source === nothing && return score
×
762
    # Apply a "penalty" of half an edit if the comparator binding is public but not
763
    # exported so that exported/local names that exactly match the search query are
764
    # listed first
765
    penalty = 1 / (2 * max(length(needle), length(haystack.name)))
×
766
    return max(score - penalty, 0)
×
767
end
768

769
function fuzzysort(search::String, candidates::Vector{AccessibleBinding})
×
770
    scores = map(cand -> fuzzyscore(search, cand), candidates)
×
771
    candidates[sortperm(scores)] |> reverse
×
772
end
773

774
# Levenshtein Distance
775

776
function levenshtein(s1, s2)
×
777
    a, b = collect(s1), collect(s2)
×
778
    m = length(a)
×
779
    n = length(b)
×
780
    d = Matrix{Int}(undef, m+1, n+1)
×
781

782
    d[1:m+1, 1] = 0:m
×
783
    d[1, 1:n+1] = 0:n
×
784

785
    for i = 1:m, j = 1:n
×
786
        d[i+1,j+1] = min(d[i  , j+1] + 1,
×
787
                         d[i+1, j  ] + 1,
788
                         d[i  , j  ] + (a[i] != b[j]))
789
    end
×
790

791
    return d[m+1, n+1]
×
792
end
793

794
function levsort(search::String, candidates::Vector{AccessibleBinding})
×
795
    scores = map(candidates) do cand
×
796
        (Float64(levenshtein(search, cand.name)), -fuzzyscore(search, cand))
×
797
    end
798
    candidates = candidates[sortperm(scores)]
×
799
    i = 0
×
800
    for outer i = 1:length(candidates)
×
801
        levenshtein(search, candidates[i].name) > 3 && break
×
802
    end
×
803
    return candidates[1:i]
×
804
end
805

806
# Result printing
807

808
function printmatch(io::IO, word, match)
×
809
    is, _ = bestmatch(word, match)
×
810
    for (i, char) = enumerate(match)
×
811
        if i in is
×
812
            printstyled(io, char, bold=true)
×
813
        else
814
            print(io, char)
×
815
        end
816
    end
×
817
end
818

819
function printmatch(io::IO, word, match::AccessibleBinding)
×
820
    match.source === nothing || print(io, match.source, '.')
×
821
    printmatch(io, word, match.name)
×
822
end
823

824
function matchlength(x::AccessibleBinding)
×
825
    n = length(x.name)
×
826
    if x.source !== nothing
×
827
        n += length(x.source) + 1  # the +1 is for the `.` separator
×
828
    end
829
    return n
×
830
end
831
matchlength(x) = length(x)
×
832

833
function printmatches(io::IO, word, matches; cols::Int = _displaysize(io)[2])
×
834
    total = 0
×
835
    for match in matches
×
836
        ml = matchlength(match)
×
837
        total + ml + 1 > cols && break
×
838
        fuzzyscore(word, match) < 0.5 && break
×
839
        print(io, " ")
×
840
        printmatch(io, word, match)
×
841
        total += ml + 1
×
842
    end
×
843
end
844

845
printmatches(args...; cols::Int = _displaysize(stdout)[2]) = printmatches(stdout, args..., cols = cols)
×
846

847
function print_joined_cols(io::IO, ss::Vector{AccessibleBinding}, delim = "", last = delim; cols::Int = _displaysize(io)[2])
×
848
    i = 0
×
849
    total = 0
×
850
    for outer i = 1:length(ss)
×
851
        total += matchlength(ss[i])
×
852
        total + max(i-2,0)*length(delim) + (i>1 ? 1 : 0)*length(last) > cols && (i-=1; break)
×
853
    end
×
854
    join(io, ss[1:i], delim, last)
×
855
end
856

857
print_joined_cols(args...; cols::Int = _displaysize(stdout)[2]) = print_joined_cols(stdout, args...; cols=cols)
×
858

859
function print_correction(io::IO, word::String, mod::Module)
×
860
    cors = map(quote_spaces, levsort(word, accessible(mod)))
×
861
    pre = "Perhaps you meant "
×
862
    print(io, pre)
×
863
    print_joined_cols(io, cors, ", ", " or "; cols = _displaysize(io)[2] - length(pre))
×
864
    println(io)
×
865
    return
×
866
end
867

868
# TODO: document where this is used
869
print_correction(word, mod::Module) = print_correction(stdout, word, mod)
×
870

871
# Completion data
872

873
moduleusings(mod) = ccall(:jl_module_usings, Any, (Any,), mod)
1✔
874

875
function accessible(mod::Module)
1✔
876
    bindings = Set(AccessibleBinding(s) for s in names(mod; all=true, imported=true)
1✔
877
                   if !isdeprecated(mod, s))
878
    for used in moduleusings(mod)
1✔
879
        union!(bindings, (AccessibleBinding(used, s) for s in names(used)
7✔
880
                          if !isdeprecated(used, s)))
881
    end
7✔
882
    union!(bindings, (AccessibleBinding(k) for k in keys(Base.Docs.keywords)))
1✔
883
    filter!(b -> !occursin('#', b.name), bindings)
1,424✔
884
    return collect(bindings)
1✔
885
end
886

887
function doc_completions(name, mod::Module=Main)
×
888
    res = fuzzysort(name, accessible(mod))
×
889

890
    # to insert an entry like `raw""` for `"@raw_str"` in `res`
891
    ms = map(c -> match(r"^@(.*?)_str$", c.name), res)
×
892
    idxs = findall(!isnothing, ms)
×
893

894
    # avoid messing up the order while inserting
895
    for i in reverse!(idxs)
×
896
        c = only((ms[i]::AbstractMatch).captures)
×
897
        insert!(res, i, AccessibleBinding(res[i].source, "$(c)\"\""))
×
898
    end
×
899
    res
×
900
end
901
doc_completions(name::Symbol) = doc_completions(string(name), mod)
×
902

903

904
# Searching and apropos
905

906
# Docsearch simply returns true or false if an object contains the given needle
907
docsearch(haystack::AbstractString, needle) = occursin(needle, haystack)
6,565✔
908
docsearch(haystack::Symbol, needle) = docsearch(string(haystack), needle)
×
909
docsearch(::Nothing, needle) = false
×
910
function docsearch(haystack::Array, needle)
×
911
    for elt in haystack
×
912
        docsearch(elt, needle) && return true
×
913
    end
×
914
    false
×
915
end
916
function docsearch(haystack, needle)
×
917
    @warn "Unable to search documentation of type $(typeof(haystack))" maxlog=1
×
918
    false
×
919
end
920

921
## Searching specific documentation objects
922
function docsearch(haystack::MultiDoc, needle)
5,492✔
923
    for v in values(haystack.docs)
10,984✔
924
        docsearch(v, needle) && return true
6,342✔
925
    end
6,099✔
926
    false
927
end
928

929
function docsearch(haystack::DocStr, needle)
6,342✔
930
    docsearch(parsedoc(haystack), needle) && return true
6,342✔
931
    if haskey(haystack.data, :fields)
12,198✔
932
        for doc in values(haystack.data[:fields])
533✔
933
            docsearch(doc, needle) && return true
44✔
934
        end
44✔
935
    end
936
    false
937
end
938

939
## doc search
940

941
## Markdown search simply strips all markup and searches plain text version
942
docsearch(haystack::Markdown.MD, needle) = docsearch(stripmd(haystack.content), needle)
6,519✔
943

944
"""
945
    stripmd(x)
946

947
Strip all Markdown markup from x, leaving the result in plain text. Used
948
internally by apropos to make docstrings containing more than one markdown
949
element searchable.
950
"""
951
stripmd(@nospecialize x) = string(x) # for random objects interpolated into the docstring
6✔
952
stripmd(x::AbstractString) = x  # base case
95,081✔
953
stripmd(x::Nothing) = " "
50✔
954
stripmd(x::Vector) = string(map(stripmd, x)...)
33,943✔
955

956
stripmd(x::Markdown.BlockQuote) = "$(stripmd(x.content))"
2✔
957
stripmd(x::Markdown.Admonition) = "$(stripmd(x.content))"
1,326✔
958
stripmd(x::Markdown.Bold) = "$(stripmd(x.text))"
38✔
959
stripmd(x::Markdown.Code) = "$(stripmd(x.code))"
43,780✔
960
stripmd(x::Markdown.Header) = stripmd(x.text)
2,490✔
961
stripmd(x::Markdown.HorizontalRule) = " "
70✔
962
stripmd(x::Markdown.Image) = "$(stripmd(x.alt)) $(x.url)"
8✔
963
stripmd(x::Markdown.Italic) = "$(stripmd(x.text))"
284✔
964
stripmd(x::Markdown.LaTeX) = "$(x.formula)"
396✔
965
stripmd(x::Markdown.LineBreak) = " "
42✔
966
stripmd(x::Markdown.Link) = "$(stripmd(x.text)) $(x.url)"
6,568✔
967
stripmd(x::Markdown.List) = join(map(stripmd, x.items), " ")
523✔
968
stripmd(x::Markdown.MD) = join(map(stripmd, x.content), " ")
6,510✔
969
stripmd(x::Markdown.Paragraph) = stripmd(x.content)
14,435✔
970
stripmd(x::Markdown.Footnote) = "$(stripmd(x.id)) $(stripmd(x.text))"
98✔
971
stripmd(x::Markdown.Table) =
27✔
972
    join([join(map(stripmd, r), " ") for r in x.rows], " ")
973

974
apropos(string) = apropos(stdout, string)
×
975
apropos(io::IO, string) = apropos(io, Regex("\\Q$string", "i"))
1✔
976

977
function apropos(io::IO, needle::Regex)
2✔
978
    for mod in modules
2✔
979
        # Module doc might be in README.md instead of the META dict
980
        docsearch(doc(mod), needle) && println(io, mod)
179✔
981
        dict = meta(mod; autoinit=false)
358✔
982
        isnothing(dict) && continue
358✔
983
        for (k, v) in dict
356✔
984
            docsearch(v, needle) && println(io, k)
5,492✔
985
        end
5,492✔
986
    end
179✔
987
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

© 2025 Coveralls, Inc