diff --git a/stdlib/REPL/src/REPLCompletions.jl b/stdlib/REPL/src/REPLCompletions.jl index 0319bc6d022e8c..ccd20eb4442e53 100644 --- a/stdlib/REPL/src/REPLCompletions.jl +++ b/stdlib/REPL/src/REPLCompletions.jl @@ -486,6 +486,8 @@ function try_get_type(sym::Expr, fn::Module) return try_get_type(Expr(:call, GlobalRef(Base, :getindex), sym.args...), fn) elseif sym.head === :. && sym.args[2] isa QuoteNode # second check catches broadcasting return try_get_type(Expr(:call, GlobalRef(Core, :getfield), sym.args...), fn) + elseif sym.head === :... + return Vararg{Any}, true end return (Any, false) end @@ -530,9 +532,10 @@ function complete_methods(ex_org::Expr, context_module::Module=Main) funct, found = get_type(ex_org.args[1], context_module)::Tuple{Any,Bool} !found && return out - args_ex, kwargs_ex = complete_methods_args(ex_org.args[2:end], ex_org, context_module, true, true) - push!(args_ex, Vararg{Any}) - complete_methods!(out, funct, args_ex, kwargs_ex, MAX_METHOD_COMPLETIONS::Int) + args_ex, kwargs_ex, kwargs_flag = complete_methods_args(ex_org.args[2:end], ex_org, context_module, true, true) + kwargs_flag == 2 && return out # one of the kwargs is invalid + kwargs_flag == 0 && push!(args_ex, Vararg{Any}) # allow more arguments if there is no semicolon + complete_methods!(out, funct, args_ex, kwargs_ex, MAX_METHOD_COMPLETIONS::Int, kwargs_flag == 1) return out end @@ -540,13 +543,17 @@ end MAX_ANY_METHOD_COMPLETIONS = 10 function complete_any_methods(ex_org::Expr, callee_module::Module, context_module::Module, moreargs::Bool, shift::Bool) out = Completion[] - args_ex, kwargs_ex = try + args_ex, kwargs_ex, kwargs_flag = try # this may throw, since we set default_any to false complete_methods_args(ex_org.args[2:end], ex_org, context_module, false, false) catch ex ex isa ArgumentError || rethrow() return out end + kwargs_flag == 2 && return out # one of the kwargs is invalid + + # moreargs determines whether to accept more args, independently of the presence of a + # semicolon for the ".?(" syntax moreargs && push!(args_ex, Vararg{Any}) seen = Base.IdSet() @@ -557,7 +564,7 @@ function complete_any_methods(ex_org::Expr, callee_module::Module, context_modul funct = Core.Typeof(func) if !in(funct, seen) push!(seen, funct) - complete_methods!(out, funct, args_ex, kwargs_ex, MAX_ANY_METHOD_COMPLETIONS::Int) + complete_methods!(out, funct, args_ex, kwargs_ex, MAX_ANY_METHOD_COMPLETIONS::Int, false) end elseif callee_module === Main && isa(func, Module) callee_module2 = func @@ -568,7 +575,7 @@ function complete_any_methods(ex_org::Expr, callee_module::Module, context_modul funct = Core.Typeof(func) if !in(funct, seen) push!(seen, funct) - complete_methods!(out, funct, args_ex, kwargs_ex, MAX_ANY_METHOD_COMPLETIONS::Int) + complete_methods!(out, funct, args_ex, kwargs_ex, MAX_ANY_METHOD_COMPLETIONS::Int, false) end end end @@ -590,36 +597,80 @@ function complete_any_methods(ex_org::Expr, callee_module::Module, context_modul return out end + +function detect_invalid_kwarg!(kwargs_ex, n, kwargs_flag, allow_splat) + if n isa Symbol + push!(kwargs_ex, n) + return kwargs_flag + end + allow_splat && isexpr(n, :...) && return kwargs_flag + return 2 # The kwarg is invalid +end + function complete_methods_args(funargs::Vector{Any}, ex_org::Expr, context_module::Module, default_any::Bool, allow_broadcasting::Bool) args_ex = Any[] kwargs_ex = Symbol[] + kwargs_flag = 0 + # kwargs_flag is: + # * 0 if there is no semicolon and no invalid kwarg + # * 1 if there is a semicolon and no invalid kwarg + # * 2 if there are two semicolons or if the kwargs in kwargs_ex are invalid, which + # means that they are not of the form "bar=foo", "bar" or "bar..." if allow_broadcasting && ex_org.head === :. && ex_org.args[2] isa Expr # handle broadcasting, but only handle number of arguments instead of # argument types - append!(args_ex, Any for _ in (ex_org.args[2]::Expr).args) + for ex in (ex_org.args[2]::Expr).args + if isexpr(ex, :parameters) + kwargs_flag = ifelse(kwargs_flag == 0, 1, 2) # there should be at most one :parameters + for x in ex.args + n = isexpr(x, :kw) ? x.args[1] : x + if !(n isa Symbol) || !isexpr(x, :...) + kwargs_flag = 2 + end + end + elseif isexpr(ex, :kw) + if !(first(ex.args) isa Symbol) + kwargs_flag = 2 + end + else + push!(args_ex, isexpr(ex, :...) ? Vararg{Any} : Any) + end + end else for ex in funargs if isexpr(ex, :parameters) + kwargs_flag = ifelse(kwargs_flag == 0, 1, 2) # there should be at most one :parameters for x in ex.args - n = isexpr(x, :kw) ? first(x.args) : x - n isa Symbol || continue # happens if the current arg is splat - push!(kwargs_ex, n) + kwargs_flag = if isexpr(x, :kw) + detect_invalid_kwarg!(kwargs_ex, first(x.args), kwargs_flag, false) + else + detect_invalid_kwarg!(kwargs_ex, x, kwargs_flag, true) + end end elseif isexpr(ex, :kw) n = first(ex.args) - n isa Symbol || continue # happens if the current arg is splat - push!(kwargs_ex, n) + kwargs_flag = detect_invalid_kwarg!(kwargs_ex, n, kwargs_flag, false) else push!(args_ex, get_type(get_type(ex, context_module)..., default_any)) end end end - return args_ex, Set{Symbol}(kwargs_ex) + return args_ex, Set{Symbol}(kwargs_ex), kwargs_flag end -function complete_methods!(out::Vector{Completion}, @nospecialize(funct), args_ex::Vector{Any}, kwargs_ex::Set{Symbol}, max_method_completions::Int) +function complete_methods!(out::Vector{Completion}, @nospecialize(funct), args_ex::Vector{Any}, kwargs_ex::Set{Symbol}, max_method_completions::Int, exact_narg::Bool) # Input types and number of arguments - t_in = Tuple{funct, args_ex...} + num_splat = 0 # number of splat arguments in args_ex + args_ex_onevararg = copy(args_ex) # args_ex_onevararg contains at most one Vararg, put in final position + for (i, arg) in enumerate(args_ex) + if Base.isvarargtype(arg) + num_splat += 1 + num_splat > 1 && continue + args_ex_onevararg[i] = Vararg{Any} + resize!(args_ex_onevararg, i) + end + end + t_in = Tuple{funct, args_ex_onevararg...} m = Base._methods_by_ftype(t_in, nothing, max_method_completions, Base.get_world_counter(), #=ambig=# true, Ref(typemin(UInt)), Ref(typemax(UInt)), Ptr{Int32}(C_NULL)) if m === false @@ -627,7 +678,26 @@ function complete_methods!(out::Vector{Completion}, @nospecialize(funct), args_e end m isa Vector || return for match in m + if exact_narg + # If there is a semicolon, the number of non-keyword arguments in the + # call cannot grow. It must thus match that of the method. + meth_nargs = match.method.nargs - 1 # remove the function itself + exn_args = length(args_ex) + if meth_nargs != exn_args + # the match may still hold if some arguments are slurped or splat + num_slurp = count(Base.isvarargtype, Base.unwrap_unionall(match.method.sig).types) + num_slurp == 0 && num_splat == 0 && continue + if num_slurp == 0 + meth_nargs ≥ exn_args - num_splat || continue + elseif num_splat == 0 + exn_args ≥ meth_nargs - num_slurp || continue + end + end + end + if !isempty(kwargs_ex) + # Only suggest a method if it can accept all the kwargs already present in + # the call, or if it slurps keyword arguments possible_kwargs = Base.kwarg_decl(match.method) slurp = false for _kw in possible_kwargs @@ -636,8 +706,6 @@ function complete_methods!(out::Vector{Completion}, @nospecialize(funct), args_e break end end - # Only suggest a method if it can accept all the kwargs already present in - # the call, or if it slurps keyword arguments slurp || kwargs_ex ⊆ possible_kwargs || continue end push!(out, MethodCompletion(match.spec_types, match.method)) @@ -781,16 +849,12 @@ function complete_keyword_argument(partial, last_idx, context_module) # inlined `complete_methods` function since we need the `kwargs_ex` variable func, found = get_value(ex.args[1], context_module) !(found::Bool) && return fail - args_ex, kwargs_ex = complete_methods_args(ex.args[2:end], ex, context_module, true, true) + args_ex, kwargs_ex, kwargs_flag = complete_methods_args(ex.args[2:end], ex, context_module, true, true) - # Only try to complete as a kwarg if the context makes it clear that the current - # argument could be a kwarg (i.e. right after ';' or if there is another kwarg) - isempty(kwargs_ex) && last_punct != ';' && - all(x -> !(x isa Expr) || (x.head !== :kw && x.head !== :parameters), ex.args[2:end]) && - return fail + kwargs_flag == 2 && return fail # one of the previous kwargs is invalid methods = Completion[] - complete_methods!(methods, Core.Typeof(func), args_ex, kwargs_ex, -1) + complete_methods!(methods, Core.Typeof(func), args_ex, kwargs_ex, -1, kwargs_flag == 1) # Finally, for each method corresponding to the function call, provide completions # suggestions for each keyword that starts like the last word and that is not already diff --git a/stdlib/REPL/test/replcompletions.jl b/stdlib/REPL/test/replcompletions.jl index 794694f13bba82..677d84b40e90f8 100644 --- a/stdlib/REPL/test/replcompletions.jl +++ b/stdlib/REPL/test/replcompletions.jl @@ -109,6 +109,8 @@ let ex = quote kwtest4(a::AbstractString; _a1b, x23) = pass kwtest4(a::String; _a1b, xαβγ) = pass kwtest4(a::SubString; x23, _something) = pass + kwtest5(a::Int, b, x...; somekwarg, somekotherkwarg) = pass + kwtest5(a::Char, b; xyz) = pass const named = (; len2=3) @@ -152,6 +154,12 @@ test_complete_noshift(s) = map_completion_text(@inferred(completions(s, lastinde module M32377 end test_complete_32377(s) = map_completion_text(completions(s,lastindex(s), M32377)) +function test_nocompletion(s) + c, _, res = test_complete(s) + @test c == String[] + @test res === false +end + let s = "" c, r = test_complete(s) @test "CompletionFoo" in c @@ -279,16 +287,10 @@ let end # inexistent completion inside a string -let s = "Base.print(\"lol" - c, r, res = test_complete(s) - @test res == false -end +test_nocompletion("Base.print(\"lol") # inexistent completion inside a cmd -let s = "run(`lol" - c, r, res = test_complete(s) - @test res == false -end +test_nocompletion("run(`lol") # test latex symbol completions let s = "\\alpha" @@ -545,27 +547,56 @@ let s = "CompletionFoo.kwtest( " @test !res @test length(c) == 1 @test occursin("x, y, w...", c[1]) + @test (c, r, res) == test_complete("CompletionFoo.kwtest(;") + @test (c, r, res) == test_complete("CompletionFoo.kwtest(; x=1, ") + @test (c, r, res) == test_complete("CompletionFoo.kwtest(; kw=1, ") + @test (c, r, res) == test_complete("CompletionFoo.kwtest(x=1, ") + @test (c, r, res) == test_complete("CompletionFoo.kwtest(x=1; ") + @test (c, r, res) == test_complete("CompletionFoo.kwtest(x=kw=1, ") + @test (c, r, res) == test_complete("CompletionFoo.kwtest(; x=kw=1, ") end -for s in ("CompletionFoo.kwtest(;", - "CompletionFoo.kwtest(; x=1, ", - "CompletionFoo.kwtest(; kw=1, ", - ) +let s = "CompletionFoo.kwtest2(1, x=1," c, r, res = test_complete(s) @test !res @test length(c) == 1 - @test occursin("x, y, w...", c[1]) + @test occursin("a; x, y, w...", c[1]) + @test (c, r, res) == test_complete("CompletionFoo.kwtest2(1; x=1, ") + @test (c, r, res) == test_complete("CompletionFoo.kwtest2(1, x=1; ") + @test (c, r, res) == test_complete("CompletionFoo.kwtest2(1, kw=1, ") + @test (c, r, res) == test_complete("CompletionFoo.kwtest2(1; kw=1, ") + @test (c, r, res) == test_complete("CompletionFoo.kwtest2(1, kw=1; ") + @test (c, r, res) == test_complete("CompletionFoo.kwtest2(y=3, 1, ") + @test (c, r, res) == test_complete("CompletionFoo.kwtest2(y=3, 1; ") + @test (c, r, res) == test_complete("CompletionFoo.kwtest2(kw=3, 1, ") + @test (c, r, res) == test_complete("CompletionFoo.kwtest2(kw=3, 1; ") +end + +let s = "CompletionFoo.kwtest4(x23=18, x; " + c, r, res = test_complete(s) + @test !res + @test length(c) == 2 + @test any(str->occursin("kwtest4(a::SubString", str), c) + @test any(str->occursin("kwtest4(a::AbstractString", str), c) + @test (c, r, res) == test_complete("CompletionFoo.kwtest4(x23=18, x, ") + @test (c, r, res) == test_complete("CompletionFoo.kwtest4(x23=18, ") end -for s in ("CompletionFoo.kwtest2(1; x=1,", - "CompletionFoo.kwtest2(1; kw=1, ", - ) +test_nocompletion("CompletionFoo.kwtest4(x23=17; ") +test_nocompletion("CompletionFoo.kwtest4.(x23=17; ") + +let s = "CompletionFoo.kwtest5(3, somekwarg=6," c, r, res = test_complete(s) @test !res @test length(c) == 1 - @test occursin("a; x, y, w...", c[1]) + @test occursin("kwtest5(a::$(Int), b, x...; somekwarg, somekotherkwarg)", c[1]) + @test (c, r, res) == test_complete("CompletionFoo.kwtest5(3, somekwarg=6, anything, ") end +test_nocompletion("CompletionFoo.kwtest5(3; somekwarg=6,") +test_nocompletion("CompletionFoo.kwtest5(3;") +test_nocompletion("CompletionFoo.kwtest5(3; somekwarg=6, anything, ") + ################################################################# # method completion with `?` (arbitrary method with given argument types) @@ -644,12 +675,31 @@ end let s = "CompletionFoo.?(; y=2, " c, r, res = test_complete(s) @test !res - @test occursin("kwtest(", c[1]) - @test !any(str->occursin(r"^test", str), c) - # kwtest2 should not appear since the number of args if wrong, but we don't currently handle this - @test_broken length(c) == 1 + @test length(c) == 4 + @test all(x -> occursin("kwtest", x), c) + # We choose to include kwtest2 and kwtest3 although the number of args if wrong. + # This is because the ".?(" syntax with no closing parenthesis does not constrain the + # number of arguments in the methods it suggests. +end + +let s = "CompletionFoo.?(3; len2=5, " + c, r, res = test_complete_noshift(s) + @test !res + @test length(c) == 1 + @test occursin("kwtest3(a::Integer; namedarg, foobar, slurp...)", c[1]) + # the other two kwtest3 methods should not appear because of specificity end +# For the ".?(" syntax, do not constrain the number of arguments even with a semicolon. +@test test_complete("CompletionFoo.?(Any[]...; ") == + test_complete("CompletionFoo.?(Cmd[]..., ") == + test_complete("CompletionFoo.?(; ") == + test_complete("CompletionFoo.?(") + +@test test_complete("CompletionFoo.?()") == test_complete("CompletionFoo.?(;)") + +test_nocompletion("CompletionFoo.?(3; len2=5; ") + ################################################################# # Test method completion with varargs @@ -759,6 +809,49 @@ let s = "CompletionFoo.test11('d', 3," @test any(str->occursin("test11(::Any, ::Any, s::String)", str), c) end +# Test method completion depending on the number of arguments + +test_nocompletion("CompletionFoo.test3(unknown; ") +test_nocompletion("CompletionFoo.test3.(unknown; ") + +let s = "CompletionFoo.test2(unknown..., somethingelse..., xyz...; " # splat may be empty + c, r, res = test_complete(s) + @test !res + @test length(c) == 3 + @test all(str->occursin("test2(", str), c) + @test (c, r, res) == test_complete("CompletionFoo.test2(unknown..., somethingelse..., xyz, ") + @test (c, r, res) == test_complete("CompletionFoo.test2(unknown..., somethingelse..., xyz; ") +end + +let s = "CompletionFoo.test('a', args..., 'b';" + c, r, res = test_complete(s) + @test !res + @test length(c) == 1 + @test occursin("test(args...)", c[1]) + @test (c, r, res) == test_complete("CompletionFoo.test(a, args..., b, c;") +end + +let s = "CompletionFoo.test(3, 5, args...,;" + c, r, res = test_complete(s) + @test !res + @test length(c) == 2 + @test any(str->occursin("test(x::T, y::T) where T<:Real", str), c) + @test any(str->occursin("test(args...)", str), c) +end + +# Test that method calls with ill-formed kwarg syntax are not completed + +test_nocompletion("CompletionFoo.kwtest(; x=2, y=4; kw=3, ") +test_nocompletion("CompletionFoo.kwtest(x=2; y=4; ") +test_nocompletion("CompletionFoo.kwtest((x=y)=4, ") +test_nocompletion("CompletionFoo.kwtest(; (x=y)=4, ") +test_nocompletion("CompletionFoo.kwtest(; w...=16, ") +test_nocompletion("CompletionFoo.kwtest(; 2, ") +test_nocompletion("CompletionFoo.kwtest(; 2=3, ") +test_nocompletion("CompletionFoo.kwtest3(im; (true ? length : length), ") +test_nocompletion("CompletionFoo.kwtest.(x=2; y=4; ") +test_nocompletion("CompletionFoo.kwtest.(; w...=16, ") + # Test of inference based getfield completion let s = "(1+2im)." c,r = test_complete(s) @@ -1337,6 +1430,26 @@ end @test "x23=" ∈ c @test "xαβγ=" ∈ c + c, r = test_complete("CompletionFoo.kwtest5(3, 5; somek") + @test c == ["somekotherkwarg=", "somekwarg="] + c, r = test_complete("CompletionFoo.kwtest5(3, 5, somekwarg=4, somek") + @test c == ["somekotherkwarg="] + c, r = test_complete("CompletionFoo.kwtest5(3, 5, 7; somekw") + @test c == ["somekwarg="] + c, r = test_complete("CompletionFoo.kwtest5(3, 5, 7, 9; somekw") + @test c == ["somekwarg="] + c, r = test_complete("CompletionFoo.kwtest5(3, 5, 7, 9, Any[]...; somek") + @test c == ["somekotherkwarg=", "somekwarg="] + c, r = test_complete("CompletionFoo.kwtest5(unknownsplat...; somekw") + @test c == ["somekwarg="] + c, r = test_complete("CompletionFoo.kwtest5(3, 5, 7, 9, somekwarg=4, somek") + @test c == ["somekotherkwarg="] + c, r = test_complete("CompletionFoo.kwtest5(String[]..., unknownsplat...; xy") + @test c == ["xyz="] + c, r = test_complete("CompletionFoo.kwtest5('a', unknownsplat...; xy") + @test c == ["xyz="] + c, r = test_complete("CompletionFoo.kwtest5('a', 3, String[]...; xy") + @test c == ["xyz="] # return true if no completion suggests a keyword argument function hasnokwsuggestions(str) @@ -1362,6 +1475,9 @@ end @test hasnokwsuggestions("CompletionFoo.kwtest3(a; unknown=4, another!kw") # only methods 1 and 3 could slurp `unknown` @test hasnokwsuggestions("CompletionFoo.kwtest3(1+3im; nameda") @test hasnokwsuggestions("CompletionFoo.kwtest3(12//7; foob") # because of specificity + @test hasnokwsuggestions("CompletionFoo.kwtest3(a, len2=b, length, foob") # length is not length=length + @test hasnokwsuggestions("CompletionFoo.kwtest5('a', 3, 5, unknownsplat...; xy") + @test hasnokwsuggestions("CompletionFoo.kwtest5(3; somek") end # Test completion in context @@ -1478,8 +1594,11 @@ let s = "test.(1,1, " @test length(c) == 4 @test r == 1:4 @test s[r] == "test" + @test (c, r, res) == test_complete_foo("test.(1, 1, String[]..., ") + @test (c, r, res) == test_complete_foo("test.(1, Any[]..., 2, ") end + let s = "prevind(\"θ\",1," c, r, res = test_complete_foo(s) @test c[1] == string(first(methods(prevind, Tuple{String, Int})))