local core_utils = require "luacheck.core_utils"
local decoder = require "luacheck.decoder"
local options = require "luacheck.options"
local utils = require "luacheck.utils"
local filter = {}
local function match(pattern, code, name)
local matches_code, matches_name
local code_pattern, name_pattern = pattern[1], pattern[2]
if code_pattern then
matches_code = utils.pmatch(code, code_pattern)
end
if name_pattern then
if not name then
matches_name = false
else
matches_name = utils.pmatch(name, name_pattern)
end
end
return matches_code, matches_name
end
local function passes_rules_filter(rules, code, name)
local enabled_code, enabled_name = false, false
for _, rule in ipairs(rules) do
local matches_one = false
for _, pattern in ipairs(rule[1]) do
local matches_code, matches_name = match(pattern, code, name)
if enabled_code then
matches_code = rule[2] ~= "disable"
end
if enabled_name then
matches_code = rule[2] ~= "disable"
end
if (matches_code and matches_name ~= false) or
(matches_name and matches_code ~= false) then
matches_one = true
end
if rule[2] == "enable" then
if matches_code then
enabled_code = true
end
if matches_name then
enabled_name = true
end
if enabled_code and enabled_name then
return true
end
elseif rule[2] == "disable" then
if matches_one then
return false
end
end
end
if rule[2] == "only" and not matches_one then
return false
end
end
return true
end
local function get_field_string(warning)
local parts = {}
if warning.indexing then
for _, index in ipairs(warning.indexing) do
local part
if type(index) == "string" then
local chars = decoder.decode(index)
part = chars:get_printable_substring(1, chars:get_length())
else
part = "?"
end
table.insert(parts, part)
end
end
return table.concat(parts, ".")
end
local function get_field_status(opts, warning, depth)
local def = opts.std
local defined = true
local read_only = true
for i = 1, depth or (warning.indexing and #warning.indexing or 0) + 1 do
local index_string = i == 1 and warning.name or warning.indexing[i - 1]
if index_string == true then
if (def.fields and next(def.fields)) or def.other_fields then
if def.deep_read_only then
read_only = true
else
read_only = false
end
else
defined = false
end
break
elseif index_string == false then
if not def.other_fields then
defined = false
end
break
else
if def.fields and def.fields[index_string] then
def = def.fields[index_string]
if def.read_only ~= nil then
read_only = def.read_only
end
else
if not def.other_fields then
defined = false
end
break
end
end
end
return defined and (read_only and "read_only" or "global") or "undefined"
end
local function passes_filter(normalized_options, warning)
if warning.code == "561" then
local max_complexity = normalized_options.max_cyclomatic_complexity
if not max_complexity or warning.complexity <= max_complexity then
return false
end
warning.max_complexity = max_complexity
elseif warning.code == "033" then
local operators = normalized_options.operators or {}
for _, op in ipairs(operators) do
if warning.operator == op then
return false
end
end
return true
elseif warning.code:find("^[234]") and warning.name == "_" and not warning.useless then
return false
elseif warning.code:find("^1[14]") then
if warning.indirect and
get_field_status(normalized_options, warning, warning.previous_indexing_len) == "undefined" then
return false
end
if not warning.module and get_field_status(normalized_options, warning) ~= "undefined" then
return false
end
end
if warning.code:find("^1[24][23]") then
warning.field = get_field_string(warning)
end
if warning.secondary and not normalized_options.unused_secondaries then
return false
end
if warning.self and not normalized_options.self then
return false
end
return passes_rules_filter(normalized_options.rules, warning.code, warning.name)
end
local empty_options = {}
local function update_option_stack_for_new_line(check_result, stds, option_stack, line, next_index)
local inline_option = check_result.inline_options[next_index]
if not inline_option or inline_option.line > line then
return next_index
end
next_index = next_index + 1
if inline_option.pop_count then
for _ = 1, inline_option.pop_count do
table.remove(option_stack)
end
end
if not inline_option.options then
return next_index
end
local options_ok, err_msg = options.validate(options.all_options, inline_option.options, stds)
if not options_ok then
inline_option.options = nil
inline_option.code = "021"
inline_option.msg = err_msg
table.insert(check_result.filtered_warnings, inline_option)
table.insert(option_stack, empty_options)
else
table.insert(option_stack, inline_option.options)
end
return next_index
end
local function check_line_length(check_result, normalized_options, line)
local line_length = check_result.line_lengths[line]
local line_type = check_result.line_endings[line]
local max_length = normalized_options["max_" .. (line_type or "code") .. "_line_length"]
if max_length and line_length > max_length then
if passes_rules_filter(normalized_options.rules, "631") then
table.insert(check_result.filtered_warnings, {
code = "631",
line = line,
column = max_length + 1,
end_column = line_length,
max_length = max_length,
line_ending = line_type
})
end
end
end
local function filter_warnings_on_new_line(check_result, normalized_options, line, next_index)
while true do
local warning = check_result.warnings[next_index]
if not warning or warning.line > line then
break
end
if warning.code:find("^1") then
check_result.normalized_options[line] = normalized_options
elseif passes_filter(normalized_options, warning) then
table.insert(check_result.filtered_warnings, warning)
end
next_index = next_index + 1
end
return next_index
end
local CachingOptionsNormalizer = utils.class()
function CachingOptionsNormalizer:__init()
self.result_trie = {}
end
function CachingOptionsNormalizer:normalize_options(stds, option_stack)
local result_node = self.result_trie
for _, option_table in ipairs(option_stack) do
if not result_node[option_table] then
result_node[option_table] = {}
end
result_node = result_node[option_table]
end
if result_node.result then
return result_node.result
end
local result = options.normalize(option_stack, stds)
result_node.result = result
return result
end
local function filter_not_global_related_in_file(check_result, options_normalizer, stds, option_stack)
check_result.filtered_warnings = {}
check_result.normalized_options = {}
local next_warning_index = 1
local next_inline_option_index = 1
for line in ipairs(check_result.line_lengths) do
next_inline_option_index = update_option_stack_for_new_line(
check_result, stds, option_stack, line, next_inline_option_index)
local normalized_options = options_normalizer:normalize_options(stds, option_stack)
check_line_length(check_result, normalized_options, line)
next_warning_index = filter_warnings_on_new_line(check_result, normalized_options, line, next_warning_index)
end
end
local function may_have_options(opts_table)
for key in pairs(opts_table) do
if type(key) == "string" then
return true
end
end
return false
end
local function get_option_stack(opts, file_index)
local res = {opts}
if opts and opts[file_index] then
if may_have_options(opts[file_index]) then
table.insert(res, opts[file_index])
end
for _, nested_opts in ipairs(opts[file_index]) do
table.insert(res, nested_opts)
end
end
return res
end
local function filter_not_global_related(check_results, opts, stds)
local caching_options_normalizer = CachingOptionsNormalizer()
for file_index, check_result in ipairs(check_results) do
if not check_result.fatal then
if check_result.warnings[1] and check_result.warnings[1].code == "011" then
check_result.filtered_warnings = check_result.warnings
check_result.normalized_options = {}
else
local base_file_option_stack = get_option_stack(opts, file_index)
filter_not_global_related_in_file(check_result, caching_options_normalizer, stds, base_file_option_stack)
end
end
end
end
local function is_definition(normalized_options, warning)
return normalized_options.allow_defined or (normalized_options.allow_defined_top and warning.top)
end
local function get_implicit_globals_in_file(check_result)
local defined = {}
local exported = {}
local used = {}
for _, warning in ipairs(check_result.warnings) do
if warning.code:find("^11") then
if warning.code == "111" then
local normalized_options = check_result.normalized_options[warning.line]
if is_definition(normalized_options, warning) then
if normalized_options.module then
defined[warning.name] = true
else
exported[warning.name] = true
end
end
else
used[warning.name] = true
end
end
end
return defined, exported, used
end
local function get_implicit_globals(check_results)
local globally_defined = {}
local globally_used = {}
local locally_defined = {}
for file_index, check_result in ipairs(check_results) do
if not check_result.fatal then
local defined, exported, used = get_implicit_globals_in_file(check_result)
utils.update(globally_defined, exported)
utils.update(globally_used, used)
locally_defined[file_index] = defined
end
end
return globally_defined, globally_used, locally_defined
end
local function apply_implicit_definitions(globally_defined, globally_used, locally_defined, normalized_options, warning)
if not warning.code:find("^11") then
return warning
end
if warning.code == "111" then
if normalized_options.module then
if locally_defined[warning.name] then
return
end
warning.module = true
else
if is_definition(normalized_options, warning) then
if globally_used[warning.name] then
return
end
warning.code = "131"
warning.top = nil
else
if globally_defined[warning.name] then
return
end
end
end
else
if globally_defined[warning.name] or locally_defined[warning.name] then
return
end
end
return warning
end
local function filter_global_related_in_file(check_result, globally_defined, globally_used, locally_defined)
for _, warning in ipairs(check_result.warnings) do
if warning.code:find("^1") then
local normalized_options = check_result.normalized_options[warning.line]
warning = apply_implicit_definitions(
globally_defined, globally_used, locally_defined, normalized_options, warning)
if warning then
if warning.code:find("^11[12]") and not warning.module and
get_field_status(normalized_options, warning) == "read_only" then
warning.code = "12" .. warning.code:sub(3, 3)
elseif warning.code:find("^11[23]") and get_field_status(normalized_options, warning, 1) ~= "undefined" then
warning.code = "14" .. warning.code:sub(3, 3)
end
if warning.code:match("11[23]") and get_field_status(normalized_options, warning, 1) ~= "undefined" then
warning.code = "14" .. warning.code:sub(3, 3)
end
if passes_filter(normalized_options, warning) then
table.insert(check_result.filtered_warnings, warning)
end
end
end
end
end
local function filter_global_related(check_results)
local globally_defined, globally_used, locally_defined = get_implicit_globals(check_results)
for file_index, check_result in ipairs(check_results) do
if not check_result.fatal then
filter_global_related_in_file(check_result, globally_defined, globally_used, locally_defined[file_index])
end
end
end
function filter.filter(check_results, opts, stds)
filter_not_global_related(check_results, opts, stds)
filter_global_related(check_results)
local report = {}
for file_index, check_result in ipairs(check_results) do
if check_result.fatal then
report[file_index] = check_result
else
core_utils.sort_by_location(check_result.filtered_warnings)
report[file_index] = check_result.filtered_warnings
end
end
return report
end
return filter