Skip to content

Commit 4594f21

Browse files
authored
Merge pull request #269 from julia-vscode/sp/noclobber
respect soft scope and stop clobbering bindings
2 parents 0a5cbbd + 5b9e6d8 commit 4594f21

File tree

6 files changed

+118
-7
lines changed

6 files changed

+118
-7
lines changed

src/bindings.jl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,10 @@ function add_binding(x, state, scope=state.scope)
344344
elseif scopehasbinding(scope, name)
345345
# TODO: some checks about rebinding of consts
346346
check_const_decl(name, b, scope)
347+
347348
scope.names[name] = b
349+
elseif is_soft_scope(scope) && parentof(scope) isa Scope && scopehasbinding(parentof(scope), valofid(b.name))
350+
add_binding(x, state, scope.parent)
348351
else
349352
scope.names[name] = b
350353
end

src/linting/checks.jl

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -492,9 +492,16 @@ function check_farg_unused(x::EXPR)
492492
return
493493
end
494494
b = bindingof(arg)
495-
if b === nothing || (isempty(b.refs) || (length(b.refs) == 1 && first(b.refs) == b.name))
495+
if b === nothing ||
496+
# no refs:
497+
isempty(b.refs) ||
498+
# only self ref:
499+
(length(b.refs) == 1 && first(b.refs) == b.name) ||
500+
# first usage is assignment:
501+
(length(b.refs) > 1 && CSTParser.hasparent(b.refs[2]) && isassignment(parentof(b.refs[2])) && parentof(b.refs[2]).args[1] == b.refs[2])
496502
seterror!(arg, UnusedFunctionArgument)
497503
end
504+
498505
if valof(b.name) === nothing
499506
elseif valof(b.name) in arg_names
500507
seterror!(arg, DuplicateFuncArgName)
@@ -868,8 +875,9 @@ end
868875

869876
function check_unused_binding(b::Binding, scope::Scope)
870877
if headof(scope.expr) !== :struct && headof(scope.expr) !== :tuple && !all_underscore(valof(b.name))
871-
if (isempty(b.refs) || length(b.refs) == 1 && b.refs[1] == b.name) && !is_sig_arg(b.name) && !is_overwritten_in_loop(b.name) && !is_overwritten_subsequently(b, scope)
872-
seterror!(b.name, UnusedBinding)
878+
refs = loose_refs(b)
879+
if (isempty(refs) || length(refs) == 1 && refs[1] == b.name) && !is_sig_arg(b.name) && !is_overwritten_in_loop(b.name) && !is_overwritten_subsequently(b, scope)
880+
seterror!(b.name, UnusedBinding)
873881
end
874882
end
875883
end

src/references.jl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ end
3838
# can't be resolved.
3939

4040
function resolve_ref(x::EXPR, scope::Scope, state::State)::Bool
41+
# if the current scope is a soft scope we should check the parent scope first
42+
# before trying to resolve the ref locally
43+
if is_soft_scope(scope) && parentof(scope) isa Scope
44+
resolve_ref(x, parentof(scope), state) && return true
45+
end
46+
4147
hasref(x) && return true
4248
resolved = false
4349

src/scope.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ Checks whether s has a binding for variable named `n`.
5656
"""
5757
scopehasbinding(s::Scope, n::String) = haskey(s.names, n)
5858

59+
is_soft_scope(scope::Scope) = scope.expr.head == :for || scope.expr.head == :while || scope.expr.head == :try
60+
5961
"""
6062
introduces_scope(x::EXPR, state)
6163

src/utils.jl

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,3 +301,35 @@ function is_nameof_func(name)
301301
f = get_parent_fexpr(name, CSTParser.defines_function)
302302
f !== nothing && CSTParser.get_name(f) == name
303303
end
304+
305+
function loose_refs(b::Binding)
306+
scope = retrieve_scope(b.val)
307+
name_str = valofid(b.name)
308+
name_str isa String || return b.refs
309+
310+
if is_soft_scope(scope) && parentof(scope) isa Scope && scopehasbinding(parentof(scope), name_str) && !scopehasbinding(scope, name_str)
311+
scope = parentof(scope)
312+
end
313+
state = LooseRefs(scope.expr, name_str, scope, [])
314+
state(scope.expr)
315+
vcat([r.refs for r in state.result]...)
316+
end
317+
318+
mutable struct LooseRefs
319+
x::EXPR
320+
name::String
321+
scope::Scope
322+
result::Vector{Binding}
323+
end
324+
325+
function (state::LooseRefs)(x::EXPR)
326+
if hasbinding(x)
327+
ex = bindingof(x).name
328+
if isidentifier(ex) && valofid(ex) == state.name
329+
push!(state.result, bindingof(x))
330+
end
331+
end
332+
if !hasscope(x) || (hasscope(x) && ((is_soft_scope(scopeof(x)) && !scopehasbinding(scopeof(x), state.name)) || scopeof(x) == state.scope))
333+
traverse(x, state)
334+
end
335+
end

test/runtests.jl

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -791,6 +791,14 @@ f(arg) = arg
791791
StaticLint.check_farg_unused(cst[1])
792792
@test StaticLint.errorof(CSTParser.get_sig(cst[1])[3]) === StaticLint.UnusedFunctionArgument
793793
end
794+
let cst = parse_and_pass(
795+
"""function f(arg)
796+
x = arg
797+
arg = x
798+
end""")
799+
StaticLint.check_farg_unused(cst[1])
800+
@test StaticLint.errorof(CSTParser.get_sig(cst[1])[3]) === nothing
801+
end
794802
let cst = parse_and_pass("function f(arg) 1 end")
795803
StaticLint.check_farg_unused(cst[1])
796804
@test StaticLint.errorof(CSTParser.get_sig(cst[1])[3]) === nothing
@@ -836,7 +844,7 @@ f(arg) = arg
836844
ASDF(1)
837845
""")
838846
# Check inner constructor is hoisted
839-
@test isempty(StaticLint.collect_hints(cst, server))
847+
@test isempty(StaticLint.collect_hints(cst, server))
840848
end
841849
end
842850

@@ -1411,7 +1419,7 @@ f(arg) = arg
14111419
f1(x)
14121420
end""")
14131421
@test bindingof(cst.args[3].args[1].args[2]).type !== nothing
1414-
1422+
14151423
cst = parse_and_pass("""
14161424
f(x::String) = true
14171425
f1(x::Char) = true
@@ -1465,7 +1473,7 @@ end
14651473
@testset "duplicate function argument" begin
14661474
cst = parse_and_pass("""
14671475
f(a,a) = a
1468-
""")
1476+
""")
14691477
@test errorof(cst[1][1][5]) == StaticLint.DuplicateFuncArgName
14701478
end
14711479

@@ -1590,7 +1598,7 @@ end
15901598
multiply!(1, 3)
15911599
""")
15921600
@test errorof(cst[2]) === nothing
1593-
1601+
15941602
@test StaticLint.haserror(parse_and_pass("function f(z::T)::Nothing where T end")[1].args[1].args[1].args[1].args[2])
15951603
@test StaticLint.haserror(parse_and_pass("function f(z::T) where T end")[1].args[1].args[1].args[2])
15961604
end
@@ -1644,3 +1652,55 @@ end
16441652
@test cst[2].meta.scope.names["V"].type isa SymbolServer.DataTypeStore
16451653
@test isempty(StaticLint.collect_hints(cst, server))
16461654
end
1655+
1656+
@testset "softscope" begin
1657+
cst = parse_and_pass("""
1658+
function foo()
1659+
x = 1
1660+
x
1661+
if rand(Bool)
1662+
x = 2
1663+
end
1664+
x
1665+
while rand(Bool)
1666+
x = 3
1667+
end
1668+
x
1669+
for _ in 1:2
1670+
x = 4
1671+
y = 1
1672+
end
1673+
x
1674+
end
1675+
""")
1676+
1677+
# check soft-scope bindings are lifted to parent scope
1678+
@test refof(cst[1][3][2]) == bindingof(cst[1][3][1][1])
1679+
@test refof(cst[1][3][4]) == bindingof(cst[1][3][3][3][1][1])
1680+
@test refof(cst[1][3][6]) == bindingof(cst[1][3][5][3][1][1])
1681+
@test refof(cst[1][3][8]) == bindingof(cst[1][3][7][3][1][1])
1682+
1683+
# check binding made in soft-scope with no matching binidng in parent scope isn't lifted
1684+
@test !haskey(scopeof(cst[1]).names, "y")
1685+
@test haskey(scopeof(cst[1][3][7]).names, "y")
1686+
1687+
1688+
@test length(StaticLint.loose_refs(bindingof(cst[1][3][1][1]))) == 8
1689+
@test length(StaticLint.loose_refs(bindingof(cst[1][3][3][3][1][1]))) == 8
1690+
@test length(StaticLint.loose_refs(bindingof(cst[1][3][5][3][1][1]))) == 8
1691+
@test length(StaticLint.loose_refs(bindingof(cst[1][3][7][3][1][1]))) == 8
1692+
1693+
cst = parse_and_pass("""
1694+
function foo()
1695+
for _ in 1:2
1696+
x = 1
1697+
x
1698+
end
1699+
x
1700+
x = 1
1701+
x
1702+
end
1703+
""")
1704+
@test length(StaticLint.loose_refs(bindingof(cst[1][3][1][3][1][1]))) == 2
1705+
@test length(StaticLint.loose_refs(bindingof(cst[1][3][3][1]))) == 2
1706+
end

0 commit comments

Comments
 (0)