Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/StaticLint/type_inf.jl
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ function infer_type_assignment_rhs(binding, state, scope)
if ref isa Binding && ref.val isa EXPR
settype!(binding, infer_eltype(ref.val, state))
end
elseif CSTParser.isdeclaration(rhs) && length(rhs.args) == 2 && !is_destructuring
# RHS is a type assertion (`y = x::T`, `x = x::T`): the assigned binding
# takes the asserted type, the same way a `::T` parameter declaration
# does. This lets field completion narrow through a local assertion.
infer_type_decl(binding, rhs.args[2], state, scope)
else
if CSTParser.is_func_call(rhs)
if CSTParser.istuple(lhs) && !is_destructuring
Expand Down
100 changes: 100 additions & 0 deletions test/test_completions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -973,3 +973,103 @@ end
result2 = get_completions(jw2, uri2, index2)
@test result2 isa CompletionResult
end

@testitem "Completions: getfield via type-asserted assignment" begin
using JuliaWorkspaces: JuliaWorkspace, add_file!, TextFile, SourceText, get_completions
using JuliaWorkspaces.URIs2: URI

structs = """
abstract type Parent end
struct Child1 <: Parent
field1::Int
end
struct Child2 <: Parent
field2::String
end
"""

function fields_at(source, marker)
uri = URI("file:///compassert/test.jl")
jw = JuliaWorkspace()
add_file!(jw, TextFile(uri, SourceText(source, "julia")))
index = findfirst(marker, source)[end]
sort([item.label for item in get_completions(jw, uri, index).items])
end

# Rebinding through an assertion: `x = x::Child1`
rebind = structs * """
function foo(x::Parent)
x = x::Child1
x.
end
"""
@test fields_at(rebind, "\n x.\n") == ["field1"]

# Fresh variable bound to an asserted value: `y = x::Child1`
freshvar = structs * """
function foo(x::Parent)
y = x::Child1
y.
end
"""
@test fields_at(freshvar, "\n y.\n") == ["field1"]

# Untyped parameter still narrows through the assertion.
untyped = structs * """
function foo(x)
y = x::Child1
y.
end
"""
@test fields_at(untyped, "\n y.\n") == ["field1"]

# Partial field text still matches.
partial = structs * """
function foo(x::Parent)
y = x::Child1
y.fie
end
"""
@test fields_at(partial, "y.fie") == ["field1"]

# Baseline: an abstract declared type with no assertion yields no fields.
baseline = structs * """
function foo(x::Parent)
x.
end
"""
@test fields_at(baseline, "\n x.\n") == String[]
end

@testitem "Completions: getfield type assertion narrows per branch" begin
using JuliaWorkspaces: JuliaWorkspace, add_file!, TextFile, SourceText, get_completions
using JuliaWorkspaces.URIs2: URI

source = """
abstract type Parent end
struct Child1 <: Parent
field1::Int
end
struct Child2 <: Parent
field2::String
end
function f(x::Parent)
if x isa Child1
y = x::Child1
y.
else
y = x::Child2
y.
end
end
"""
uri = URI("file:///compassertbranch/test.jl")
jw = JuliaWorkspace()
add_file!(jw, TextFile(uri, SourceText(source, "julia")))

dots = collect(findall("y.\n", source))
if_fields = sort([i.label for i in get_completions(jw, uri, dots[1][end]).items])
else_fields = sort([i.label for i in get_completions(jw, uri, dots[2][end]).items])
@test if_fields == ["field1"]
@test else_fields == ["field2"]
end
35 changes: 35 additions & 0 deletions test/test_typeinf.jl
Original file line number Diff line number Diff line change
Expand Up @@ -323,3 +323,38 @@ end
@test z !== nothing
@test z.type === nothing
end

@testitem "type assertion assignment inference" setup=[shared_static_lint] begin
using JuliaWorkspaces.StaticLint: scopeof

cst, meta_dict = parse_and_pass("""
abstract type Parent end
struct Child1 <: Parent
field1::Int
end

function rebind(x::Parent)
x = x::Child1
x
end

function freshvar(x::Parent)
y = x::Child1
y
end

function untyped(x)
y = x::Child1
y
end
""");

Child1 = scopeof(cst, meta_dict).names["Child1"]

# `x = x::Child1` narrows the reassignment binding to Child1.
@test scopeof(scopeof(cst, meta_dict).names["rebind"].val, meta_dict).names["x"].type === Child1
# `y = x::Child1` gives the fresh binding the asserted type.
@test scopeof(scopeof(cst, meta_dict).names["freshvar"].val, meta_dict).names["y"].type === Child1
# Works even when the source variable is untyped.
@test scopeof(scopeof(cst, meta_dict).names["untyped"].val, meta_dict).names["y"].type === Child1
end
Loading