-- A registry of warnings and errors identified by different processing steps. local get_option = require("explcheck-config").get_option local Issues = {} function Issues.new(cls, pathname, options) -- Instantiate the class. local self = {} setmetatable(self, cls) cls.__index = cls -- Initialize the class. self.errors = {} self.warnings = {} self.seen_issues = {} self.suppressed_issue_map = {} for issue_identifier, suppressed_issues in pairs(get_option("suppressed_issue_map", options, pathname)) do issue_identifier = self._normalize_identifier(issue_identifier) self.suppressed_issue_map[issue_identifier] = suppressed_issues end self.ignored_issues = {} for _, issue_identifier in ipairs(get_option("ignored_issues", options, pathname)) do self:ignore(issue_identifier) end return self end -- Normalize an issue identifier. function Issues._normalize_identifier(identifier) return identifier:lower() end -- Convert an issue identifier to either a table of warnings or a table of errors. function Issues:_get_issue_table(identifier) identifier = self._normalize_identifier(identifier) local prefix = identifier:sub(1, 1) if prefix == "s" or prefix == "w" then return self.warnings elseif prefix == "t" or prefix == "e" then return self.errors else assert(false, 'Identifier "' .. identifier .. '" has an unknown prefix "' .. prefix .. '"') end end -- Add an issue to the table of issues. function Issues:add(identifier, message, range, context) identifier = self._normalize_identifier(identifier) -- Discard duplicate issues. local range_start = (range ~= nil and range:start()) or false local range_end = (range ~= nil and range:stop()) or false if self.seen_issues[identifier] == nil then self.seen_issues[identifier] = {} end if self.seen_issues[identifier][range_start] == nil then self.seen_issues[identifier][range_start] = {} end if self.seen_issues[identifier][range_start][range_end] == nil then self.seen_issues[identifier][range_start][range_end] = true else return end -- Suppress any dependent issues. if self.suppressed_issue_map[identifier] ~= nil then for _, suppressed_issue_identifier in ipairs(self.suppressed_issue_map[identifier]) do suppressed_issue_identifier = self._normalize_identifier(suppressed_issue_identifier) self:ignore(suppressed_issue_identifier, range) end end -- Construct the issue. local issue = {identifier, message, range, context} -- Determine if the issue should be ignored. for _, ignore_issue in ipairs(self.ignored_issues) do if ignore_issue(issue) then return end end -- Add the issue to the table of issues. local issue_table = self:_get_issue_table(identifier) table.insert(issue_table, issue) end -- Prevent issues from being present in the table of issues. function Issues:ignore(identifier_prefix, range) if identifier_prefix ~= nil then identifier_prefix = self._normalize_identifier(identifier_prefix) end -- Determine which issues should be ignored. local function match_issue_range(issue_range) return ( issue_range:start() >= range:start() and issue_range:start() <= range:stop() -- issue starts within range or issue_range:start() <= range:start() and issue_range:stop() >= range:stop() -- issue is in middle of range or issue_range:stop() >= range:start() and issue_range:stop() <= range:stop() -- issue ends within range ) end local function match_issue_identifier(issue_identifier) -- Match the prefix of an issue, allowing us to ignore whole sets of issues with prefixes like "s" or "w4". return issue_identifier:sub(1, #identifier_prefix) == identifier_prefix end local ignore_issue, issue_tables if identifier_prefix == nil and range == nil then -- Prevent any issues. issue_tables = {self.warnings, self.errors} ignore_issue = function() return true end elseif identifier_prefix == nil then -- Prevent any issues within the given range. issue_tables = {self.warnings, self.errors} ignore_issue = function(issue) local issue_range = issue[3] if issue_range == nil then -- file-wide issue return false else -- ranged issue return match_issue_range(issue_range) end end elseif range == nil then -- Prevent any issues with the given identifier. assert(identifier_prefix ~= nil) issue_tables = {self:_get_issue_table(identifier_prefix)} ignore_issue = function(issue) local issue_identifier = issue[1] return match_issue_identifier(issue_identifier) end else -- Prevent any issues with the given identifier that are also either within the given range or file-wide. assert(range ~= nil and identifier_prefix ~= nil) issue_tables = {self:_get_issue_table(identifier_prefix)} ignore_issue = function(issue) local issue_identifier = issue[1] local issue_range = issue[3] if issue_range == nil then -- file-wide issue return match_issue_identifier(issue_identifier) else -- ranged issue return match_issue_range(issue_range) and match_issue_identifier(issue_identifier) end end end -- Remove the issue if it has already been added. for _, issue_table in ipairs(issue_tables) do local filtered_issues = {} for _, issue in ipairs(issue_table) do if not ignore_issue(issue) then table.insert(filtered_issues, issue) end end for issue_index, issue in ipairs(filtered_issues) do issue_table[issue_index] = issue end for issue_index = #filtered_issues + 1, #issue_table, 1 do issue_table[issue_index] = nil end end -- Prevent the issue from being added later. table.insert(self.ignored_issues, ignore_issue) end -- Check whether two registries only contain issues with the same codes. function Issues:has_same_codes_as(other) -- Collect codes of all issues. local self_codes, other_codes = {}, {} for _, table_name in ipairs({'warnings', 'errors'}) do for _, tables in ipairs({{self[table_name], self_codes}, {other[table_name], other_codes}}) do local issue_table, codes = table.unpack(tables) for _, issue in ipairs(issue_table) do local code = issue[1] codes[code] = true end end end -- Check whether this registry has any extra codes. for code, _ in pairs(self_codes) do if other_codes[code] == nil then return false end end -- Check whether the other registry has any extra codes. for code, _ in pairs(other_codes) do if self_codes[code] == nil then return false end end return true end -- Sort the warnings/errors using location as the primary key. function Issues.sort(warnings_and_errors) local sorted_warnings_and_errors = {} for _, issue in ipairs(warnings_and_errors) do table.insert(sorted_warnings_and_errors, issue) end table.sort(sorted_warnings_and_errors, function(a, b) local a_identifier, b_identifier = a[1], b[1] local a_range, b_range = (a[3] and a[3]:start()) or 0, (b[3] and b[3]:start()) or 0 return a_range < b_range or (a_range == b_range and a_identifier < b_identifier) end) return sorted_warnings_and_errors end return function(...) return Issues:new(...) end