Skip to content

Commit cc467dd

Browse files
authored
Merge pull request #34 from arichard4/circular_ref
2 parents 17ec37a + a24332e commit cc467dd

File tree

4 files changed

+120
-51
lines changed

4 files changed

+120
-51
lines changed

spec/cli_spec.lua

Lines changed: 51 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -191,19 +191,20 @@ Checking spec/samples/bad_code.lua 5 warnings
191191
spec/samples/bad_code.lua:8:10: variable 'opt' was previously defined as an argument on line 7
192192
spec/samples/bad_code.lua:9:11: accessing undefined variable 'hepler'
193193
194-
Checking spec/samples/unused_code.lua 9 warnings
195-
196-
spec/samples/unused_code.lua:3:18: unused argument 'baz'
197-
spec/samples/unused_code.lua:4:8: unused loop variable 'i'
198-
spec/samples/unused_code.lua:5:13: unused variable 'q'
199-
spec/samples/unused_code.lua:7:11: unused loop variable 'a'
200-
spec/samples/unused_code.lua:7:14: unused loop variable 'b'
201-
spec/samples/unused_code.lua:7:17: unused loop variable 'c'
202-
spec/samples/unused_code.lua:13:7: value assigned to variable 'x' is overwritten on line 14 before use
203-
spec/samples/unused_code.lua:14:1: value assigned to variable 'x' is overwritten on line 15 before use
204-
spec/samples/unused_code.lua:21:7: variable 'z' is never accessed
205-
206-
Total: 14 warnings / 0 errors in 3 files
194+
Checking spec/samples/unused_code.lua 10 warnings
195+
196+
spec/samples/unused_code.lua:2:7: variable 'foo' is mutated but never accessed
197+
spec/samples/unused_code.lua:4:18: unused argument 'baz'
198+
spec/samples/unused_code.lua:5:8: unused loop variable 'i'
199+
spec/samples/unused_code.lua:6:13: unused variable 'q'
200+
spec/samples/unused_code.lua:8:11: unused loop variable 'a'
201+
spec/samples/unused_code.lua:8:14: unused loop variable 'b'
202+
spec/samples/unused_code.lua:8:17: unused loop variable 'c'
203+
spec/samples/unused_code.lua:21:7: value assigned to variable 'x' is overwritten on line 22 before use
204+
spec/samples/unused_code.lua:22:1: value assigned to variable 'x' is overwritten on line 23 before use
205+
spec/samples/unused_code.lua:29:7: variable 'z' is never accessed
206+
207+
Total: 15 warnings / 0 errors in 3 files
207208
]], get_output "-q spec/samples/bad_code.lua spec/samples/good_code.lua spec/samples/unused_code.lua --no-config")
208209
assert.equal([[
209210
Total: 0 warnings / 0 errors in 1 file
@@ -213,14 +214,14 @@ Total: 0 warnings / 0 errors in 1 file
213214
it("suppresses warnings output with -qq", function()
214215
assert.equal([[
215216
Checking spec/samples/bad_code.lua 5 warnings
216-
Checking spec/samples/unused_code.lua 9 warnings
217+
Checking spec/samples/unused_code.lua 10 warnings
217218
218-
Total: 14 warnings / 0 errors in 3 files
219+
Total: 15 warnings / 0 errors in 3 files
219220
]], get_output "-qq spec/samples/bad_code.lua spec/samples/good_code.lua spec/samples/unused_code.lua --no-config")
220221
end)
221222

222223
it("suppresses file info output with -qqq", function()
223-
assert.equal([[Total: 14 warnings / 0 errors in 3 files
224+
assert.equal([[Total: 15 warnings / 0 errors in 3 files
224225
]], get_output "-qqq spec/samples/bad_code.lua spec/samples/good_code.lua spec/samples/unused_code.lua --no-config")
225226
end)
226227

@@ -318,32 +319,34 @@ Total: 1 warning / 0 errors in 1 file
318319

319320
it("recognizes different types of variables", function()
320321
assert.equal([[
321-
Checking spec/samples/unused_code.lua 9 warnings
322+
Checking spec/samples/unused_code.lua 10 warnings
322323
323-
spec/samples/unused_code.lua:3:18: unused argument 'baz'
324-
spec/samples/unused_code.lua:4:8: unused loop variable 'i'
325-
spec/samples/unused_code.lua:5:13: unused variable 'q'
326-
spec/samples/unused_code.lua:7:11: unused loop variable 'a'
327-
spec/samples/unused_code.lua:7:14: unused loop variable 'b'
328-
spec/samples/unused_code.lua:7:17: unused loop variable 'c'
329-
spec/samples/unused_code.lua:13:7: value assigned to variable 'x' is overwritten on line 14 before use
330-
spec/samples/unused_code.lua:14:1: value assigned to variable 'x' is overwritten on line 15 before use
331-
spec/samples/unused_code.lua:21:7: variable 'z' is never accessed
324+
spec/samples/unused_code.lua:2:7: variable 'foo' is mutated but never accessed
325+
spec/samples/unused_code.lua:4:18: unused argument 'baz'
326+
spec/samples/unused_code.lua:5:8: unused loop variable 'i'
327+
spec/samples/unused_code.lua:6:13: unused variable 'q'
328+
spec/samples/unused_code.lua:8:11: unused loop variable 'a'
329+
spec/samples/unused_code.lua:8:14: unused loop variable 'b'
330+
spec/samples/unused_code.lua:8:17: unused loop variable 'c'
331+
spec/samples/unused_code.lua:21:7: value assigned to variable 'x' is overwritten on line 22 before use
332+
spec/samples/unused_code.lua:22:1: value assigned to variable 'x' is overwritten on line 23 before use
333+
spec/samples/unused_code.lua:29:7: variable 'z' is never accessed
332334
333-
Total: 9 warnings / 0 errors in 1 file
335+
Total: 10 warnings / 0 errors in 1 file
334336
]], get_output "spec/samples/unused_code.lua --no-config")
335337
end)
336338

337339
it("allows to ignore unused arguments", function()
338340
assert.equal([[
339-
Checking spec/samples/unused_code.lua 4 warnings
341+
Checking spec/samples/unused_code.lua 5 warnings
340342
341-
spec/samples/unused_code.lua:5:13: unused variable 'q'
342-
spec/samples/unused_code.lua:13:7: value assigned to variable 'x' is overwritten on line 14 before use
343-
spec/samples/unused_code.lua:14:1: value assigned to variable 'x' is overwritten on line 15 before use
344-
spec/samples/unused_code.lua:21:7: variable 'z' is never accessed
343+
spec/samples/unused_code.lua:2:7: variable 'foo' is mutated but never accessed
344+
spec/samples/unused_code.lua:6:13: unused variable 'q'
345+
spec/samples/unused_code.lua:21:7: value assigned to variable 'x' is overwritten on line 22 before use
346+
spec/samples/unused_code.lua:22:1: value assigned to variable 'x' is overwritten on line 23 before use
347+
spec/samples/unused_code.lua:29:7: variable 'z' is never accessed
345348
346-
Total: 4 warnings / 0 errors in 1 file
349+
Total: 5 warnings / 0 errors in 1 file
347350
]], get_output "spec/samples/unused_code.lua --no-unused-args --no-config")
348351
end)
349352

@@ -1159,13 +1162,13 @@ Checking spec/samples/unused_secondaries.lua 1 warning
11591162
11601163
Checking spec/samples/unused_code.lua 7 warnings
11611164
1162-
spec/samples/unused_code.lua:3:18: unused argument 'baz'
1163-
spec/samples/unused_code.lua:4:8: unused loop variable 'i'
1164-
spec/samples/unused_code.lua:7:11: unused loop variable 'a'
1165-
spec/samples/unused_code.lua:7:14: unused loop variable 'b'
1166-
spec/samples/unused_code.lua:7:17: unused loop variable 'c'
1167-
spec/samples/unused_code.lua:13:7: value assigned to variable 'x' is overwritten on line 14 before use
1168-
spec/samples/unused_code.lua:14:1: value assigned to variable 'x' is overwritten on line 15 before use
1165+
spec/samples/unused_code.lua:4:18: unused argument 'baz'
1166+
spec/samples/unused_code.lua:5:8: unused loop variable 'i'
1167+
spec/samples/unused_code.lua:8:11: unused loop variable 'a'
1168+
spec/samples/unused_code.lua:8:14: unused loop variable 'b'
1169+
spec/samples/unused_code.lua:8:17: unused loop variable 'c'
1170+
spec/samples/unused_code.lua:21:7: value assigned to variable 'x' is overwritten on line 22 before use
1171+
spec/samples/unused_code.lua:22:1: value assigned to variable 'x' is overwritten on line 23 before use
11691172
11701173
Total: 8 warnings / 0 errors in 2 files
11711174
]], get_output "spec/samples/unused_secondaries.lua spec/samples/unused_code.lua --config=spec/configs/multioverride_config.luacheckrc")
@@ -1182,15 +1185,15 @@ Checking spec/samples/bad_code.lua 4 warnings
11821185
11831186
Checking spec/samples/unused_code.lua 1 warning
11841187
1185-
spec/samples/unused_code.lua:5:13: unused variable 'q'
1188+
spec/samples/unused_code.lua:6:13: unused variable 'q'
11861189
11871190
Total: 5 warnings / 0 errors in 2 files
11881191
]], get_output "spec/samples/bad_code.lua spec/samples/unused_code.lua --config=spec/configs/override_config.luacheckrc --enable=211")
11891192
end)
11901193

11911194
it("allows using cli-specific options in top level config", function()
11921195
assert.equal([[Files: 2
1193-
Warnings: 14
1196+
Warnings: 15
11941197
Errors: 0
11951198
Quiet: 0
11961199
Color: false
@@ -1213,12 +1216,12 @@ Checking spec/samples/read_globals.lua 5 warnings
12131216
Checking spec/samples/read_globals_inline_options.lua 3 warnings
12141217
Checking spec/samples/redefined.lua 7 warnings
12151218
Checking spec/samples/reversed_fornum.lua 1 warning
1216-
Checking spec/samples/unused_code.lua 9 warnings
1219+
Checking spec/samples/unused_code.lua 10 warnings
12171220
Checking spec/samples/unused_secondaries.lua 4 warnings
12181221
Checking spec/samples/utf8.lua 4 warnings
12191222
Checking spec/samples/utf8_error.lua 1 error
12201223
1221-
Total: 72 warnings / 5 errors in 19 files
1224+
Total: 73 warnings / 5 errors in 19 files
12221225
]]):gsub("(spec/samples)/", "%1"..package.config:sub(1, 1)),
12231226
get_output "spec/samples --config=spec/configs/exclude_files_config.luacheckrc -qq --exclude-files spec/samples/global_fields.lua")
12241227
end)
@@ -1238,12 +1241,12 @@ Checking read_globals.lua 5 warnings
12381241
Checking read_globals_inline_options.lua 3 warnings
12391242
Checking redefined.lua 7 warnings
12401243
Checking reversed_fornum.lua 1 warning
1241-
Checking unused_code.lua 9 warnings
1244+
Checking unused_code.lua 10 warnings
12421245
Checking unused_secondaries.lua 4 warnings
12431246
Checking utf8.lua 4 warnings
12441247
Checking utf8_error.lua 1 error
12451248
1246-
Total: 72 warnings / 5 errors in 19 files
1249+
Total: 73 warnings / 5 errors in 19 files
12471250
]], get_output(". --config=spec/configs/exclude_files_config.luacheckrc -qq --exclude-files global_fields.lua", "spec/samples/"))
12481251
end)
12491252

@@ -1260,12 +1263,12 @@ Checking line_length.lua 8 warnings
12601263
Checking python_code.lua 1 error
12611264
Checking redefined.lua 7 warnings
12621265
Checking reversed_fornum.lua 1 warning
1263-
Checking unused_code.lua 9 warnings
1266+
Checking unused_code.lua 10 warnings
12641267
Checking unused_secondaries.lua 4 warnings
12651268
Checking utf8.lua 4 warnings
12661269
Checking utf8_error.lua 1 error
12671270
1268-
Total: 64 warnings / 5 errors in 17 files
1271+
Total: 65 warnings / 5 errors in 17 files
12691272
]], get_output(". --config=spec/configs/exclude_files_config.luacheckrc -qq --exclude-files global_fields.lua --exclude-files " .. quote("./read*"), "spec/samples/"))
12701273
end)
12711274

spec/samples/bad_code.lua

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,3 @@ function embrace(opt)
88
local opt = opt or "default"
99
return hepler(opt.."?")
1010
end
11-

spec/samples/unused_code.lua

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
-- Foo is unused- only accessed circularly
12
local foo = {}
23

34
function foo.bar(baz)
@@ -10,6 +11,13 @@ function foo.bar(baz)
1011
end
1112
end
1213

14+
foo[foo] = 1
15+
foo[1] = foo
16+
foo[foo] = foo
17+
foo.meta = function()
18+
return function() print(foo) end
19+
end
20+
1321
local x = 5
1422
x = 6
1523
x = 7; print(x)
@@ -21,3 +29,18 @@ y = 6
2129
local z = 5;
2230
(function() z = 4 end)()
2331
z = 6
32+
33+
-- Function call: RHS of the assignment 3 lines down isn't *only* a circular reference
34+
local t = {}
35+
function t.func() print(t) return {val = 1} end
36+
t[t] = t.func().val + 1
37+
38+
-- Method call: RHS of the assignment 3 lines down isn't *only* a circular reference
39+
local s = {}
40+
function s:func() print(self) return {val = 1} end
41+
s[s] = s:func().val + 1
42+
43+
-- False negative: luacheck can't (yet) track more complicated function assignments
44+
local q = {}
45+
local function func() print(q) end
46+
q.func = func

src/luacheck/stages/resolve_locals.lua

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,46 @@ local function in_scope(var, index)
7878
return (var.scope_start <= index) and (index <= var.scope_end)
7979
end
8080

81+
local function contains_call(node)
82+
if node.tag == "Call" or node.tag == "Invoke" then
83+
return true
84+
end
85+
86+
if node.tag ~= "Function" then
87+
for _, sub_node in ipairs(node) do
88+
if type(sub_node) == 'table' and contains_call(sub_node) then
89+
return true
90+
end
91+
end
92+
end
93+
94+
return false
95+
end
96+
97+
local function is_circular_reference(item, var)
98+
-- No support for matching multiple assignment to the specific assignment
99+
if not item.lhs or #item.lhs ~= 1 or not item.rhs or #item.rhs ~= 1 then
100+
return false
101+
end
102+
103+
-- Case t[t.function()] = t.func()
104+
-- Functions can have side-effects, so this isn't purely circular
105+
local right_assignment = item.rhs[1]
106+
if contains_call(right_assignment) then
107+
return false
108+
end
109+
110+
local left_assignment = item.lhs[1]
111+
if contains_call(left_assignment) then
112+
return false
113+
end
114+
local node = left_assignment[1]
115+
if node.var == var then
116+
return true
117+
end
118+
return false
119+
end
120+
81121
-- Called when main assignment propagation reaches a line item.
82122
local function main_assignment_propagation_callback(line, index, item, var, value)
83123
-- Check entrance condition.
@@ -91,7 +131,9 @@ local function main_assignment_propagation_callback(line, index, item, var, valu
91131

92132
-- Accesses (and mutations) of the variable can resolve to reaching assignment.
93133
if item.accesses and item.accesses[var] then
94-
add_resolution(line, item, var, value)
134+
if not is_circular_reference(item, var) then
135+
add_resolution(line, item, var, value)
136+
end
95137
end
96138

97139
if item.mutations and item.mutations[var] then
@@ -102,7 +144,9 @@ local function main_assignment_propagation_callback(line, index, item, var, valu
102144
-- can resolve to reaching assignment.
103145
if item.lines then
104146
for _, created_line in ipairs(item.lines) do
105-
add_resolutions(created_line, created_line.accessed_upvalues[var], var, value)
147+
if not is_circular_reference(item, var) then
148+
add_resolutions(created_line, created_line.accessed_upvalues[var], var, value)
149+
end
106150
add_resolutions(created_line, created_line.mutated_upvalues[var], var, value, true)
107151
end
108152
end

0 commit comments

Comments
 (0)