Skip to content

Commit 556bb79

Browse files
authored
Merge pull request #338 from julia-vscode/sp/loop-over-length-check
LoopOverLength check
2 parents 86eff5d + 683aea0 commit 556bb79

File tree

2 files changed

+206
-44
lines changed

2 files changed

+206
-44
lines changed

src/linting/checks.jl

Lines changed: 101 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,40 @@
1-
@enum(LintCodes,
2-
MissingRef,
3-
IncorrectCallArgs,
4-
IncorrectIterSpec,
5-
NothingEquality,
6-
NothingNotEq,
7-
ConstIfCondition,
8-
EqInIfConditional,
9-
PointlessOR,
10-
PointlessAND,
11-
UnusedBinding,
12-
InvalidTypeDeclaration,
13-
UnusedTypeParameter,
14-
IncludeLoop,
15-
MissingFile,
16-
InvalidModuleName,
17-
TypePiracy,
18-
UnusedFunctionArgument,
19-
CannotDeclareConst,
20-
InvalidRedefofConst,
21-
NotEqDef,
22-
KwDefaultMismatch,
23-
InappropriateUseOfLiteral,
24-
ShouldBeInALoop,
25-
TypeDeclOnGlobalVariable,
26-
UnsupportedConstLocalVariable,
27-
UnassignedKeywordArgument,
28-
CannotDefineFuncAlreadyHasValue,
29-
DuplicateFuncArgName,
30-
IncludePathContainsNULL)
31-
32-
33-
34-
const LintCodeDescriptions = Dict{LintCodes,String}(IncorrectCallArgs => "Possible method call error.",
1+
@enum(
2+
LintCodes,
3+
4+
MissingRef,
5+
IncorrectCallArgs,
6+
IncorrectIterSpec,
7+
NothingEquality,
8+
NothingNotEq,
9+
ConstIfCondition,
10+
EqInIfConditional,
11+
PointlessOR,
12+
PointlessAND,
13+
UnusedBinding,
14+
InvalidTypeDeclaration,
15+
UnusedTypeParameter,
16+
IncludeLoop,
17+
MissingFile,
18+
InvalidModuleName,
19+
TypePiracy,
20+
UnusedFunctionArgument,
21+
CannotDeclareConst,
22+
InvalidRedefofConst,
23+
NotEqDef,
24+
KwDefaultMismatch,
25+
InappropriateUseOfLiteral,
26+
ShouldBeInALoop,
27+
TypeDeclOnGlobalVariable,
28+
UnsupportedConstLocalVariable,
29+
UnassignedKeywordArgument,
30+
CannotDefineFuncAlreadyHasValue,
31+
DuplicateFuncArgName,
32+
IncludePathContainsNULL,
33+
IndexFromLength
34+
)
35+
36+
const LintCodeDescriptions = Dict{LintCodes,String}(
37+
IncorrectCallArgs => "Possible method call error.",
3538
IncorrectIterSpec => "A loop iterator has been used that will likely error.",
3639
NothingEquality => "Compare against `nothing` using `===`",
3740
NothingNotEq => "Compare against `nothing` using `!==`",
@@ -58,8 +61,9 @@ const LintCodeDescriptions = Dict{LintCodes,String}(IncorrectCallArgs => "Possib
5861
UnassignedKeywordArgument => "Keyword argument not assigned.",
5962
CannotDefineFuncAlreadyHasValue => "Cannot define function ; it already has a value.",
6063
DuplicateFuncArgName => "Function argument name not unique.",
61-
IncludePathContainsNULL => "Cannot include file, path cotains NULL characters."
62-
)
64+
IncludePathContainsNULL => "Cannot include file, path contains NULL characters.",
65+
IndexFromLength => "Indexing with indices obtained from `length`, `size` etc is discouraged. Use `eachindex` or `axes` instead."
66+
)
6367

6468
haserror(m::Meta) = m.error !== nothing
6569
haserror(x::EXPR) = hasmeta(x) && haserror(x.meta)
@@ -343,22 +347,75 @@ isdocumented(x::EXPR) = parentof(x) isa EXPR && CSTParser.ismacrocall(parentof(x
343347

344348
function check_loop_iter(x::EXPR, env::ExternalEnv)
345349
if headof(x) === :for
346-
if length(x.args) > 0 && CSTParser.is_range(x.args[1])
347-
rng = rhs_of_iterator(x.args[1])
348-
if headof(rng) === :FLOAT || headof(rng) === :INTEGER || (iscall(rng) && refof(rng.args[1]) === getsymbols(env)[:Base][:length])
349-
seterror!(x.args[1], IncorrectIterSpec)
350+
if length(x.args) > 1
351+
body = x.args[2]
352+
if headof(x.args[1]) === :block && x.args[1].args !== nothing
353+
for arg in x.args[1].args
354+
check_incorrect_iter_spec(arg, body, env)
355+
end
356+
else
357+
check_incorrect_iter_spec(x.args[1], body, env)
350358
end
351359
end
352360
elseif headof(x) === :generator
361+
body = x.args[1]
353362
for i = 2:length(x.args)
354-
if CSTParser.is_range(x.args[i])
355-
rng = rhs_of_iterator(x.args[i])
356-
if headof(rng) === :FLOAT || headof(rng) === :INTEGER || (iscall(rng) && refof(rng.args[1]) === getsymbols(env)[:Base][:length])
357-
seterror!(x.args[i], IncorrectIterSpec)
363+
check_incorrect_iter_spec(x.args[i], body, env)
364+
end
365+
end
366+
end
367+
368+
function check_incorrect_iter_spec(x, body, env)
369+
if x.args !== nothing && CSTParser.is_range(x)
370+
rng = rhs_of_iterator(x)
371+
372+
if headof(rng) === :FLOAT || headof(rng) === :INTEGER || (iscall(rng) && refof(rng.args[1]) === getsymbols(env)[:Base][:length])
373+
seterror!(x, IncorrectIterSpec)
374+
elseif iscall(rng) && valof(rng.args[1]) == ":" &&
375+
length(rng.args) === 3 &&
376+
headof(rng.args[2]) === :INTEGER &&
377+
iscall(rng.args[3]) &&
378+
length(rng.args[3].args) > 1 && (
379+
refof(rng.args[3].args[1]) === getsymbols(env)[:Base][:length] ||
380+
refof(rng.args[3].args[1]) === getsymbols(env)[:Base][:size]
381+
)
382+
if length(x.args) >= 1
383+
lhs = x.args[1]
384+
arr = rng.args[3].args[2]
385+
b = refof(arr)
386+
387+
# 1:length(arr) indexing is ok for Vector and Array specifically
388+
if b isa Binding && (CoreTypes.isarray(b.type) || CoreTypes.isvector(b.type))
389+
return
390+
end
391+
if !all_underscore(valof(lhs))
392+
if check_is_used_in_getindex(body, lhs, arr)
393+
seterror!(x, IndexFromLength)
394+
end
395+
end
396+
end
397+
end
398+
end
399+
end
400+
401+
function check_is_used_in_getindex(expr, lhs, arr)
402+
if headof(expr) === :ref && expr.args !== nothing && length(expr.args) > 1
403+
this_arr = expr.args[1]
404+
if hasref(this_arr) && hasref(arr) && refof(this_arr) == refof(arr)
405+
for index_arg in expr.args[2:end]
406+
if hasref(index_arg) && hasref(lhs) && refof(index_arg) == refof(lhs)
407+
seterror!(expr, IndexFromLength)
408+
return true
358409
end
359410
end
360411
end
361412
end
413+
if expr.args !== nothing
414+
for arg in expr.args
415+
check_is_used_in_getindex(arg, lhs, arr) && return true
416+
end
417+
end
418+
return false
362419
end
363420

364421
function check_nothing_equality(x::EXPR, env::ExternalEnv)

test/runtests.jl

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1840,3 +1840,108 @@ end
18401840
@test length(b.refs) == 2
18411841
end
18421842
end
1843+
1844+
@testset "iteration over 1:length(...)" begin
1845+
cst = parse_and_pass("arr = []; [1 for _ in 1:length(arr)]")
1846+
@test isempty(StaticLint.collect_hints(cst, server))
1847+
cst = parse_and_pass("arr = []; [arr[i] for i in 1:length(arr)]")
1848+
@test length(StaticLint.collect_hints(cst, server)) == 2
1849+
cst = parse_and_pass("arr = []; [i for i in 1:length(arr)]")
1850+
@test length(StaticLint.collect_hints(cst, server)) == 0
1851+
1852+
cst = parse_and_pass("""
1853+
arr = []
1854+
for _ in 1:length(arr)
1855+
end
1856+
""")
1857+
@test isempty(StaticLint.collect_hints(cst, server))
1858+
cst = parse_and_pass("""
1859+
arr = []
1860+
for i in 1:length(arr)
1861+
arr[i]
1862+
end
1863+
""")
1864+
@test length(StaticLint.collect_hints(cst, server)) == 2
1865+
cst = parse_and_pass("""
1866+
arr = []
1867+
for i in 1:length(arr)
1868+
println(i)
1869+
end
1870+
""")
1871+
@test length(StaticLint.collect_hints(cst, server)) == 0
1872+
1873+
cst = parse_and_pass("""
1874+
arr = []
1875+
for _ in 1:length(arr), _ in 1:length(arr)
1876+
end
1877+
""")
1878+
@test isempty(StaticLint.collect_hints(cst, server))
1879+
cst = parse_and_pass("""
1880+
arr = []
1881+
for i in 1:length(arr), j in 1:length(arr)
1882+
arr[i] + arr[j]
1883+
end
1884+
""")
1885+
@test length(StaticLint.collect_hints(cst, server)) == 4
1886+
cst = parse_and_pass("""
1887+
arr = []
1888+
for i in 1:length(arr), j in 1:length(arr)
1889+
println(i + j)
1890+
end
1891+
""")
1892+
@test length(StaticLint.collect_hints(cst, server)) == 0
1893+
1894+
cst = parse_and_pass("""
1895+
function f(arr::Vector)
1896+
for i in 1:length(arr), j in 1:length(arr)
1897+
arr[i] + arr[j]
1898+
end
1899+
end
1900+
""")
1901+
@test length(StaticLint.collect_hints(cst, server)) == 0
1902+
1903+
cst = parse_and_pass("""
1904+
function f(arr::Array)
1905+
for i in 1:length(arr), j in 1:length(arr)
1906+
arr[i] + arr[j]
1907+
end
1908+
end
1909+
""")
1910+
@test length(StaticLint.collect_hints(cst, server)) == 0
1911+
1912+
cst = parse_and_pass("""
1913+
function f(arr::Matrix)
1914+
for i in 1:length(arr), j in 1:length(arr)
1915+
arr[i] + arr[j]
1916+
end
1917+
end
1918+
""")
1919+
@test length(StaticLint.collect_hints(cst, server)) == 0
1920+
1921+
cst = parse_and_pass("""
1922+
function f(arr::Array{T,N}) where T where N
1923+
for i in 1:length(arr), j in 1:length(arr)
1924+
arr[i] + arr[j]
1925+
end
1926+
end
1927+
""")
1928+
@test length(StaticLint.collect_hints(cst, server)) == 0
1929+
1930+
cst = parse_and_pass("""
1931+
function f(arr::AbstractArray)
1932+
for i in 1:length(arr), j in 1:length(arr)
1933+
arr[i] + arr[j]
1934+
end
1935+
end
1936+
""")
1937+
@test length(StaticLint.collect_hints(cst, server)) == 4
1938+
1939+
cst = parse_and_pass("""
1940+
function f(arr)
1941+
for i in 1:length(arr), j in 1:length(arr)
1942+
arr[i] + arr[j]
1943+
end
1944+
end
1945+
""")
1946+
@test length(StaticLint.collect_hints(cst, server)) == 4
1947+
end

0 commit comments

Comments
 (0)