local options = require "luacheck.options"
|
local utils = require "luacheck.utils"
|
|
local stage = {}
|
|
stage.warnings = {
|
-- Also produced during filtering for options that did not pass validation.
|
["021"] = {message_format = "{msg}", fields = {"msg"}},
|
["022"] = {message_format = "unpaired push directive", fields = {}},
|
["023"] = {message_format = "unpaired pop directive", fields = {}}
|
}
|
|
stage.inline_option_fields = {"line", "pop_count", "options", "column", "end_column"}
|
|
local limit_opts = utils.array_to_set({"max_line_length", "max_code_line_length", "max_string_line_length",
|
"max_comment_line_length", "max_cyclomatic_complexity"})
|
|
local function is_valid_option_name(name)
|
if name == "std" or options.variadic_inline_options[name] then
|
return true
|
end
|
|
name = name:gsub("^no_", "")
|
return options.nullary_inline_options[name] or limit_opts[name]
|
end
|
|
-- Splits a token array for an inline option invocation into
|
-- option name and argument array, or nil if invocation is invalid.
|
local function split_invocation(tokens)
|
-- Name of the option can be split into several space separated tokens.
|
-- Since some valid names are prefixes of some other names
|
-- (e.g. `unused` and `unused arguments`), the longest prefix of token
|
-- array that is a valid option name should be considered.
|
local cur_name
|
local last_valid_name
|
local last_valid_name_end_index
|
|
for i, token in ipairs(tokens) do
|
cur_name = cur_name and (cur_name .. "_" .. token) or token
|
|
if is_valid_option_name(cur_name) then
|
last_valid_name = cur_name
|
last_valid_name_end_index = i
|
end
|
end
|
|
if not last_valid_name then
|
return
|
end
|
|
local args = {}
|
|
for i = last_valid_name_end_index + 1, #tokens do
|
table.insert(args, tokens[i])
|
end
|
|
return last_valid_name, args
|
end
|
|
local function unexpected_num_args(name, args, expected)
|
return ("inline option '%s' expects %d argument%s, %d given"):format(
|
name, expected, expected == 1 and "" or "s", #args)
|
end
|
|
-- Parses inline option body, returns options or nil and error message.
|
local function parse_options(body)
|
local opts = {}
|
|
local parts = utils.split(body, ",")
|
|
for _, name_and_args in ipairs(parts) do
|
local tokens = utils.split(name_and_args)
|
local name, args = split_invocation(tokens)
|
|
if not name then
|
if #tokens == 0 then
|
return nil, (#parts == 1) and "empty inline option" or "empty inline option invocation"
|
else
|
return nil, ("unknown inline option '%s'"):format(table.concat(tokens, " "))
|
end
|
end
|
|
if name == "std" then
|
if #args ~= 1 then
|
return nil, unexpected_num_args(name, args, 1)
|
end
|
|
opts.std = args[1]
|
elseif name == "ignore" and #args == 0 then
|
opts.ignore = {".*"}
|
elseif options.variadic_inline_options[name] then
|
opts[name] = args
|
else
|
local full_name = name:gsub("_", " ")
|
local subs
|
name, subs = name:gsub("^no_", "")
|
local flag = subs == 0
|
|
if options.nullary_inline_options[name] then
|
if #args ~= 0 then
|
return nil, unexpected_num_args(full_name, args, 0)
|
end
|
|
opts[name] = flag
|
else
|
assert(limit_opts[name])
|
|
if flag then
|
if #args ~= 1 then
|
return nil, unexpected_num_args(full_name, args, 1)
|
end
|
|
local value = tonumber(args[1])
|
|
if not value then
|
return nil, ("inline option '%s' expects number as argument"):format(name)
|
end
|
|
opts[name] = value
|
else
|
if #args ~= 0 then
|
return nil, unexpected_num_args(full_name, args, 0)
|
end
|
|
opts[name] = false
|
end
|
end
|
end
|
end
|
|
return opts
|
end
|
|
-- Parses comment contents, returns up to two `options` values (tables or "push" or "pop").
|
-- On an invalid inline comment returns nil and an error message.
|
local function parse_inline_comment(comment_contents)
|
local body = utils.after(utils.strip(comment_contents), "^luacheck:")
|
|
if not body then
|
return
|
end
|
|
local opts1, opts2
|
|
-- Remove comments in balanced parens.
|
body = utils.strip((body:gsub("%b()", " ")))
|
local after_push = body:match("^push%s+(.*)")
|
|
if after_push then
|
opts2 = "push"
|
body = after_push
|
elseif body == "push" or body == "pop" then
|
return body
|
end
|
|
local err_msg
|
opts1, err_msg = parse_options(body)
|
return opts1, err_msg or opts2
|
end
|
|
-- Returns an array of tables with column range info and an `options` field
|
-- containing a table of options or "push" or "pop".
|
-- Warns about invalid inline option comments.
|
local function parse_inline_comments(chstate)
|
local res = {}
|
|
for _, comment in ipairs(chstate.comments) do
|
local opts1, opts2 = parse_inline_comment(comment.contents)
|
|
if opts1 then
|
table.insert(res, {
|
line = comment.line,
|
column = chstate:offset_to_column(comment.line, comment.offset),
|
end_column = chstate:offset_to_column(comment.line, comment.end_offset),
|
options = opts1
|
})
|
|
if opts2 then
|
table.insert(res, {
|
line = comment.line,
|
column = chstate:offset_to_column(comment.line, comment.offset),
|
end_column = chstate:offset_to_column(comment.line, comment.end_offset),
|
options = opts2
|
})
|
end
|
elseif opts2 then
|
chstate:warn_range("021", comment, {msg = opts2})
|
end
|
end
|
|
return res
|
end
|
|
-- Adds a table with `line`, `column`, and `options` fields to given array.
|
-- For each function a table with `options` set to "push" for the function start
|
-- and a talbe with `options` set to "pop" for the function end are added.
|
local function add_function_boundaries(inline_options_and_boundaries, chstate)
|
for _, line in ipairs(chstate.top_line.lines) do
|
local fn_node = line.node
|
|
table.insert(inline_options_and_boundaries, {
|
line = fn_node.line,
|
column = chstate:offset_to_column(fn_node.line, fn_node.offset),
|
options = "push"
|
})
|
|
table.insert(inline_options_and_boundaries, {
|
line = fn_node.end_range.line,
|
column = chstate:offset_to_column(fn_node.end_range.line, fn_node.end_range.offset),
|
options = "pop"
|
})
|
end
|
end
|
|
local function get_order(t)
|
if t.options == "push" then
|
return 1
|
elseif t.options == "pop" then
|
return 3
|
else
|
return 2
|
end
|
end
|
|
local function options_and_boundaries_comparator(t1, t2)
|
if t1.line ~= t2.line then
|
return t1.line < t2.line
|
end
|
|
-- For options and boundaries on the same line, all pushes are applied before options before pops.
|
-- (Valid pops will be moved to the start of the next line later.)
|
local order1 = get_order(t1)
|
local order2 = get_order(t2)
|
|
if order1 ~= order2 then
|
return order1 < order2
|
else
|
return t1.column < t2.column
|
end
|
end
|
|
-- Applies bounadaries withing `inline_options_and_boundaries` to replace them with pop count
|
-- instructions in the resulting array.
|
-- Comments on lines with code are popped at the end of line.
|
-- Warns about unpaired push and pop directives.
|
local function apply_boundaries(chstate, inline_options_and_boundaries)
|
local res = {}
|
local res_last
|
|
-- While iterating over inline options and boundaries track push
|
-- boundaries that were not popped yet plus the number of options
|
-- that would be on the option stack after applying all already
|
-- processed option table pushes and pops.
|
local pushes = utils.Stack()
|
local push_option_counts = utils.Stack()
|
local option_count = 0
|
|
for _, item in ipairs(inline_options_and_boundaries) do
|
if item.options == "push" then
|
pushes:push(item)
|
push_option_counts:push(option_count)
|
elseif item.options == "pop" then
|
-- Function boundaries are implicit, don't allow inline options to pop
|
-- them, don't allow function boundaries to pop inline option pushes either.
|
-- Inline options boundaries have end_column, function boundaries don't.
|
if not pushes.top or (item.end_column and not pushes.top.end_column) then
|
-- Inline option pop against nothing or a function push, mark as unpaired.
|
chstate:warn_column_range("023", item)
|
else
|
if not item.end_column then
|
-- Function pop, remove any unpaired inline option pushes.
|
while pushes.top and pushes.top.end_column do
|
chstate:warn_column_range("022", pushes.top)
|
pushes:pop()
|
push_option_counts:pop()
|
end
|
end
|
|
pushes:pop()
|
local prev_option_count = push_option_counts:pop()
|
local pop_count = option_count - prev_option_count
|
|
if pop_count > 0 then
|
-- Place the pop instruction at the start of the next line so that getting option stack
|
-- for a line amounts to applying both the pop instruction and the option push for the line.
|
local line = item.line + 1
|
|
-- Collapse with a previous table if it's on the same line. It can only be a pop count table.
|
if res_last and res_last.line == line then
|
res_last.pop_count = res_last.pop_count + pop_count
|
else
|
res_last = {
|
line = line,
|
pop_count = pop_count
|
}
|
|
table.insert(res, res_last)
|
end
|
end
|
|
-- Update option stack size for this pop.
|
option_count = prev_option_count
|
end
|
else
|
-- Inline options table. Check if there is a pop count table for this line already.
|
if res_last and res_last.line == item.line then
|
res_last.options = item.options
|
res_last.column = item.column
|
res_last.end_column = item.end_column
|
else
|
res_last = item
|
table.insert(res, item)
|
end
|
|
if chstate.code_lines[item.line] then
|
-- Inline comment on a line with some code, immediately pop it.
|
res_last = {
|
line = item.line + 1,
|
pop_count = 1
|
}
|
table.insert(res, res_last)
|
else
|
option_count = option_count + 1
|
end
|
end
|
end
|
|
-- Any remaining pushes are unpaired inline comments from the main chunk.
|
while pushes.top do
|
chstate:warn_column_range("022", pushes:pop())
|
end
|
|
return res
|
end
|
|
-- Warns about invalid inline options.
|
-- Sets `chstate.inline_options` to an array of tables that describe the way inline option tables
|
-- are pushed onto and popped from the option stack when iterating over lines.
|
-- Each table has field `line` that the array is sorted by and also ether or both sets of fields:
|
-- * `pop_count` - refers to a number of option tables that should be popped from the stack before processing
|
-- warnings on this line.
|
-- * `options`, `column`, `end_column` - refers to an option table that should be pushed onto the stack
|
-- before processing warnings on this line but after popping tables if `pop_count` is present.
|
function stage.run(chstate)
|
local inline_options_and_boundaries = parse_inline_comments(chstate)
|
add_function_boundaries(inline_options_and_boundaries, chstate)
|
table.sort(inline_options_and_boundaries, options_and_boundaries_comparator)
|
chstate.inline_options = apply_boundaries(chstate, inline_options_and_boundaries)
|
end
|
|
return stage
|