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

lunarmodules / luacheck / 24662353255

20 Apr 2026 10:48AM UTC coverage: 97.095% (+0.07%) from 97.027%
24662353255

push

github

web-flow
fix(ci): build LuaJIT using system malloc for lualanes tests (#144)

6318 of 6507 relevant lines covered (97.1%)

26928.88 hits per line

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

95.81
/src/luacheck/stages/parse_inline_options.lua
1
local options = require "luacheck.options"
870✔
2
local utils = require "luacheck.utils"
870✔
3

4
local stage = {}
870✔
5

6
stage.warnings = {
870✔
7
   -- Also produced during filtering for options that did not pass validation.
8
   ["021"] = {message_format = "{msg}", fields = {"msg"}},
870✔
9
   ["022"] = {message_format = "unpaired push directive", fields = {}},
870✔
10
   ["023"] = {message_format = "unpaired pop directive", fields = {}}
870✔
11
}
870✔
12

13
stage.inline_option_fields = {"line", "pop_count", "options", "column", "end_column"}
870✔
14

15
local limit_opts = utils.array_to_set({"max_line_length", "max_code_line_length", "max_string_line_length",
1,450✔
16
   "max_comment_line_length", "max_cyclomatic_complexity"})
580✔
17

18
local function is_valid_option_name(name)
19
   if name == "std" or options.variadic_inline_options[name] then
1,404✔
20
      return true
312✔
21
   end
22

23
   name = name:gsub("^no_", "")
1,092✔
24
   return options.nullary_inline_options[name] or limit_opts[name]
1,092✔
25
end
26

27
-- Splits a token array for an inline option invocation into
28
-- option name and argument array, or nil if invocation is invalid.
29
local function split_invocation(tokens)
30
   -- Name of the option can be split into several space separated tokens.
31
   -- Since some valid names are prefixes of some other names
32
   -- (e.g. `unused` and `unused arguments`), the longest prefix of token
33
   -- array that is a valid option name should be considered.
34
   local cur_name
35
   local last_valid_name
36
   local last_valid_name_end_index
37

38
   for i, token in ipairs(tokens) do
1,914✔
39
      cur_name = cur_name and (cur_name .. "_" .. token) or token
1,404✔
40

41
      if is_valid_option_name(cur_name) then
1,872✔
42
         last_valid_name = cur_name
498✔
43
         last_valid_name_end_index = i
498✔
44
      end
45
   end
46

47
   if not last_valid_name then
510✔
48
      return
30✔
49
   end
50

51
   local args = {}
480✔
52

53
   for i = last_valid_name_end_index + 1, #tokens do
930✔
54
      table.insert(args, tokens[i])
450✔
55
   end
56

57
   return last_valid_name, args
480✔
58
end
59

60
local function unexpected_num_args(name, args, expected)
61
   return ("inline option '%s' expects %d argument%s, %d given"):format(
30✔
62
      name, expected, expected == 1 and "" or "s", #args)
27✔
63
end
64

65
-- Parses inline option body, returns options or nil and error message.
66
local function parse_options(body)
67
   local opts = {}
492✔
68

69
   local parts = utils.split(body, ",")
492✔
70

71
   for _, name_and_args in ipairs(parts) do
954✔
72
      local tokens = utils.split(name_and_args)
510✔
73
      local name, args = split_invocation(tokens)
510✔
74

75
      if not name then
510✔
76
         if #tokens == 0 then
30✔
77
            return nil, (#parts == 1) and "empty inline option" or "empty inline option invocation"
12✔
78
         else
79
            return nil, ("unknown inline option '%s'"):format(table.concat(tokens, " "))
18✔
80
         end
81
      end
82

83
      if name == "std" then
480✔
84
         if #args ~= 1 then
72✔
85
            return nil, unexpected_num_args(name, args, 1)
16✔
86
         end
87

88
         opts.std = args[1]
60✔
89
      elseif name == "ignore" and #args == 0 then
408✔
90
         opts.ignore = {".*"}
24✔
91
      elseif options.variadic_inline_options[name] then
384✔
92
         opts[name] = args
216✔
93
      else
94
         local full_name = name:gsub("_", " ")
168✔
95
         local subs
96
         name, subs = name:gsub("^no_", "")
168✔
97
         local flag = subs == 0
168✔
98

99
         if options.nullary_inline_options[name] then
168✔
100
            if #args ~= 0 then
48✔
101
               return nil, unexpected_num_args(full_name, args, 0)
8✔
102
            end
103

104
            opts[name] = flag
42✔
105
         else
106
            assert(limit_opts[name])
120✔
107

108
            if flag then
120✔
109
               if #args ~= 1 then
96✔
110
                  return nil, unexpected_num_args(full_name, args, 1)
×
111
               end
112

113
               local value = tonumber(args[1])
96✔
114

115
               if not value then
96✔
116
                  return nil, ("inline option '%s' expects number as argument"):format(name)
×
117
               end
118

119
               opts[name] = value
96✔
120
            else
121
               if #args ~= 0 then
24✔
122
                  return nil, unexpected_num_args(full_name, args, 0)
×
123
               end
124

125
               opts[name] = false
24✔
126
            end
127
         end
128
      end
129
   end
130

131
   return opts
444✔
132
end
133

134
-- Parses comment contents, returns up to two `options` values (tables or "push" or "pop").
135
-- On an invalid inline comment returns nil and an error message.
136
local function parse_inline_comment(comment_contents)
137
   local body = utils.after(utils.strip(comment_contents), "^luacheck:")
1,456✔
138

139
   if not body then
1,092✔
140
      return
414✔
141
   end
142

143
   local opts1, opts2
144

145
   -- Remove comments in balanced parens.
146
   body = utils.strip((body:gsub("%b()", " ")))
904✔
147
   local after_push = body:match("^push%s+(.*)")
678✔
148

149
   if after_push then
678✔
150
      opts2 = "push"
102✔
151
      body = after_push
102✔
152
   elseif body == "push" or body == "pop" then
576✔
153
      return body
186✔
154
   end
155

156
   local err_msg
157
   opts1, err_msg = parse_options(body)
656✔
158
   return opts1, err_msg or opts2
492✔
159
end
160

161
-- Returns an array of tables with column range info and an `options` field
162
-- containing a table of options or "push" or "pop".
163
-- Warns about invalid inline option comments.
164
local function parse_inline_comments(chstate)
165
   local res = {}
1,350✔
166

167
   for _, comment in ipairs(chstate.comments) do
2,442✔
168
      local opts1, opts2 = parse_inline_comment(comment.contents)
1,092✔
169

170
      if opts1 then
1,092✔
171
         table.insert(res, {
1,260✔
172
            line = comment.line,
630✔
173
            column = chstate:offset_to_column(comment.line, comment.offset),
840✔
174
            end_column = chstate:offset_to_column(comment.line, comment.end_offset),
840✔
175
            options = opts1
630✔
176
         })
177

178
         if opts2 then
630✔
179
            table.insert(res, {
204✔
180
               line = comment.line,
102✔
181
               column = chstate:offset_to_column(comment.line, comment.offset),
136✔
182
               end_column = chstate:offset_to_column(comment.line, comment.end_offset),
136✔
183
               options = opts2
102✔
184
            })
185
         end
186
      elseif opts2 then
462✔
187
         chstate:warn_range("021", comment, {msg = opts2})
48✔
188
      end
189
   end
190

191
   return res
1,350✔
192
end
193

194
-- Adds a table with `line`, `column`, and `options` fields to given array.
195
-- For each function a table with `options` set to "push" for the function start
196
-- and a table with `options` set to "pop" for the function end are added.
197
local function add_function_boundaries(inline_options_and_boundaries, chstate)
198
   for _, line in ipairs(chstate.top_line.lines) do
2,862✔
199
      local fn_node = line.node
1,512✔
200

201
      table.insert(inline_options_and_boundaries, {
3,024✔
202
         line = fn_node.line,
1,512✔
203
         column = chstate:offset_to_column(fn_node.line, fn_node.offset),
2,016✔
204
         options = "push"
1,008✔
205
      })
206

207
      table.insert(inline_options_and_boundaries, {
3,024✔
208
         line = fn_node.end_range.line,
1,512✔
209
         column = chstate:offset_to_column(fn_node.end_range.line, fn_node.end_range.offset),
2,016✔
210
         options = "pop"
1,008✔
211
      })
212
   end
213
end
214

215
local function get_order(t)
216
   if t.options == "push" then
1,620✔
217
      return 1
684✔
218
   elseif t.options == "pop" then
936✔
219
      return 3
660✔
220
   else
221
      return 2
276✔
222
   end
223
end
224

225
local function options_and_boundaries_comparator(t1, t2)
226
   if t1.line ~= t2.line then
11,382✔
227
      return t1.line < t2.line
10,572✔
228
   end
229

230
   -- For options and boundaries on the same line, all pushes are applied before options before pops.
231
   -- (Valid pops will be moved to the start of the next line later.)
232
   local order1 = get_order(t1)
810✔
233
   local order2 = get_order(t2)
810✔
234

235
   if order1 ~= order2 then
810✔
236
      return order1 < order2
618✔
237
   else
238
      return t1.column < t2.column
192✔
239
   end
240
end
241

242
-- Applies boundaries within `inline_options_and_boundaries` to replace them with pop count
243
-- instructions in the resulting array.
244
-- Comments on lines with code are popped at the end of line.
245
-- Warns about unpaired push and pop directives.
246
local function apply_boundaries(chstate, inline_options_and_boundaries)
247
   local res = {}
1,350✔
248
   local res_last
249

250
   -- While iterating over inline options and boundaries track push
251
   -- boundaries that were not popped yet plus the number of options
252
   -- that would be on the option stack after applying all already
253
   -- processed option table pushes and pops.
254
   local pushes = utils.Stack()
1,350✔
255
   local push_option_counts = utils.Stack()
1,350✔
256
   local option_count = 0
1,350✔
257

258
   for _, item in ipairs(inline_options_and_boundaries) do
5,106✔
259
      if item.options == "push" then
3,756✔
260
         pushes:push(item)
1,656✔
261
         push_option_counts:push(option_count)
2,208✔
262
      elseif item.options == "pop" then
2,100✔
263
         -- Function boundaries are implicit, don't allow inline options to pop
264
         -- them, don't allow function boundaries to pop inline option pushes either.
265
         -- Inline options boundaries have end_column, function boundaries don't.
266
         if not pushes.top or (item.end_column and not pushes.top.end_column) then
1,656✔
267
            -- Inline option pop against nothing or a function push, mark as unpaired.
268
            chstate:warn_column_range("023", item)
32✔
269
         else
270
            if not item.end_column then
1,632✔
271
               -- Function pop, remove any unpaired inline option pushes.
272
               while pushes.top and pushes.top.end_column do
1,512✔
273
                  chstate:warn_column_range("022", pushes.top)
×
274
                  pushes:pop()
×
275
                  push_option_counts:pop()
×
276
               end
277
            end
278

279
            pushes:pop()
1,632✔
280
            local prev_option_count = push_option_counts:pop()
1,632✔
281
            local pop_count = option_count - prev_option_count
1,632✔
282

283
            if pop_count > 0 then
1,632✔
284
               -- Place the pop instruction at the start of the next line so that getting option stack
285
               -- for a line amounts to applying both the pop instruction and the option push for the line.
286
               local line = item.line + 1
138✔
287

288
               -- Collapse with a previous table if it's on the same line. It can only be a pop count table.
289
               if res_last and res_last.line == line then
138✔
290
                  res_last.pop_count = res_last.pop_count + pop_count
×
291
               else
292
                  res_last = {
138✔
293
                     line = line,
138✔
294
                     pop_count = pop_count
138✔
295
                  }
138✔
296

297
                  table.insert(res, res_last)
138✔
298
               end
299
            end
300

301
            -- Update option stack size for this pop.
302
            option_count = prev_option_count
1,632✔
303
         end
304
      else
305
         -- Inline options table. Check if there is a pop count table for this line already.
306
         if res_last and res_last.line == item.line then
444✔
307
            res_last.options = item.options
36✔
308
            res_last.column = item.column
36✔
309
            res_last.end_column = item.end_column
36✔
310
         else
311
            res_last = item
408✔
312
            table.insert(res, item)
408✔
313
         end
314

315
         if chstate.code_lines[item.line] then
444✔
316
            -- Inline comment on a line with some code, immediately pop it.
317
            res_last = {
78✔
318
               line = item.line + 1,
78✔
319
               pop_count = 1
52✔
320
            }
78✔
321
            table.insert(res, res_last)
78✔
322
         else
323
            option_count = option_count + 1
366✔
324
         end
325
      end
326
   end
327

328
   -- Any remaining pushes are unpaired inline comments from the main chunk.
329
   while pushes.top do
1,374✔
330
      chstate:warn_column_range("022", pushes:pop())
40✔
331
   end
332

333
   return res
1,350✔
334
end
335

336
-- Warns about invalid inline options.
337
-- Sets `chstate.inline_options` to an array of tables that describe the way inline option tables
338
-- are pushed onto and popped from the option stack when iterating over lines.
339
-- Each table has field `line` that the array is sorted by and also ether or both sets of fields:
340
-- * `pop_count` - refers to a number of option tables that should be popped from the stack before processing
341
--   warnings on this line.
342
-- * `options`, `column`, `end_column` - refers to an option table that should be pushed onto the stack
343
--   before processing warnings on this line but after popping tables if `pop_count` is present.
344
function stage.run(chstate)
870✔
345
   local inline_options_and_boundaries = parse_inline_comments(chstate)
1,350✔
346
   add_function_boundaries(inline_options_and_boundaries, chstate)
1,350✔
347
   table.sort(inline_options_and_boundaries, options_and_boundaries_comparator)
1,350✔
348
   chstate.inline_options = apply_boundaries(chstate, inline_options_and_boundaries)
1,800✔
349
end
350

351
return stage
870✔
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