Skip to content

Commit

Permalink
REPL: improve getfield type completion (#40624)
Browse files Browse the repository at this point in the history
fix #40247
  • Loading branch information
aviatesk committed May 10, 2021
1 parent 27d3931 commit 9bceffd
Show file tree
Hide file tree
Showing 2 changed files with 86 additions and 36 deletions.
32 changes: 20 additions & 12 deletions stdlib/REPL/src/REPLCompletions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -387,14 +387,24 @@ get_value(sym::QuoteNode, fn) = isdefined(fn, sym.value) ? (getfield(fn, sym.val
get_value(sym::GlobalRef, fn) = get_value(sym.name, sym.mod)
get_value(sym, fn) = (sym, true)

# Return the value of a getfield call expression
function get_value_getfield(ex::Expr, fn)
# Example :((top(getfield))(Base,:max))
val, found = get_value_getfield(ex.args[2],fn) #Look up Base in Main and returns the module
(found && length(ex.args) >= 3) || return (nothing, false)
return get_value_getfield(ex.args[3], val) #Look up max in Base and returns the function if found.
# Return the type of a getfield call expression
function get_type_getfield(ex::Expr, fn::Module)
length(ex.args) == 3 || return Any, false # should never happen, but just for safety
obj, x = ex.args[2:3]
objt, found = get_type(obj, fn)
objt isa DataType || return Any, false
found || return Any, false
if x isa QuoteNode
fld = x.value
elseif isexpr(x, :quote) || isexpr(x, :inert)
fld = x.args[1]
else
fld = nothing # we don't know how to get the value of variable `x` here
end
fld isa Symbol || return Any, false
hasfield(objt, fld) || return Any, false
return fieldtype(objt, fld), true
end
get_value_getfield(sym, fn) = get_value(sym, fn)

# Determines the return type with Base.return_types of a function call using the type information of the arguments.
function get_type_call(expr::Expr)
Expand Down Expand Up @@ -424,18 +434,16 @@ function get_type_call(expr::Expr)
return (return_type, true)
end

# Returns the return type. example: get_type(:(Base.strip("", ' ')), Main) returns (String, true)
# Returns the return type. example: get_type(:(Base.strip("", ' ')), Main) returns (SubString{String}, true)
function try_get_type(sym::Expr, fn::Module)
val, found = get_value(sym, fn)
found && return Core.Typeof(val), found
if sym.head === :call
# getfield call is special cased as the evaluation of getfield provides good type information,
# is inexpensive and it is also performed in the complete_symbol function.
a1 = sym.args[1]
if isa(a1,GlobalRef) && isconst(a1.mod,a1.name) && isdefined(a1.mod,a1.name) &&
eval(a1) === Core.getfield
val, found = get_value_getfield(sym, Main)
return found ? Core.Typeof(val) : Any, found
if a1 === :getfield || a1 === GlobalRef(Core, :getfield)
return get_type_getfield(sym, fn)
end
return get_type_call(sym)
elseif sym.head === :thunk
Expand Down
90 changes: 66 additions & 24 deletions stdlib/REPL/test/replcompletions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,11 @@ function map_completion_text(completions)
return map(completion_text, c), r, res
end

test_complete(s) = map_completion_text(@inferred(completions(s,lastindex(s))))
test_scomplete(s) = map_completion_text(@inferred(shell_completions(s,lastindex(s))))
test_bslashcomplete(s) = map_completion_text(@inferred(bslash_completions(s,lastindex(s)))[2])
test_complete_context(s) = map_completion_text(@inferred(completions(s,lastindex(s),Main.CompletionFoo)))
test_complete(s) = map_completion_text(@inferred(completions(s, lastindex(s))))
test_scomplete(s) = map_completion_text(@inferred(shell_completions(s, lastindex(s))))
test_bslashcomplete(s) = map_completion_text(@inferred(bslash_completions(s, lastindex(s)))[2])
test_complete_context(s, m) = map_completion_text(@inferred(completions(s,lastindex(s), m)))
test_complete_foo(s) = test_complete_context(s, Main.CompletionFoo)

module M32377 end
test_complete_32377(s) = map_completion_text(completions(s,lastindex(s), M32377))
Expand Down Expand Up @@ -297,7 +298,7 @@ end

# test latex symbol completion in getindex expressions (#24705)
let s = "tuple[\\alpha"
c, r, res = test_complete_context(s)
c, r, res = test_complete_foo(s)
@test c[1] == "α"
@test r == 7:12
@test length(c) == 1
Expand Down Expand Up @@ -987,100 +988,100 @@ end

# No CompletionFoo.CompletionFoo
let s = ""
c, r = test_complete_context(s)
c, r = test_complete_foo(s)
@test !("CompletionFoo" in c)
end

# Can see `rand()` after `using Random`
let s = "r"
c, r = test_complete_context(s)
c, r = test_complete_foo(s)
@test "rand" in c
@test r == 1:1
@test s[r] == "r"
end

# Can see `Test.AbstractTestSet` after `import Test`
let s = "Test.A"
c, r = test_complete_context(s)
c, r = test_complete_foo(s)
@test "AbstractTestSet" in c
@test r == 6:6
@test s[r] == "A"
end

# Can complete relative import
let s = "import ..M"
c, r = test_complete_context(s)
c, r = test_complete_foo(s)
@test_broken "Main" in c
@test r == 10:10
@test s[r] == "M"
end

let s = ""
c, r = test_complete_context(s)
c, r = test_complete_foo(s)
@test "bar" in c
@test r === 1:0
@test s[r] == ""
end

let s = "f"
c, r = test_complete_context(s)
c, r = test_complete_foo(s)
@test "foo" in c
@test r == 1:1
@test s[r] == "f"
@test !("foobar" in c)
end

let s = "@f"
c, r = test_complete_context(s)
c, r = test_complete_foo(s)
@test "@foobar" in c
@test r == 1:2
@test s[r] == "@f"
@test !("foo" in c)
end

let s = "type_test.x"
c, r = test_complete_context(s)
c, r = test_complete_foo(s)
@test "xx" in c
@test r == 11:11
@test s[r] == "x"
end

let s = "bar.no_val_available"
c, r = test_complete_context(s)
c, r = test_complete_foo(s)
@test length(c)==0
end

let s = "type_test.xx.y"
c, r = test_complete_context(s)
c, r = test_complete_foo(s)
@test "yy" in c
@test r == 14:14
@test s[r] == "y"
end

let s = ":(function foo(::Int) end).args[1].args[2]."
c, r = test_complete_context(s)
c, r = test_complete_foo(s)
@test c == Any[]
end

let s = "log(log.(x),"
c, r = test_complete_context(s)
c, r = test_complete_foo(s)
@test !isempty(c)
end

let s = "Base.return_types(getin"
c, r = test_complete_context(s)
c, r = test_complete_foo(s)
@test "getindex" in c
@test r == 19:23
@test s[r] == "getin"
end

let s = "using Test, Random"
c, r = test_complete_context(s)
c, r = test_complete_foo(s)
@test !("RandomDevice" in c)
end

let s = "test(1,1, "
c, r, res = test_complete_context(s)
c, r, res = test_complete_foo(s)
@test !res
@test c[1] == string(first(methods(Main.CompletionFoo.test, Tuple{Int, Int})))
@test length(c) == 3
Expand All @@ -1089,28 +1090,28 @@ let s = "test(1,1, "
end

let s = "test.(1,1, "
c, r, res = test_complete_context(s)
c, r, res = test_complete_foo(s)
@test !res
@test length(c) == 4
@test r == 1:4
@test s[r] == "test"
end

let s = "prevind(\"θ\",1,"
c, r, res = test_complete_context(s)
c, r, res = test_complete_foo(s)
@test c[1] == string(first(methods(prevind, Tuple{String, Int})))
@test r == 1:7
@test s[r] == "prevind"
end

# Issue #32840
let s = "typeof(+)."
c, r = test_complete_context(s)
c, r = test_complete_foo(s)
@test length(c) == length(fieldnames(DataType))
end

let s = "test_dict[\"ab"
c, r = test_complete_context(s)
c, r = test_complete_foo(s)
@test c == Any["\"abc\"", "\"abcd\""]
end

Expand All @@ -1120,3 +1121,44 @@ let
(test_complete("Main.@noexist."); @test true)
(test_complete("@Main.noexist."); @test true)
end

@testset "https://github.com/JuliaLang/julia/issues/40247" begin
# getfield type completion can work for complicated expression

let
m = Module()
@eval m begin
struct Rs
rs::Vector{Regex}
end
var = nothing
function foo()
global var = 1
return Rs([r"foo"])
end
end

c, r = test_complete_context("foo().rs[1].", m)
@test m.var 1 # getfield type completion should never execute `foo()`
@test length(c) == fieldcount(Regex)
end

let
m = Module()
@eval m begin
struct R
r::Regex
end
var = nothing
function foo()
global var = 1
return R(r"foo")
end
end

c, r = test_complete_context("foo().r.", m)
# the current implementation of `REPL.REPLCompletions.completions(::String, ::Int, ::Module)`
# cuts off "foo().r." to `.r.`, and the getfield type completion doesn't work for this simpler case
@test_broken length(c) == fieldcount(Regex)
end
end

0 comments on commit 9bceffd

Please sign in to comment.