388 lines
11 KiB
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
|