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

JuliaLang / julia / #37659

21 Oct 2023 08:20PM UTC coverage: 86.179% (-1.3%) from 87.459%
#37659

push

local

web-flow
Throw OverflowError on typemin(Int)//(-1) (#51085)

Fixes #32443

Currently `typemin(Int)//(-1) == typemin(Int)//(1)`, ignoring an
overflow.

As noted by @JeffreySarnoff in
[#32443](https://github.com/JuliaLang/julia/issues/32443#issuecomment-506816722)
This should throw an error instead of silently overflowing.

To fix this I am using `checked_neg` instead of `-` in the Rational
constructor.

With this PR `(-one(T))//typemin(T)` will now also throw an
`OverflowError` instead of an `ArgumentError`

2 of 2 new or added lines in 1 file covered. (100.0%)

72449 of 84068 relevant lines covered (86.18%)

12247769.91 hits per line

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

78.75
/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
13

14
import REPL
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)
28✔
24
    internal_accesses = Set{Pair{Module,Symbol}}()
28✔
25
    quote
20✔
26
        docs = $REPL.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)
×
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)
108✔
37
    line = strip(line)
108✔
38
    ternary_operator_help = (line == "?" || line == "?:")
128✔
39
    if startswith(line, '?') && !ternary_operator_help
64✔
40
        line = line[2:end]
1✔
41
        extended_help_on[] = nothing
1✔
42
        brief = false
1✔
43
    else
44
        extended_help_on[] = line
63✔
45
        brief = true
63✔
46
    end
47
    # interpret anything starting with # or #= as asking for help on comments
48
    if startswith(line, "#")
128✔
49
        if startswith(line, "#=")
×
50
            line = "#="
×
51
        else
52
            line = "#"
×
53
        end
54
    end
55
    x = Meta.parse(line, raise = false, depwarn = false)
64✔
56
    assym = Symbol(line)
64✔
57
    expr =
137✔
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))
66✔
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
115✔
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)
64✔
76
end
77
_helpmode(line::AbstractString, mod::Module=Main) = _helpmode(stdout, line, mod)
28✔
78

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

93
function formatdoc(d::DocStr)
2,832✔
94
    buffer = IOBuffer()
2,832✔
95
    for part in d.text
5,664✔
96
        formatdoc(buffer, d, part)
3,450✔
97
    end
4,068✔
98
    Markdown.MD(Any[Markdown.parse(seekstart(buffer))])
2,832✔
99
end
100
@noinline formatdoc(buffer, d, part) = print(buffer, part)
3,450✔
101

102
function parsedoc(d::DocStr)
5,406✔
103
    if d.object === nothing
5,406✔
104
        md = formatdoc(d)
2,832✔
105
        md.meta[:module] = d.data[:module]
2,832✔
106
        md.meta[:path]   = d.data[:path]
2,832✔
107
        d.object = md
2,832✔
108
    end
109
    d.object
5,406✔
110
end
111

112
## Trimming long help ("# Extended help")
113

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

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

124
trimdocs(doc, brief::Bool) = doc
2✔
125

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

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

157
_trimdocs(md, brief::Bool) = md, false
161✔
158

159

160
is_tuple(expr) = false
24✔
161
is_tuple(expr::Expr) = expr.head == :tuple
×
162

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

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

185
function insert_internal_warning(md::Markdown.MD, internal_access::Set{Pair{Module,Symbol}})
20✔
186
    if !isempty(internal_access)
20✔
187
        items = Any[Any[Markdown.Paragraph(Any[Markdown.Code("", s)])] for s in sort("$mod.$sym" for (mod, sym) in internal_access)]
12✔
188
        admonition = Markdown.Admonition("warning", "Warning", Any[
12✔
189
            Markdown.Paragraph(Any["The following bindings may be internal; they may change or be removed in future versions:"]),
190
            Markdown.List(items, -1, false)])
191
        pushfirst!(md.content, admonition)
6✔
192
    end
193
    md
20✔
194
end
195
function insert_internal_warning(other, internal_access::Set{Pair{Module,Symbol}})
×
196
    other
×
197
end
198

199
"""
200
    Docs.doc(binding, sig)
201

202
Return all documentation that matches both `binding` and `sig`.
203

204
If `getdoc` returns a non-`nothing` result on the value of the binding, then a
205
dynamic docstring is returned instead of one based on the binding itself.
206
"""
207
function doc(binding::Binding, sig::Type = Union{})
205✔
208
    if defined(binding)
205✔
209
        result = getdoc(resolve(binding), sig)
165✔
210
        result === nothing || return result
×
211
    end
212
    results, groups = DocStr[], MultiDoc[]
174✔
213
    # Lookup `binding` and `sig` for matches in all modules of the docsystem.
214
    for mod in modules
174✔
215
        dict = meta(mod; autoinit=false)
23,428✔
216
        isnothing(dict) && continue
23,428✔
217
        if haskey(dict, binding)
11,800✔
218
            multidoc = dict[binding]
86✔
219
            push!(groups, multidoc)
86✔
220
            for msig in multidoc.order
86✔
221
                sig <: msig && push!(results, multidoc.docs[msig])
86✔
222
            end
86✔
223
        end
224
    end
11,888✔
225
    if isempty(groups)
174✔
226
        # When no `MultiDoc`s are found that match `binding` then we check whether `binding`
227
        # is an alias of some other `Binding`. When it is we then re-run `doc` with that
228
        # `Binding`, otherwise if it's not an alias then we generate a summary for the
229
        # `binding` and display that to the user instead.
230
        alias = aliasof(binding)
90✔
231
        alias == binding ? summarize(alias, sig) : doc(alias, sig)
90✔
232
    else
233
        # There was at least one match for `binding` while searching. If there weren't any
234
        # matches for `sig` then we concatenate *all* the docs from the matching `Binding`s.
235
        if isempty(results)
84✔
236
            for group in groups, each in group.order
3✔
237
                push!(results, group.docs[each])
3✔
238
            end
6✔
239
        end
240
        # Get parsed docs and concatenate them.
241
        md = catdoc(mapany(parsedoc, results)...)
84✔
242
        # Save metadata in the generated markdown.
243
        if isa(md, Markdown.MD)
84✔
244
            # We don't know how to insert an internal symbol warning into non-markdown
245
            # content, so we don't.
246
            md.meta[:results] = results
82✔
247
            md.meta[:binding] = binding
82✔
248
            md.meta[:typesig] = sig
82✔
249
        end
250
        return md
84✔
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))
×
256
doc(object, sig::Type = Union{}) = doc(aliasof(object, typeof(object)), sig)
276✔
257
doc(object, sig...)              = doc(object, Tuple{sig...})
×
258

259
function lookup_doc(ex)
49✔
260
    if isa(ex, Expr) && ex.head !== :(.) && Base.isoperator(ex.head)
30✔
261
        # handle syntactic operators, e.g. +=, ::, .=
262
        ex = ex.head
×
263
    end
264
    if haskey(keywords, ex)
50✔
265
        return parsedoc(keywords[ex])
6✔
266
    elseif Meta.isexpr(ex, :incomplete)
25✔
267
        return :($(Markdown.md"No documentation found."))
×
268
    elseif !isa(ex, Expr) && !isa(ex, Symbol)
25✔
269
        return :($(doc)($(typeof)($(esc(ex)))))
1✔
270
    end
271
    if isa(ex, Symbol) && Base.isoperator(ex)
54✔
272
        str = string(ex)
6✔
273
        isdotted = startswith(str, ".")
12✔
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 ("&&", "||")
3✔
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)))
43✔
288
    if isexpr(ex, :call) || isexpr(ex, :macrocall) || isexpr(ex, :where)
48✔
289
        sig = esc(signature(ex))
4✔
290
        :($(doc)($binding, $sig))
4✔
291
    else
292
        :($(doc)($binding))
35✔
293
    end
294
end
295

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

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

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

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

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

427
function summarize(io::IO, @nospecialize(T), binding::Binding)
3✔
428
    T = typeof(T)
3✔
429
    println(io, "`", binding, "` is of type `", T, "`.\n")
3✔
430
    summarize(io, T, binding)
3✔
431
end
432

433
# repl search and completions for help
434

435

436
quote_spaces(x) = any(isspace, x) ? "'" * x * "'" : x
60,694✔
437

438
function repl_search(io::IO, s::Union{Symbol,String}, mod::Module)
23✔
439
    pre = "search:"
23✔
440
    print(io, pre)
23✔
441
    printmatches(io, s, map(quote_spaces, doc_completions(s, mod)), cols = _displaysize(io)[2] - length(pre))
23✔
442
    println(io, "\n")
23✔
443
end
444

445
# TODO: document where this is used
446
repl_search(s, mod::Module) = repl_search(stdout, s, mod)
×
447

448
function repl_corrections(io::IO, s, mod::Module)
6✔
449
    print(io, "Couldn't find ")
6✔
450
    quot = any(isspace, s) ? "'" : ""
6✔
451
    print(io, quot)
6✔
452
    printstyled(io, s, color=:cyan)
6✔
453
    print(io, quot, '\n')
6✔
454
    print_correction(io, s, mod)
6✔
455
end
456
repl_corrections(s) = repl_corrections(stdout, s)
×
457

458
# inverse of latex_symbols Dict, lazily created as needed
459
const symbols_latex = Dict{String,String}()
460
function symbol_latex(s::String)
43✔
461
    if isempty(symbols_latex) && isassigned(Base.REPL_MODULE_REF)
43✔
462
        for (k,v) in Iterators.flatten((REPLCompletions.latex_symbols,
1✔
463
                                        REPLCompletions.emoji_symbols))
464
            symbols_latex[v] = k
3,699✔
465
        end
3,701✔
466

467
        # Overwrite with canonical mapping when a symbol has several completions (#39148)
468
        merge!(symbols_latex, REPLCompletions.symbols_latex_canonical)
1✔
469
    end
470

471
    return get(symbols_latex, s, "")
43✔
472
end
473
function repl_latex(io::IO, s0::String)
23✔
474
    # This has rampant `Core.Box` problems (#15276). Use the tricks of
475
    # https://docs.julialang.org/en/v1/manual/performance-tips/#man-performance-captured
476
    # We're changing some of the values so the `let` trick isn't applicable.
477
    s::String = s0
23✔
478
    latex::String = symbol_latex(s)
23✔
479
    if isempty(latex)
23✔
480
        # Decompose NFC-normalized identifier to match tab-completion
481
        # input if the first search came up empty.
482
        s = normalize(s, :NFD)
20✔
483
        latex = symbol_latex(s)
20✔
484
    end
485
    if !isempty(latex)
23✔
486
        print(io, "\"")
3✔
487
        printstyled(io, s, color=:cyan)
3✔
488
        print(io, "\" can be typed by ")
3✔
489
        printstyled(io, latex, "<tab>", color=:cyan)
3✔
490
        println(io, '\n')
3✔
491
    elseif any(c -> haskey(symbols_latex, string(c)), s)
102✔
492
        print(io, "\"")
1✔
493
        printstyled(io, s, color=:cyan)
1✔
494
        print(io, "\" can be typed by ")
1✔
495
        state::Char = '\0'
1✔
496
        with_output_color(:cyan, io) do io
1✔
497
            for c in s
1✔
498
                cstr = string(c)
8✔
499
                if haskey(symbols_latex, cstr)
8✔
500
                    latex = symbols_latex[cstr]
8✔
501
                    if length(latex) == 3 && latex[2] in ('^','_')
11✔
502
                        # coalesce runs of sub/superscripts
503
                        if state != latex[2]
12✔
504
                            '\0' != state && print(io, "<tab>")
2✔
505
                            print(io, latex[1:2])
2✔
506
                            state = latex[2]
4✔
507
                        end
508
                        print(io, latex[3])
6✔
509
                    else
510
                        if '\0' != state
2✔
511
                            print(io, "<tab>")
1✔
512
                            state = '\0'
1✔
513
                        end
514
                        print(io, latex, "<tab>")
10✔
515
                    end
516
                else
517
                    if '\0' != state
×
518
                        print(io, "<tab>")
×
519
                        state = '\0'
×
520
                    end
521
                    print(io, c)
×
522
                end
523
            end
8✔
524
            '\0' != state && print(io, "<tab>")
1✔
525
        end
526
        println(io, '\n')
20✔
527
    end
528
end
529
repl_latex(s::String) = repl_latex(stdout, s)
×
530

531
macro repl(ex, brief::Bool=false, mod::Module=Main) repl(ex; brief, mod) end
532
macro repl(io, ex, brief, mod, internal_accesses) repl(io, ex; brief, mod, internal_accesses) end
50✔
533

534
function repl(io::IO, s::Symbol; brief::Bool=true, mod::Module=Main, internal_accesses::Union{Nothing, Set{Pair{Module,Symbol}}}=nothing)
46✔
535
    str = string(s)
23✔
536
    quote
29✔
537
        repl_latex($io, $str)
538
        repl_search($io, $str, $mod)
539
        $(if !isdefined(mod, s) && !Base.isbindingresolved(mod, s) && !haskey(keywords, s) && !Base.isoperator(s)
540
               # n.b. we call isdefined for the side-effect of resolving the binding, if possible
541
               :(repl_corrections($io, $str, $mod))
6✔
542
          end)
543
        $(_repl(s, brief, mod, internal_accesses))
544
    end
545
end
546
isregex(x) = isexpr(x, :macrocall, 3) && x.args[1] === Symbol("@r_str") && !isempty(x.args[3])
25✔
547

548
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)
50✔
549
repl(io::IO, str::AbstractString; brief::Bool=true, mod::Module=Main, internal_accesses::Union{Nothing, Set{Pair{Module,Symbol}}}=nothing) = :(apropos($io, $str))
2✔
550
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✔
551
#repl(io::IO, other) = lookup_doc(other) # TODO
552

553
repl(x; brief::Bool=true, mod::Module=Main) = repl(stdout, x; brief, mod)
×
554

555
function _repl(x, brief::Bool=true, mod::Module=Main, internal_accesses::Union{Nothing, Set{Pair{Module,Symbol}}}=nothing)
47✔
556
    if isexpr(x, :call)
40✔
557
        x = x::Expr
×
558
        # determine the types of the values
559
        kwargs = nothing
×
560
        pargs = Any[]
×
561
        for arg in x.args[2:end]
×
562
            if isexpr(arg, :parameters)
×
563
                kwargs = mapany(arg.args) do kwarg
×
564
                    if kwarg isa Symbol
565
                        kwarg = :($kwarg::Any)
566
                    elseif isexpr(kwarg, :kw)
567
                        lhs = kwarg.args[1]
568
                        rhs = kwarg.args[2]
569
                        if lhs isa Symbol
570
                            if rhs isa Symbol
571
                                kwarg.args[1] = :($lhs::(@isdefined($rhs) ? typeof($rhs) : Any))
572
                            else
573
                                kwarg.args[1] = :($lhs::typeof($rhs))
574
                            end
575
                        end
576
                    end
577
                    kwarg
578
                end
579
            elseif isexpr(arg, :kw)
×
580
                if kwargs === nothing
×
581
                    kwargs = Any[]
×
582
                end
583
                lhs = arg.args[1]
×
584
                rhs = arg.args[2]
×
585
                if lhs isa Symbol
×
586
                    if rhs isa Symbol
×
587
                        arg.args[1] = :($lhs::(@isdefined($rhs) ? typeof($rhs) : Any))
×
588
                    else
589
                        arg.args[1] = :($lhs::typeof($rhs))
×
590
                    end
591
                end
592
                push!(kwargs, arg)
×
593
            else
594
                if arg isa Symbol
×
595
                    arg = :($arg::(@isdefined($arg) ? typeof($arg) : Any))
×
596
                elseif !isexpr(arg, :(::))
×
597
                    arg = :(::typeof($arg))
×
598
                end
599
                push!(pargs, arg)
×
600
            end
601
        end
×
602
        if kwargs === nothing
×
603
            x.args = Any[x.args[1], pargs...]
×
604
        else
605
            x.args = Any[x.args[1], Expr(:parameters, kwargs...), pargs...]
×
606
        end
607
    end
608
    #docs = lookup_doc(x) # TODO
609
    docs = esc(:(@doc $x))
57✔
610
    docs = if isfield(x)
40✔
611
        quote
19✔
612
            if isa($(esc(x.args[1])), DataType)
613
                fielddoc($(esc(x.args[1])), $(esc(x.args[2])))
614
            else
615
                $docs
616
            end
617
        end
618
    else
619
        docs
45✔
620
    end
621
    docs = log_nonpublic_access(macroexpand(mod, docs), mod, internal_accesses)
47✔
622
    :(REPL.trimdocs($docs, $brief))
47✔
623
end
624

625
"""
626
    fielddoc(binding, field)
627

628
Return documentation for a particular `field` of a type if it exists.
629
"""
630
function fielddoc(binding::Binding, field::Symbol)
4✔
631
    for mod in modules
4✔
632
        dict = meta(mod; autoinit=false)
544✔
633
        isnothing(dict) && continue
544✔
634
        if haskey(dict, binding)
272✔
635
            multidoc = dict[binding]
×
636
            if haskey(multidoc.docs, Union{})
×
637
                fields = multidoc.docs[Union{}].data[:fields]
×
638
                if haskey(fields, field)
×
639
                    doc = fields[field]
×
640
                    return isa(doc, Markdown.MD) ? doc : Markdown.parse(doc)
×
641
                end
642
            end
643
        end
644
    end
276✔
645
    fs = fieldnames(resolve(binding))
4✔
646
    fields = isempty(fs) ? "no fields" : (length(fs) == 1 ? "field " : "fields ") *
7✔
647
                                          join(("`$f`" for f in fs), ", ", ", and ")
648
    Markdown.parse("`$(resolve(binding))` has $fields.")
4✔
649
end
650

651
# As with the additional `doc` methods, this converts an object to a `Binding` first.
652
fielddoc(object, field::Symbol) = fielddoc(aliasof(object, typeof(object)), field)
4✔
653

654

655
# Search & Rescue
656
# Utilities for correcting user mistakes and (eventually)
657
# doing full documentation searches from the repl.
658

659
# Fuzzy Search Algorithm
660

661
function matchinds(needle, haystack; acronym::Bool = false)
474✔
662
    chars = collect(needle)
237✔
663
    is = Int[]
237✔
664
    lastc = '\0'
×
665
    for (i, char) in enumerate(haystack)
474✔
666
        while !isempty(chars) && isspace(first(chars))
2,072✔
667
            popfirst!(chars) # skip spaces
4✔
668
        end
4✔
669
        isempty(chars) && break
1,047✔
670
        if lowercase(char) == lowercase(chars[1]) &&
1,150✔
671
           (!acronym || !isletter(lastc))
672
            push!(is, i)
311✔
673
            popfirst!(chars)
311✔
674
        end
675
        lastc = char
×
676
    end
1,021✔
677
    return is
237✔
678
end
679

680
longer(x, y) = length(x) ≥ length(y) ? (x, true) : (y, false)
174✔
681

682
bestmatch(needle, haystack) =
174✔
683
    longer(matchinds(needle, haystack, acronym = true),
684
           matchinds(needle, haystack))
685

686
# Optimal string distance: Counts the minimum number of insertions, deletions,
687
# transpositions or substitutions to go from one string to the other.
688
function string_distance(a::AbstractString, lena::Integer, b::AbstractString, lenb::Integer)
98,503✔
689
    if lena > lenb
98,503✔
690
        a, b = b, a
×
691
        lena, lenb = lenb, lena
×
692
    end
693
    start = 0
98,503✔
694
    for (i, j) in zip(a, b)
195,641✔
695
        if a == b
97,273✔
696
            start += 1
183✔
697
        else
698
            break
×
699
        end
700
    end
318✔
701
    start == lena && return lenb - start
98,503✔
702
    vzero = collect(1:(lenb - start))
969,292✔
703
    vone = similar(vzero)
97,090✔
704
    prev_a, prev_b = first(a), first(b)
192,299✔
705
    current = 0
×
706
    for (i, ai) in enumerate(a)
194,180✔
707
        i > start || (prev_a = ai; continue)
305,752✔
708
        left = i - start - 1
305,752✔
709
        current = i - start
305,752✔
710
        transition_next = 0
×
711
        for (j, bj) in enumerate(b)
611,504✔
712
            j > start || (prev_b = bj; continue)
3,247,436✔
713
            # No need to look beyond window of lower right diagonal
714
            above = current
×
715
            this_transition = transition_next
×
716
            transition_next = vone[j - start]
3,247,436✔
717
            vone[j - start] = current = left
3,247,436✔
718
            left = vzero[j - start]
3,247,436✔
719
            if ai != bj
3,247,436✔
720
                # Minimum between substitution, deletion and insertion
721
                current = min(current + 1, above + 1, left + 1)
3,145,974✔
722
                if i > start + 1 && j > start + 1 && ai == prev_b && prev_a == bj
3,145,974✔
723
                    current = min(current, (this_transition += 1))
3,980✔
724
                end
725
            end
726
            vzero[j - start] = current
3,247,436✔
727
            prev_b = bj
×
728
        end
6,189,120✔
729
        prev_a = ai
305,752✔
730
    end
514,414✔
731
    current
97,090✔
732
end
733

734
function fuzzyscore(needle::AbstractString, haystack::AbstractString)
12,286✔
735
    lena, lenb = length(needle), length(haystack)
98,503✔
736
    1 - (string_distance(needle, lena, haystack, lenb) / max(lena, lenb))
98,503✔
737
end
738

739
function fuzzysort(search::String, candidates::Vector{String})
43✔
740
    scores = map(cand -> fuzzyscore(search, cand), candidates)
86,260✔
741
    candidates[sortperm(scores)] |> reverse
43✔
742
end
743

744
# Levenshtein Distance
745

746
function levenshtein(s1, s2)
12,586✔
747
    a, b = collect(s1), collect(s2)
12,586✔
748
    m = length(a)
12,586✔
749
    n = length(b)
12,586✔
750
    d = Matrix{Int}(undef, m+1, n+1)
12,586✔
751

752
    d[1:m+1, 1] = 0:m
72,102✔
753
    d[1, 1:n+1] = 0:n
130,153✔
754

755
    for i = 1:m, j = 1:n
59,487✔
756
        d[i+1,j+1] = min(d[i  , j+1] + 1,
414,328✔
757
                         d[i+1, j  ] + 1,
758
                         d[i  , j  ] + (a[i] != b[j]))
759
    end
448,672✔
760

761
    return d[m+1, n+1]
12,586✔
762
end
763

764
function levsort(search::String, candidates::Vector{String})
6✔
765
    scores = map(cand -> (Float64(levenshtein(search, cand)), -fuzzyscore(search, cand)), candidates)
12,153✔
766
    candidates = candidates[sortperm(scores)]
6✔
767
    i = 0
6✔
768
    for outer i = 1:length(candidates)
12✔
769
        levenshtein(search, candidates[i]) > 3 && break
439✔
770
    end
433✔
771
    return candidates[1:i]
6✔
772
end
773

774
# Result printing
775

776
function printmatch(io::IO, word, match)
118✔
777
    is, _ = bestmatch(word, match)
174✔
778
    for (i, char) = enumerate(match)
236✔
779
        if i in is
1,507✔
780
            printstyled(io, char, bold=true)
244✔
781
        else
782
            print(io, char)
292✔
783
        end
784
    end
536✔
785
end
786

787
function printmatches(io::IO, word, matches; cols::Int = _displaysize(io)[2])
46✔
788
    total = 0
23✔
789
    for match in matches
23✔
790
        total + length(match) + 1 > cols && break
141✔
791
        fuzzyscore(word, match) < 0.5 && break
135✔
792
        print(io, " ")
118✔
793
        printmatch(io, word, match)
118✔
794
        total += length(match) + 1
118✔
795
    end
141✔
796
end
797

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

800
function print_joined_cols(io::IO, ss::Vector{String}, delim = "", last = delim; cols::Int = _displaysize(io)[2])
12✔
801
    i = 0
6✔
802
    total = 0
6✔
803
    for outer i = 1:length(ss)
12✔
804
        total += length(ss[i])
64✔
805
        total + max(i-2,0)*length(delim) + (i>1 ? 1 : 0)*length(last) > cols && (i-=1; break)
68✔
806
    end
60✔
807
    join(io, ss[1:i], delim, last)
6✔
808
end
809

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

812
function print_correction(io::IO, word::String, mod::Module)
6✔
813
    cors = map(quote_spaces, levsort(word, accessible(mod)))
6✔
814
    pre = "Perhaps you meant "
6✔
815
    print(io, pre)
6✔
816
    print_joined_cols(io, cors, ", ", " or "; cols = _displaysize(io)[2] - length(pre))
6✔
817
    println(io)
6✔
818
    return
6✔
819
end
820

821
# TODO: document where this is used
822
print_correction(word, mod::Module) = print_correction(stdout, word, mod)
×
823

824
# Completion data
825

826

827
moduleusings(mod) = ccall(:jl_module_usings, Any, (Any,), mod)
49✔
828

829
filtervalid(names) = filter(x->!occursin(r"#", x), map(string, names))
150,034✔
830

831
accessible(mod::Module) =
49✔
832
    Symbol[filter!(s -> !Base.isdeprecated(mod, s), names(mod, all=true, imported=true));
101,417✔
833
           map(names, moduleusings(mod))...;
834
           collect(keys(Base.Docs.keywords))] |> unique |> filtervalid
835

836
function doc_completions(name, mod::Module=Main)
46✔
837
    res = fuzzysort(name, accessible(mod))
46✔
838

839
    # to insert an entry like `raw""` for `"@raw_str"` in `res`
840
    ms = match.(r"^@(.*?)_str$", res)
86✔
841
    idxs = findall(!isnothing, ms)
86✔
842

843
    # avoid messing up the order while inserting
844
    for i in reverse!(idxs)
43✔
845
        c = only((ms[i]::AbstractMatch).captures)
1,122✔
846
        insert!(res, i, "$(c)\"\"")
1,122✔
847
    end
604✔
848
    res
43✔
849
end
850
doc_completions(name::Symbol) = doc_completions(string(name), mod)
×
851

852

853
# Searching and apropos
854

855
# Docsearch simply returns true or false if an object contains the given needle
856
docsearch(haystack::AbstractString, needle) = occursin(needle, haystack)
5,458✔
857
docsearch(haystack::Symbol, needle) = docsearch(string(haystack), needle)
×
858
docsearch(::Nothing, needle) = false
×
859
function docsearch(haystack::Array, needle)
×
860
    for elt in haystack
×
861
        docsearch(elt, needle) && return true
×
862
    end
×
863
    false
×
864
end
865
function docsearch(haystack, needle)
×
866
    @warn "Unable to search documentation of type $(typeof(haystack))" maxlog=1
×
867
    false
×
868
end
869

870
## Searching specific documentation objects
871
function docsearch(haystack::MultiDoc, needle)
4,946✔
872
    for v in values(haystack.docs)
9,892✔
873
        docsearch(v, needle) && return true
5,314✔
874
    end
2,895✔
875
    false
2,159✔
876
end
877

878
function docsearch(haystack::DocStr, needle)
5,314✔
879
    docsearch(parsedoc(haystack), needle) && return true
5,314✔
880
    if haskey(haystack.data, :fields)
2,527✔
881
        for doc in values(haystack.data[:fields])
236✔
882
            docsearch(doc, needle) && return true
10✔
883
        end
10✔
884
    end
885
    false
2,527✔
886
end
887

888
## doc search
889

890
## Markdown search simply strips all markup and searches plain text version
891
docsearch(haystack::Markdown.MD, needle) = docsearch(stripmd(haystack.content), needle)
5,448✔
892

893
"""
894
    stripmd(x)
895

896
Strip all Markdown markup from x, leaving the result in plain text. Used
897
internally by apropos to make docstrings containing more than one markdown
898
element searchable.
899
"""
900
stripmd(@nospecialize x) = string(x) # for random objects interpolated into the docstring
×
901
stripmd(x::AbstractString) = x  # base case
79,112✔
902
stripmd(x::Nothing) = " "
42✔
903
stripmd(x::Vector) = string(map(stripmd, x)...)
27,701✔
904

905
stripmd(x::Markdown.BlockQuote) = "$(stripmd(x.content))"
×
906
stripmd(x::Markdown.Admonition) = "$(stripmd(x.content))"
976✔
907
stripmd(x::Markdown.Bold) = "$(stripmd(x.text))"
33✔
908
stripmd(x::Markdown.Code) = "$(stripmd(x.code))"
36,640✔
909
stripmd(x::Markdown.Header) = stripmd(x.text)
2,134✔
910
stripmd(x::Markdown.HorizontalRule) = " "
68✔
911
stripmd(x::Markdown.Image) = "$(stripmd(x.alt)) $(x.url)"
×
912
stripmd(x::Markdown.Italic) = "$(stripmd(x.text))"
227✔
913
stripmd(x::Markdown.LaTeX) = "$(x.formula)"
363✔
914
stripmd(x::Markdown.LineBreak) = " "
34✔
915
stripmd(x::Markdown.Link) = "$(stripmd(x.text)) $(x.url)"
5,284✔
916
stripmd(x::Markdown.List) = join(map(stripmd, x.items), " ")
449✔
917
stripmd(x::Markdown.MD) = join(map(stripmd, x.content), " ")
5,438✔
918
stripmd(x::Markdown.Paragraph) = stripmd(x.content)
11,845✔
919
stripmd(x::Markdown.Footnote) = "$(stripmd(x.id)) $(stripmd(x.text))"
82✔
920
stripmd(x::Markdown.Table) =
16✔
921
    join([join(map(stripmd, r), " ") for r in x.rows], " ")
922

923
"""
924
    apropos([io::IO=stdout], pattern::Union{AbstractString,Regex})
925

926
Search available docstrings for entries containing `pattern`.
927

928
When `pattern` is a string, case is ignored. Results are printed to `io`.
929

930
`apropos` can be called from the help mode in the REPL by wrapping the query in double quotes:
931
```
932
help?> "pattern"
933
```
934
"""
935
apropos(string) = apropos(stdout, string)
×
936
apropos(io::IO, string) = apropos(io, Regex("\\Q$string", "i"))
1✔
937

938
function apropos(io::IO, needle::Regex)
2✔
939
    for mod in modules
2✔
940
        # Module doc might be in README.md instead of the META dict
941
        docsearch(doc(mod), needle) && println(io, mod)
134✔
942
        dict = meta(mod; autoinit=false)
268✔
943
        isnothing(dict) && continue
268✔
944
        for (k, v) in dict
268✔
945
            docsearch(v, needle) && println(io, k)
4,946✔
946
        end
4,946✔
947
    end
134✔
948
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