• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

lunarmodules / luacheck / 28591389878

02 Jul 2026 12:51PM UTC coverage: 96.978% (-0.1%) from 97.095%
28591389878

push

github

web-flow
Merge c69dd7ebc into f47ad699b

12 of 20 new or added lines in 4 files covered. (60.0%)

1 existing line in 1 file now uncovered.

6322 of 6519 relevant lines covered (96.98%)

26883.71 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.99
/src/luacheck/parser.lua
1
local lexer = require "luacheck.lexer"
876✔
2
local utils = require "luacheck.utils"
876✔
3

4
local parser = {}
876✔
5

6
-- A table with range info, or simply range, has `line`, `offset`, and `end_offset` fields.
7
-- `line` is the line of the first character.
8
-- Parser state table has range info for the current token, and all AST
9
-- node tables have range info for themself, including parens around expressions
10
-- that are otherwise not reflected in the AST structure.
11

12
parser.SyntaxError = utils.class()
1,168✔
13

14
function parser.SyntaxError:__init(msg, range, prev_range)
1,752✔
15
   self.msg = msg
924✔
16
   self.line = range.line
924✔
17
   self.offset = range.offset
924✔
18
   self.end_offset = range.end_offset
924✔
19

20
   if prev_range then
924✔
21
      self.prev_line = prev_range.line
276✔
22
      self.prev_offset = prev_range.offset
276✔
23
      self.prev_end_offset = prev_range.end_offset
276✔
24
   end
25
end
26

27
function parser.syntax_error(msg, range, prev_range)
876✔
28
   error(parser.SyntaxError(msg, range, prev_range), 0)
1,232✔
29
end
30

31
local function mark_line_endings(state, token_type)
32
   for line = state.line, state.lexer.line - 1 do
77,214✔
33
      state.line_endings[line] = token_type
228✔
34
   end
35
end
36

37
local function skip_token(state)
38
   while true do
39
      local token, token_value, line, offset, error_end_offset = lexer.next_token(state.lexer)
80,748✔
40
      state.token = token
80,748✔
41
      state.token_value = token_value
80,748✔
42
      state.line = line
80,748✔
43
      state.offset = offset
80,748✔
44
      state.end_offset = error_end_offset or (state.lexer.offset - 1)
80,748✔
45

46
      if not token then
80,748✔
47
         parser.syntax_error(token_value, state)
12✔
48
      end
49

50
      if token == "short_comment" then
80,736✔
51
         state.comments[#state.comments + 1] = {
1,152✔
52
            contents = token_value,
1,152✔
53
            line = line,
1,152✔
54
            offset = offset,
1,152✔
55
            end_offset = state.end_offset
1,152✔
56
         }
1,152✔
57

58
         state.line_endings[line] = "comment"
1,152✔
59
      elseif token == "long_comment" then
79,584✔
60
         mark_line_endings(state, "comment")
104✔
61
      else
62
         if token ~= "eof" then
79,506✔
63
            mark_line_endings(state, "string")
76,908✔
64
            state.code_lines[line] = true
76,908✔
65
            state.code_lines[state.lexer.line] = true
76,908✔
66
         end
67

68
         return
79,506✔
69
      end
70
   end
71
end
72

73
local function token_name(token)
74
   return token == "name" and "identifier" or (token == "eof" and "<eof>" or ("'" .. token .. "'"))
738✔
75
end
76

77
local function parse_error(state, msg, prev_range, token_prefix, message_suffix)
78
   local token_repr
79

80
   if state.token == "eof" then
846✔
81
      token_repr = "<eof>"
288✔
82
   else
83
      token_repr = lexer.get_quoted_substring_or_line(state.lexer, state.line, state.offset, state.end_offset)
744✔
84
   end
85

86
   if token_prefix then
846✔
87
      token_repr = token_prefix .. " " .. token_repr
30✔
88
   end
89

90
   msg = msg .. " near " .. token_repr
846✔
91

92
   if message_suffix then
846✔
93
      msg = msg .. " " .. message_suffix
102✔
94
   end
95

96
   parser.syntax_error(msg, state, prev_range)
846✔
97
end
98

99
local function check_token(state, token)
100
   if state.token ~= token then
29,040✔
101
      parse_error(state, "expected " .. token_name(token))
344✔
102
   end
103
end
104

105
local function check_and_skip_token(state, token)
106
   check_token(state, token)
6,912✔
107
   skip_token(state)
6,768✔
108
end
109

110
local function test_and_skip_token(state, token)
111
   if state.token == token then
21,330✔
112
      skip_token(state)
4,626✔
113
      return true
4,614✔
114
   end
115
end
116

117
local function copy_range(range)
118
   return {
29,136✔
119
      line = range.line,
29,136✔
120
      offset = range.offset,
29,136✔
121
      end_offset = range.end_offset
29,136✔
122
   }
29,136✔
123
end
124

125
local new_state
126
local parse_block
127
local missing_closing_token_error
128

129
-- Attempt to guess a better location for missing `end` and `until` errors (usually they uselessly point to eof).
130
-- Guessed error token should be selected in such a way that inserting previously missing closing token
131
-- in front of it should fix the error or at least move its opening token forward.
132
-- The idea is to track the stack of opening tokens and their indentations.
133
-- For the first statement or closing token with the same or smaller indentation than the opening token
134
-- on the top of the stack:
135
-- * If it has the same indentation but is not the appropriate closing token for the opening one, pick it
136
--   as the guessed error location.
137
-- * If it has a lower indentation level, pick it as the guessed error location even it closes the opening token.
138
-- Example:
139
-- local function f()
140
--    <code>
141
--
142
--    if cond then                   <- `if` is the guessed opening token
143
--       <code>
144
--
145
--    <code not starting with `end`> <- first token on this line is the guessed error location
146
-- end
147
-- Another one:
148
-- local function g()
149
--    <code>
150
--
151
--    if cond then  <- `if` is the guessed opening token
152
--       <code>
153
--
154
-- end              <- `end` is the guessed error location
155

156
local opening_token_to_closing = {
876✔
157
   ["("] = ")",
584✔
158
   ["["] = "]",
584✔
159
   ["{"] = "}",
584✔
160
   ["do"] = "end",
584✔
161
   ["if"] = "end",
584✔
162
   ["else"] = "end",
584✔
163
   ["elseif"] = "end",
584✔
164
   ["while"] = "end",
584✔
165
   ["repeat"] = "until",
584✔
166
   ["for"] = "end",
584✔
167
   ["function"] = "end"
584✔
168
}
169

170
local function get_indentation(state, line)
171
   local ws_start, ws_end = state.lexer.src:find("^[ \t\v\f]*", state.lexer.line_offsets[line])
1,152✔
172
   return ws_end - ws_start
1,152✔
173
end
174

175
local UnpairedTokenGuesser = utils.class()
876✔
176

177
function UnpairedTokenGuesser:__init(state, error_opening_range, error_closing_token)
876✔
178
   self.old_state = state
204✔
179
   self.error_offset = state.offset
204✔
180
   self.error_opening_range = error_opening_range
204✔
181
   self.error_closing_token = error_closing_token
204✔
182
   self.opening_tokens_stack = utils.Stack()
272✔
183
end
184

185
function UnpairedTokenGuesser:guess()
876✔
186
   -- Need to reinitialize lexer (e.g. to skip shebang again).
187
   self.state = new_state(self.old_state.lexer.src)
272✔
188
   self.state.unpaired_token_guesser = self
204✔
189
   skip_token(self.state)
204✔
190
   parse_block(self.state)
204✔
191
   error("No syntax error in second parse", 0)
×
192
end
193

194
function UnpairedTokenGuesser:on_block_start(opening_token_range, opening_token)
876✔
195
   local token_wrapper = copy_range(opening_token_range)
468✔
196
   token_wrapper.token = opening_token
468✔
197
   token_wrapper.closing_token = opening_token_to_closing[opening_token]
468✔
198
   token_wrapper.eligible = token_wrapper.closing_token == self.error_closing_token
468✔
199
   token_wrapper.indentation = get_indentation(self.state, opening_token_range.line)
624✔
200
   self.opening_tokens_stack:push(token_wrapper)
468✔
201
end
202

203
function UnpairedTokenGuesser:set_guessed()
876✔
204
   -- Keep the first detected location.
205
   if self.guessed then
324✔
206
      return
174✔
207
   end
208

209
   self.guessed = self.opening_tokens_stack.top
150✔
210
   self.guessed.error_token = self.state.token
150✔
211
   self.guessed.error_range = copy_range(self.state)
200✔
212
end
213

214
function UnpairedTokenGuesser:check_token()
876✔
215
   local top = self.opening_tokens_stack.top
1,092✔
216

217
   if top and top.eligible and self.state.line > top.line then
1,092✔
218
      local token_indentation = get_indentation(self.state, self.state.line)
684✔
219

220
      if token_indentation < top.indentation then
684✔
221
         self:set_guessed()
176✔
222
      elseif token_indentation == top.indentation then
552✔
223
         local token = self.state.token
264✔
224

225
         if token ~= top.closing_token and
264✔
226
               ((top.token ~= "if" and top.token ~= "elseif") or (token ~= "elseif" and token ~= "else")) then
228✔
227
            self:set_guessed()
192✔
228
         end
229
      end
230
   end
231

232
   if self.state.offset == self.error_offset then
1,092✔
233
      if self.guessed and self.guessed.error_range.offset ~= self.state.offset then
204✔
234
         self.state.line = self.guessed.error_range.line
102✔
235
         self.state.offset = self.guessed.error_range.offset
102✔
236
         self.state.end_offset = self.guessed.error_range.end_offset
102✔
237
         self.state.token = self.guessed.error_token
102✔
238
         missing_closing_token_error(self.state, self.guessed, self.guessed.token, self.guessed.closing_token, true)
102✔
239
      end
240
   end
241
end
242

243
function UnpairedTokenGuesser:on_block_end()
876✔
244
   self:check_token()
420✔
245
   self.opening_tokens_stack:pop()
318✔
246

247
   if not self.opening_tokens_stack.top then
318✔
248
      -- Inserting an end token into a balanced sequence of tokens adds an error earlier than original one.
249
      self.guessed = nil
162✔
250
   end
251
end
252

253
function UnpairedTokenGuesser:on_statement()
876✔
254
   self:check_token()
672✔
255
end
256

257
function missing_closing_token_error(state, opening_range, opening_token, closing_token, is_guess)
730✔
258
   local msg = "expected " .. token_name(closing_token)
408✔
259

260
   if opening_range and opening_range.line ~= state.line then
306✔
261
      msg = msg .. " (to close " .. token_name(opening_token) .. " on line " .. tostring(opening_range.line) .. ")"
232✔
262
   end
263

264
   local token_prefix
265
   local message_suffix
266

267
   if is_guess then
306✔
268
      if state.token == closing_token then
102✔
269
         -- "expected 'end' near 'end'" seems confusing.
270
         token_prefix = "less indented"
30✔
271
      end
272

273
      message_suffix = "(indentation-based guess)"
102✔
274
   end
275

276
   parse_error(state, msg, opening_range, token_prefix, message_suffix)
306✔
277
end
278

279
local function check_closing_token(state, opening_range, opening_token)
280
   local closing_token = opening_token_to_closing[opening_token] or "eof"
15,510✔
281

282
   if state.token == closing_token then
15,510✔
283
      return
14,454✔
284
   end
285

286
   if (opening_token == "if" or opening_token == "elseif") and (state.token == "else" or state.token == "elseif") then
1,056✔
287
      return
648✔
288
   end
289

290
   if closing_token == "end" or closing_token == "until" then
408✔
291
      if not state.unpaired_token_guesser then
306✔
292
         UnpairedTokenGuesser(state, opening_range, closing_token):guess()
272✔
293
      end
294
   end
295

296
   missing_closing_token_error(state, opening_range, opening_token, closing_token)
204✔
297
end
298

299
local function check_and_skip_closing_token(state, opening_range, opening_token)
300
   check_closing_token(state, opening_range, opening_token)
8,628✔
301
   skip_token(state)
8,568✔
302
end
303

304
local function check_name(state)
305
   check_token(state, "name")
22,128✔
306
   return state.token_value
22,014✔
307
end
308

309
local function new_outer_node(range, tag, node)
310
   node = node or {}
29,316✔
311
   node.line = range.line
29,316✔
312
   node.offset = range.offset
29,316✔
313
   node.end_offset = range.end_offset
29,316✔
314
   node.tag = tag
29,316✔
315
   return node
29,316✔
316
end
317

318
local function new_inner_node(start_range, end_range, tag, node)
319
   node = node or {}
23,508✔
320
   node.line = start_range.line
23,508✔
321
   node.offset = start_range.offset
23,508✔
322
   node.end_offset = end_range.end_offset
23,508✔
323
   node.tag = tag
23,508✔
324
   return node
23,508✔
325
end
326

327
local parse_expression
328

329
local function parse_expression_list(state, list)
330
   list = list or {}
8,466✔
331

332
   repeat
333
      list[#list + 1] = parse_expression(state)
12,676✔
334
   until not test_and_skip_token(state, ",")
12,544✔
335

336
   return list
8,334✔
337
end
338

339
local function parse_id(state, tag)
340
   local ast_node = new_outer_node(state, tag or "Id")
21,924✔
341

342
   if state.token == "global" then
21,924✔
343
      ast_node[1] = "global"
30✔
344
   else
345
      ast_node[1] = check_name(state)
29,160✔
346
   end
347

348
   -- Skip name.
349
   skip_token(state)
21,828✔
350
   return ast_node
21,828✔
351
end
352

353
local function atom(tag)
354
   return function(state)
355
      local ast_node = new_outer_node(state, tag)
6,372✔
356
      ast_node[1] = state.token_value
6,372✔
357
      skip_token(state)
6,372✔
358
      return ast_node
6,372✔
359
   end
360
end
361

362
local simple_expressions = {}
876✔
363

364
simple_expressions.number = atom("Number")
1,168✔
365
simple_expressions.string = atom("String")
1,168✔
366
simple_expressions["nil"] = atom("Nil")
1,168✔
367
simple_expressions["true"] = atom("True")
1,168✔
368
simple_expressions["false"] = atom("False")
1,168✔
369
simple_expressions["..."] = atom("Dots")
1,168✔
370

371
simple_expressions["{"] = function(state)
372
   local start_range = copy_range(state)
1,002✔
373
   local ast_node = {}
1,002✔
374
   skip_token(state)
1,002✔
375

376
   repeat
377
      if state.token == "}" then
1,584✔
378
         break
357✔
379
      end
380

381
      local key_node, value_node
382
      local first_token_range = copy_range(state)
870✔
383

384
      if state.token == "name" then
870✔
385
         local name = state.token_value
750✔
386
         skip_token(state)  -- Skip name.
750✔
387

388
         if test_and_skip_token(state, "=") then
1,000✔
389
            -- `name` = `expr`.
390
            key_node = new_outer_node(first_token_range, "String", {name})
600✔
391
            value_node = parse_expression(state)
598✔
392
         else
393
            -- `name` is beginning of an expression in array part.
394
            -- Backtrack lexer to before name.
395
            state.lexer.line = first_token_range.line
300✔
396
            state.lexer.offset = first_token_range.offset
300✔
397
            skip_token(state)  -- Load name again.
300✔
398
            value_node = parse_expression(state)
400✔
399
         end
400
      elseif state.token == "[" then
120✔
401
         -- [ `expr` ] = `expr`.
402
         skip_token(state)
48✔
403
         key_node = parse_expression(state)
64✔
404
         check_and_skip_closing_token(state, first_token_range, "[")
48✔
405
         check_and_skip_token(state, "=")
36✔
406
         value_node = parse_expression(state)
48✔
407
      else
408
         -- Expression in array part.
409
         value_node = parse_expression(state)
90✔
410
      end
411

412
      if key_node then
834✔
413
         -- Pair.
414
         ast_node[#ast_node + 1] = new_inner_node(first_token_range, value_node, "Pair", {key_node, value_node})
640✔
415
      else
416
         -- Array part item.
417
         ast_node[#ast_node + 1] = value_node
354✔
418
      end
419
   until not (test_and_skip_token(state, ",") or test_and_skip_token(state, ";"))
1,204✔
420

421
   new_inner_node(start_range, state, "Table", ast_node)
966✔
422
   check_and_skip_closing_token(state, start_range, "{")
966✔
423
   return ast_node
954✔
424
end
425

426
-- Parses argument list and the statements.
427
local function parse_function(state, function_range)
428
   local paren_range = copy_range(state)
1,830✔
429
   check_and_skip_token(state, "(")
1,830✔
430
   local args = {}
1,800✔
431

432
   -- Are there arguments?
433
   if state.token ~= ")" then
1,800✔
434
      repeat
435
         if state.token == "name" then
1,044✔
436
            args[#args + 1] = parse_id(state)
1,032✔
437
         elseif state.token == "..." then
270✔
438
            args[#args + 1] = simple_expressions["..."](state)
336✔
439
            break
168✔
440
         else
441
            parse_error(state, "expected argument")
18✔
442
         end
443
      until not test_and_skip_token(state, ",")
1,032✔
444
   end
445

446
   check_and_skip_closing_token(state, paren_range, "(")
1,782✔
447
   local body = parse_block(state, function_range, "function")
1,764✔
448
   local end_range = copy_range(state)
1,674✔
449
   -- Skip "function".
450
   skip_token(state)
1,674✔
451
   return new_inner_node(function_range, end_range, "Function", {args, body, end_range = end_range})
1,674✔
452
end
453

454
simple_expressions["function"] = function(state)
455
   local function_range = copy_range(state)
396✔
456
   -- Skip "function".
457
   skip_token(state)
396✔
458
   return parse_function(state, function_range)
396✔
459
end
460

461
-- A call handler parses arguments of a call with given base node that determines resulting node start location,
462
-- given tag, and array to which the arguments should be appended.
463
local call_handlers = {}
876✔
464

465
call_handlers["("] = function(state, base_node, tag, node)
466
   local paren_range = copy_range(state)
4,320✔
467
   -- Skip "(".
468
   skip_token(state)
4,320✔
469

470
   if state.token ~= ")" then
4,320✔
471
      parse_expression_list(state, node)
2,820✔
472
   end
473

474
   new_inner_node(base_node, state, tag, node)
4,314✔
475
   check_and_skip_closing_token(state, paren_range, "(")
4,314✔
476
   return node
4,302✔
477
end
478

479
call_handlers["{"] = function(state, base_node, tag, node)
480
   local arg_node = simple_expressions[state.token](state)
336✔
481
   node[#node + 1] = arg_node
336✔
482
   return new_inner_node(base_node, arg_node, tag, node)
336✔
483
end
484

485
call_handlers.string = call_handlers["{"]
876✔
486

487
local suffix_handlers = {}
876✔
488

489
suffix_handlers["."] = function(state, base_node)
490
   -- Skip ".".
491
   skip_token(state)
1,908✔
492
   local index_node = parse_id(state, "String")
1,908✔
493
   return new_inner_node(base_node, index_node, "Index", {base_node, index_node})
1,908✔
494
end
495

496
suffix_handlers["["] = function(state, base_node)
497
   local bracket_range = copy_range(state)
1,002✔
498
   -- Skip "[".
499
   skip_token(state)
1,002✔
500
   local index_node = parse_expression(state)
1,002✔
501
   local ast_node = new_inner_node(base_node, state, "Index", {base_node, index_node})
1,002✔
502
   check_and_skip_closing_token(state, bracket_range, "[")
1,002✔
503
   return ast_node
1,002✔
504
end
505

506
suffix_handlers[":"] = function(state, base_node)
507
   -- Skip ":".
508
   skip_token(state)
570✔
509
   local method_name = parse_id(state, "String")
570✔
510
   local call_handler = call_handlers[state.token]
564✔
511

512
   if not call_handler then
564✔
513
      parse_error(state, "expected method arguments")
6✔
514
   end
515

516
   return call_handler(state, base_node, "Invoke", {base_node, method_name})
558✔
517
end
518

519
suffix_handlers["("] = function(state, base_node)
520
   return call_handlers[state.token](state, base_node, "Call", {base_node})
4,098✔
521
end
522

523
suffix_handlers["{"] = suffix_handlers["("]
876✔
524
suffix_handlers.string = suffix_handlers["("]
876✔
525

526
local function parse_simple_expression(state, kind, no_literals)
527
   local expression
528

529
   if state.token == "(" then
21,648✔
530
      local paren_range = copy_range(state)
534✔
531
      skip_token(state)
534✔
532
      local inner_expression = parse_expression(state)
534✔
533
      expression = new_inner_node(paren_range, state, "Paren", {inner_expression})
688✔
534
      check_and_skip_closing_token(state, paren_range, "(")
686✔
535
   elseif state.token == "name" or state.token == "global" then
21,114✔
536
      expression = parse_id(state)
18,256✔
537
   else
538
      local literal_handler = simple_expressions[state.token]
7,422✔
539

540
      if not literal_handler or no_literals then
7,422✔
541
         parse_error(state, "expected " .. (kind or "expression"))
240✔
542
      end
543

544
      return literal_handler(state)
7,182✔
545
   end
546

547
   while true do
548
      local suffix_handler = suffix_handlers[state.token]
21,750✔
549

550
      if suffix_handler then
21,750✔
551
         expression = suffix_handler(state, expression)
10,094✔
552
      else
553
         return expression
14,172✔
554
      end
555
   end
556
end
557

558
local unary_operators = {
876✔
559
   ["not"] = "not",
584✔
560
   ["-"] = "unm",
584✔
561
   ["~"] = "bnot",
584✔
562
   ["#"] = "len"
584✔
563
}
564

565
local unary_priority = 12
876✔
566

567
local binary_operators = {
876✔
568
   ["+"] = "add", ["-"] = "sub",
584✔
569
   ["*"] = "mul", ["%"] = "mod",
584✔
570
   ["^"] = "pow",
584✔
571
   ["/"] = "div", ["//"] = "idiv",
584✔
572
   ["&"] = "band", ["|"] = "bor", ["~"] = "bxor",
584✔
573
   ["<<"] = "shl", [">>"] = "shr",
584✔
574
   [".."] = "concat",
584✔
575
   ["~="] = "ne", ["=="] = "eq",
584✔
576
   ["<"] = "lt", ["<="] = "le",
584✔
577
   [">"] = "gt", [">="] = "ge",
584✔
578
   ["and"] = "and", ["or"] = "or"
584✔
579
}
580

581
local compound_operators = {
876✔
582
   ["+"] = "add", ["-"] = "sub",
584✔
583
   ["*"] = "mul", ["%"] = "mod",
584✔
584
   ["^"] = "pow",
584✔
585
   ["/"] = "div", ["//"] = "idiv",
584✔
586
   ["&"] = "band", ["|"] = "bor", ["~"] = "bxor",
584✔
587
   ["<<"] = "shl", [">>"] = "shr",
584✔
588
   [".."] = "concat"
584✔
589
}
590

591
local left_priorities = {
876✔
592
   add = 10, sub = 10,
584✔
593
   mul = 11, mod = 11,
584✔
594
   pow = 14,
584✔
595
   div = 11, idiv = 11,
584✔
596
   band = 6, bor = 4, bxor = 5,
584✔
597
   shl = 7, shr = 7,
584✔
598
   concat = 9,
584✔
599
   ne = 3, eq = 3,
584✔
600
   lt = 3, le = 3,
584✔
601
   gt = 3, ge = 3,
584✔
602
   ["and"] = 2, ["or"] = 1
584✔
603
}
604

605
local right_priorities = {
876✔
606
   add = 10, sub = 10,
584✔
607
   mul = 11, mod = 11,
584✔
608
   pow = 13,
584✔
609
   div = 11, idiv = 11,
584✔
610
   band = 6, bor = 4, bxor = 5,
584✔
611
   shl = 7, shr = 7,
584✔
612
   concat = 8,
584✔
613
   ne = 3, eq = 3,
584✔
614
   lt = 3, le = 3,
584✔
615
   gt = 3, ge = 3,
584✔
616
   ["and"] = 2, ["or"] = 1
584✔
617
}
618

619
local function parse_subexpression(state, limit, kind)
620
   local expression
621
   local unary_operator = unary_operators[state.token]
16,896✔
622

623
   if unary_operator then
16,896✔
624
      local operator_range = copy_range(state)
408✔
625
      -- Skip operator.
626
      skip_token(state)
408✔
627
      local operand = parse_subexpression(state, unary_priority)
408✔
628
      expression = new_inner_node(operator_range, operand, "Op", {unary_operator, operand})
544✔
629
   else
630
      expression = parse_simple_expression(state, kind)
21,912✔
631
   end
632

633
   -- Expand while operators have priorities higher than `limit`.
634
   while true do
635
      local binary_operator = binary_operators[state.token]
18,864✔
636

637
      if not binary_operator or left_priorities[binary_operator] <= limit then
18,864✔
638
         break
201✔
639
      end
640

641
       -- Skip operator.
642
      skip_token(state)
2,184✔
643
      -- Read subexpression with higher priority.
644
      local subexpression = parse_subexpression(state, right_priorities[binary_operator])
2,184✔
645
      expression = new_inner_node(expression, subexpression, "Op", {binary_operator, expression, subexpression})
2,912✔
646
   end
647

648
   return expression
16,680✔
649
end
650

651
function parse_expression(state, kind)
730✔
652
   return parse_subexpression(state, 0, kind)
14,304✔
653
end
654

655
local statements = {}
876✔
656

657
statements["if"] = function(state)
658
   local start_range = copy_range(state)
1,140✔
659
   -- Skip "if".
660
   skip_token(state)
1,140✔
661
   local ast_node = {}
1,140✔
662

663
   -- The loop is entered after skipping "if" or "elseif".
664
   -- Block start token info is set to the last skipped "if", "elseif", or "else" token.
665
   local block_start_token = "if"
1,140✔
666
   local block_start_range = start_range
1,140✔
667

668
   while true do
669
      ast_node[#ast_node + 1] = parse_expression(state, "condition")
1,840✔
670
      -- Add range of the "then" token to the block statement array.
671
      local branch_range = copy_range(state)
1,362✔
672
      check_and_skip_token(state, "then")
1,362✔
673
      ast_node[#ast_node + 1] = parse_block(state, block_start_range, block_start_token, branch_range)
1,774✔
674

675
      if state.token == "else" then
1,290✔
676
         branch_range = copy_range(state)
536✔
677
         block_start_token = "else"
402✔
678
         block_start_range = branch_range
402✔
679
         skip_token(state)
402✔
680
         ast_node[#ast_node + 1] = parse_block(state, block_start_range, block_start_token, branch_range)
520✔
681
         break
236✔
682
      elseif state.token == "elseif" then
888✔
683
         block_start_token = "elseif"
246✔
684
         block_start_range = copy_range(state)
328✔
685
         skip_token(state)
328✔
686
      else
687
         break
688
      end
689
   end
690

691
   new_inner_node(start_range, state, "If", ast_node)
996✔
692
   -- Skip "end".
693
   skip_token(state)
996✔
694
   return ast_node
996✔
695
end
696

697
statements["while"] = function(state)
698
   local start_range = copy_range(state)
312✔
699
   -- Skip "while".
700
   skip_token(state)
312✔
701
   local condition = parse_expression(state, "condition")
312✔
702
   check_and_skip_token(state, "do")
300✔
703
   local block = parse_block(state, start_range, "while")
288✔
704
   local ast_node = new_inner_node(start_range, state, "While", {condition, block})
240✔
705
   -- Skip "end".
706
   skip_token(state)
240✔
707
   return ast_node
240✔
708
end
709

710
statements["do"] = function(state)
711
   local start_range = copy_range(state)
522✔
712
   -- Skip "do".
713
   skip_token(state)
522✔
714
   local block = parse_block(state, start_range, "do")
522✔
715
   local ast_node = new_inner_node(start_range, state, "Do", block)
342✔
716
   -- Skip "end".
717
   skip_token(state)
342✔
718
   return ast_node
342✔
719
end
720

721
statements["for"] = function(state)
722
   local start_range = copy_range(state)
546✔
723
   -- Skip "for".
724
   skip_token(state)
546✔
725

726
   local ast_node = {}
546✔
727
   local tag
728
   local first_var = parse_id(state)
546✔
729

730
   if state.token == "=" then
522✔
731
      -- Numeric "for" loop.
732
      tag = "Fornum"
234✔
733
      -- Skip "=".
734
      skip_token(state)
234✔
735
      ast_node[1] = first_var
234✔
736
      ast_node[2] = parse_expression(state)
312✔
737
      check_and_skip_token(state, ",")
234✔
738
      ast_node[3] = parse_expression(state)
304✔
739

740
      if test_and_skip_token(state, ",") then
304✔
741
         ast_node[4] = parse_expression(state)
56✔
742
      end
743

744
      check_and_skip_token(state, "do")
228✔
745
      ast_node[#ast_node + 1] = parse_block(state, start_range, "for")
288✔
746
   elseif state.token == "," or state.token == "in" then
288✔
747
      -- Generic "for" loop.
748
      tag = "Forin"
276✔
749

750
      local iter_vars = {first_var}
276✔
751
      while test_and_skip_token(state, ",") do
680✔
752
         iter_vars[#iter_vars + 1] = parse_id(state)
312✔
753
      end
754

755
      ast_node[1] = iter_vars
276✔
756
      check_and_skip_token(state, "in")
276✔
757
      ast_node[2] = parse_expression_list(state)
366✔
758
      check_and_skip_token(state, "do")
270✔
759
      ast_node[3] = parse_block(state, start_range, "for")
356✔
760
   else
761
      parse_error(state, "expected '=', ',' or 'in'")
12✔
762
   end
763

764
   new_inner_node(start_range, state, tag, ast_node)
456✔
765
   -- Skip "end".
766
   skip_token(state)
456✔
767
   return ast_node
456✔
768
end
769

770
statements["repeat"] = function(state)
771
   local start_range = copy_range(state)
180✔
772
   -- Skip "repeat".
773
   skip_token(state)
180✔
774
   local block = parse_block(state, start_range, "repeat")
180✔
775
   -- Skip "until".
776
   skip_token(state)
120✔
777
   local condition = parse_expression(state, "condition")
120✔
778
   return new_inner_node(start_range, condition, "Repeat", {block, condition})
114✔
779
end
780

781
statements["function"] = function(state)
782
   local start_range = copy_range(state)
768✔
783
   -- Skip "function".
784
   skip_token(state)
768✔
785
   local lhs = parse_id(state)
768✔
786
   local implicit_self_range
787

788
   while (not implicit_self_range) and (state.token == "." or state.token == ":") do
1,158✔
789
      implicit_self_range = (state.token == ":") and copy_range(state)
512✔
790
      -- Skip "." or ":".
791
      skip_token(state)
426✔
792
      local index_node = parse_id(state, "String")
426✔
793
      lhs = new_inner_node(lhs, index_node, "Index", {lhs, index_node})
560✔
794
   end
795

796
   local function_node = parse_function(state, start_range)
732✔
797

798
   if implicit_self_range then
636✔
799
      -- Insert implicit "self" argument.
800
      local self_arg = new_outer_node(implicit_self_range, "Id", {"self", implicit = true})
246✔
801
      table.insert(function_node[1], 1, self_arg)
246✔
802
   end
803

804
   return new_inner_node(start_range, function_node, "Set", {{lhs}, {function_node}})
636✔
805
end
806

807
statements["local"] = function(state)
808
   local start_range = copy_range(state)
2,676✔
809
   -- Skip "local".
810
   skip_token(state)
2,676✔
811

812
   if state.token == "function" then
2,676✔
813
      -- Local function.
814
      local function_range = copy_range(state)
708✔
815
      -- Skip "function".
816
      skip_token(state)
708✔
817
      local var = parse_id(state)
708✔
818
      local function_node = parse_function(state, function_range)
702✔
819
      return new_inner_node(start_range, function_node, "Localrec", {{var}, {function_node}})
648✔
820
   end
821

822
   -- Local definition, potentially with assignment.
823
   local lhs = {}
1,968✔
824
   local rhs
825

826
   repeat
827
      lhs[#lhs + 1] = parse_id(state)
3,056✔
828

829
      -- Check if a Lua 5.4 attribute is present
830
      if state.token == "<" then
2,274✔
831
         -- For now, just consume and ignore it.
832
         skip_token(state)
42✔
833
         check_name(state)
42✔
834
         skip_token(state)
42✔
835
         check_and_skip_token(state, ">")
42✔
836
      end
837
   until not test_and_skip_token(state, ",")
3,024✔
838

839
   if test_and_skip_token(state, "=") then
2,580✔
840
      rhs = parse_expression_list(state)
1,892✔
841
   end
842

843
   return new_inner_node(start_range, rhs and rhs[#rhs] or lhs[#lhs], "Local", {lhs, rhs})
1,914✔
844
end
845

846
statements["::"] = function(state)
847
   local start_range = copy_range(state)
126✔
848
   -- Skip "::".
849
   skip_token(state)
126✔
850
   local name = check_name(state)
126✔
851
   -- Skip label name.
852
   skip_token(state)
114✔
853
   local ast_node = new_inner_node(start_range, state, "Label", {name})
114✔
854
   check_and_skip_token(state, "::")
114✔
855
   return ast_node
114✔
856
end
857

858
local closing_tokens = utils.array_to_set({"end", "eof", "else", "elseif", "until"})
876✔
859

860
statements["return"] = function(state)
861
   local start_range = copy_range(state)
1,962✔
862
   -- Skip "return".
863
   skip_token(state)
1,962✔
864

865
   if closing_tokens[state.token] or state.token == ";" then
1,962✔
866
      -- No return values.
867
      return new_outer_node(start_range, "Return")
168✔
868
   else
869
      local returns = parse_expression_list(state)
1,794✔
870
      return new_inner_node(start_range, returns[#returns], "Return", returns)
1,716✔
871
   end
872
end
873

874
statements["break"] = function(state)
875
   local ast_node = new_outer_node(state, "Break")
96✔
876
   -- Skip "break".
877
   skip_token(state)
96✔
878
   return ast_node
96✔
879
end
880

881
statements["goto"] = function(state)
882
   local start_range = copy_range(state)
66✔
883
   -- Skip "goto".
884
   skip_token(state)
66✔
885
   local name = check_name(state)
66✔
886
   local ast_node = new_outer_node(start_range, "Goto", {name})
60✔
887
   -- Skip label name.
888
   skip_token(state)
60✔
889
   return ast_node
60✔
890
end
891

892
statements["global"] = function(state)
NEW
893
   local start_range = copy_range(state)
×
894
   -- Skip "global".
NEW
895
   skip_token(state)
×
896

897
   -- Global definition, potentially with assignment.
NEW
898
   local lhs = {}
×
899
   local rhs
900

901
   repeat
NEW
902
      lhs[#lhs + 1] = parse_id(state)
×
NEW
903
   until not test_and_skip_token(state, ",")
×
904

NEW
905
   if test_and_skip_token(state, "=") then
×
NEW
906
      rhs = parse_expression_list(state)
×
907
   end
908

NEW
909
   return new_inner_node(start_range, rhs and rhs[#rhs] or lhs[#lhs], "Global", {lhs, rhs})
×
910
end
911

912
local function parse_expression_statement(state)
913
   local lhs
914
   local start_range = copy_range(state)
4,920✔
915

916
   -- Handle lhs of an assignment or a single expression.
917
   repeat
918
      local item_start_range = lhs and copy_range(state) or start_range
5,240✔
919
      local expected = lhs and "identifier or field" or "statement"
5,160✔
920
      local primary_expression = parse_simple_expression(state, expected, true)
5,160✔
921

922
      if primary_expression.tag == "Paren" then
5,028✔
923
         -- (expr) in lhs is invalid.
924
         parser.syntax_error("expected " .. expected .. " near '('", item_start_range)
12✔
925
      end
926

927
      if primary_expression.tag == "Call" or primary_expression.tag == "Invoke" then
5,016✔
928
         if lhs then
2,556✔
929
            -- The is an assignment, and a call is not valid in lhs.
930
            parse_error(state, "expected call or indexing")
6✔
931
         else
932
            -- This is a call.
933
            return primary_expression
2,550✔
934
         end
935
      end
936

937
      -- This is an assignment.
938
      lhs = lhs or {}
2,460✔
939
      lhs[#lhs + 1] = primary_expression
2,460✔
940
   until not test_and_skip_token(state, ",")
3,280✔
941

942
   local compound_operator = compound_operators[state.token]
2,220✔
943
   if compound_operator then
2,220✔
944
      -- This is an assignment in the form `lhs op= rhs`.
945

946
      if #lhs ~= 1 then
72✔
947
         -- Multiple lhs values are not valid
948
         parse_error(state, "compound assignment not allowed on tuples near " .. compound_operator .. "=")
×
949
      end
950

951
      -- Skip operator.
952
      skip_token(state)
72✔
953
      check_and_skip_token(state, "=")
72✔
954

955
      local rhs = parse_expression_list(state)
72✔
956
      if #rhs ~= 1 then
72✔
957
         parse_error(state, "compound assignment not allowed on tuples near " .. compound_operator .. "=")
×
958
      end
959

960
      return new_inner_node(start_range, rhs[1], "OpSet", {lhs, rhs, compound_operator})
72✔
961
   else
962
      -- This is an assignment in the form `lhs = rhs`.
963
      check_and_skip_token(state, "=")
2,148✔
964
      local rhs = parse_expression_list(state)
2,082✔
965
      return new_inner_node(start_range, rhs[#rhs], "Set", {lhs, rhs})
2,052✔
966
   end
967
end
968

969
local function parse_statement(state)
970
   return (statements[state.token] or parse_expression_statement)(state)
13,314✔
971
end
972

973
function parse_block(state, opening_token_range, opening_token, block)
730✔
974
   local unpaired_token_guesser = state.unpaired_token_guesser
8,124✔
975

976
   if unpaired_token_guesser and opening_token then
8,124✔
977
      unpaired_token_guesser:on_block_start(opening_token_range, opening_token)
468✔
978
   end
979

980
   block = block or {}
8,124✔
981
   local after_statement = false
8,124✔
982

983
   while not closing_tokens[state.token] do
18,648✔
984
      local first_token = state.token
13,548✔
985

986
      if first_token == ";" then
13,548✔
987
         if not after_statement then
234✔
988
            table.insert(state.hanging_semicolons, copy_range(state))
64✔
989
         end
990

991
         -- Skip ";".
992
         skip_token(state)
234✔
993
         -- Further semicolons are considered hanging.
994
         after_statement = false
234✔
995
      else
996
         if unpaired_token_guesser then
13,314✔
997
            unpaired_token_guesser:on_statement()
672✔
998
         end
999

1000
         local statement = parse_statement(state)
13,314✔
1001
         after_statement = true
12,174✔
1002
         block[#block + 1] = statement
12,174✔
1003

1004
         if statement.tag == "Return" then
12,174✔
1005
            -- "return" must be the last statement.
1006
            -- However, one ";" after it is allowed.
1007
            test_and_skip_token(state, ";")
1,884✔
1008
            break
1,256✔
1009
         end
1010
      end
1011
   end
1012

1013
   if unpaired_token_guesser and opening_token then
6,984✔
1014
      unpaired_token_guesser:on_block_end()
420✔
1015
   end
1016

1017
   check_closing_token(state, opening_token_range, opening_token)
6,882✔
1018

1019
   return block
6,534✔
1020
end
1021

1022
function new_state(src, line_offsets, line_lengths)
730✔
1023
   return {
3,132✔
1024
      lexer = lexer.new_state(src, line_offsets, line_lengths),
4,176✔
1025
      -- Set of line numbers containing code.
1026
      code_lines = {},
3,132✔
1027
      -- Maps line numbers to "comment", "string", or nil based on whether the line ending is within a token
1028
      line_endings = {},
3,132✔
1029
      -- Array of {contents = string} with range info.
1030
      comments = {},
3,132✔
1031
       -- Array of ranges of semicolons not following a statement.
1032
      hanging_semicolons = {}
3,132✔
1033
   }
3,132✔
1034
end
1035

1036
-- Parses source characters.
1037
-- Returns AST (in almost MetaLua format), array of comments - tables {contents = string} with range info,
1038
-- set of line numbers containing code, map of types of tokens wrapping line endings (nil, "string", or "comment"),
1039
-- array of ranges of hanging semicolons (not after statements), array of line start offsets, array of line lengths.
1040
-- The last two tables can be passed as arguments to be filled.
1041
-- On error throws an instance of parser.SyntaxError: table {msg = msg, prev_range = prev_range?} with range info,
1042
-- prev_range may refer to some extra relevant location.
1043
function parser.parse(src, line_offsets, line_lengths)
876✔
1044
   local state = new_state(src, line_offsets, line_lengths)
2,928✔
1045
   skip_token(state)
2,928✔
1046
   local ast = parse_block(state)
2,928✔
1047
   return ast, state.comments, state.code_lines, state.line_endings, state.hanging_semicolons,
2,058✔
1048
      state.lexer.line_offsets, state.lexer.line_lengths
2,058✔
1049
end
1050

1051
return parser
876✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc