• 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

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

3
module REPLCompletions
4

5
export completions, shell_completions, bslash_completions, completion_text
6

7
using Core: Const
8
const CC = Core.Compiler
9
using Base.Meta
10
using Base: propertynames, something, IdSet
11
using Base.Filesystem: _readdirx
12

13
abstract type Completion end
14

15
struct TextCompletion <: Completion
16
    text::String
×
17
end
18

19
struct KeywordCompletion <: Completion
20
    keyword::String
21
end
22

23
struct KeyvalCompletion <: Completion
24
    keyval::String
25
end
26

27
struct PathCompletion <: Completion
28
    path::String
29
end
30

31
struct ModuleCompletion <: Completion
32
    parent::Module
33
    mod::String
34
end
35

36
struct PackageCompletion <: Completion
37
    package::String
38
end
39

40
struct PropertyCompletion <: Completion
41
    value
42
    property::Symbol
43
end
44

45
struct FieldCompletion <: Completion
46
    typ::DataType
47
    field::Symbol
48
end
49

50
struct MethodCompletion <: Completion
51
    tt # may be used by an external consumer to infer return type, etc.
52
    method::Method
53
    MethodCompletion(@nospecialize(tt), method::Method) = new(tt, method)
1✔
54
end
55

56
struct BslashCompletion <: Completion
57
    bslash::String
58
end
59

60
struct ShellCompletion <: Completion
61
    text::String
62
end
63

64
struct DictCompletion <: Completion
65
    dict::AbstractDict
66
    key::String
67
end
68

69
struct KeywordArgumentCompletion <: Completion
70
    kwarg::String
71
end
72

73
# interface definition
74
function Base.getproperty(c::Completion, name::Symbol)
75
    if name === :text
1✔
76
        return getfield(c, :text)::String
×
77
    elseif name === :keyword
1✔
78
        return getfield(c, :keyword)::String
×
79
    elseif name === :path
1✔
80
        return getfield(c, :path)::String
×
81
    elseif name === :parent
1✔
82
        return getfield(c, :parent)::Module
×
83
    elseif name === :mod
1✔
84
        return getfield(c, :mod)::String
×
85
    elseif name === :package
1✔
86
        return getfield(c, :package)::String
×
87
    elseif name === :property
1✔
88
        return getfield(c, :property)::Symbol
×
89
    elseif name === :field
1✔
90
        return getfield(c, :field)::Symbol
×
91
    elseif name === :method
×
92
        return getfield(c, :method)::Method
1✔
93
    elseif name === :bslash
×
94
        return getfield(c, :bslash)::String
×
95
    elseif name === :text
×
96
        return getfield(c, :text)::String
×
97
    elseif name === :key
×
98
        return getfield(c, :key)::String
×
99
    elseif name === :kwarg
×
100
        return getfield(c, :kwarg)::String
×
101
    end
102
    return getfield(c, name)
×
103
end
104

105
_completion_text(c::TextCompletion) = c.text
×
106
_completion_text(c::KeywordCompletion) = c.keyword
×
107
_completion_text(c::KeyvalCompletion) = c.keyval
×
108
_completion_text(c::PathCompletion) = c.path
×
109
_completion_text(c::ModuleCompletion) = c.mod
×
110
_completion_text(c::PackageCompletion) = c.package
×
111
_completion_text(c::PropertyCompletion) = sprint(Base.show_sym, c.property)
×
112
_completion_text(c::FieldCompletion) = sprint(Base.show_sym, c.field)
×
113
_completion_text(c::MethodCompletion) = repr(c.method)
1✔
114
_completion_text(c::BslashCompletion) = c.bslash
×
115
_completion_text(c::ShellCompletion) = c.text
×
116
_completion_text(c::DictCompletion) = c.key
×
117
_completion_text(c::KeywordArgumentCompletion) = c.kwarg*'='
×
118

119
completion_text(c) = _completion_text(c)::String
1✔
120

121
const Completions = Tuple{Vector{Completion}, UnitRange{Int}, Bool}
122

123
function completes_global(x, name)
×
124
    return startswith(x, name) && !('#' in x)
×
125
end
126

127
function appendmacro!(syms, macros, needle, endchar)
×
128
    for macsym in macros
×
129
        s = String(macsym)
×
130
        if endswith(s, needle)
×
131
            from = nextind(s, firstindex(s))
×
132
            to = prevind(s, sizeof(s)-sizeof(needle)+1)
×
133
            push!(syms, s[from:to]*endchar)
×
134
        end
135
    end
×
136
end
137

138
function filtered_mod_names(ffunc::Function, mod::Module, name::AbstractString, all::Bool = false, imported::Bool = false)
×
139
    ssyms = names(mod, all = all, imported = imported)
×
140
    all || filter!(Base.Fix1(Base.isexported, mod), ssyms)
×
141
    filter!(ffunc, ssyms)
×
142
    macros = filter(x -> startswith(String(x), "@" * name), ssyms)
×
143
    syms = String[sprint((io,s)->Base.show_sym(io, s; allow_macroname=true), s) for s in ssyms if completes_global(String(s), name)]
×
144
    appendmacro!(syms, macros, "_str", "\"")
×
145
    appendmacro!(syms, macros, "_cmd", "`")
×
146
    return [ModuleCompletion(mod, sym) for sym in syms]
×
147
end
148

149
# REPL Symbol Completions
150
function complete_symbol(@nospecialize(ex), name::String, @nospecialize(ffunc), context_module::Module=Main)
×
151
    mod = context_module
×
152

153
    lookup_module = true
×
154
    t = Union{}
×
155
    val = nothing
×
156
    if ex !== nothing
×
157
        res = repl_eval_ex(ex, context_module)
×
158
        res === nothing && return Completion[]
×
159
        if res isa Const
×
160
            val = res.val
×
161
            if isa(val, Module)
×
162
                mod = val
×
163
                lookup_module = true
×
164
            else
165
                lookup_module = false
×
166
                t = typeof(val)
×
167
            end
168
        else
169
            lookup_module = false
×
170
            t = CC.widenconst(res)
×
171
        end
172
    end
173

174
    suggestions = Completion[]
×
175
    if lookup_module
×
176
        # We will exclude the results that the user does not want, as well
177
        # as excluding Main.Main.Main, etc., because that's most likely not what
178
        # the user wants
179
        p = let mod=mod, modname=nameof(mod)
×
180
            (s::Symbol) -> !Base.isdeprecated(mod, s) && s != modname && ffunc(mod, s)::Bool && !(mod === Main && s === :MainInclude)
×
181
        end
182
        # Looking for a binding in a module
183
        if mod == context_module
×
184
            # Also look in modules we got through `using`
185
            mods = ccall(:jl_module_usings, Any, (Any,), context_module)::Vector
×
186
            for m in mods
×
187
                append!(suggestions, filtered_mod_names(p, m::Module, name))
×
188
            end
×
189
            append!(suggestions, filtered_mod_names(p, mod, name, true, true))
×
190
        else
191
            append!(suggestions, filtered_mod_names(p, mod, name, true, false))
×
192
        end
193
    elseif val !== nothing # looking for a property of an instance
×
194
        try
×
195
            for property in propertynames(val, false)
×
196
                # TODO: support integer arguments (#36872)
197
                if property isa Symbol && startswith(string(property), name)
×
198
                    push!(suggestions, PropertyCompletion(val, property))
×
199
                end
200
            end
×
201
        catch
×
202
        end
203
    elseif field_completion_eligible(t)
×
204
        # Looking for a member of a type
205
        add_field_completions!(suggestions, name, t)
×
206
    end
207
    return suggestions
×
208
end
209

210
function add_field_completions!(suggestions::Vector{Completion}, name::String, @nospecialize(t))
×
211
    if isa(t, Union)
×
212
        add_field_completions!(suggestions, name, t.a)
×
213
        add_field_completions!(suggestions, name, t.b)
×
214
    else
215
        @assert isconcretetype(t)
×
216
        fields = fieldnames(t)
×
217
        for field in fields
×
218
            isa(field, Symbol) || continue # Tuple type has ::Int field name
×
219
            s = string(field)
×
220
            if startswith(s, name)
×
221
                push!(suggestions, FieldCompletion(t, field))
×
222
            end
223
        end
×
224
    end
225
end
226

227
const GENERIC_PROPERTYNAMES_METHOD = which(propertynames, (Any,))
228

229
function field_completion_eligible(@nospecialize t)
×
230
    if isa(t, Union)
×
231
        return field_completion_eligible(t.a) && field_completion_eligible(t.b)
×
232
    end
233
    isconcretetype(t) || return false
×
234
    # field completion is correct only when `getproperty` fallbacks to `getfield`
235
    match = Base._which(Tuple{typeof(propertynames),t}; raise=false)
×
236
    match === nothing && return false
×
237
    return match.method === GENERIC_PROPERTYNAMES_METHOD
×
238
end
239

240
function complete_from_list(T::Type, list::Vector{String}, s::Union{String,SubString{String}})
×
241
    r = searchsorted(list, s)
×
242
    i = first(r)
×
243
    n = length(list)
×
244
    while i <= n && startswith(list[i],s)
×
245
        r = first(r):i
×
246
        i += 1
×
247
    end
×
248
    Completion[T(kw) for kw in list[r]]
×
249
end
250

251
const sorted_keywords = [
252
    "abstract type", "baremodule", "begin", "break", "catch", "ccall",
253
    "const", "continue", "do", "else", "elseif", "end", "export",
254
    "finally", "for", "function", "global", "if", "import",
255
    "let", "local", "macro", "module", "mutable struct",
256
    "primitive type", "quote", "return", "struct",
257
    "try", "using", "while"]
258

259
complete_keyword(s::Union{String,SubString{String}}) = complete_from_list(KeywordCompletion, sorted_keywords, s)
×
260

261
const sorted_keyvals = ["false", "true"]
262

263
complete_keyval(s::Union{String,SubString{String}}) = complete_from_list(KeyvalCompletion, sorted_keyvals, s)
×
264

265
function do_raw_escape(s)
×
266
    # escape_raw_string with delim='`' and ignoring the rule for the ending \
267
    return replace(s, r"(\\+)`" => s"\1\\`")
×
268
end
269
function do_shell_escape(s)
×
270
    return Base.shell_escape_posixly(s)
×
271
end
272
function do_string_escape(s)
×
273
    return escape_string(s, ('\"','$'))
×
274
end
275

276
const PATH_cache_lock = Base.ReentrantLock()
277
const PATH_cache = Set{String}()
278
PATH_cache_task::Union{Task,Nothing} = nothing # used for sync in tests
279
next_cache_update::Float64 = 0.0
280
function maybe_spawn_cache_PATH()
×
281
    global PATH_cache_task, next_cache_update
×
282
    @lock PATH_cache_lock begin
×
283
        PATH_cache_task isa Task && !istaskdone(PATH_cache_task) && return
×
284
        time() < next_cache_update && return
×
285
        PATH_cache_task = Threads.@spawn REPLCompletions.cache_PATH()
×
286
        Base.errormonitor(PATH_cache_task)
×
287
    end
288
end
289

290
# caches all reachable files in PATH dirs
291
function cache_PATH()
×
292
    path = get(ENV, "PATH", nothing)
×
293
    path isa String || return
×
294

295
    global next_cache_update
×
296

297
    # Calling empty! on PATH_cache would be annoying for async typing hints as completions would temporarily disappear.
298
    # So keep track of what's added this time and at the end remove any that didn't appear this time from the global cache.
299
    this_PATH_cache = Set{String}()
×
300

301
    @debug "caching PATH files" PATH=path
×
302
    pathdirs = split(path, @static Sys.iswindows() ? ";" : ":")
×
303

304
    next_yield_time = time() + 0.01
×
305

306
    t = @elapsed for pathdir in pathdirs
×
307
        actualpath = try
×
308
            realpath(pathdir)
×
309
        catch ex
310
            ex isa Base.IOError || rethrow()
×
311
            # Bash doesn't expect every folder in PATH to exist, so neither shall we
312
            continue
×
313
        end
314

315
        if actualpath != pathdir && in(actualpath, pathdirs)
×
316
            # Remove paths which (after resolving links) are in the env path twice.
317
            # Many distros eg. point /bin to /usr/bin but have both in the env path.
318
            continue
×
319
        end
320

321
        path_entries = try
×
322
            _readdirx(pathdir)
×
323
        catch e
324
            # Bash allows dirs in PATH that can't be read, so we should as well.
325
            if isa(e, Base.IOError) || isa(e, Base.ArgumentError)
×
326
                continue
×
327
            else
328
                # We only handle IOError and ArgumentError here
329
                rethrow()
×
330
            end
331
        end
332
        for entry in path_entries
×
333
            # In a perfect world, we would filter on whether the file is executable
334
            # here, or even on whether the current user can execute the file in question.
335
            try
×
336
                if isfile(entry)
×
337
                    @lock PATH_cache_lock push!(PATH_cache, entry.name)
×
338
                    push!(this_PATH_cache, entry.name)
×
339
                end
340
            catch e
341
                # `isfile()` can throw in rare cases such as when probing a
342
                # symlink that points to a file within a directory we do not
343
                # have read access to.
344
                if isa(e, Base.IOError)
×
345
                    continue
×
346
                else
347
                    rethrow()
×
348
                end
349
            end
350
            if time() >= next_yield_time
×
351
                yield() # to avoid blocking typing when -t1
×
352
                next_yield_time = time() + 0.01
×
353
            end
354
        end
×
355
    end
×
356

357
    @lock PATH_cache_lock begin
×
358
        intersect!(PATH_cache, this_PATH_cache) # remove entries from PATH_cache that weren't found this time
×
359
        next_cache_update = time() + 10 # earliest next update can run is 10s after
×
360
    end
361

362
    @debug "caching PATH files took $t seconds" length(pathdirs) length(PATH_cache)
×
363
    return PATH_cache
×
364
end
365

366
function complete_path(path::AbstractString;
×
367
                       use_envpath=false,
368
                       shell_escape=false,
369
                       raw_escape=false,
370
                       string_escape=false)
371
    @assert !(shell_escape && string_escape)
×
372
    if Base.Sys.isunix() && occursin(r"^~(?:/|$)", path)
×
373
        # if the path is just "~", don't consider the expanded username as a prefix
374
        if path == "~"
×
375
            dir, prefix = homedir(), ""
×
376
        else
377
            dir, prefix = splitdir(homedir() * path[2:end])
×
378
        end
379
    else
380
        dir, prefix = splitdir(path)
×
381
    end
382
    entries = try
×
383
        if isempty(dir)
×
384
            _readdirx()
×
385
        elseif isdir(dir)
×
386
            _readdirx(dir)
×
387
        else
388
            return Completion[], dir, false
×
389
        end
390
    catch ex
391
        ex isa Base.IOError || rethrow()
×
392
        return Completion[], dir, false
×
393
    end
394

395
    matches = Set{String}()
×
396
    for entry in entries
×
397
        if startswith(entry.name, prefix)
×
398
            is_dir = try isdir(entry) catch ex; ex isa Base.IOError ? false : rethrow() end
×
399
            push!(matches, is_dir ? entry.name * "/" : entry.name)
×
400
        end
401
    end
×
402

403
    if use_envpath && isempty(dir)
×
404
        # Look for files in PATH as well. These are cached in `cache_PATH` in an async task to not block typing.
405
        # If we cannot get lock because its still caching just pass over this so that typing isn't laggy.
406
        maybe_spawn_cache_PATH() # only spawns if enough time has passed and the previous caching task has completed
×
407
        @lock PATH_cache_lock begin
×
408
            for file in PATH_cache
×
409
                startswith(file, prefix) && push!(matches, file)
×
410
            end
×
411
        end
412
    end
413

414
    matches = ((shell_escape ? do_shell_escape(s) : string_escape ? do_string_escape(s) : s) for s in matches)
×
415
    matches = ((raw_escape ? do_raw_escape(s) : s) for s in matches)
×
416
    matches = Completion[PathCompletion(s) for s in matches]
×
417
    return matches, dir, !isempty(matches)
×
418
end
419

420
function complete_path(path::AbstractString,
×
421
                       pos::Int;
422
                       use_envpath=false,
423
                       shell_escape=false,
424
                       string_escape=false)
425
    ## TODO: enable this depwarn once Pkg is fixed
426
    #Base.depwarn("complete_path with pos argument is deprecated because the return value [2] is incorrect to use", :complete_path)
427
    paths, dir, success = complete_path(path; use_envpath, shell_escape, string_escape)
×
428
    if Base.Sys.isunix() && occursin(r"^~(?:/|$)", path)
×
429
        # if the path is just "~", don't consider the expanded username as a prefix
430
        if path == "~"
×
431
            dir, prefix = homedir(), ""
×
432
        else
433
            dir, prefix = splitdir(homedir() * path[2:end])
×
434
        end
435
    else
436
        dir, prefix = splitdir(path)
×
437
    end
438
    startpos = pos - lastindex(prefix) + 1
×
439
    Sys.iswindows() && map!(paths, paths) do c::PathCompletion
×
440
        # emulation for unnecessarily complicated return value, since / is a
441
        # perfectly acceptable path character which does not require quoting
442
        # but is required by Pkg's awkward parser handling
443
        return endswith(c.path, "/") ? PathCompletion(chop(c.path) * "\\\\") : c
×
444
    end
445
    return paths, startpos:pos, success
×
446
end
447

448
function complete_expanduser(path::AbstractString, r)
×
449
    expanded =
×
450
        try expanduser(path)
×
451
        catch e
452
            e isa ArgumentError || rethrow()
×
453
            path
×
454
        end
455
    return Completion[PathCompletion(expanded)], r, path != expanded
×
456
end
457

458
# Returns a range that includes the method name in front of the first non
459
# closed start brace from the end of the string.
460
function find_start_brace(s::AbstractString; c_start='(', c_end=')')
×
461
    r = reverse(s)
×
462
    i = firstindex(r)
×
463
    braces = in_comment = 0
×
464
    in_single_quotes = in_double_quotes = in_back_ticks = false
×
465
    while i <= ncodeunits(r)
×
466
        c, i = iterate(r, i)
×
467
        if c == '#' && i <= ncodeunits(r) && iterate(r, i)[1] == '='
×
468
            c, i = iterate(r, i) # consume '='
×
469
            new_comments = 1
×
470
            # handle #=#=#=#, by counting =# pairs
471
            while i <= ncodeunits(r) && iterate(r, i)[1] == '#'
×
472
                c, i = iterate(r, i) # consume '#'
×
473
                iterate(r, i)[1] == '=' || break
×
474
                c, i = iterate(r, i) # consume '='
×
475
                new_comments += 1
×
476
            end
×
477
            if c == '='
×
478
                in_comment += new_comments
×
479
            else
480
                in_comment -= new_comments
×
481
            end
482
        elseif !in_single_quotes && !in_double_quotes && !in_back_ticks && in_comment == 0
×
483
            if c == c_start
×
484
                braces += 1
×
485
            elseif c == c_end
×
486
                braces -= 1
×
487
            elseif c == '\''
×
488
                in_single_quotes = true
×
489
            elseif c == '"'
×
490
                in_double_quotes = true
×
491
            elseif c == '`'
×
492
                in_back_ticks = true
×
493
            end
494
        else
495
            if in_single_quotes &&
×
496
                c == '\'' && i <= ncodeunits(r) && iterate(r, i)[1] != '\\'
497
                in_single_quotes = false
×
498
            elseif in_double_quotes &&
×
499
                c == '"' && i <= ncodeunits(r) && iterate(r, i)[1] != '\\'
500
                in_double_quotes = false
×
501
            elseif in_back_ticks &&
×
502
                c == '`' && i <= ncodeunits(r) && iterate(r, i)[1] != '\\'
503
                in_back_ticks = false
×
504
            elseif in_comment > 0 &&
×
505
                c == '=' && i <= ncodeunits(r) && iterate(r, i)[1] == '#'
506
                # handle =#=#=#=, by counting #= pairs
507
                c, i = iterate(r, i) # consume '#'
×
508
                old_comments = 1
×
509
                while i <= ncodeunits(r) && iterate(r, i)[1] == '='
×
510
                    c, i = iterate(r, i) # consume '='
×
511
                    iterate(r, i)[1] == '#' || break
×
512
                    c, i = iterate(r, i) # consume '#'
×
513
                    old_comments += 1
×
514
                end
×
515
                if c == '#'
×
516
                    in_comment -= old_comments
×
517
                else
518
                    in_comment += old_comments
×
519
                end
520
            end
521
        end
522
        braces == 1 && break
×
523
    end
×
524
    braces != 1 && return 0:-1, -1
×
525
    method_name_end = reverseind(s, i)
×
526
    startind = nextind(s, something(findprev(in(non_identifier_chars), s, method_name_end), 0))::Int
×
527
    return (startind:lastindex(s), method_name_end)
×
528
end
529

530
struct REPLCacheToken end
531

532
struct REPLInterpreter <: CC.AbstractInterpreter
533
    limit_aggressive_inference::Bool
534
    world::UInt
535
    inf_params::CC.InferenceParams
536
    opt_params::CC.OptimizationParams
537
    inf_cache::Vector{CC.InferenceResult}
538
    function REPLInterpreter(limit_aggressive_inference::Bool=false;
1✔
539
                             world::UInt = Base.get_world_counter(),
540
                             inf_params::CC.InferenceParams = CC.InferenceParams(;
541
                                 aggressive_constant_propagation=true,
542
                                 unoptimize_throw_blocks=false),
543
                             opt_params::CC.OptimizationParams = CC.OptimizationParams(),
544
                             inf_cache::Vector{CC.InferenceResult} = CC.InferenceResult[])
545
        return new(limit_aggressive_inference, world, inf_params, opt_params, inf_cache)
1✔
546
    end
547
end
548
CC.InferenceParams(interp::REPLInterpreter) = interp.inf_params
1✔
549
CC.OptimizationParams(interp::REPLInterpreter) = interp.opt_params
×
550
CC.get_inference_world(interp::REPLInterpreter) = interp.world
1✔
551
CC.get_inference_cache(interp::REPLInterpreter) = interp.inf_cache
×
552
CC.cache_owner(::REPLInterpreter) = REPLCacheToken()
×
553

554
# REPLInterpreter is only used for type analysis, so it should disable optimization entirely
555
CC.may_optimize(::REPLInterpreter) = false
×
556

557
# REPLInterpreter analyzes a top-level frame, so better to not bail out from it
558
CC.bail_out_toplevel_call(::REPLInterpreter, ::CC.InferenceLoopState, ::CC.InferenceState) = false
×
559

560
# `REPLInterpreter` aggressively resolves global bindings to enable reasonable completions
561
# for lines like `Mod.a.|` (where `|` is the cursor position).
562
# Aggressive binding resolution poses challenges for the inference cache validation
563
# (until https://github.com/JuliaLang/julia/issues/40399 is implemented).
564
# To avoid the cache validation issues, `REPLInterpreter` only allows aggressive binding
565
# resolution for top-level frame representing REPL input code and for child uncached frames
566
# that are constant propagated from the top-level frame ("repl-frame"s). This works, even if
567
# those global bindings are not constant and may be mutated in the future, since:
568
# a.) "repl-frame"s are never cached, and
569
# b.) mutable values are never observed by any cached frames.
570
#
571
# `REPLInterpreter` also aggressively concrete evaluate `:inconsistent` calls within
572
# "repl-frame" to provide reasonable completions for lines like `Ref(Some(42))[].|`.
573
# Aggressive concrete evaluation allows us to get accurate type information about complex
574
# expressions that otherwise can not be constant folded, in a safe way, i.e. it still
575
# doesn't evaluate effectful expressions like `pop!(xs)`.
576
# Similarly to the aggressive binding resolution, aggressive concrete evaluation doesn't
577
# present any cache validation issues because "repl-frame" is never cached.
578

579
# `REPLInterpreter` is specifically used by `repl_eval_ex`, where all top-level frames are
580
# `repl_frame` always. However, this assumption wouldn't stand if `REPLInterpreter` were to
581
# be employed, for instance, by `typeinf_ext_toplevel`.
582
is_repl_frame(sv::CC.InferenceState) = sv.linfo.def isa Module && sv.cache_mode === CC.CACHE_MODE_NULL
×
583

584
function is_call_graph_uncached(sv::CC.InferenceState)
×
585
    CC.is_cached(sv) && return false
1✔
586
    parent = sv.parent
1✔
587
    parent === nothing && return true
1✔
588
    return is_call_graph_uncached(parent::CC.InferenceState)
×
589
end
590

591
# aggressive global binding resolution within `repl_frame`
592
function CC.abstract_eval_globalref(interp::REPLInterpreter, g::GlobalRef,
593
                                    sv::CC.InferenceState)
594
    if (interp.limit_aggressive_inference ? is_repl_frame(sv) : is_call_graph_uncached(sv))
2✔
595
        if CC.isdefined_globalref(g)
1✔
596
            return CC.RTEffects(Const(ccall(:jl_get_globalref_value, Any, (Any,), g)), Union{}, CC.EFFECTS_TOTAL)
1✔
597
        end
598
        return CC.RTEffects(Union{}, UndefVarError, CC.EFFECTS_THROWS)
×
599
    end
600
    return @invoke CC.abstract_eval_globalref(interp::CC.AbstractInterpreter, g::GlobalRef,
×
601
                                              sv::CC.InferenceState)
602
end
603

604
function is_repl_frame_getproperty(sv::CC.InferenceState)
×
605
    def = sv.linfo.def
×
606
    def isa Method || return false
×
607
    def.name === :getproperty || return false
×
608
    CC.is_cached(sv) && return false
×
609
    return is_repl_frame(sv.parent)
×
610
end
611

612
# aggressive global binding resolution for `getproperty(::Module, ::Symbol)` calls within `repl_frame`
613
function CC.builtin_tfunction(interp::REPLInterpreter, @nospecialize(f),
×
614
                              argtypes::Vector{Any}, sv::CC.InferenceState)
615
    if f === Core.getglobal && (interp.limit_aggressive_inference ? is_repl_frame_getproperty(sv) : is_call_graph_uncached(sv))
×
616
        if length(argtypes) == 2
×
617
            a1, a2 = argtypes
×
618
            if isa(a1, Const) && isa(a2, Const)
×
619
                a1val, a2val = a1.val, a2.val
×
620
                if isa(a1val, Module) && isa(a2val, Symbol)
×
621
                    g = GlobalRef(a1val, a2val)
×
622
                    if CC.isdefined_globalref(g)
×
623
                        return Const(ccall(:jl_get_globalref_value, Any, (Any,), g))
×
624
                    end
625
                    return Union{}
×
626
                end
627
            end
628
        end
629
    end
630
    return @invoke CC.builtin_tfunction(interp::CC.AbstractInterpreter, f::Any,
×
631
                                        argtypes::Vector{Any}, sv::CC.InferenceState)
632
end
633

634
# aggressive concrete evaluation for `:inconsistent` frames within `repl_frame`
635
function CC.concrete_eval_eligible(interp::REPLInterpreter, @nospecialize(f),
×
636
                                   result::CC.MethodCallResult, arginfo::CC.ArgInfo,
637
                                   sv::CC.InferenceState)
638
    if (interp.limit_aggressive_inference ? is_repl_frame(sv) : is_call_graph_uncached(sv))
×
639
        neweffects = CC.Effects(result.effects; consistent=CC.ALWAYS_TRUE)
×
640
        result = CC.MethodCallResult(result.rt, result.exct, result.edgecycle, result.edgelimited,
×
641
                                     result.edge, neweffects)
642
    end
643
    ret = @invoke CC.concrete_eval_eligible(interp::CC.AbstractInterpreter, f::Any,
×
644
                                            result::CC.MethodCallResult, arginfo::CC.ArgInfo,
645
                                            sv::CC.InferenceState)
646
    if ret === :semi_concrete_eval
×
647
        # while the base eligibility check probably won't permit semi-concrete evaluation
648
        # for `REPLInterpreter` (given it completely turns off optimization),
649
        # this ensures we don't inadvertently enter irinterp
650
        ret = :none
×
651
    end
652
    return ret
×
653
end
654

655
# allow constant propagation for mutable constants
656
function CC.const_prop_argument_heuristic(interp::REPLInterpreter, arginfo::CC.ArgInfo, sv::CC.InferenceState)
657
    if !interp.limit_aggressive_inference
×
658
        any(@nospecialize(a)->isa(a, Const), arginfo.argtypes) && return true # even if mutable
×
659
    end
660
    return @invoke CC.const_prop_argument_heuristic(interp::CC.AbstractInterpreter, arginfo::CC.ArgInfo, sv::CC.InferenceState)
×
661
end
662

663
function resolve_toplevel_symbols!(src::Core.CodeInfo, mod::Module)
664
    @ccall jl_resolve_globals_in_ir(
1✔
665
        #=jl_array_t *stmts=# src.code::Any,
666
        #=jl_module_t *m=# mod::Any,
667
        #=jl_svec_t *sparam_vals=# Core.svec()::Any,
668
        #=int binding_effects=# 0::Int)::Cvoid
669
    return src
×
670
end
671

672
# lower `ex` and run type inference on the resulting top-level expression
673
function repl_eval_ex(@nospecialize(ex), context_module::Module; limit_aggressive_inference::Bool=false)
2✔
674
    if (isexpr(ex, :toplevel) || isexpr(ex, :tuple)) && !isempty(ex.args)
2✔
675
        # get the inference result for the last expression
676
        ex = ex.args[end]
×
677
    end
678
    lwr = try
1✔
679
        Meta.lower(context_module, ex)
1✔
680
    catch # macro expansion failed, etc.
681
        return nothing
×
682
    end
683
    if lwr isa Symbol
1✔
684
        return isdefined(context_module, lwr) ? Const(getfield(context_module, lwr)) : nothing
×
685
    end
686
    lwr isa Expr || return Const(lwr) # `ex` is literal
1✔
687
    isexpr(lwr, :thunk) || return nothing # lowered to `Expr(:error, ...)` or similar
1✔
688
    src = lwr.args[1]::Core.CodeInfo
1✔
689

690
    # construct top-level `MethodInstance`
691
    mi = ccall(:jl_new_method_instance_uninit, Ref{Core.MethodInstance}, ());
1✔
692
    mi.specTypes = Tuple{}
1✔
693

694
    mi.def = context_module
1✔
695
    resolve_toplevel_symbols!(src, context_module)
1✔
696
    @atomic mi.uninferred = src
1✔
697

698
    interp = REPLInterpreter(limit_aggressive_inference)
1✔
699
    result = CC.InferenceResult(mi)
1✔
700
    frame = CC.InferenceState(result, src, #=cache=#:no, interp)
1✔
701

702
    # NOTE Use the fixed world here to make `REPLInterpreter` robust against
703
    #      potential invalidations of `Core.Compiler` methods.
704
    Base.invoke_in_world(COMPLETION_WORLD[], CC.typeinf, interp, frame)
1✔
705

706
    result = frame.result.result
1✔
707
    result === Union{} && return nothing # for whatever reason, callers expect this as the Bottom and/or Top type instead
1✔
708
    return result
1✔
709
end
710

711
# `COMPLETION_WORLD[]` will be initialized within `__init__`
712
# (to allow us to potentially remove REPL from the sysimage in the future).
713
# Note that inference from the `code_typed` call below will use the current world age
714
# rather than `typemax(UInt)`, since `Base.invoke_in_world` uses the current world age
715
# when the given world age is higher than the current one.
716
const COMPLETION_WORLD = Ref{UInt}(typemax(UInt))
717

718
# Generate code cache for `REPLInterpreter` now:
719
# This code cache will be available at the world of `COMPLETION_WORLD`,
720
# assuming no invalidation will happen before initializing REPL.
721
# Once REPL is loaded, `REPLInterpreter` will be resilient against future invalidations.
722
code_typed(CC.typeinf, (REPLInterpreter, CC.InferenceState))
723

724
# Method completion on function call expression that look like :(max(1))
725
MAX_METHOD_COMPLETIONS::Int = 40
726
function _complete_methods(ex_org::Expr, context_module::Module, shift::Bool)
1✔
727
    funct = repl_eval_ex(ex_org.args[1], context_module)
1✔
728
    funct === nothing && return 2, nothing, [], Set{Symbol}()
1✔
729
    funct = CC.widenconst(funct)
1✔
730
    args_ex, kwargs_ex, kwargs_flag = complete_methods_args(ex_org, context_module, true, true)
1✔
731
    return kwargs_flag, funct, args_ex, kwargs_ex
1✔
732
end
733

734
function complete_methods(ex_org::Expr, context_module::Module=Main, shift::Bool=false)
1✔
735
    kwargs_flag, funct, args_ex, kwargs_ex = _complete_methods(ex_org, context_module, shift)::Tuple{Int, Any, Vector{Any}, Set{Symbol}}
2✔
736
    out = Completion[]
1✔
737
    kwargs_flag == 2 && return out # one of the kwargs is invalid
1✔
738
    kwargs_flag == 0 && push!(args_ex, Vararg{Any}) # allow more arguments if there is no semicolon
1✔
739
    complete_methods!(out, funct, args_ex, kwargs_ex, shift ? -2 : MAX_METHOD_COMPLETIONS, kwargs_flag == 1)
1✔
740
    return out
1✔
741
end
742

743
MAX_ANY_METHOD_COMPLETIONS::Int = 10
744
function recursive_explore_names!(seen::IdSet, callee_module::Module, initial_module::Module, exploredmodules::IdSet{Module}=IdSet{Module}())
×
745
    push!(exploredmodules, callee_module)
×
746
    for name in names(callee_module; all=true, imported=true)
×
747
        if !Base.isdeprecated(callee_module, name) && !startswith(string(name), '#') && isdefined(initial_module, name)
×
748
            func = getfield(callee_module, name)
×
749
            if !isa(func, Module)
×
750
                funct = Core.Typeof(func)
×
751
                push!(seen, funct)
×
752
            elseif isa(func, Module) && func ∉ exploredmodules
×
753
                recursive_explore_names!(seen, func, initial_module, exploredmodules)
×
754
            end
755
        end
756
    end
×
757
end
758
function recursive_explore_names(callee_module::Module, initial_module::Module)
×
759
    seen = IdSet{Any}()
×
760
    recursive_explore_names!(seen, callee_module, initial_module)
×
761
    seen
×
762
end
763

764
function complete_any_methods(ex_org::Expr, callee_module::Module, context_module::Module, moreargs::Bool, shift::Bool)
×
765
    out = Completion[]
×
766
    args_ex, kwargs_ex, kwargs_flag = try
×
767
        # this may throw, since we set default_any to false
768
        complete_methods_args(ex_org, context_module, false, false)
×
769
    catch ex
770
        ex isa ArgumentError || rethrow()
×
771
        return out
×
772
    end
773
    kwargs_flag == 2 && return out # one of the kwargs is invalid
×
774

775
    # moreargs determines whether to accept more args, independently of the presence of a
776
    # semicolon for the ".?(" syntax
777
    moreargs && push!(args_ex, Vararg{Any})
×
778

779
    for seen_name in recursive_explore_names(callee_module, callee_module)
×
780
        complete_methods!(out, seen_name, args_ex, kwargs_ex, MAX_ANY_METHOD_COMPLETIONS, false)
×
781
    end
×
782

783
    if !shift
×
784
        # Filter out methods where all arguments are `Any`
785
        filter!(out) do c
×
786
            isa(c, TextCompletion) && return false
×
787
            isa(c, MethodCompletion) || return true
×
788
            sig = Base.unwrap_unionall(c.method.sig)::DataType
×
789
            return !all(@nospecialize(T) -> T === Any || T === Vararg{Any}, sig.parameters[2:end])
×
790
        end
791
    end
792

793
    return out
×
794
end
795

796
function detect_invalid_kwarg!(kwargs_ex::Vector{Symbol}, @nospecialize(x), kwargs_flag::Int, possible_splat::Bool)
797
    n = isexpr(x, :kw) ? x.args[1] : x
×
798
    if n isa Symbol
×
799
        push!(kwargs_ex, n)
×
800
        return kwargs_flag
×
801
    end
802
    possible_splat && isexpr(x, :...) && return kwargs_flag
×
803
    return 2 # The kwarg is invalid
×
804
end
805

806
function detect_args_kwargs(funargs::Vector{Any}, context_module::Module, default_any::Bool, broadcasting::Bool)
1✔
807
    args_ex = Any[]
1✔
808
    kwargs_ex = Symbol[]
1✔
809
    kwargs_flag = 0
×
810
    # kwargs_flag is:
811
    # * 0 if there is no semicolon and no invalid kwarg
812
    # * 1 if there is a semicolon and no invalid kwarg
813
    # * 2 if there are two semicolons or more, or if some kwarg is invalid, which
814
    #        means that it is not of the form "bar=foo", "bar" or "bar..."
815
    for i in (1+!broadcasting):length(funargs)
2✔
816
        ex = funargs[i]
×
817
        if isexpr(ex, :parameters)
×
818
            kwargs_flag = ifelse(kwargs_flag == 0, 1, 2) # there should be at most one :parameters
×
819
            for x in ex.args
×
820
                kwargs_flag = detect_invalid_kwarg!(kwargs_ex, x, kwargs_flag, true)
×
821
            end
×
822
        elseif isexpr(ex, :kw)
×
823
            kwargs_flag = detect_invalid_kwarg!(kwargs_ex, ex, kwargs_flag, false)
×
824
        else
825
            if broadcasting
×
826
                # handle broadcasting, but only handle number of arguments instead of
827
                # argument types
828
                push!(args_ex, Any)
×
829
            else
830
                argt = repl_eval_ex(ex, context_module)
×
831
                if argt !== nothing
×
832
                    push!(args_ex, CC.widenconst(argt))
×
833
                elseif default_any
×
834
                    push!(args_ex, Any)
×
835
                else
836
                    throw(ArgumentError("argument not found"))
×
837
                end
838
            end
839
        end
840
    end
×
841
    return args_ex, Set{Symbol}(kwargs_ex), kwargs_flag
1✔
842
end
843

844
is_broadcasting_expr(ex::Expr) = ex.head === :. && isexpr(ex.args[2], :tuple)
1✔
845

846
function complete_methods_args(ex::Expr, context_module::Module, default_any::Bool, allow_broadcasting::Bool)
847
    if allow_broadcasting && is_broadcasting_expr(ex)
1✔
848
        return detect_args_kwargs((ex.args[2]::Expr).args, context_module, default_any, true)
×
849
    end
850
    return detect_args_kwargs(ex.args, context_module, default_any, false)
1✔
851
end
852

853
function complete_methods!(out::Vector{Completion}, @nospecialize(funct), args_ex::Vector{Any}, kwargs_ex::Set{Symbol}, max_method_completions::Int, exact_nargs::Bool)
1✔
854
    # Input types and number of arguments
855
    t_in = Tuple{funct, args_ex...}
1✔
856
    m = Base._methods_by_ftype(t_in, nothing, max_method_completions, Base.get_world_counter(),
1✔
857
        #=ambig=# true, Ref(typemin(UInt)), Ref(typemax(UInt)), Ptr{Int32}(C_NULL))
858
    if !isa(m, Vector)
1✔
859
        push!(out, TextCompletion(sprint(Base.show_signature_function, funct) * "( too many methods, use SHIFT-TAB to show )"))
×
860
        return
×
861
    end
862
    for match in m
1✔
863
        # TODO: if kwargs_ex, filter out methods without kwargs?
864
        push!(out, MethodCompletion(match.spec_types, match.method))
1✔
865
    end
1✔
866
    # TODO: filter out methods with wrong number of arguments if `exact_nargs` is set
867
end
868

869
include("latex_symbols.jl")
870
include("emoji_symbols.jl")
871

872
const non_identifier_chars = [" \t\n\r\"\\'`\$><=:;|&{}()[],+-*/?%^~"...]
873
const whitespace_chars = [" \t\n\r"...]
874
# "\"'`"... is added to whitespace_chars as non of the bslash_completions
875
# characters contain any of these characters. It prohibits the
876
# bslash_completions function to try and complete on escaped characters in strings
877
const bslash_separators = [whitespace_chars..., "\"'`"...]
878

879
const subscripts = Dict(k[3]=>v[1] for (k,v) in latex_symbols if startswith(k, "\\_") && length(k)==3)
880
const subscript_regex = Regex("^\\\\_[" * join(isdigit(k) || isletter(k) ? "$k" : "\\$k" for k in keys(subscripts)) * "]+\\z")
881
const superscripts = Dict(k[3]=>v[1] for (k,v) in latex_symbols if startswith(k, "\\^") && length(k)==3)
882
const superscript_regex = Regex("^\\\\\\^[" * join(isdigit(k) || isletter(k) ? "$k" : "\\$k" for k in keys(superscripts)) * "]+\\z")
883

884
# Aux function to detect whether we're right after a
885
# using or import keyword
886
function afterusing(string::String, startpos::Int)
×
887
    (isempty(string) || startpos == 0) && return false
×
888
    str = string[1:prevind(string,startpos)]
×
889
    isempty(str) && return false
×
890
    rstr = reverse(str)
×
891
    r = findfirst(r"\s(gnisu|tropmi)\b", rstr)
×
892
    r === nothing && return false
×
893
    fr = reverseind(str, last(r))
×
894
    return occursin(r"^\b(using|import)\s*((\w+[.])*\w+\s*,\s*)*$", str[fr:end])
×
895
end
896

897
function close_path_completion(dir, paths, str, pos)
×
898
    length(paths) == 1 || return false  # Only close if there's a single choice...
×
899
    path = (paths[1]::PathCompletion).path
×
900
    path = unescape_string(replace(path, "\\\$"=>"\$"))
×
901
    path = joinpath(dir, path)
×
902
    # ...except if it's a directory...
903
    try
×
904
        isdir(path)
×
905
    catch e
906
        e isa Base.IOError || rethrow() # `path` cannot be determined to be a file
×
907
    end && return false
908
    # ...and except if there's already a " at the cursor.
909
    return lastindex(str) <= pos || str[nextind(str, pos)] != '"'
×
910
end
911

912
function bslash_completions(string::String, pos::Int)
×
913
    slashpos = something(findprev(isequal('\\'), string, pos), 0)
×
914
    if (something(findprev(in(bslash_separators), string, pos), 0) < slashpos &&
×
915
        !(1 < slashpos && (string[prevind(string, slashpos)]=='\\')))
916
        # latex / emoji symbol substitution
917
        s = string[slashpos:pos]
×
918
        latex = get(latex_symbols, s, "")
×
919
        if !isempty(latex) # complete an exact match
×
920
            return (true, (Completion[BslashCompletion(latex)], slashpos:pos, true))
×
921
        elseif occursin(subscript_regex, s)
×
922
            sub = map(c -> subscripts[c], s[3:end])
×
923
            return (true, (Completion[BslashCompletion(sub)], slashpos:pos, true))
×
924
        elseif occursin(superscript_regex, s)
×
925
            sup = map(c -> superscripts[c], s[3:end])
×
926
            return (true, (Completion[BslashCompletion(sup)], slashpos:pos, true))
×
927
        end
928
        emoji = get(emoji_symbols, s, "")
×
929
        if !isempty(emoji)
×
930
            return (true, (Completion[BslashCompletion(emoji)], slashpos:pos, true))
×
931
        end
932
        # return possible matches; these cannot be mixed with regular
933
        # Julian completions as only latex / emoji symbols contain the leading \
934
        if startswith(s, "\\:") # emoji
×
935
            namelist = Iterators.filter(k -> startswith(k, s), keys(emoji_symbols))
×
936
        else # latex
937
            namelist = Iterators.filter(k -> startswith(k, s), keys(latex_symbols))
×
938
        end
939
        return (true, (Completion[BslashCompletion(name) for name in sort!(collect(namelist))], slashpos:pos, true))
×
940
    end
941
    return (false, (Completion[], 0:-1, false))
×
942
end
943

944
function dict_identifier_key(str::String, tag::Symbol, context_module::Module=Main)
×
945
    if tag === :string
×
946
        str_close = str*"\""
×
947
    elseif tag === :cmd
×
948
        str_close = str*"`"
×
949
    else
950
        str_close = str
×
951
    end
952
    frange, end_of_identifier = find_start_brace(str_close, c_start='[', c_end=']')
×
953
    isempty(frange) && return (nothing, nothing, nothing)
×
954
    objstr = str[1:end_of_identifier]
×
955
    objex = Meta.parse(objstr, raise=false, depwarn=false)
×
956
    objt = repl_eval_ex(objex, context_module)
×
957
    isa(objt, Core.Const) || return (nothing, nothing, nothing)
×
958
    obj = objt.val
×
959
    isa(obj, AbstractDict) || return (nothing, nothing, nothing)
×
960
    length(obj)::Int < 1_000_000 || return (nothing, nothing, nothing)
×
961
    begin_of_key = something(findnext(!isspace, str, nextind(str, end_of_identifier) + 1), # +1 for [
×
962
                             lastindex(str)+1)
963
    return (obj, str[begin_of_key:end], begin_of_key)
×
964
end
965

966
# This needs to be a separate non-inlined function, see #19441
967
@noinline function find_dict_matches(identifier::AbstractDict, partial_key)
×
968
    matches = String[]
×
969
    for key in keys(identifier)
×
970
        rkey = repr(key)
×
971
        startswith(rkey,partial_key) && push!(matches,rkey)
×
972
    end
×
973
    return matches
×
974
end
975

976
# Identify an argument being completed in a method call. If the argument is empty, method
977
# suggestions will be provided instead of argument completions.
978
function identify_possible_method_completion(partial, last_idx)
×
979
    fail = 0:-1, Expr(:nothing), 0:-1, 0
×
980

981
    # First, check that the last punctuation is either ',', ';' or '('
982
    idx_last_punct = something(findprev(x -> ispunct(x) && x != '_' && x != '!', partial, last_idx), 0)::Int
×
983
    idx_last_punct == 0 && return fail
×
984
    last_punct = partial[idx_last_punct]
×
985
    last_punct == ',' || last_punct == ';' || last_punct == '(' || return fail
×
986

987
    # Then, check that `last_punct` is only followed by an identifier or nothing
988
    before_last_word_start = something(findprev(in(non_identifier_chars), partial, last_idx), 0)
×
989
    before_last_word_start == 0 && return fail
×
990
    all(isspace, @view partial[nextind(partial, idx_last_punct):before_last_word_start]) || return fail
×
991

992
    # Check that `last_punct` is either the last '(' or placed after a previous '('
993
    frange, method_name_end = find_start_brace(@view partial[1:idx_last_punct])
×
994
    method_name_end ∈ frange || return fail
×
995

996
    # Strip the preceding ! operators, if any, and close the expression with a ')'
997
    s = replace(partial[frange], r"\G\!+([^=\(]+)" => s"\1"; count=1) * ')'
×
998
    ex = Meta.parse(s, raise=false, depwarn=false)
×
999
    isa(ex, Expr) || return fail
×
1000

1001
    # `wordrange` is the position of the last argument to complete
1002
    wordrange = nextind(partial, before_last_word_start):last_idx
×
1003
    return frange, ex, wordrange, method_name_end
×
1004
end
1005

1006
# Provide completion for keyword arguments in function calls
1007
function complete_keyword_argument(partial, last_idx, context_module)
×
1008
    frange, ex, wordrange, = identify_possible_method_completion(partial, last_idx)
×
1009
    fail = Completion[], 0:-1, frange
×
1010
    ex.head === :call || is_broadcasting_expr(ex) || return fail
×
1011

1012
    kwargs_flag, funct, args_ex, kwargs_ex = _complete_methods(ex, context_module, true)::Tuple{Int, Any, Vector{Any}, Set{Symbol}}
×
1013
    kwargs_flag == 2 && return fail # one of the previous kwargs is invalid
×
1014

1015
    methods = Completion[]
×
1016
    complete_methods!(methods, funct, Any[Vararg{Any}], kwargs_ex, -1, kwargs_flag == 1)
×
1017
    # TODO: use args_ex instead of Any[Vararg{Any}] and only provide kwarg completion for
1018
    # method calls compatible with the current arguments.
1019

1020
    # For each method corresponding to the function call, provide completion suggestions
1021
    # for each keyword that starts like the last word and that is not already used
1022
    # previously in the expression. The corresponding suggestion is "kwname=".
1023
    # If the keyword corresponds to an existing name, also include "kwname" as a suggestion
1024
    # since the syntax "foo(; kwname)" is equivalent to "foo(; kwname=kwname)".
1025
    last_word = partial[wordrange] # the word to complete
×
1026
    kwargs = Set{String}()
×
1027
    for m in methods
×
1028
        m::MethodCompletion
×
1029
        possible_kwargs = Base.kwarg_decl(m.method)
×
1030
        current_kwarg_candidates = String[]
×
1031
        for _kw in possible_kwargs
×
1032
            kw = String(_kw)
×
1033
            if !endswith(kw, "...") && startswith(kw, last_word) && _kw ∉ kwargs_ex
×
1034
                push!(current_kwarg_candidates, kw)
×
1035
            end
1036
        end
×
1037
        union!(kwargs, current_kwarg_candidates)
×
1038
    end
×
1039

1040
    suggestions = Completion[KeywordArgumentCompletion(kwarg) for kwarg in kwargs]
×
1041

1042
    # Only add these if not in kwarg space. i.e. not in `foo(; `
1043
    if kwargs_flag == 0
×
1044
        append!(suggestions, complete_symbol(nothing, last_word, Returns(true), context_module))
×
1045
        append!(suggestions, complete_keyval(last_word))
×
1046
    end
1047

1048
    return sort!(suggestions, by=completion_text), wordrange
×
1049
end
1050

1051
function project_deps_get_completion_candidates(pkgstarts::String, project_file::String)
×
1052
    loading_candidates = String[]
×
1053
    d = Base.parsed_toml(project_file)
×
1054
    pkg = get(d, "name", nothing)::Union{String, Nothing}
×
1055
    if pkg !== nothing && startswith(pkg, pkgstarts)
×
1056
        push!(loading_candidates, pkg)
×
1057
    end
1058
    deps = get(d, "deps", nothing)::Union{Dict{String, Any}, Nothing}
×
1059
    if deps !== nothing
×
1060
        for (pkg, _) in deps
×
1061
            startswith(pkg, pkgstarts) && push!(loading_candidates, pkg)
×
1062
        end
×
1063
    end
1064
    return Completion[PackageCompletion(name) for name in loading_candidates]
×
1065
end
1066

1067
function complete_identifiers!(suggestions::Vector{Completion}, @nospecialize(ffunc),
×
1068
                               context_module::Module, string::String, name::String,
1069
                               pos::Int, dotpos::Int, startpos::Int;
1070
                               comp_keywords=false)
1071
    ex = nothing
×
1072
    if comp_keywords
×
1073
        append!(suggestions, complete_keyword(name))
×
1074
        append!(suggestions, complete_keyval(name))
×
1075
    end
1076
    if dotpos > 1 && string[dotpos] == '.'
×
1077
        s = string[1:prevind(string, dotpos)]
×
1078
        # First see if the whole string up to `pos` is a valid expression. If so, use it.
1079
        ex = Meta.parse(s, raise=false, depwarn=false)
×
1080
        if isexpr(ex, :incomplete)
×
1081
            s = string[startpos:pos]
×
1082
            # Heuristic to find the start of the expression. TODO: This would be better
1083
            # done with a proper error-recovering parser.
1084
            if 0 < startpos <= lastindex(string) && string[startpos] == '.'
×
1085
                i = prevind(string, startpos)
×
1086
                while 0 < i
×
1087
                    c = string[i]
×
1088
                    if c in (')', ']')
×
1089
                        if c == ')'
×
1090
                            c_start = '('
×
1091
                            c_end = ')'
×
1092
                        elseif c == ']'
×
1093
                            c_start = '['
×
1094
                            c_end = ']'
×
1095
                        end
1096
                        frange, end_of_identifier = find_start_brace(string[1:prevind(string, i)], c_start=c_start, c_end=c_end)
×
1097
                        isempty(frange) && break # unbalanced parens
×
1098
                        startpos = first(frange)
×
1099
                        i = prevind(string, startpos)
×
1100
                    elseif c in ('\'', '\"', '\`')
×
1101
                        s = "$c$c"*string[startpos:pos]
×
1102
                        break
×
1103
                    else
1104
                        break
×
1105
                    end
1106
                    s = string[startpos:pos]
×
1107
                end
×
1108
            end
1109
            if something(findlast(in(non_identifier_chars), s), 0) < something(findlast(isequal('.'), s), 0)
×
1110
                lookup_name, name = rsplit(s, ".", limit=2)
×
1111
                name = String(name)
×
1112
                ex = Meta.parse(lookup_name, raise=false, depwarn=false)
×
1113
            end
1114
            isexpr(ex, :incomplete) && (ex = nothing)
×
1115
        elseif isexpr(ex, (:using, :import))
×
1116
            arg1 = ex.args[1]
×
1117
            if isexpr(arg1, :.)
×
1118
                # We come here for cases like:
1119
                # - `string`: "using Mod1.Mod2.M"
1120
                # - `ex`: :(using Mod1.Mod2)
1121
                # - `name`: "M"
1122
                # Now we transform `ex` to `:(Mod1.Mod2)` to allow `complete_symbol` to
1123
                # complete for inner modules whose name starts with `M`.
1124
                # Note that `ffunc` is set to `module_filter` within `completions`
1125
                ex = nothing
×
1126
                firstdot = true
×
1127
                for arg = arg1.args
×
1128
                    if arg === :.
×
1129
                        # override `context_module` if multiple `.` accessors are used
1130
                        if firstdot
×
1131
                            firstdot = false
×
1132
                        else
1133
                            context_module = parentmodule(context_module)
×
1134
                        end
1135
                    elseif arg isa Symbol
×
1136
                        if ex === nothing
×
1137
                            ex = arg
×
1138
                        else
1139
                            ex = Expr(:., ex, QuoteNode(arg))
×
1140
                        end
1141
                    else # invalid expression
1142
                        ex = nothing
×
1143
                        break
×
1144
                    end
1145
                end
×
1146
            end
1147
        elseif isexpr(ex, :call) && length(ex.args) > 1
×
1148
            isinfix = s[end] != ')'
×
1149
            # A complete call expression that does not finish with ')' is an infix call.
1150
            if !isinfix
×
1151
                # Handle infix call argument completion of the form bar + foo(qux).
1152
                frange, end_of_identifier = find_start_brace(@view s[1:prevind(s, end)])
×
1153
                isinfix = Meta.parse(@view(s[frange[1]:end]), raise=false, depwarn=false) == ex.args[end]
×
1154
            end
1155
            if isinfix
×
1156
                ex = ex.args[end]
×
1157
            end
1158
        elseif isexpr(ex, :macrocall) && length(ex.args) > 1
×
1159
            # allow symbol completions within potentially incomplete macrocalls
1160
            if s[end] ≠ '`' && s[end] ≠ ')'
×
1161
                ex = ex.args[end]
×
1162
            end
1163
        end
1164
    end
1165
    append!(suggestions, complete_symbol(ex, name, ffunc, context_module))
×
1166
    return sort!(unique(suggestions), by=completion_text), (dotpos+1):pos, true
×
1167
end
1168

1169
function completions(string::String, pos::Int, context_module::Module=Main, shift::Bool=true)
×
1170
    # First parse everything up to the current position
1171
    partial = string[1:pos]
×
1172
    inc_tag = Base.incomplete_tag(Meta.parse(partial, raise=false, depwarn=false))
×
1173

1174
    # ?(x, y)TAB lists methods you can call with these objects
1175
    # ?(x, y TAB lists methods that take these objects as the first two arguments
1176
    # MyModule.?(x, y)TAB restricts the search to names in MyModule
1177
    rexm = match(r"(\w+\.|)\?\((.*)$", partial)
×
1178
    if rexm !== nothing
×
1179
        # Get the module scope
1180
        if isempty(rexm.captures[1])
×
1181
            callee_module = context_module
×
1182
        else
1183
            modname = Symbol(rexm.captures[1][1:end-1])
×
1184
            if isdefined(context_module, modname)
×
1185
                callee_module = getfield(context_module, modname)
×
1186
                if !isa(callee_module, Module)
×
1187
                    callee_module = context_module
×
1188
                end
1189
            else
1190
                callee_module = context_module
×
1191
            end
1192
        end
1193
        moreargs = !endswith(rexm.captures[2], ')')
×
1194
        callstr = "_(" * rexm.captures[2]
×
1195
        if moreargs
×
1196
            callstr *= ')'
×
1197
        end
1198
        ex_org = Meta.parse(callstr, raise=false, depwarn=false)
×
1199
        if isa(ex_org, Expr)
×
1200
            return complete_any_methods(ex_org, callee_module::Module, context_module, moreargs, shift), (0:length(rexm.captures[1])+1) .+ rexm.offset, false
×
1201
        end
1202
    end
1203

1204
    # if completing a key in a Dict
1205
    identifier, partial_key, loc = dict_identifier_key(partial, inc_tag, context_module)
×
1206
    if identifier !== nothing
×
1207
        matches = find_dict_matches(identifier, partial_key)
×
1208
        length(matches)==1 && (lastindex(string) <= pos || string[nextind(string,pos)] != ']') && (matches[1]*=']')
×
1209
        length(matches)>0 && return Completion[DictCompletion(identifier, match) for match in sort!(matches)], loc::Int:pos, true
×
1210
    end
1211

1212
    ffunc = Returns(true)
×
1213
    suggestions = Completion[]
×
1214

1215
    # Check if this is a var"" string macro that should be completed like
1216
    # an identifier rather than a string.
1217
    # TODO: It would be nice for the parser to give us more information here
1218
    # so that we can lookup the macro by identity rather than pattern matching
1219
    # its invocation.
1220
    varrange = findprev("var\"", string, pos)
×
1221

1222
    if varrange !== nothing
×
1223
        ok, ret = bslash_completions(string, pos)
×
1224
        ok && return ret
×
1225
        startpos = first(varrange) + 4
×
1226
        dotpos = something(findprev(isequal('.'), string, first(varrange)-1), 0)
×
1227
        name = string[startpos:pos]
×
1228
        return complete_identifiers!(Completion[], ffunc, context_module, string, name, pos,
×
1229
                                     dotpos, startpos)
1230
    elseif inc_tag === :cmd
×
1231
        # TODO: should this call shell_completions instead of partially reimplementing it?
1232
        let m = match(r"[\t\n\r\"`><=*?|]| (?!\\)", reverse(partial)) # fuzzy shell_parse in reverse
×
1233
            startpos = nextind(partial, reverseind(partial, m.offset))
×
1234
            r = startpos:pos
×
1235
            scs::String = string[r]
×
1236

1237
            expanded = complete_expanduser(scs, r)
×
1238
            expanded[3] && return expanded  # If user expansion available, return it
×
1239

1240
            path::String = replace(scs, r"(\\+)\g1(\\?)`" => "\1\2`") # fuzzy unescape_raw_string: match an even number of \ before ` and replace with half as many
×
1241
            # This expansion with "\\ "=>' ' replacement and shell_escape=true
1242
            # assumes the path isn't further quoted within the cmd backticks.
1243
            path = replace(path, r"\\ " => " ", r"\$" => "\$") # fuzzy shell_parse (reversed by shell_escape_posixly)
×
1244
            paths, dir, success = complete_path(path, shell_escape=true, raw_escape=true)
×
1245

1246
            if success && !isempty(dir)
×
1247
                let dir = do_raw_escape(do_shell_escape(dir))
×
1248
                    # if escaping of dir matches scs prefix, remove that from the completions
1249
                    # otherwise make it the whole completion
1250
                    if endswith(dir, "/") && startswith(scs, dir)
×
1251
                        r = (startpos + sizeof(dir)):pos
×
1252
                    elseif startswith(scs, dir * "/")
×
1253
                        r = nextind(string, startpos + sizeof(dir)):pos
×
1254
                    else
1255
                        map!(paths, paths) do c::PathCompletion
×
1256
                            return PathCompletion(dir * "/" * c.path)
×
1257
                        end
1258
                    end
1259
                end
1260
            end
1261
            return sort!(paths, by=p->p.path), r, success
×
1262
        end
1263
    elseif inc_tag === :string
×
1264
        # Find first non-escaped quote
1265
        let m = match(r"\"(?!\\)", reverse(partial))
×
1266
            startpos = nextind(partial, reverseind(partial, m.offset))
×
1267
            r = startpos:pos
×
1268
            scs::String = string[r]
×
1269

1270
            expanded = complete_expanduser(scs, r)
×
1271
            expanded[3] && return expanded  # If user expansion available, return it
×
1272

1273
            path = try
×
1274
                unescape_string(replace(scs, "\\\$"=>"\$"))
×
1275
            catch ex
1276
                ex isa ArgumentError || rethrow()
×
1277
                nothing
×
1278
            end
1279
            if !isnothing(path)
×
1280
                paths, dir, success = complete_path(path::String, string_escape=true)
×
1281

1282
                if close_path_completion(dir, paths, path, pos)
×
1283
                    paths[1] = PathCompletion((paths[1]::PathCompletion).path * "\"")
×
1284
                end
1285

1286
                if success && !isempty(dir)
×
1287
                    let dir = do_string_escape(dir)
×
1288
                        # if escaping of dir matches scs prefix, remove that from the completions
1289
                        # otherwise make it the whole completion
1290
                        if endswith(dir, "/") && startswith(scs, dir)
×
1291
                            r = (startpos + sizeof(dir)):pos
×
1292
                        elseif startswith(scs, dir * "/")
×
1293
                            r = nextind(string, startpos + sizeof(dir)):pos
×
1294
                        else
1295
                            map!(paths, paths) do c::PathCompletion
×
1296
                                return PathCompletion(dir * "/" * c.path)
×
1297
                            end
1298
                        end
1299
                    end
1300
                end
1301

1302
                # Fallthrough allowed so that Latex symbols can be completed in strings
1303
                success && return sort!(paths, by=p->p.path), r, success
×
1304
            end
1305
        end
1306
    end
1307

1308
    ok, ret = bslash_completions(string, pos)
×
1309
    ok && return ret
×
1310

1311
    # Make sure that only bslash_completions is working on strings
1312
    inc_tag === :string && return Completion[], 0:-1, false
×
1313
    if inc_tag === :other
×
1314
        frange, ex, wordrange, method_name_end = identify_possible_method_completion(partial, pos)
×
1315
        if last(frange) != -1 && all(isspace, @view partial[wordrange]) # no last argument to complete
×
1316
            if ex.head === :call
×
1317
                return complete_methods(ex, context_module, shift), first(frange):method_name_end, false
×
1318
            elseif is_broadcasting_expr(ex)
×
1319
                return complete_methods(ex, context_module, shift), first(frange):(method_name_end - 1), false
×
1320
            end
1321
        end
1322
    elseif inc_tag === :comment
×
1323
        return Completion[], 0:-1, false
×
1324
    end
1325

1326
    # Check whether we can complete a keyword argument in a function call
1327
    kwarg_completion, wordrange = complete_keyword_argument(partial, pos, context_module)
×
1328
    isempty(wordrange) || return kwarg_completion, wordrange, !isempty(kwarg_completion)
×
1329

1330
    dotpos = something(findprev(isequal('.'), string, pos), 0)
×
1331
    startpos = nextind(string, something(findprev(in(non_identifier_chars), string, pos), 0))
×
1332
    # strip preceding ! operator
1333
    if (m = match(r"\G\!+", partial, startpos)) isa RegexMatch
×
1334
        startpos += length(m.match)
×
1335
    end
1336

1337
    name = string[max(startpos, dotpos+1):pos]
×
1338
    comp_keywords = !isempty(name) && startpos > dotpos
×
1339
    if afterusing(string, startpos)
×
1340
        # We're right after using or import. Let's look only for packages
1341
        # and modules we can reach from here
1342

1343
        # If there's no dot, we're in toplevel, so we should
1344
        # also search for packages
1345
        s = string[startpos:pos]
×
1346
        if dotpos <= startpos
×
1347
            for dir in Base.load_path()
×
1348
                if basename(dir) in Base.project_names && isfile(dir)
×
1349
                    append!(suggestions, project_deps_get_completion_candidates(s, dir))
×
1350
                end
1351
                isdir(dir) || continue
×
1352
                for entry in _readdirx(dir)
×
1353
                    pname = entry.name
×
1354
                    if pname[1] != '.' && pname != "METADATA" &&
×
1355
                        pname != "REQUIRE" && startswith(pname, s)
1356
                        # Valid file paths are
1357
                        #   <Mod>.jl
1358
                        #   <Mod>/src/<Mod>.jl
1359
                        #   <Mod>.jl/src/<Mod>.jl
1360
                        if isfile(entry)
×
1361
                            endswith(pname, ".jl") && push!(suggestions,
×
1362
                                                            PackageCompletion(pname[1:prevind(pname, end-2)]))
1363
                        else
1364
                            mod_name = if endswith(pname, ".jl")
×
1365
                                pname[1:prevind(pname, end-2)]
×
1366
                            else
1367
                                pname
×
1368
                            end
1369
                            if isfile(joinpath(entry, "src",
×
1370
                                               "$mod_name.jl"))
1371
                                push!(suggestions, PackageCompletion(mod_name))
×
1372
                            end
1373
                        end
1374
                    end
1375
                end
×
1376
            end
×
1377
        end
1378
        ffunc = module_filter
×
1379
        comp_keywords = false
×
1380
    end
1381

1382
    startpos == 0 && (pos = -1)
×
1383
    dotpos < startpos && (dotpos = startpos - 1)
×
1384
    return complete_identifiers!(suggestions, ffunc, context_module, string, name, pos,
×
1385
                                 dotpos, startpos;
1386
                                 comp_keywords)
1387
end
1388

1389
module_filter(mod::Module, x::Symbol) =
×
1390
    Base.isbindingresolved(mod, x) && isdefined(mod, x) && isa(getglobal(mod, x), Module)
1391

1392
function shell_completions(string, pos)
×
1393
    # First parse everything up to the current position
1394
    scs = string[1:pos]
×
1395
    args, last_arg_start = try
×
1396
        Base.shell_parse(scs, true)::Tuple{Expr,Int}
×
1397
    catch ex
1398
        ex isa ArgumentError || ex isa ErrorException || rethrow()
×
1399
        return Completion[], 0:-1, false
×
1400
    end
1401
    ex = args.args[end]::Expr
×
1402
    # Now look at the last thing we parsed
1403
    isempty(ex.args) && return Completion[], 0:-1, false
×
1404
    lastarg = ex.args[end]
×
1405
    # As Base.shell_parse throws away trailing spaces (unless they are escaped),
1406
    # we need to special case here.
1407
    # If the last char was a space, but shell_parse ignored it search on "".
1408
    if isexpr(lastarg, :incomplete) || isexpr(lastarg, :error)
×
1409
        partial = string[last_arg_start:pos]
×
1410
        ret, range = completions(partial, lastindex(partial))
×
1411
        range = range .+ (last_arg_start - 1)
×
1412
        return ret, range, true
×
1413
    elseif endswith(scs, ' ') && !endswith(scs, "\\ ")
×
1414
        r = pos+1:pos
×
1415
        paths, dir, success = complete_path("", use_envpath=false, shell_escape=true)
×
1416
        return paths, r, success
×
1417
    elseif all(@nospecialize(arg) -> arg isa AbstractString, ex.args)
×
1418
        # Join these and treat this as a path
1419
        path::String = join(ex.args)
×
1420
        r = last_arg_start:pos
×
1421

1422
        # Also try looking into the env path if the user wants to complete the first argument
1423
        use_envpath = length(args.args) < 2
×
1424

1425
        # TODO: call complete_expanduser here?
1426

1427
        paths, dir, success = complete_path(path, use_envpath=use_envpath, shell_escape=true)
×
1428

1429
        if success && !isempty(dir)
×
1430
            let dir = do_shell_escape(dir)
×
1431
                # if escaping of dir matches scs prefix, remove that from the completions
1432
                # otherwise make it the whole completion
1433
                partial = string[last_arg_start:pos]
×
1434
                if endswith(dir, "/") && startswith(partial, dir)
×
1435
                    r = (last_arg_start + sizeof(dir)):pos
×
1436
                elseif startswith(partial, dir * "/")
×
1437
                    r = nextind(string, last_arg_start + sizeof(dir)):pos
×
1438
                else
1439
                    map!(paths, paths) do c::PathCompletion
×
1440
                        return PathCompletion(dir * "/" * c.path)
×
1441
                    end
1442
                end
1443
            end
1444
        end
1445

1446
        return paths, r, success
×
1447
    end
1448
    return Completion[], 0:-1, false
×
1449
end
1450

1451
function __init__()
3✔
1452
    COMPLETION_WORLD[] = Base.get_world_counter()
3✔
1453
    return nothing
3✔
1454
end
1455

1456
end # module
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