local core_utils = require "luacheck.core_utils" local decoder = require "luacheck.decoder" local options = require "luacheck.options" local utils = require "luacheck.utils" local filter = {} -- Returns two optional booleans indicating if warning matches pattern by code and name. 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 -- Warnings without name field can't match by name. 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) -- A warning is enabled when its code and name are enabled. 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 a factor is enabled, warning can't be disabled by it. 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 -- Enable as matching to some `enable` pattern by code and to another by name. return true end elseif rule[2] == "disable" then if matches_one then -- Disable as matching to `disable` pattern. return false end end end if rule[2] == "only" and not matches_one then -- Disable as not matching to any of `only` patterns. return false end end -- Enable by default. 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 -- Indexing with something that may or may not be a string. 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 -- Indexing with not a string. if not def.other_fields then defined = false end break else -- Indexing with a constant string. if def.fields and def.fields[index_string] then -- The field is defined, recurse into it. def = def.fields[index_string] if def.read_only ~= nil then read_only = def.read_only end else -- The field is not defined, but it may be okay to index if `other_fields` is true. 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 -- Checks if a warning passes options filter. May add some fields required for formatting. 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: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 = {} -- Updates option_stack for given line with next_index pointing to the inline option past the previous line. -- Adds warnings for invalid inline options to check_result, filtered_warnings. -- Returns updated next_index. 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 -- No inline options on this line, option stack for the line is ready. 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 -- No inline option push on this line, option stack for the line is ready. return next_index end local options_ok, err_msg = options.validate(options.all_options, inline_option.options, stds) if not options_ok then -- Warn about invalid inline option, push a dummy empty table instead to keep pop counts correct. inline_option.options = nil inline_option.code = "021" inline_option.msg = err_msg table.insert(check_result.filtered_warnings, inline_option) -- Reuse empty table identity so that normalized option caching works better. table.insert(option_stack, empty_options) else table.insert(option_stack, inline_option.options) end return next_index end -- Warns (adds to check_result.filtered_warnings) about a line if it's too long -- and the warning is not filtered out by options. 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 -- Adds warnings passing filtering and not related to globals to check_result.filtered_warnings. -- If there is a global related warning on this line, sets check_results[line] to normalized_optuons. 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 -- No more warnings on this line. 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 -- Normalizing options is relatively expensive because full std definitions are quite large. -- `CachingOptionsNormalizer` implements a caching layer that reduces number of `options.normalize` calls. -- Caching is done based on identities of option tables. 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 -- May mutate base_opts_stack. local function filter_not_global_related_in_file(check_result, options_normalizer, stds, option_stack) check_result.filtered_warnings = {} check_result.normalized_options = {} -- Iterate over lines, warnings, and inline options at the same time, keeping opts_stack up to date. 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 -- Don't add useless per-file option tables, that messes up normalized option caching -- since it memorizes based on option table identities. 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 -- For each file check result: -- * Stores invalid inline options, not filtered out not global-related warnings, and newly created line length warnings -- in .filtered_warnings. -- * Stores a map from line numbers to normalized options for lines of global-related warnings in .normalized_options. 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 -- Special case syntax errors, they don't have line numbers so normal filtering does not work. 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 -- A global is implicitly defined in a file if opts.allow_defined == true and it is set anywhere in the file, -- or opts.allow_defined_top == true and it is set in the top level function scope. -- By default, accessing and setting globals in a file is allowed for explicitly defined globals (standard and custom) -- for that file and implicitly defined globals from that file and -- all other files except modules (files with opts.module == true). -- Accessing other globals results in "accessing undefined variable" warning. -- Setting other globals results in "setting non-standard global variable" warning. -- Unused implicitly defined global results in "unused global variable" warning. -- For modules, accessing globals uses same rules as normal files, however, -- setting globals is only allowed for implicitly defined globals from the module. -- Setting a global not defined in the module results in "setting non-module global variable" warning. local function is_definition(normalized_options, warning) return normalized_options.allow_defined or (normalized_options.allow_defined_top and warning.top) end -- Extracts sets of defined, exported and used globals from a file check result. 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 -- Returns set of globals defines across all files except modules, a set of globals used across all files, -- and an array of sets of globals defined per file, parallel to the check results array. 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 -- Mutates the warning and returns it or discards it by returning nothing if it's filtered out. 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 -- Processes an array of results of the check stage (or tables with .fatal field) into the final report. -- `opts[i]`, if present, is used as options when processing `report[i]` together with options in its array part. -- This function may mutate check results or reuse its parts in the return value. 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