Clients/Client2020/ExtraContent/LuaPackages/CodeCoverage/LineScanner.lua

388 lines
11 KiB
Lua

--[[
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
{"<FULLID>", "x ?[%[%.]? ?[ntfx0']* ?%]?"}, -- identifier, possibly indexed once
{"<IDS>", "x ?, ?x[x, ]*"}, -- at least two comma-separated identifiers
{"<FIELDNAME>", "%[? ?[ntfx0']+ ?%]?"}, -- field, possibly like ["this"]
{"<PARENS>", "[ %(]*"} -- 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 <IDS>", -- "local var1, ..., varN"
fixup "local <IDS>=", -- "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 "<FIELDNAME>=.+[,;]", -- "[123] = 23," "['foo'] = "asd","
fixup "<FIELDNAME>=function", -- "[123] = function(...)"
fixup "<FIELDNAME>=<PARENS>'", -- "[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 "<FULLID>", -- Same for local variables indexed once
fixup "local x=function", -- "local a = function(arg1, ..., argN)"
fixup "local x=<PARENS>'", -- "local a = [[", possibly with opening parens
fixup "local x=(<PARENS>", -- "local a = (", possibly with several parens
fixup "local <IDS>=(<PARENS>", -- "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 "<FULLID>=<PARENS>'", -- "a.b = [[", possibly with opening parens
fixup "<FULLID>=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