Skip to content

Commit 89b4eb7

Browse files
committed
Add tests and tweak behavior of missing end/until location guessing
Do report locations earlier than original error opening tokens, but not within well balanced nested blocks in main chunk (inserting a missing token there always causes an error).
1 parent b49ab65 commit 89b4eb7

File tree

2 files changed

+261
-20
lines changed

2 files changed

+261
-20
lines changed

spec/parser_spec.lua

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1017,6 +1017,231 @@ end
10171017
end)
10181018
end)
10191019

1020+
describe("indentation-based missing until/end location guessing", function()
1021+
it("provides a better location on the same indentation level for missing end", function()
1022+
assert.same({line = 11, offset = 145, end_offset = 150, prev_line = 2, prev_offset = 23, prev_end_offset = 24,
1023+
msg = "expected 'end' (to close 'if' on line 2) near 'whoops' (indentation-based guess)"}, get_error([[
1024+
local function f()
1025+
if cond then
1026+
do_thing()
1027+
1028+
do_more_things()
1029+
1030+
while true do
1031+
things_keep_happening()
1032+
end
1033+
1034+
whoops()
1035+
end
1036+
]]))
1037+
1038+
assert.same({line = 10, offset = 131, end_offset = 136, prev_line = 7, prev_offset = 84, prev_end_offset = 89,
1039+
msg = "expected 'until' (to close 'repeat' on line 7) near 'whoops' (indentation-based guess)"
1040+
}, get_error([[
1041+
local function f()
1042+
if cond then
1043+
do_thing()
1044+
1045+
do_more_things()
1046+
1047+
repeat
1048+
things_keep_happening()
1049+
1050+
whoops()
1051+
end
1052+
]]))
1053+
assert.same({line = 8, offset = 64, end_offset = 68, prev_line = 5, prev_offset = 41, prev_end_offset = 48,
1054+
msg = "expected 'end' (to close 'function' on line 5) near 'local' (indentation-based guess)"
1055+
}, get_error([[
1056+
local function f()
1057+
good()
1058+
end
1059+
1060+
local function g()
1061+
bad()
1062+
1063+
local function t()
1064+
irrelevant()
1065+
end
1066+
]]))
1067+
1068+
assert.same({line = 9, offset = 56, end_offset = 65, prev_line = 4, prev_offset = 15, prev_end_offset = 16,
1069+
msg = "expected 'end' (to close 'do' on line 4) near 'two_things' (indentation-based guess)"
1070+
}, get_error([[
1071+
do end
1072+
do
1073+
end
1074+
do
1075+
do end
1076+
do
1077+
end
1078+
one_thing()
1079+
two_things()
1080+
]]))
1081+
1082+
assert.same({line = 8, offset = 91, end_offset = 92, prev_line = 3, prev_offset = 16, prev_end_offset = 20,
1083+
msg = "expected 'end' (to close 'while' on line 3) near 'if' (indentation-based guess)"
1084+
}, get_error([[
1085+
do
1086+
do
1087+
while cond
1088+
do
1089+
thing = thing
1090+
another = thing
1091+
1092+
if yes then end
1093+
end
1094+
end
1095+
]]))
1096+
1097+
assert.same({line = 6, offset = 117, end_offset = 125, prev_line = 3, prev_offset = 74, prev_end_offset = 76,
1098+
msg = "expected 'end' (to close 'for' on line 3) near 'something' (indentation-based guess)"
1099+
}, get_error([[
1100+
function g()
1101+
for i in ipairs("this is not even an error...") do
1102+
for i = 1, 2, 3 do
1103+
thing()
1104+
1105+
something = smth
1106+
end
1107+
]]))
1108+
end)
1109+
1110+
it("provides a better location on a lower indentation level for missing end", function()
1111+
assert.same({line = 5, offset = 36, end_offset = 38, prev_line = 2, prev_offset = 7, prev_end_offset = 11,
1112+
msg = "expected 'end' (to close 'while' on line 2) near less indented 'end' (indentation-based guess)"
1113+
}, get_error([[
1114+
do
1115+
while true do
1116+
thing()
1117+
1118+
end
1119+
]]))
1120+
1121+
assert.same({line = 5, offset = 51, end_offset = 51, prev_line = 2, prev_offset = 7, prev_end_offset = 11,
1122+
msg = "expected 'end' (to close 'while' on line 2) near 'a' (indentation-based guess)"
1123+
}, get_error([[
1124+
do
1125+
while true do
1126+
thing()
1127+
more()
1128+
a()
1129+
]]))
1130+
end)
1131+
1132+
it("provides a better location for various configurations of if statements", function()
1133+
assert.same({line = 6, offset = 67, end_offset = 69, prev_line = 2, prev_offset = 7, prev_end_offset = 8,
1134+
msg = "expected 'end' (to close 'if' on line 2) near less indented 'end' (indentation-based guess)"
1135+
}, get_error([[
1136+
do
1137+
if thing({
1138+
long, long, long, line}) then
1139+
something()
1140+
1141+
end
1142+
]]))
1143+
1144+
assert.same({line = 7, offset = 66, end_offset = 66, prev_line = 4, prev_offset = 43, prev_end_offset = 46,
1145+
msg = "expected 'end' (to close 'else' on line 4) near 'a' (indentation-based guess)"
1146+
}, get_error([[
1147+
do
1148+
if cond() then
1149+
something()
1150+
else
1151+
thing()
1152+
1153+
a = b
1154+
end
1155+
]]))
1156+
1157+
assert.same({line = 6, offset = 66, end_offset = 68, prev_line = 4, prev_offset = 43, prev_end_offset = 48,
1158+
msg = "expected 'end' (to close 'elseif' on line 4) near less indented 'end' (indentation-based guess)"
1159+
}, get_error([[
1160+
do
1161+
if cond() then
1162+
something()
1163+
elseif something then
1164+
1165+
end
1166+
]]))
1167+
1168+
assert.same({line = 10, offset = 119, end_offset = 119, prev_line = 8, prev_offset = 99, prev_end_offset = 104,
1169+
msg = "expected 'end' (to close 'elseif' on line 8) near 'e' (indentation-based guess)"
1170+
}, get_error([[
1171+
do
1172+
if cond() then
1173+
s()
1174+
elseif something then
1175+
b()
1176+
elseif a() then
1177+
c()
1178+
elseif d() then
1179+
1180+
e()
1181+
end
1182+
]]))
1183+
end)
1184+
1185+
it("reports the first guess location outside complete blocks", function()
1186+
assert.same({line = 12, offset = 92, end_offset = 98, prev_line = 10, prev_offset = 61, prev_end_offset = 65,
1187+
msg = "expected 'end' (to close 'while' on line 10) near 'another' (indentation-based guess)"
1188+
}, get_error([[
1189+
do
1190+
while true do
1191+
thing()
1192+
1193+
another()
1194+
end
1195+
end
1196+
1197+
do
1198+
while true do
1199+
thing()
1200+
another()
1201+
end
1202+
1203+
do
1204+
while true do
1205+
thing()
1206+
another()
1207+
end
1208+
]]))
1209+
end)
1210+
1211+
it("does not report blocks with different closing token comparing to original error", function()
1212+
assert.same({line = 10, offset = 87, end_offset = 91, prev_line = 8, prev_offset = 60, prev_end_offset = 65,
1213+
msg = "expected 'until' (to close 'repeat' on line 8) near less indented 'until' (indentation-based guess)"
1214+
}, get_error([[
1215+
do
1216+
while true do
1217+
thing()
1218+
1219+
a()
1220+
1221+
repeat
1222+
repeat
1223+
thing()
1224+
until cond
1225+
end
1226+
]]))
1227+
1228+
assert.same({line = 8, offset = 58, end_offset = 63, prev_line = 5, prev_offset = 30, prev_end_offset = 31,
1229+
msg = "expected 'end' (to close 'do' on line 5) near 'thing3' (indentation-based guess)"
1230+
}, get_error([[
1231+
repeat
1232+
thing1()
1233+
1234+
do
1235+
do
1236+
thing2()
1237+
1238+
thing3()
1239+
end
1240+
until another_thing
1241+
]]))
1242+
end)
1243+
end)
1244+
10201245
it("provides correct location info", function()
10211246
assert.same({
10221247
{tag = "Localrec", line = 1, offset = 1, end_offset = 80,

src/luacheck/parser.lua

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ local UnpairedTokenGuesser = utils.class()
176176

177177
function UnpairedTokenGuesser:__init(state, error_opening_range, error_closing_token)
178178
self.old_state = state
179+
self.error_offset = state.offset
179180
self.error_opening_range = error_opening_range
180181
self.error_closing_token = error_closing_token
181182
self.opening_tokens_stack = utils.Stack()
@@ -199,39 +200,54 @@ function UnpairedTokenGuesser:on_block_start(opening_token_range, opening_token)
199200
self.opening_tokens_stack:push(token_wrapper)
200201
end
201202

202-
function UnpairedTokenGuesser:error()
203-
local top = self.opening_tokens_stack.top
204-
missing_closing_token_error(self.state, top, top.token, top.closing_token, true)
205-
end
206-
207-
function UnpairedTokenGuesser:check_token()
208-
if self.state.offset < self.error_opening_range.offset then
203+
function UnpairedTokenGuesser:set_guessed()
204+
-- Keep the first detected location.
205+
if self.guessed then
209206
return
210207
end
211208

209+
self.guessed = self.opening_tokens_stack.top
210+
self.guessed.error_token = self.state.token
211+
self.guessed.error_range = copy_range(self.state)
212+
end
213+
214+
function UnpairedTokenGuesser:check_token()
212215
local top = self.opening_tokens_stack.top
213216

214-
if not top or not top.eligible then
215-
return
216-
end
217+
if top and top.eligible then
218+
local token_indentation = get_indentation(self.state, self.state.line)
217219

218-
local token_indentation = get_indentation(self.state, self.state.line)
220+
if token_indentation < top.indentation then
221+
self:set_guessed()
222+
elseif token_indentation == top.indentation then
223+
local token = self.state.token
219224

220-
if token_indentation < top.indentation then
221-
self:error()
222-
elseif token_indentation == top.indentation then
223-
local token = self.state.token
225+
if token ~= top.closing_token and
226+
((top.token ~= "if" and top.token ~= "elseif") or (token ~= "elseif" and token ~= "else")) then
227+
self:set_guessed()
228+
end
229+
end
230+
end
224231

225-
if token ~= top.closing_token and
226-
((top.token ~= "if" and top.token ~= "elseif") or (token ~= "elseif" and token ~= "else")) then
227-
self:error()
232+
if self.state.offset == self.error_offset then
233+
if self.guessed and self.guessed.error_range.offset ~= self.state.offset then
234+
self.state.line = self.guessed.error_range.line
235+
self.state.offset = self.guessed.error_range.offset
236+
self.state.end_offset = self.guessed.error_range.end_offset
237+
self.state.token = self.guessed.error_token
238+
missing_closing_token_error(self.state, self.guessed, self.guessed.token, self.guessed.closing_token, true)
228239
end
229240
end
230241
end
231242

232243
function UnpairedTokenGuesser:on_block_end()
233244
self:check_token()
234245
self.opening_tokens_stack:pop()
246+
247+
if not self.opening_tokens_stack.top then
248+
-- Inserting an end token into a balanced sequence of tokens adds an error earlier than original one.
249+
self.guessed = nil
250+
end
235251
end
236252

237253
function UnpairedTokenGuesser:on_statement()
@@ -927,12 +943,12 @@ function parse_block(state, opening_token_range, opening_token, block)
927943
end
928944
end
929945

930-
check_closing_token(state, opening_token_range, opening_token)
931-
932946
if unpaired_token_guesser and opening_token then
933947
unpaired_token_guesser:on_block_end()
934948
end
935949

950+
check_closing_token(state, opening_token_range, opening_token)
951+
936952
return block
937953
end
938954

0 commit comments

Comments
 (0)