Skip to content

Commit 0a5cbbd

Browse files
authored
Merge pull request #255 from julia-vscode/unused-binding
check for unused bindings
2 parents 70437bc + ad96c7c commit 0a5cbbd

File tree

7 files changed

+205
-12
lines changed

7 files changed

+205
-12
lines changed

src/StaticLint.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ function (state::Delayed)(x::EXPR)
117117
if state.scope != s0
118118
for (k, b) in state.scope.names
119119
infer_type_by_use(b, state.server)
120+
check_unused_binding(b, state.scope)
120121
end
121122
state.scope = s0
122123
end
@@ -160,6 +161,7 @@ function semantic_pass(file, modified_expr=nothing)
160161
traverse(x, Delayed(scopeof(x), server))
161162
for (k, b) in scopeof(x).names
162163
infer_type_by_use(b, state.server)
164+
check_unused_binding(b, scopeof(x))
163165
end
164166
else
165167
traverse(x, Delayed(retrieve_delayed_scope(x), server))

src/imports.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ function add_to_imported_modules(scope::Scope, name::Symbol, val)
7878
if scope.modules isa Dict
7979
scope.modules[name] = val
8080
else
81-
modules = Dict(name => val)
81+
Dict(name => val)
8282
end
8383
end
8484
no_modules_above(s::Scope) = !CSTParser.defines_module(s.expr) || s.parent === nothing || no_modules_above(s.parent)

src/linting/checks.jl

Lines changed: 133 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ const LintCodeDescriptions = Dict{LintCodes,String}(IncorrectCallArgs => "Possib
3939
EqInIfConditional => "Unbracketed assignment in if conditional statements is not allowed, did you mean to use ==?",
4040
PointlessOR => "The first argument of a `||` call is a boolean literal.",
4141
PointlessAND => "The first argument of a `&&` call is `false`.",
42-
UnusedBinding => "The variable name has been bound but not used.",
42+
UnusedBinding => "Variable has been assigned but not used.",
4343
InvalidTypeDeclaration => "A non-DataType has been used in a type declaration statement.",
4444
UnusedTypeParameter => "A DataType parameter has been specified but not used.",
4545
IncludeLoop => "Loop detected, this file has already been included.",
@@ -571,6 +571,9 @@ function should_mark_missing_getfield_ref(x, server)
571571
# a module, we should know this.
572572
return true
573573
elseif lhsref isa Binding
574+
# by-use type inference runs after we've resolved references so we may not have known lhsref's type first time round, lets try and find `x` again
575+
resolve_getfield(x, lhsref, ResolveOnly(retrieve_scope(x), server))
576+
hasref(x) && return false # We've resolved
574577
if lhsref.val isa Binding
575578
lhsref = lhsref.val
576579
end
@@ -862,3 +865,132 @@ function check_const(x::EXPR)
862865
end
863866
end
864867
end
868+
869+
function check_unused_binding(b::Binding, scope::Scope)
870+
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)
873+
end
874+
end
875+
end
876+
877+
all_underscore(s) = false
878+
all_underscore(s::String) = all(==(0x5f), codeunits(s))
879+
880+
function is_sig_arg(x)
881+
is_in_fexpr(x, CSTParser.iscall)
882+
end
883+
884+
function is_overwritten_in_loop(x)
885+
# Cuts out false positives for check_unused_binding - the linear nature of our
886+
# semantic passes mean a variable declared at the end of a loop's block but used at
887+
# the start won't appear to be referenced.
888+
889+
# Cheap version:
890+
# is_in_fexpr(x, x -> x.head === :while || x.head === :for)
891+
892+
# We really want to check whether the enclosing scope(s) of the loop has a binding
893+
# with matching name.
894+
# Is this too expensive?
895+
loop = maybe_get_parent_fexpr(x, x -> x.head === :while || x.head === :for)
896+
if loop !== nothing
897+
s = scopeof(loop)
898+
if s isa Scope && parentof(s) isa Scope
899+
s2 = check_parent_scopes_for(s, valof(x))
900+
if s2 isa Scope
901+
prev_binding = parentof(s2).names[valof(x)]
902+
if prev_binding isa Binding
903+
s = ComesBefore(prev_binding.name, s2.expr, 0)
904+
traverse(parentof(s2).expr, s)
905+
return s.result == 1
906+
# for r in prev_binding.refs
907+
# if r isa EXPR && is_in_fexpr(r, x -> x === loop)
908+
# return true
909+
# end
910+
# end
911+
else
912+
return false
913+
end
914+
end
915+
else
916+
return false
917+
end
918+
else
919+
false
920+
end
921+
false
922+
end
923+
924+
"""
925+
ComesBefore
926+
927+
Check whether x1 comes before x2
928+
"""
929+
mutable struct ComesBefore
930+
x1::EXPR
931+
x2::EXPR
932+
result::Int
933+
end
934+
935+
function (state::ComesBefore)(x::EXPR)
936+
state.result > 0 && return
937+
if x == state.x1
938+
state.result = 1
939+
return
940+
elseif x == state.x2
941+
state.result = 2
942+
return
943+
end
944+
if !hasscope(x)
945+
traverse(x, state)
946+
state.result > 0 && return
947+
end
948+
end
949+
950+
"""
951+
check_parent_scopes_for(s::Scope, name)
952+
953+
Checks whether the parent scope of `s` has the name `name`.
954+
"""
955+
function check_parent_scopes_for(s::Scope, name)
956+
# This returns `s` rather than the parent so that s.expr can be used in the linear
957+
# search (e.g. `bound_before`)
958+
if s.expr.head !== :module && parentof(s) isa Scope && haskey(parentof(s).names, name)
959+
s
960+
elseif s.parent isa Scope
961+
check_parent_scopes_for(parentof(s), name)
962+
end
963+
end
964+
965+
966+
967+
function is_overwritten_subsequently(b::Binding, scope::Scope)
968+
valof(b.name) === nothing && return false
969+
s = BoundAfter(b.name, valof(b.name), 0)
970+
traverse(scope.expr, s)
971+
return s.result == 2
972+
end
973+
974+
"""
975+
ComesBefore
976+
977+
Check whether x1 comes before x2
978+
"""
979+
mutable struct BoundAfter
980+
x1::EXPR
981+
name::String
982+
result::Int
983+
end
984+
985+
function (state::BoundAfter)(x::EXPR)
986+
state.result > 1 && return
987+
if x == state.x1
988+
state.result = 1
989+
return
990+
end
991+
if scopeof(x) isa Scope && haskey(scopeof(x).names, state.name)
992+
state.result = 2
993+
return
994+
end
995+
traverse(x, state)
996+
end

src/references.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ end
2020
# run over the entire top-level scope.
2121
function resolve_ref(x, state)
2222
if !(parentof(x) isa EXPR && headof(parentof(x)) === :quotenode)
23-
resolved = resolve_ref(x, state.scope, state)
23+
resolve_ref(x, state.scope, state)
2424
end
2525
end
2626

src/type_inf.jl

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ function infer_type_assignment_rhs(binding, state, scope)
5454
elseif isidentifier(rhs) || is_getfield_w_quotenode(rhs)
5555
refof_rhs = isidentifier(rhs) ? refof(rhs) : refof_maybe_getfield(rhs)
5656
if refof_rhs isa Binding
57-
rhs_bind = refof(rhs)
5857
if refof_rhs.val isa SymbolServer.GenericStore && refof_rhs.val.typ isa SymbolServer.FakeTypeName
5958
settype!(binding, maybe_lookup(refof_rhs.val.typ.name, state.server))
6059
elseif refof_rhs.val isa SymbolServer.FunctionStore

src/utils.jl

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,6 @@ function iterate_over_ss_methods(b::SymbolServer.FunctionStore, tls::Scope, serv
217217
# above should be modified,
218218
rootmod = SymbolServer._lookup(b.extends.parent, getsymbolserver(server)) # points to the module containing the initial function declaration
219219
if rootmod !== nothing && haskey(rootmod, b.extends.name) # check rootmod exists, and that it has the variable
220-
rootfunc = rootmod[b.extends.name]
221220
# find extensoions
222221
if haskey(getsymbolextendeds(server), b.extends) # method extensions listed
223222
for vr in getsymbolextendeds(server)[b.extends] # iterate over packages with extensions
@@ -251,7 +250,6 @@ function iterate_over_ss_methods(b::SymbolServer.DataTypeStore, tls::Scope, serv
251250
# above should be modified,
252251
rootmod = SymbolServer._lookup(bname.parent, getsymbolserver(server), true) # points to the module containing the initial function declaration
253252
if rootmod !== nothing && haskey(rootmod, bname.name) # check rootmod exists, and that it has the variable
254-
rootfunc = rootmod[bname.name]
255253
# find extensoions
256254
if haskey(getsymbolextendeds(server), bname) # method extensions listed
257255
for vr in getsymbolextendeds(server)[bname] # iterate over packages with extensions

test/runtests.jl

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -876,7 +876,7 @@ f(arg) = arg
876876
f1
877877
f2
878878
end
879-
Base.getproperty(x::T, s) = 1
879+
Base.getproperty(x::T, s) = (x,s)
880880
f(x::T) = x.f3
881881
""")
882882
@test !StaticLint.hasref(cst.args[3].args[2].args[1].args[2].args[1])
@@ -887,7 +887,7 @@ f(arg) = arg
887887
f1
888888
f2
889889
end
890-
Base.getproperty(x::T{Int}, s) = 1
890+
Base.getproperty(x::T{Int}, s) = (x,s)
891891
f(x::T) = x.f3
892892
""")
893893
@test !StaticLint.hasref(cst.args[3].args[2].args[1].args[2].args[1])
@@ -1368,13 +1368,13 @@ f(arg) = arg
13681368
ret = "hello"
13691369
end
13701370
end""")
1371-
@test !StaticLint.haserror(cst.args[2].args[2].args[1].args[3].args[1].args[1])
1372-
@test !StaticLint.haserror(cst.args[3].args[2].args[1].args[3].args[1].args[1])
1371+
@test errorof(cst.args[2].args[2].args[1].args[3].args[1].args[1]) !== StaticLint.InvalidRedefofConst
1372+
@test errorof(cst.args[3].args[2].args[1].args[3].args[1].args[1]) !== StaticLint.InvalidRedefofConst
13731373
end
13741374

13751375
if VERSION > v"1.5-"
13761376
@testset "issue #210" begin
1377-
cst = parse_and_pass("""h()::@NamedTuple{a::Int,b::String} = (a=1, b = "s")""")
1377+
cst = parse_and_pass("""h()::@NamedTuple{a::Int,b::String} = ()""")
13781378
@test isempty(StaticLint.collect_hints(cst, server))
13791379
end
13801380
end
@@ -1515,6 +1515,69 @@ end
15151515
@test isempty(StaticLint.collect_hints(cst, server))
15161516
end
15171517

1518+
@testset "unused bindings" begin
1519+
cst = parse_and_pass("""
1520+
function f(arg, arg2)
1521+
arg*arg2
1522+
arg3 = 1
1523+
end
1524+
""")
1525+
@test errorof(cst[1][3][2][1]) !== nothing
1526+
1527+
cst = parse_and_pass("""
1528+
function f()
1529+
arg = false
1530+
while arg
1531+
if arg
1532+
end
1533+
arg = true
1534+
end
1535+
end
1536+
""")
1537+
@test isempty(StaticLint.collect_hints(cst, server))
1538+
1539+
cst = parse_and_pass("""
1540+
function f(arg)
1541+
arg
1542+
while true
1543+
arg = 1
1544+
end
1545+
end
1546+
""")
1547+
@test isempty(StaticLint.collect_hints(cst, server))
1548+
1549+
cst = parse_and_pass("""
1550+
function f(arg)
1551+
arg
1552+
while true
1553+
while true
1554+
arg = 1
1555+
end
1556+
end
1557+
end
1558+
""")
1559+
@test isempty(StaticLint.collect_hints(cst, server))
1560+
1561+
cst = parse_and_pass("""
1562+
function f()
1563+
(a = 1, b = 2)
1564+
end
1565+
""")
1566+
@test isempty(StaticLint.collect_hints(cst, server))
1567+
1568+
cst = parse_and_pass("""
1569+
function f()
1570+
arg = 0
1571+
if 1
1572+
while true
1573+
arg = 1
1574+
end
1575+
end
1576+
end
1577+
""")
1578+
@test isempty(StaticLint.collect_hints(cst, server))
1579+
end
1580+
15181581
@testset "unwrap sig" begin
15191582
cst = parse_and_pass("""
15201583
function multiply!(x::T, y::Integer) where {T} end
@@ -1530,7 +1593,6 @@ end
15301593

15311594
@test StaticLint.haserror(parse_and_pass("function f(z::T)::Nothing where T end")[1].args[1].args[1].args[1].args[2])
15321595
@test StaticLint.haserror(parse_and_pass("function f(z::T) where T end")[1].args[1].args[1].args[2])
1533-
15341596
end
15351597

15361598
@testset "clear .type refs" begin

0 commit comments

Comments
 (0)