--[[ The MIT License (MIT) Copyright (c) 2007 - 2018 Hisham Muhammad. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Original: https://github.com/keplerproject/luacov/6e6232d766f051d9668dab785582451bfd69ad17/master/src/luacov/linescanner.lua ]] local LineScanner = {} LineScanner.__index = LineScanner function LineScanner:new() return setmetatable( { first = true, comment = false, after_function = false, enabled = true }, self ) end -- Raw version of string.gsub local function replace(s, old, new) old = old:gsub("%p", "%%%0") new = new:gsub("%%", "%%%%") return (s:gsub(old, new)) end local fixups = { {"=", " ?= ?"}, -- '=' may be surrounded by spaces {"(", " ?%( ?"}, -- '(' may be surrounded by spaces {")", " ?%) ?"}, -- ')' may be surrounded by spaces {"", "x ?[%[%.]? ?[ntfx0']* ?%]?"}, -- identifier, possibly indexed once {"", "x ?, ?x[x, ]*"}, -- at least two comma-separated identifiers {"", "%[? ?[ntfx0']+ ?%]?"}, -- field, possibly like ["this"] {"", "[ %(]*"} -- optional opening parentheses } -- Utility function to make patterns more readable local function fixup(pat) for _, fixup_pair in ipairs(fixups) do pat = replace(pat, fixup_pair[1], fixup_pair[2]) end return pat end --- Lines that are always excluded from accounting local any_hits_exclusions = { "", -- Empty line "end[,; %)]*", -- Single "end" "else", -- Single "else" "repeat", -- Single "repeat" "do", -- Single "do" "if", -- Single "if" "then", -- Single "then" "while t do", -- "while true do" generates no code "if t then", -- "if true then" generates no code "local x", -- "local var" fixup "local x=", -- "local var =" fixup "local ", -- "local var1, ..., varN" fixup "local =", -- "local var1, ..., varN =" "local function x" -- "local function f (arg1, ..., argN)" } --- Lines that are only excluded from accounting when they have 0 hits local zero_hits_exclusions = { "[ntfx0',= ]+,", -- "var1 var2," multi columns table stuff "{ ?} ?,", -- Empty table before comma leaves no trace in tables and calls fixup "=.+[,;]", -- "[123] = 23," "['foo'] = "asd"," fixup "=function", -- "[123] = function(...)" fixup "='", -- "[123] = [[", possibly with opening parens "return function", -- "return function(arg1, ..., argN)" "function", -- "function(arg1, ..., argN)" "[ntfx0]", -- Single token expressions leave no trace in tables, function calls and sometimes assignments "''", -- Same for strings "{ ?}", -- Same for empty tables fixup "", -- Same for local variables indexed once fixup "local x=function", -- "local a = function(arg1, ..., argN)" fixup "local x='", -- "local a = [[", possibly with opening parens fixup "local x=(", -- "local a = (", possibly with several parens fixup "local =(", -- "local a, b = (", possibly with several parens fixup "local x=n", -- "local a = nil; local b = nil" produces no trace for the second statement fixup "='", -- "a.b = [[", possibly with opening parens fixup "=function", -- "a = function(arg1, ..., argN)" "} ?,", -- "}," generates no trace if the table ends with a key-value pair "} ?, ?function", -- same with "}, function(...)" "break", -- "break" generates no trace in Lua 5.2+ "{", -- "{" opening table "}?[ %)]*", -- optional closing paren, possibly with several closing parens "[ntf0']+ ?}[ %)]*" -- a constant at the end of a table, possibly with closing parens (for LuaJIT) } local function excluded(exclusions, line) for _, e in ipairs(exclusions) do if line:match("^ *" .. e .. " *$") then return true end end return false end function LineScanner:find(pattern) return self.line:find(pattern, self.i) end -- Skips string literal with quote stored as self.quote. -- @return boolean indicating success. function LineScanner:skip_string() -- Look for closing quote, possibly after even number of backslashes. local _, quote_i = self:find("^(\\*)%1" .. self.quote) if not quote_i then _, quote_i = self:find("[^\\](\\*)%1" .. self.quote) end if quote_i then self.i = quote_i + 1 self.quote = nil table.insert(self.simple_line_buffer, "'") return true else return false end end -- Skips long string literal with equal signs stored as self.equals. -- @return boolean indicating success. function LineScanner:skip_long_string() local _, bracket_i = self:find("%]" .. self.equals .. "%]") if bracket_i then self.i = bracket_i + 1 self.equals = nil if self.comment then self.comment = false else table.insert(self.simple_line_buffer, "'") end return true else return false end end -- Skips function arguments. -- @return boolean indicating success. function LineScanner:skip_args() local _, paren_i = self:find("%)") if paren_i then self.i = paren_i + 1 self.args = nil return true else return false end end function LineScanner:skip_whitespace() local next_i = self:find("%S") or #self.line + 1 if next_i ~= self.i then self.i = next_i table.insert(self.simple_line_buffer, " ") end end function LineScanner:skip_number() if self:find("^0[xX]") then self.i = self.i + 2 end local _ _, _, self.i = self:find("^[%x%.]*()") if self:find("^[eEpP][%+%-]") then -- Skip exponent, too. self.i = self.i + 2 _, _, self.i = self:find("^[%x%.]*()") end -- Skip LuaJIT number suffixes (i, ll, ull). _, _, self.i = self:find("^[iull]*()") table.insert(self.simple_line_buffer, "0") end local keywords = {["nil"] = "n", ["true"] = "t", ["false"] = "f"} for _, keyword in ipairs( { "and", "break", "do", "else", "elseif", "end", "for", "function", "goto", "if", "in", "local", "not", "or", "repeat", "return", "then", "until", "while" } ) do keywords[keyword] = keyword end function LineScanner:skip_name() -- It is guaranteed that the first character matches "%a_". local _, _, name = self:find("^([%w_]*)") self.i = self.i + #name if keywords[name] then name = keywords[name] else name = "x" end table.insert(self.simple_line_buffer, name) if name == "function" then -- This flag indicates that the next pair of parentheses (function args) must be skipped. self.after_function = true end end -- Source lines can be explicitly ignored using `enable` and `disable` inline options. -- An inline option is a simple comment: `-- luacov: enable` or `-- luacov: disable`. -- Inline option parsing is not whitespace sensitive. -- All lines starting from a line containing `disable` option and up to a line containing `enable` -- option (or end of file) are excluded. function LineScanner:check_inline_options(comment_body) if comment_body:find("^%s*luacov:%s*enable%s*$") then self.enabled = true elseif comment_body:find("^%s*luacov:%s*disable%s*$") then self.enabled = false end end -- Consumes and analyzes a line. -- @return boolean indicating whether line must be excluded. -- @return boolean indicating whether line must be excluded if not hit. function LineScanner:consume(line) if self.first then self.first = false if line:match("^#!") then -- Ignore Unix hash-bang magic line. return true, true end end self.line = line -- As scanner goes through the line, it puts its simplified parts into buffer. -- Punctuation is preserved. Whitespace is replaced with single space. -- Literal strings are replaced with "''", so that a string literal -- containing special characters does not confuse exclusion rules. -- Numbers are replaced with "0". -- Identifiers are replaced with "x". -- Literal keywords (nil, true and false) are replaced with "n", "t" and "f", -- other keywords are preserved. -- Function declaration arguments are removed. self.simple_line_buffer = {} self.i = 1 while self.i <= #line do -- One iteration of this loop handles one token, where -- string literal start and end are considered distinct tokens. if self.quote then if not self:skip_string() then -- String literal ends on another line. break end elseif self.equals then if not self:skip_long_string() then -- Long string literal or comment ends on another line. break end elseif self.args then if not self:skip_args() then -- Function arguments end on another line. break end else self:skip_whitespace() if self:find("^%.%d") then self.i = self.i + 1 end if self:find("^%d") then self:skip_number() elseif self:find("^[%a_]") then self:skip_name() else if self:find("^%-%-") then self.comment = true self.i = self.i + 2 end local _, bracket_i, equals = self:find("^%[(=*)%[") if equals then self.i = bracket_i + 1 self.equals = equals if not self.comment then table.insert(self.simple_line_buffer, "'") end elseif self.comment then -- Simple comment, check if it contains inline options and skip line. self.comment = false local comment_body = self.line:sub(self.i) self:check_inline_options(comment_body) break else local char = line:sub(self.i, self.i) if char == "." then -- Dot can't be saved as one character because of -- ".." and "..." tokens and the fact that number literals -- can start with one. local _, _, dots = self:find("^(%.*)") self.i = self.i + #dots table.insert(self.simple_line_buffer, dots) else self.i = self.i + 1 if char == "'" or char == '"' then table.insert(self.simple_line_buffer, "'") self.quote = char elseif self.after_function and char == "(" then -- This is the opening parenthesis of function declaration args. self.after_function = false self.args = true else -- Save other punctuation literally. -- This inserts an empty string when at the end of line, -- which is fine. table.insert(self.simple_line_buffer, char) end end end end end end if not self.enabled then -- Disabled by inline options, always exclude the line. return true, true end local simple_line = table.concat(self.simple_line_buffer) return excluded(any_hits_exclusions, simple_line), excluded(zero_hits_exclusions, simple_line) end return LineScanner